/* Minification failed. Returning unminified contents.
(20,5148-5153): run-time warning JS1028: Expected identifier or string: catch
(20,11360-11366): run-time warning JS1028: Expected identifier or string: delete
(20,13946-13952): run-time warning JS1028: Expected identifier or string: delete
(2462,18-24): run-time warning JS1010: Expected identifier: delete
(2917,13-18): run-time warning JS1010: Expected identifier: class
(2919,13-18): run-time warning JS1010: Expected identifier: class
(2921,13-18): run-time warning JS1010: Expected identifier: class
(2462,18-24): run-time error JS1137: 'delete' is a new reserved word and should not be used as an identifier: delete
(2917,13-18): run-time error JS1137: 'class' is a new reserved word and should not be used as an identifier: class
(2919,13-18): run-time error JS1137: 'class' is a new reserved word and should not be used as an identifier: class
(2921,13-18): run-time error JS1137: 'class' is a new reserved word and should not be used as an identifier: class
 */
setTimeout(function ()
{
    var a = document.createElement("script");
    var b = document.getElementsByTagName("script")[0];
    a.src = document.location.protocol + "//script.crazyegg.com/pages/scripts/0000/9014.js?" + Math.floor(new Date().getTime() / 3600000);
    a.async = true; a.type = "text/javascript";
    b.parentNode.insertBefore(a, b)
}

, 1);;/*!
  * https://github.com/paulmillr/es6-shim
  * @license es6-shim Copyright 2013-2016 by Paul Miller (http://paulmillr.com)
  *   and contributors,  MIT License
  * es6-shim: v0.35.1
  * see https://github.com/paulmillr/es6-shim/blob/0.35.1/LICENSE
  * Details and documentation:
  * https://github.com/paulmillr/es6-shim/
  */
(function(e,t){if(typeof define==="function"&&define.amd){define(t)}else if(typeof exports==="object"){module.exports=t()}else{e.returnExports=t()}})(this,function(){"use strict";var e=Function.call.bind(Function.apply);var t=Function.call.bind(Function.call);var r=Array.isArray;var n=Object.keys;var o=function notThunker(t){return function notThunk(){return!e(t,this,arguments)}};var i=function(e){try{e();return false}catch(e){return true}};var a=function valueOrFalseIfThrows(e){try{return e()}catch(e){return false}};var u=o(i);var f=function(){return!i(function(){Object.defineProperty({},"x",{get:function(){}})})};var s=!!Object.defineProperty&&f();var c=function foo(){}.name==="foo";var l=Function.call.bind(Array.prototype.forEach);var p=Function.call.bind(Array.prototype.reduce);var v=Function.call.bind(Array.prototype.filter);var y=Function.call.bind(Array.prototype.some);var h=function(e,t,r,n){if(!n&&t in e){return}if(s){Object.defineProperty(e,t,{configurable:true,enumerable:false,writable:true,value:r})}else{e[t]=r}};var b=function(e,t,r){l(n(t),function(n){var o=t[n];h(e,n,o,!!r)})};var g=Function.call.bind(Object.prototype.toString);var d=typeof/abc/==="function"?function IsCallableSlow(e){return typeof e==="function"&&g(e)==="[object Function]"}:function IsCallableFast(e){return typeof e==="function"};var m={getter:function(e,t,r){if(!s){throw new TypeError("getters require true ES5 support")}Object.defineProperty(e,t,{configurable:true,enumerable:false,get:r})},proxy:function(e,t,r){if(!s){throw new TypeError("getters require true ES5 support")}var n=Object.getOwnPropertyDescriptor(e,t);Object.defineProperty(r,t,{configurable:n.configurable,enumerable:n.enumerable,get:function getKey(){return e[t]},set:function setKey(r){e[t]=r}})},redefine:function(e,t,r){if(s){var n=Object.getOwnPropertyDescriptor(e,t);n.value=r;Object.defineProperty(e,t,n)}else{e[t]=r}},defineByDescriptor:function(e,t,r){if(s){Object.defineProperty(e,t,r)}else if("value"in r){e[t]=r.value}},preserveToString:function(e,t){if(t&&d(t.toString)){h(e,"toString",t.toString.bind(t),true)}}};var O=Object.create||function(e,t){var r=function Prototype(){};r.prototype=e;var o=new r;if(typeof t!=="undefined"){n(t).forEach(function(e){m.defineByDescriptor(o,e,t[e])})}return o};var w=function(e,t){if(!Object.setPrototypeOf){return false}return a(function(){var r=function Subclass(t){var r=new e(t);Object.setPrototypeOf(r,Subclass.prototype);return r};Object.setPrototypeOf(r,e);r.prototype=O(e.prototype,{constructor:{value:r}});return t(r)})};var j=function(){if(typeof self!=="undefined"){return self}if(typeof window!=="undefined"){return window}if(typeof global!=="undefined"){return global}throw new Error("unable to locate global object")};var S=j();var T=S.isFinite;var I=Function.call.bind(String.prototype.indexOf);var E=Function.apply.bind(Array.prototype.indexOf);var P=Function.call.bind(Array.prototype.concat);var C=Function.call.bind(String.prototype.slice);var M=Function.call.bind(Array.prototype.push);var x=Function.apply.bind(Array.prototype.push);var N=Function.call.bind(Array.prototype.shift);var A=Math.max;var R=Math.min;var _=Math.floor;var k=Math.abs;var F=Math.exp;var L=Math.log;var D=Math.sqrt;var z=Function.call.bind(Object.prototype.hasOwnProperty);var q;var W=function(){};var G=S.Map;var H=G&&G.prototype["delete"];var V=G&&G.prototype.get;var B=G&&G.prototype.has;var U=G&&G.prototype.set;var $=S.Symbol||{};var J=$.species||"@@species";var X=Number.isNaN||function isNaN(e){return e!==e};var K=Number.isFinite||function isFinite(e){return typeof e==="number"&&T(e)};var Z=d(Math.sign)?Math.sign:function sign(e){var t=Number(e);if(t===0){return t}if(X(t)){return t}return t<0?-1:1};var Y=function isArguments(e){return g(e)==="[object Arguments]"};var Q=function isArguments(e){return e!==null&&typeof e==="object"&&typeof e.length==="number"&&e.length>=0&&g(e)!=="[object Array]"&&g(e.callee)==="[object Function]"};var ee=Y(arguments)?Y:Q;var te={primitive:function(e){return e===null||typeof e!=="function"&&typeof e!=="object"},string:function(e){return g(e)==="[object String]"},regex:function(e){return g(e)==="[object RegExp]"},symbol:function(e){return typeof S.Symbol==="function"&&typeof e==="symbol"}};var re=function overrideNative(e,t,r){var n=e[t];h(e,t,r,true);m.preserveToString(e[t],n)};var ne=typeof $==="function"&&typeof $["for"]==="function"&&te.symbol($());var oe=te.symbol($.iterator)?$.iterator:"_es6-shim iterator_";if(S.Set&&typeof(new S.Set)["@@iterator"]==="function"){oe="@@iterator"}if(!S.Reflect){h(S,"Reflect",{},true)}var ie=S.Reflect;var ae=String;var ue=typeof document==="undefined"||!document?null:document.all;var fe=ue==null?function isNullOrUndefined(e){return e==null}:function isNullOrUndefinedAndNotDocumentAll(e){return e==null&&e!==ue};var se={Call:function Call(t,r){var n=arguments.length>2?arguments[2]:[];if(!se.IsCallable(t)){throw new TypeError(t+" is not a function")}return e(t,r,n)},RequireObjectCoercible:function(e,t){if(fe(e)){throw new TypeError(t||"Cannot call method on "+e)}return e},TypeIsObject:function(e){if(e===void 0||e===null||e===true||e===false){return false}return typeof e==="function"||typeof e==="object"||e===ue},ToObject:function(e,t){return Object(se.RequireObjectCoercible(e,t))},IsCallable:d,IsConstructor:function(e){return se.IsCallable(e)},ToInt32:function(e){return se.ToNumber(e)>>0},ToUint32:function(e){return se.ToNumber(e)>>>0},ToNumber:function(e){if(g(e)==="[object Symbol]"){throw new TypeError("Cannot convert a Symbol value to a number")}return+e},ToInteger:function(e){var t=se.ToNumber(e);if(X(t)){return 0}if(t===0||!K(t)){return t}return(t>0?1:-1)*_(k(t))},ToLength:function(e){var t=se.ToInteger(e);if(t<=0){return 0}if(t>Number.MAX_SAFE_INTEGER){return Number.MAX_SAFE_INTEGER}return t},SameValue:function(e,t){if(e===t){if(e===0){return 1/e===1/t}return true}return X(e)&&X(t)},SameValueZero:function(e,t){return e===t||X(e)&&X(t)},IsIterable:function(e){return se.TypeIsObject(e)&&(typeof e[oe]!=="undefined"||ee(e))},GetIterator:function(e){if(ee(e)){return new q(e,"value")}var t=se.GetMethod(e,oe);if(!se.IsCallable(t)){throw new TypeError("value is not an iterable")}var r=se.Call(t,e);if(!se.TypeIsObject(r)){throw new TypeError("bad iterator")}return r},GetMethod:function(e,t){var r=se.ToObject(e)[t];if(fe(r)){return void 0}if(!se.IsCallable(r)){throw new TypeError("Method not callable: "+t)}return r},IteratorComplete:function(e){return!!e.done},IteratorClose:function(e,t){var r=se.GetMethod(e,"return");if(r===void 0){return}var n,o;try{n=se.Call(r,e)}catch(e){o=e}if(t){return}if(o){throw o}if(!se.TypeIsObject(n)){throw new TypeError("Iterator's return method returned a non-object.")}},IteratorNext:function(e){var t=arguments.length>1?e.next(arguments[1]):e.next();if(!se.TypeIsObject(t)){throw new TypeError("bad iterator")}return t},IteratorStep:function(e){var t=se.IteratorNext(e);var r=se.IteratorComplete(t);return r?false:t},Construct:function(e,t,r,n){var o=typeof r==="undefined"?e:r;if(!n&&ie.construct){return ie.construct(e,t,o)}var i=o.prototype;if(!se.TypeIsObject(i)){i=Object.prototype}var a=O(i);var u=se.Call(e,a,t);return se.TypeIsObject(u)?u:a},SpeciesConstructor:function(e,t){var r=e.constructor;if(r===void 0){return t}if(!se.TypeIsObject(r)){throw new TypeError("Bad constructor")}var n=r[J];if(fe(n)){return t}if(!se.IsConstructor(n)){throw new TypeError("Bad @@species")}return n},CreateHTML:function(e,t,r,n){var o=se.ToString(e);var i="<"+t;if(r!==""){var a=se.ToString(n);var u=a.replace(/"/g,"&quot;");i+=" "+r+'="'+u+'"'}var f=i+">";var s=f+o;return s+"</"+t+">"},IsRegExp:function IsRegExp(e){if(!se.TypeIsObject(e)){return false}var t=e[$.match];if(typeof t!=="undefined"){return!!t}return te.regex(e)},ToString:function ToString(e){return ae(e)}};if(s&&ne){var ce=function defineWellKnownSymbol(e){if(te.symbol($[e])){return $[e]}var t=$["for"]("Symbol."+e);Object.defineProperty($,e,{configurable:false,enumerable:false,writable:false,value:t});return t};if(!te.symbol($.search)){var le=ce("search");var pe=String.prototype.search;h(RegExp.prototype,le,function search(e){return se.Call(pe,e,[this])});var ve=function search(e){var t=se.RequireObjectCoercible(this);if(!fe(e)){var r=se.GetMethod(e,le);if(typeof r!=="undefined"){return se.Call(r,e,[t])}}return se.Call(pe,t,[se.ToString(e)])};re(String.prototype,"search",ve)}if(!te.symbol($.replace)){var ye=ce("replace");var he=String.prototype.replace;h(RegExp.prototype,ye,function replace(e,t){return se.Call(he,e,[this,t])});var be=function replace(e,t){var r=se.RequireObjectCoercible(this);if(!fe(e)){var n=se.GetMethod(e,ye);if(typeof n!=="undefined"){return se.Call(n,e,[r,t])}}return se.Call(he,r,[se.ToString(e),t])};re(String.prototype,"replace",be)}if(!te.symbol($.split)){var ge=ce("split");var de=String.prototype.split;h(RegExp.prototype,ge,function split(e,t){return se.Call(de,e,[this,t])});var me=function split(e,t){var r=se.RequireObjectCoercible(this);if(!fe(e)){var n=se.GetMethod(e,ge);if(typeof n!=="undefined"){return se.Call(n,e,[r,t])}}return se.Call(de,r,[se.ToString(e),t])};re(String.prototype,"split",me)}var Oe=te.symbol($.match);var we=Oe&&function(){var e={};e[$.match]=function(){return 42};return"a".match(e)!==42}();if(!Oe||we){var je=ce("match");var Se=String.prototype.match;h(RegExp.prototype,je,function match(e){return se.Call(Se,e,[this])});var Te=function match(e){var t=se.RequireObjectCoercible(this);if(!fe(e)){var r=se.GetMethod(e,je);if(typeof r!=="undefined"){return se.Call(r,e,[t])}}return se.Call(Se,t,[se.ToString(e)])};re(String.prototype,"match",Te)}}var Ie=function wrapConstructor(e,t,r){m.preserveToString(t,e);if(Object.setPrototypeOf){Object.setPrototypeOf(e,t)}if(s){l(Object.getOwnPropertyNames(e),function(n){if(n in W||r[n]){return}m.proxy(e,n,t)})}else{l(Object.keys(e),function(n){if(n in W||r[n]){return}t[n]=e[n]})}t.prototype=e.prototype;m.redefine(e.prototype,"constructor",t)};var Ee=function(){return this};var Pe=function(e){if(s&&!z(e,J)){m.getter(e,J,Ee)}};var Ce=function(e,t){var r=t||function iterator(){return this};h(e,oe,r);if(!e[oe]&&te.symbol(oe)){e[oe]=r}};var Me=function createDataProperty(e,t,r){if(s){Object.defineProperty(e,t,{configurable:true,enumerable:true,writable:true,value:r})}else{e[t]=r}};var xe=function createDataPropertyOrThrow(e,t,r){Me(e,t,r);if(!se.SameValue(e[t],r)){throw new TypeError("property is nonconfigurable")}};var Ne=function(e,t,r,n){if(!se.TypeIsObject(e)){throw new TypeError("Constructor requires `new`: "+t.name)}var o=t.prototype;if(!se.TypeIsObject(o)){o=r}var i=O(o);for(var a in n){if(z(n,a)){var u=n[a];h(i,a,u,true)}}return i};if(String.fromCodePoint&&String.fromCodePoint.length!==1){var Ae=String.fromCodePoint;re(String,"fromCodePoint",function fromCodePoint(e){return se.Call(Ae,this,arguments)})}var Re={fromCodePoint:function fromCodePoint(e){var t=[];var r;for(var n=0,o=arguments.length;n<o;n++){r=Number(arguments[n]);if(!se.SameValue(r,se.ToInteger(r))||r<0||r>1114111){throw new RangeError("Invalid code point "+r)}if(r<65536){M(t,String.fromCharCode(r))}else{r-=65536;M(t,String.fromCharCode((r>>10)+55296));M(t,String.fromCharCode(r%1024+56320))}}return t.join("")},raw:function raw(e){var t=se.ToObject(e,"bad callSite");var r=se.ToObject(t.raw,"bad raw value");var n=r.length;var o=se.ToLength(n);if(o<=0){return""}var i=[];var a=0;var u,f,s,c;while(a<o){u=se.ToString(a);s=se.ToString(r[u]);M(i,s);if(a+1>=o){break}f=a+1<arguments.length?arguments[a+1]:"";c=se.ToString(f);M(i,c);a+=1}return i.join("")}};if(String.raw&&String.raw({raw:{0:"x",1:"y",length:2}})!=="xy"){re(String,"raw",Re.raw)}b(String,Re);var _e=function repeat(e,t){if(t<1){return""}if(t%2){return repeat(e,t-1)+e}var r=repeat(e,t/2);return r+r};var ke=Infinity;var Fe={repeat:function repeat(e){var t=se.ToString(se.RequireObjectCoercible(this));var r=se.ToInteger(e);if(r<0||r>=ke){throw new RangeError("repeat count must be less than infinity and not overflow maximum string size")}return _e(t,r)},startsWith:function startsWith(e){var t=se.ToString(se.RequireObjectCoercible(this));if(se.IsRegExp(e)){throw new TypeError('Cannot call method "startsWith" with a regex')}var r=se.ToString(e);var n;if(arguments.length>1){n=arguments[1]}var o=A(se.ToInteger(n),0);return C(t,o,o+r.length)===r},endsWith:function endsWith(e){var t=se.ToString(se.RequireObjectCoercible(this));if(se.IsRegExp(e)){throw new TypeError('Cannot call method "endsWith" with a regex')}var r=se.ToString(e);var n=t.length;var o;if(arguments.length>1){o=arguments[1]}var i=typeof o==="undefined"?n:se.ToInteger(o);var a=R(A(i,0),n);return C(t,a-r.length,a)===r},includes:function includes(e){if(se.IsRegExp(e)){throw new TypeError('"includes" does not accept a RegExp')}var t=se.ToString(e);var r;if(arguments.length>1){r=arguments[1]}return I(this,t,r)!==-1},codePointAt:function codePointAt(e){var t=se.ToString(se.RequireObjectCoercible(this));var r=se.ToInteger(e);var n=t.length;if(r>=0&&r<n){var o=t.charCodeAt(r);var i=r+1===n;if(o<55296||o>56319||i){return o}var a=t.charCodeAt(r+1);if(a<56320||a>57343){return o}return(o-55296)*1024+(a-56320)+65536}}};if(String.prototype.includes&&"a".includes("a",Infinity)!==false){re(String.prototype,"includes",Fe.includes)}if(String.prototype.startsWith&&String.prototype.endsWith){var Le=i(function(){"/a/".startsWith(/a/)});var De=a(function(){return"abc".startsWith("a",Infinity)===false});if(!Le||!De){re(String.prototype,"startsWith",Fe.startsWith);re(String.prototype,"endsWith",Fe.endsWith)}}if(ne){var ze=a(function(){var e=/a/;e[$.match]=false;return"/a/".startsWith(e)});if(!ze){re(String.prototype,"startsWith",Fe.startsWith)}var qe=a(function(){var e=/a/;e[$.match]=false;return"/a/".endsWith(e)});if(!qe){re(String.prototype,"endsWith",Fe.endsWith)}var We=a(function(){var e=/a/;e[$.match]=false;return"/a/".includes(e)});if(!We){re(String.prototype,"includes",Fe.includes)}}b(String.prototype,Fe);var Ge=["\t\n\v\f\r \xa0\u1680\u180e\u2000\u2001\u2002\u2003","\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028","\u2029\ufeff"].join("");var He=new RegExp("(^["+Ge+"]+)|(["+Ge+"]+$)","g");var Ve=function trim(){return se.ToString(se.RequireObjectCoercible(this)).replace(He,"")};var Be=["\x85","\u200b","\ufffe"].join("");var Ue=new RegExp("["+Be+"]","g");var $e=/^[-+]0x[0-9a-f]+$/i;var Je=Be.trim().length!==Be.length;h(String.prototype,"trim",Ve,Je);var Xe=function(e){return{value:e,done:arguments.length===0}};var Ke=function(e){se.RequireObjectCoercible(e);this._s=se.ToString(e);this._i=0};Ke.prototype.next=function(){var e=this._s;var t=this._i;if(typeof e==="undefined"||t>=e.length){this._s=void 0;return Xe()}var r=e.charCodeAt(t);var n,o;if(r<55296||r>56319||t+1===e.length){o=1}else{n=e.charCodeAt(t+1);o=n<56320||n>57343?1:2}this._i=t+o;return Xe(e.substr(t,o))};Ce(Ke.prototype);Ce(String.prototype,function(){return new Ke(this)});var Ze={from:function from(e){var r=this;var n;if(arguments.length>1){n=arguments[1]}var o,i;if(typeof n==="undefined"){o=false}else{if(!se.IsCallable(n)){throw new TypeError("Array.from: when provided, the second argument must be a function")}if(arguments.length>2){i=arguments[2]}o=true}var a=typeof(ee(e)||se.GetMethod(e,oe))!=="undefined";var u,f,s;if(a){f=se.IsConstructor(r)?Object(new r):[];var c=se.GetIterator(e);var l,p;s=0;while(true){l=se.IteratorStep(c);if(l===false){break}p=l.value;try{if(o){p=typeof i==="undefined"?n(p,s):t(n,i,p,s)}f[s]=p}catch(e){se.IteratorClose(c,true);throw e}s+=1}u=s}else{var v=se.ToObject(e);u=se.ToLength(v.length);f=se.IsConstructor(r)?Object(new r(u)):new Array(u);var y;for(s=0;s<u;++s){y=v[s];if(o){y=typeof i==="undefined"?n(y,s):t(n,i,y,s)}xe(f,s,y)}}f.length=u;return f},of:function of(){var e=arguments.length;var t=this;var n=r(t)||!se.IsCallable(t)?new Array(e):se.Construct(t,[e]);for(var o=0;o<e;++o){xe(n,o,arguments[o])}n.length=e;return n}};b(Array,Ze);Pe(Array);q=function(e,t){this.i=0;this.array=e;this.kind=t};b(q.prototype,{next:function(){var e=this.i;var t=this.array;if(!(this instanceof q)){throw new TypeError("Not an ArrayIterator")}if(typeof t!=="undefined"){var r=se.ToLength(t.length);for(;e<r;e++){var n=this.kind;var o;if(n==="key"){o=e}else if(n==="value"){o=t[e]}else if(n==="entry"){o=[e,t[e]]}this.i=e+1;return Xe(o)}}this.array=void 0;return Xe()}});Ce(q.prototype);var Ye=Array.of===Ze.of||function(){var e=function Foo(e){this.length=e};e.prototype=[];var t=Array.of.apply(e,[1,2]);return t instanceof e&&t.length===2}();if(!Ye){re(Array,"of",Ze.of)}var Qe={copyWithin:function copyWithin(e,t){var r=se.ToObject(this);var n=se.ToLength(r.length);var o=se.ToInteger(e);var i=se.ToInteger(t);var a=o<0?A(n+o,0):R(o,n);var u=i<0?A(n+i,0):R(i,n);var f;if(arguments.length>2){f=arguments[2]}var s=typeof f==="undefined"?n:se.ToInteger(f);var c=s<0?A(n+s,0):R(s,n);var l=R(c-u,n-a);var p=1;if(u<a&&a<u+l){p=-1;u+=l-1;a+=l-1}while(l>0){if(u in r){r[a]=r[u]}else{delete r[a]}u+=p;a+=p;l-=1}return r},fill:function fill(e){var t;if(arguments.length>1){t=arguments[1]}var r;if(arguments.length>2){r=arguments[2]}var n=se.ToObject(this);var o=se.ToLength(n.length);t=se.ToInteger(typeof t==="undefined"?0:t);r=se.ToInteger(typeof r==="undefined"?o:r);var i=t<0?A(o+t,0):R(t,o);var a=r<0?o+r:r;for(var u=i;u<o&&u<a;++u){n[u]=e}return n},find:function find(e){var r=se.ToObject(this);var n=se.ToLength(r.length);if(!se.IsCallable(e)){throw new TypeError("Array#find: predicate must be a function")}var o=arguments.length>1?arguments[1]:null;for(var i=0,a;i<n;i++){a=r[i];if(o){if(t(e,o,a,i,r)){return a}}else if(e(a,i,r)){return a}}},findIndex:function findIndex(e){var r=se.ToObject(this);var n=se.ToLength(r.length);if(!se.IsCallable(e)){throw new TypeError("Array#findIndex: predicate must be a function")}var o=arguments.length>1?arguments[1]:null;for(var i=0;i<n;i++){if(o){if(t(e,o,r[i],i,r)){return i}}else if(e(r[i],i,r)){return i}}return-1},keys:function keys(){return new q(this,"key")},values:function values(){return new q(this,"value")},entries:function entries(){return new q(this,"entry")}};if(Array.prototype.keys&&!se.IsCallable([1].keys().next)){delete Array.prototype.keys}if(Array.prototype.entries&&!se.IsCallable([1].entries().next)){delete Array.prototype.entries}if(Array.prototype.keys&&Array.prototype.entries&&!Array.prototype.values&&Array.prototype[oe]){b(Array.prototype,{values:Array.prototype[oe]});if(te.symbol($.unscopables)){Array.prototype[$.unscopables].values=true}}if(c&&Array.prototype.values&&Array.prototype.values.name!=="values"){var et=Array.prototype.values;re(Array.prototype,"values",function values(){return se.Call(et,this,arguments)});h(Array.prototype,oe,Array.prototype.values,true)}b(Array.prototype,Qe);if(1/[true].indexOf(true,-0)<0){h(Array.prototype,"indexOf",function indexOf(e){var t=E(this,arguments);if(t===0&&1/t<0){return 0}return t},true)}Ce(Array.prototype,function(){return this.values()});if(Object.getPrototypeOf){Ce(Object.getPrototypeOf([].values()))}var tt=function(){return a(function(){return Array.from({length:-1}).length===0})}();var rt=function(){var e=Array.from([0].entries());return e.length===1&&r(e[0])&&e[0][0]===0&&e[0][1]===0}();if(!tt||!rt){re(Array,"from",Ze.from)}var nt=function(){return a(function(){return Array.from([0],void 0)})}();if(!nt){var ot=Array.from;re(Array,"from",function from(e){if(arguments.length>1&&typeof arguments[1]!=="undefined"){return se.Call(ot,this,arguments)}else{return t(ot,this,e)}})}var it=-(Math.pow(2,32)-1);var at=function(e,r){var n={length:it};n[r?(n.length>>>0)-1:0]=true;return a(function(){t(e,n,function(){throw new RangeError("should not reach here")},[]);return true})};if(!at(Array.prototype.forEach)){var ut=Array.prototype.forEach;re(Array.prototype,"forEach",function forEach(e){return se.Call(ut,this.length>=0?this:[],arguments)},true)}if(!at(Array.prototype.map)){var ft=Array.prototype.map;re(Array.prototype,"map",function map(e){return se.Call(ft,this.length>=0?this:[],arguments)},true)}if(!at(Array.prototype.filter)){var st=Array.prototype.filter;re(Array.prototype,"filter",function filter(e){return se.Call(st,this.length>=0?this:[],arguments)},true)}if(!at(Array.prototype.some)){var ct=Array.prototype.some;re(Array.prototype,"some",function some(e){return se.Call(ct,this.length>=0?this:[],arguments)},true)}if(!at(Array.prototype.every)){var lt=Array.prototype.every;re(Array.prototype,"every",function every(e){return se.Call(lt,this.length>=0?this:[],arguments)},true)}if(!at(Array.prototype.reduce)){var pt=Array.prototype.reduce;re(Array.prototype,"reduce",function reduce(e){return se.Call(pt,this.length>=0?this:[],arguments)},true)}if(!at(Array.prototype.reduceRight,true)){var vt=Array.prototype.reduceRight;re(Array.prototype,"reduceRight",function reduceRight(e){return se.Call(vt,this.length>=0?this:[],arguments)},true)}var yt=Number("0o10")!==8;var ht=Number("0b10")!==2;var bt=y(Be,function(e){return Number(e+0+e)===0});if(yt||ht||bt){var gt=Number;var dt=/^0b[01]+$/i;var mt=/^0o[0-7]+$/i;var Ot=dt.test.bind(dt);var wt=mt.test.bind(mt);var jt=function(e){var t;if(typeof e.valueOf==="function"){t=e.valueOf();if(te.primitive(t)){return t}}if(typeof e.toString==="function"){t=e.toString();if(te.primitive(t)){return t}}throw new TypeError("No default value")};var St=Ue.test.bind(Ue);var Tt=$e.test.bind($e);var It=function(){var e=function Number(t){var r;if(arguments.length>0){r=te.primitive(t)?t:jt(t,"number")}else{r=0}if(typeof r==="string"){r=se.Call(Ve,r);if(Ot(r)){r=parseInt(C(r,2),2)}else if(wt(r)){r=parseInt(C(r,2),8)}else if(St(r)||Tt(r)){r=NaN}}var n=this;var o=a(function(){gt.prototype.valueOf.call(n);return true});if(n instanceof e&&!o){return new gt(r)}return gt(r)};return e}();Ie(gt,It,{});b(It,{NaN:gt.NaN,MAX_VALUE:gt.MAX_VALUE,MIN_VALUE:gt.MIN_VALUE,NEGATIVE_INFINITY:gt.NEGATIVE_INFINITY,POSITIVE_INFINITY:gt.POSITIVE_INFINITY});Number=It;m.redefine(S,"Number",It)}var Et=Math.pow(2,53)-1;b(Number,{MAX_SAFE_INTEGER:Et,MIN_SAFE_INTEGER:-Et,EPSILON:2.220446049250313e-16,parseInt:S.parseInt,parseFloat:S.parseFloat,isFinite:K,isInteger:function isInteger(e){return K(e)&&se.ToInteger(e)===e},isSafeInteger:function isSafeInteger(e){return Number.isInteger(e)&&k(e)<=Number.MAX_SAFE_INTEGER},isNaN:X});h(Number,"parseInt",S.parseInt,Number.parseInt!==S.parseInt);if([,1].find(function(){return true})===1){re(Array.prototype,"find",Qe.find)}if([,1].findIndex(function(){return true})!==0){re(Array.prototype,"findIndex",Qe.findIndex)}var Pt=Function.bind.call(Function.bind,Object.prototype.propertyIsEnumerable);var Ct=function ensureEnumerable(e,t){if(s&&Pt(e,t)){Object.defineProperty(e,t,{enumerable:false})}};var Mt=function sliceArgs(){var e=Number(this);var t=arguments.length;var r=t-e;var n=new Array(r<0?0:r);for(var o=e;o<t;++o){n[o-e]=arguments[o]}return n};var xt=function assignTo(e){return function assignToSource(t,r){t[r]=e[r];return t}};var Nt=function(e,t){var r=n(Object(t));var o;if(se.IsCallable(Object.getOwnPropertySymbols)){o=v(Object.getOwnPropertySymbols(Object(t)),Pt(t))}return p(P(r,o||[]),xt(t),e)};var At={assign:function(e,t){var r=se.ToObject(e,"Cannot convert undefined or null to object");return p(se.Call(Mt,1,arguments),Nt,r)},is:function is(e,t){return se.SameValue(e,t)}};var Rt=Object.assign&&Object.preventExtensions&&function(){var e=Object.preventExtensions({1:2});try{Object.assign(e,"xy")}catch(t){return e[1]==="y"}}();if(Rt){re(Object,"assign",At.assign)}b(Object,At);if(s){var _t={setPrototypeOf:function(e,r){var n;var o=function(e,t){if(!se.TypeIsObject(e)){throw new TypeError("cannot set prototype on a non-object")}if(!(t===null||se.TypeIsObject(t))){throw new TypeError("can only set prototype to an object or null"+t)}};var i=function(e,r){o(e,r);t(n,e,r);return e};try{n=e.getOwnPropertyDescriptor(e.prototype,r).set;t(n,{},null)}catch(t){if(e.prototype!=={}[r]){return}n=function(e){this[r]=e};i.polyfill=i(i({},null),e.prototype)instanceof e}return i}(Object,"__proto__")};b(Object,_t)}if(Object.setPrototypeOf&&Object.getPrototypeOf&&Object.getPrototypeOf(Object.setPrototypeOf({},null))!==null&&Object.getPrototypeOf(Object.create(null))===null){(function(){var e=Object.create(null);var t=Object.getPrototypeOf;var r=Object.setPrototypeOf;Object.getPrototypeOf=function(r){var n=t(r);return n===e?null:n};Object.setPrototypeOf=function(t,n){var o=n===null?e:n;return r(t,o)};Object.setPrototypeOf.polyfill=false})()}var kt=!i(function(){Object.keys("foo")});if(!kt){var Ft=Object.keys;re(Object,"keys",function keys(e){return Ft(se.ToObject(e))});n=Object.keys}var Lt=i(function(){Object.keys(/a/g)});if(Lt){var Dt=Object.keys;re(Object,"keys",function keys(e){if(te.regex(e)){var t=[];for(var r in e){if(z(e,r)){M(t,r)}}return t}return Dt(e)});n=Object.keys}if(Object.getOwnPropertyNames){var zt=!i(function(){Object.getOwnPropertyNames("foo")});if(!zt){var qt=typeof window==="object"?Object.getOwnPropertyNames(window):[];var Wt=Object.getOwnPropertyNames;re(Object,"getOwnPropertyNames",function getOwnPropertyNames(e){var t=se.ToObject(e);if(g(t)==="[object Window]"){try{return Wt(t)}catch(e){return P([],qt)}}return Wt(t)})}}if(Object.getOwnPropertyDescriptor){var Gt=!i(function(){Object.getOwnPropertyDescriptor("foo","bar")});if(!Gt){var Ht=Object.getOwnPropertyDescriptor;re(Object,"getOwnPropertyDescriptor",function getOwnPropertyDescriptor(e,t){return Ht(se.ToObject(e),t)})}}if(Object.seal){var Vt=!i(function(){Object.seal("foo")});if(!Vt){var Bt=Object.seal;re(Object,"seal",function seal(e){if(!se.TypeIsObject(e)){return e}return Bt(e)})}}if(Object.isSealed){var Ut=!i(function(){Object.isSealed("foo")});if(!Ut){var $t=Object.isSealed;re(Object,"isSealed",function isSealed(e){if(!se.TypeIsObject(e)){return true}return $t(e)})}}if(Object.freeze){var Jt=!i(function(){Object.freeze("foo")});if(!Jt){var Xt=Object.freeze;re(Object,"freeze",function freeze(e){if(!se.TypeIsObject(e)){return e}return Xt(e)})}}if(Object.isFrozen){var Kt=!i(function(){Object.isFrozen("foo")});if(!Kt){var Zt=Object.isFrozen;re(Object,"isFrozen",function isFrozen(e){if(!se.TypeIsObject(e)){return true}return Zt(e)})}}if(Object.preventExtensions){var Yt=!i(function(){Object.preventExtensions("foo")});if(!Yt){var Qt=Object.preventExtensions;re(Object,"preventExtensions",function preventExtensions(e){if(!se.TypeIsObject(e)){return e}return Qt(e)})}}if(Object.isExtensible){var er=!i(function(){Object.isExtensible("foo")});if(!er){var tr=Object.isExtensible;re(Object,"isExtensible",function isExtensible(e){if(!se.TypeIsObject(e)){return false}return tr(e)})}}if(Object.getPrototypeOf){var rr=!i(function(){Object.getPrototypeOf("foo")});if(!rr){var nr=Object.getPrototypeOf;re(Object,"getPrototypeOf",function getPrototypeOf(e){return nr(se.ToObject(e))})}}var or=s&&function(){var e=Object.getOwnPropertyDescriptor(RegExp.prototype,"flags");return e&&se.IsCallable(e.get)}();if(s&&!or){var ir=function flags(){if(!se.TypeIsObject(this)){throw new TypeError("Method called on incompatible type: must be an object.")}var e="";if(this.global){e+="g"}if(this.ignoreCase){e+="i"}if(this.multiline){e+="m"}if(this.unicode){e+="u"}if(this.sticky){e+="y"}return e};m.getter(RegExp.prototype,"flags",ir)}var ar=s&&a(function(){return String(new RegExp(/a/g,"i"))==="/a/i"});var ur=ne&&s&&function(){var e=/./;e[$.match]=false;return RegExp(e)===e}();var fr=a(function(){return RegExp.prototype.toString.call({source:"abc"})==="/abc/"});var sr=fr&&a(function(){return RegExp.prototype.toString.call({source:"a",flags:"b"})==="/a/b"});if(!fr||!sr){var cr=RegExp.prototype.toString;h(RegExp.prototype,"toString",function toString(){var e=se.RequireObjectCoercible(this);if(te.regex(e)){return t(cr,e)}var r=ae(e.source);var n=ae(e.flags);return"/"+r+"/"+n},true);m.preserveToString(RegExp.prototype.toString,cr)}if(s&&(!ar||ur)){var lr=Object.getOwnPropertyDescriptor(RegExp.prototype,"flags").get;var pr=Object.getOwnPropertyDescriptor(RegExp.prototype,"source")||{};var vr=function(){return this.source};var yr=se.IsCallable(pr.get)?pr.get:vr;var hr=RegExp;var br=function(){return function RegExp(e,t){var r=se.IsRegExp(e);var n=this instanceof RegExp;if(!n&&r&&typeof t==="undefined"&&e.constructor===RegExp){return e}var o=e;var i=t;if(te.regex(e)){o=se.Call(yr,e);i=typeof t==="undefined"?se.Call(lr,e):t;return new RegExp(o,i)}else if(r){o=e.source;i=typeof t==="undefined"?e.flags:t}return new hr(e,t)}}();Ie(hr,br,{$input:true});RegExp=br;m.redefine(S,"RegExp",br)}if(s){var gr={input:"$_",lastMatch:"$&",lastParen:"$+",leftContext:"$`",rightContext:"$'"};l(n(gr),function(e){if(e in RegExp&&!(gr[e]in RegExp)){m.getter(RegExp,gr[e],function get(){return RegExp[e]})}})}Pe(RegExp);var dr=1/Number.EPSILON;var mr=function roundTiesToEven(e){return e+dr-dr};var Or=Math.pow(2,-23);var wr=Math.pow(2,127)*(2-Or);var jr=Math.pow(2,-126);var Sr=Math.E;var Tr=Math.LOG2E;var Ir=Math.LOG10E;var Er=Number.prototype.clz;delete Number.prototype.clz;var Pr={acosh:function acosh(e){var t=Number(e);if(X(t)||e<1){return NaN}if(t===1){return 0}if(t===Infinity){return t}return L(t/Sr+D(t+1)*D(t-1)/Sr)+1},asinh:function asinh(e){var t=Number(e);if(t===0||!T(t)){return t}return t<0?-asinh(-t):L(t+D(t*t+1))},atanh:function atanh(e){var t=Number(e);if(X(t)||t<-1||t>1){return NaN}if(t===-1){return-Infinity}if(t===1){return Infinity}if(t===0){return t}return.5*L((1+t)/(1-t))},cbrt:function cbrt(e){var t=Number(e);if(t===0){return t}var r=t<0;var n;if(r){t=-t}if(t===Infinity){n=Infinity}else{n=F(L(t)/3);n=(t/(n*n)+2*n)/3}return r?-n:n},clz32:function clz32(e){var t=Number(e);var r=se.ToUint32(t);if(r===0){return 32}return Er?se.Call(Er,r):31-_(L(r+.5)*Tr)},cosh:function cosh(e){var t=Number(e);if(t===0){return 1}if(X(t)){return NaN}if(!T(t)){return Infinity}if(t<0){t=-t}if(t>21){return F(t)/2}return(F(t)+F(-t))/2},expm1:function expm1(e){var t=Number(e);if(t===-Infinity){return-1}if(!T(t)||t===0){return t}if(k(t)>.5){return F(t)-1}var r=t;var n=0;var o=1;while(n+r!==n){n+=r;o+=1;r*=t/o}return n},hypot:function hypot(e,t){var r=0;var n=0;for(var o=0;o<arguments.length;++o){var i=k(Number(arguments[o]));if(n<i){r*=n/i*(n/i);r+=1;n=i}else{r+=i>0?i/n*(i/n):i}}return n===Infinity?Infinity:n*D(r)},log2:function log2(e){return L(e)*Tr},log10:function log10(e){return L(e)*Ir},log1p:function log1p(e){var t=Number(e);if(t<-1||X(t)){return NaN}if(t===0||t===Infinity){return t}if(t===-1){return-Infinity}return 1+t-1===0?t:t*(L(1+t)/(1+t-1))},sign:Z,sinh:function sinh(e){var t=Number(e);if(!T(t)||t===0){return t}if(k(t)<1){return(Math.expm1(t)-Math.expm1(-t))/2}return(F(t-1)-F(-t-1))*Sr/2},tanh:function tanh(e){var t=Number(e);if(X(t)||t===0){return t}if(t>=20){return 1}if(t<=-20){return-1}return(Math.expm1(t)-Math.expm1(-t))/(F(t)+F(-t))},trunc:function trunc(e){var t=Number(e);return t<0?-_(-t):_(t)},imul:function imul(e,t){var r=se.ToUint32(e);var n=se.ToUint32(t);var o=r>>>16&65535;var i=r&65535;var a=n>>>16&65535;var u=n&65535;return i*u+(o*u+i*a<<16>>>0)|0},fround:function fround(e){var t=Number(e);if(t===0||t===Infinity||t===-Infinity||X(t)){return t}var r=Z(t);var n=k(t);if(n<jr){return r*mr(n/jr/Or)*jr*Or}var o=(1+Or/Number.EPSILON)*n;var i=o-(o-n);if(i>wr||X(i)){return r*Infinity}return r*i}};b(Math,Pr);h(Math,"log1p",Pr.log1p,Math.log1p(-1e-17)!==-1e-17);h(Math,"asinh",Pr.asinh,Math.asinh(-1e7)!==-Math.asinh(1e7));h(Math,"tanh",Pr.tanh,Math.tanh(-2e-17)!==-2e-17);h(Math,"acosh",Pr.acosh,Math.acosh(Number.MAX_VALUE)===Infinity);h(Math,"cbrt",Pr.cbrt,Math.abs(1-Math.cbrt(1e-300)/1e-100)/Number.EPSILON>8);h(Math,"sinh",Pr.sinh,Math.sinh(-2e-17)!==-2e-17);var Cr=Math.expm1(10);h(Math,"expm1",Pr.expm1,Cr>22025.465794806718||Cr<22025.465794806718);var Mr=Math.round;var xr=Math.round(.5-Number.EPSILON/4)===0&&Math.round(-.5+Number.EPSILON/3.99)===1;var Nr=dr+1;var Ar=2*dr-1;var Rr=[Nr,Ar].every(function(e){return Math.round(e)===e});h(Math,"round",function round(e){var t=_(e);var r=t===-1?-0:t+1;
return e-t<.5?t:r},!xr||!Rr);m.preserveToString(Math.round,Mr);var _r=Math.imul;if(Math.imul(4294967295,5)!==-5){Math.imul=Pr.imul;m.preserveToString(Math.imul,_r)}if(Math.imul.length!==2){re(Math,"imul",function imul(e,t){return se.Call(_r,Math,arguments)})}var kr=function(){var e=S.setTimeout;if(typeof e!=="function"&&typeof e!=="object"){return}se.IsPromise=function(e){if(!se.TypeIsObject(e)){return false}if(typeof e._promise==="undefined"){return false}return true};var r=function(e){if(!se.IsConstructor(e)){throw new TypeError("Bad promise constructor")}var t=this;var r=function(e,r){if(t.resolve!==void 0||t.reject!==void 0){throw new TypeError("Bad Promise implementation!")}t.resolve=e;t.reject=r};t.resolve=void 0;t.reject=void 0;t.promise=new e(r);if(!(se.IsCallable(t.resolve)&&se.IsCallable(t.reject))){throw new TypeError("Bad promise constructor")}};var n;if(typeof window!=="undefined"&&se.IsCallable(window.postMessage)){n=function(){var e=[];var t="zero-timeout-message";var r=function(r){M(e,r);window.postMessage(t,"*")};var n=function(r){if(r.source===window&&r.data===t){r.stopPropagation();if(e.length===0){return}var n=N(e);n()}};window.addEventListener("message",n,true);return r}}var o=function(){var e=S.Promise;var t=e&&e.resolve&&e.resolve();return t&&function(e){return t.then(e)}};var i=se.IsCallable(S.setImmediate)?S.setImmediate:typeof process==="object"&&process.nextTick?process.nextTick:o()||(se.IsCallable(n)?n():function(t){e(t,0)});var a=function(e){return e};var u=function(e){throw e};var f=0;var s=1;var c=2;var l=0;var p=1;var v=2;var y={};var h=function(e,t,r){i(function(){g(e,t,r)})};var g=function(e,t,r){var n,o;if(t===y){return e(r)}try{n=e(r);o=t.resolve}catch(e){n=e;o=t.reject}o(n)};var d=function(e,t){var r=e._promise;var n=r.reactionLength;if(n>0){h(r.fulfillReactionHandler0,r.reactionCapability0,t);r.fulfillReactionHandler0=void 0;r.rejectReactions0=void 0;r.reactionCapability0=void 0;if(n>1){for(var o=1,i=0;o<n;o++,i+=3){h(r[i+l],r[i+v],t);e[i+l]=void 0;e[i+p]=void 0;e[i+v]=void 0}}}r.result=t;r.state=s;r.reactionLength=0};var m=function(e,t){var r=e._promise;var n=r.reactionLength;if(n>0){h(r.rejectReactionHandler0,r.reactionCapability0,t);r.fulfillReactionHandler0=void 0;r.rejectReactions0=void 0;r.reactionCapability0=void 0;if(n>1){for(var o=1,i=0;o<n;o++,i+=3){h(r[i+p],r[i+v],t);e[i+l]=void 0;e[i+p]=void 0;e[i+v]=void 0}}}r.result=t;r.state=c;r.reactionLength=0};var O=function(e){var t=false;var r=function(r){var n;if(t){return}t=true;if(r===e){return m(e,new TypeError("Self resolution"))}if(!se.TypeIsObject(r)){return d(e,r)}try{n=r.then}catch(t){return m(e,t)}if(!se.IsCallable(n)){return d(e,r)}i(function(){j(e,r,n)})};var n=function(r){if(t){return}t=true;return m(e,r)};return{resolve:r,reject:n}};var w=function(e,r,n,o){if(e===I){t(e,r,n,o,y)}else{t(e,r,n,o)}};var j=function(e,t,r){var n=O(e);var o=n.resolve;var i=n.reject;try{w(r,t,o,i)}catch(e){i(e)}};var T,I;var E=function(){var e=function Promise(t){if(!(this instanceof e)){throw new TypeError('Constructor Promise requires "new"')}if(this&&this._promise){throw new TypeError("Bad construction")}if(!se.IsCallable(t)){throw new TypeError("not a valid resolver")}var r=Ne(this,e,T,{_promise:{result:void 0,state:f,reactionLength:0,fulfillReactionHandler0:void 0,rejectReactionHandler0:void 0,reactionCapability0:void 0}});var n=O(r);var o=n.reject;try{t(n.resolve,o)}catch(e){o(e)}return r};return e}();T=E.prototype;var P=function(e,t,r,n){var o=false;return function(i){if(o){return}o=true;t[e]=i;if(--n.count===0){var a=r.resolve;a(t)}}};var C=function(e,t,r){var n=e.iterator;var o=[];var i={count:1};var a,u;var f=0;while(true){try{a=se.IteratorStep(n);if(a===false){e.done=true;break}u=a.value}catch(t){e.done=true;throw t}o[f]=void 0;var s=t.resolve(u);var c=P(f,o,r,i);i.count+=1;w(s.then,s,c,r.reject);f+=1}if(--i.count===0){var l=r.resolve;l(o)}return r.promise};var x=function(e,t,r){var n=e.iterator;var o,i,a;while(true){try{o=se.IteratorStep(n);if(o===false){e.done=true;break}i=o.value}catch(t){e.done=true;throw t}a=t.resolve(i);w(a.then,a,r.resolve,r.reject)}return r.promise};b(E,{all:function all(e){var t=this;if(!se.TypeIsObject(t)){throw new TypeError("Promise is not object")}var n=new r(t);var o,i;try{o=se.GetIterator(e);i={iterator:o,done:false};return C(i,t,n)}catch(e){var a=e;if(i&&!i.done){try{se.IteratorClose(o,true)}catch(e){a=e}}var u=n.reject;u(a);return n.promise}},race:function race(e){var t=this;if(!se.TypeIsObject(t)){throw new TypeError("Promise is not object")}var n=new r(t);var o,i;try{o=se.GetIterator(e);i={iterator:o,done:false};return x(i,t,n)}catch(e){var a=e;if(i&&!i.done){try{se.IteratorClose(o,true)}catch(e){a=e}}var u=n.reject;u(a);return n.promise}},reject:function reject(e){var t=this;if(!se.TypeIsObject(t)){throw new TypeError("Bad promise constructor")}var n=new r(t);var o=n.reject;o(e);return n.promise},resolve:function resolve(e){var t=this;if(!se.TypeIsObject(t)){throw new TypeError("Bad promise constructor")}if(se.IsPromise(e)){var n=e.constructor;if(n===t){return e}}var o=new r(t);var i=o.resolve;i(e);return o.promise}});b(T,{catch:function(e){return this.then(null,e)},then:function then(e,t){var n=this;if(!se.IsPromise(n)){throw new TypeError("not a promise")}var o=se.SpeciesConstructor(n,E);var i;var b=arguments.length>2&&arguments[2]===y;if(b&&o===E){i=y}else{i=new r(o)}var g=se.IsCallable(e)?e:a;var d=se.IsCallable(t)?t:u;var m=n._promise;var O;if(m.state===f){if(m.reactionLength===0){m.fulfillReactionHandler0=g;m.rejectReactionHandler0=d;m.reactionCapability0=i}else{var w=3*(m.reactionLength-1);m[w+l]=g;m[w+p]=d;m[w+v]=i}m.reactionLength+=1}else if(m.state===s){O=m.result;h(g,i,O)}else if(m.state===c){O=m.result;h(d,i,O)}else{throw new TypeError("unexpected Promise state")}return i.promise}});y=new r(E);I=T.then;return E}();if(S.Promise){delete S.Promise.accept;delete S.Promise.defer;delete S.Promise.prototype.chain}if(typeof kr==="function"){b(S,{Promise:kr});var Fr=w(S.Promise,function(e){return e.resolve(42).then(function(){})instanceof e});var Lr=!i(function(){S.Promise.reject(42).then(null,5).then(null,W)});var Dr=i(function(){S.Promise.call(3,W)});var zr=function(e){var t=e.resolve(5);t.constructor={};var r=e.resolve(t);try{r.then(null,W).then(null,W)}catch(e){return true}return t===r}(S.Promise);var qr=s&&function(){var e=0;var t=Object.defineProperty({},"then",{get:function(){e+=1}});Promise.resolve(t);return e===1}();var Wr=function BadResolverPromise(e){var t=new Promise(e);e(3,function(){});this.then=t.then;this.constructor=BadResolverPromise};Wr.prototype=Promise.prototype;Wr.all=Promise.all;var Gr=a(function(){return!!Wr.all([1,2])});if(!Fr||!Lr||!Dr||zr||!qr||Gr){Promise=kr;re(S,"Promise",kr)}if(Promise.all.length!==1){var Hr=Promise.all;re(Promise,"all",function all(e){return se.Call(Hr,this,arguments)})}if(Promise.race.length!==1){var Vr=Promise.race;re(Promise,"race",function race(e){return se.Call(Vr,this,arguments)})}if(Promise.resolve.length!==1){var Br=Promise.resolve;re(Promise,"resolve",function resolve(e){return se.Call(Br,this,arguments)})}if(Promise.reject.length!==1){var Ur=Promise.reject;re(Promise,"reject",function reject(e){return se.Call(Ur,this,arguments)})}Ct(Promise,"all");Ct(Promise,"race");Ct(Promise,"resolve");Ct(Promise,"reject");Pe(Promise)}var $r=function(e){var t=n(p(e,function(e,t){e[t]=true;return e},{}));return e.join(":")===t.join(":")};var Jr=$r(["z","a","bb"]);var Xr=$r(["z",1,"a","3",2]);if(s){var Kr=function fastkey(e,t){if(!t&&!Jr){return null}if(fe(e)){return"^"+se.ToString(e)}else if(typeof e==="string"){return"$"+e}else if(typeof e==="number"){if(!Xr){return"n"+e}return e}else if(typeof e==="boolean"){return"b"+e}return null};var Zr=function emptyObject(){return Object.create?Object.create(null):{}};var Yr=function addIterableToMap(e,n,o){if(r(o)||te.string(o)){l(o,function(e){if(!se.TypeIsObject(e)){throw new TypeError("Iterator value "+e+" is not an entry object")}n.set(e[0],e[1])})}else if(o instanceof e){t(e.prototype.forEach,o,function(e,t){n.set(t,e)})}else{var i,a;if(!fe(o)){a=n.set;if(!se.IsCallable(a)){throw new TypeError("bad map")}i=se.GetIterator(o)}if(typeof i!=="undefined"){while(true){var u=se.IteratorStep(i);if(u===false){break}var f=u.value;try{if(!se.TypeIsObject(f)){throw new TypeError("Iterator value "+f+" is not an entry object")}t(a,n,f[0],f[1])}catch(e){se.IteratorClose(i,true);throw e}}}}};var Qr=function addIterableToSet(e,n,o){if(r(o)||te.string(o)){l(o,function(e){n.add(e)})}else if(o instanceof e){t(e.prototype.forEach,o,function(e){n.add(e)})}else{var i,a;if(!fe(o)){a=n.add;if(!se.IsCallable(a)){throw new TypeError("bad set")}i=se.GetIterator(o)}if(typeof i!=="undefined"){while(true){var u=se.IteratorStep(i);if(u===false){break}var f=u.value;try{t(a,n,f)}catch(e){se.IteratorClose(i,true);throw e}}}}};var en={Map:function(){var e={};var r=function MapEntry(e,t){this.key=e;this.value=t;this.next=null;this.prev=null};r.prototype.isRemoved=function isRemoved(){return this.key===e};var n=function isMap(e){return!!e._es6map};var o=function requireMapSlot(e,t){if(!se.TypeIsObject(e)||!n(e)){throw new TypeError("Method Map.prototype."+t+" called on incompatible receiver "+se.ToString(e))}};var i=function MapIterator(e,t){o(e,"[[MapIterator]]");this.head=e._head;this.i=this.head;this.kind=t};i.prototype={next:function next(){var e=this.i;var t=this.kind;var r=this.head;if(typeof this.i==="undefined"){return Xe()}while(e.isRemoved()&&e!==r){e=e.prev}var n;while(e.next!==r){e=e.next;if(!e.isRemoved()){if(t==="key"){n=e.key}else if(t==="value"){n=e.value}else{n=[e.key,e.value]}this.i=e;return Xe(n)}}this.i=void 0;return Xe()}};Ce(i.prototype);var a;var u=function Map(){if(!(this instanceof Map)){throw new TypeError('Constructor Map requires "new"')}if(this&&this._es6map){throw new TypeError("Bad construction")}var e=Ne(this,Map,a,{_es6map:true,_head:null,_map:G?new G:null,_size:0,_storage:Zr()});var t=new r(null,null);t.next=t.prev=t;e._head=t;if(arguments.length>0){Yr(Map,e,arguments[0])}return e};a=u.prototype;m.getter(a,"size",function(){if(typeof this._size==="undefined"){throw new TypeError("size method called on incompatible Map")}return this._size});b(a,{get:function get(e){o(this,"get");var t;var r=Kr(e,true);if(r!==null){t=this._storage[r];if(t){return t.value}else{return}}if(this._map){t=V.call(this._map,e);if(t){return t.value}else{return}}var n=this._head;var i=n;while((i=i.next)!==n){if(se.SameValueZero(i.key,e)){return i.value}}},has:function has(e){o(this,"has");var t=Kr(e,true);if(t!==null){return typeof this._storage[t]!=="undefined"}if(this._map){return B.call(this._map,e)}var r=this._head;var n=r;while((n=n.next)!==r){if(se.SameValueZero(n.key,e)){return true}}return false},set:function set(e,t){o(this,"set");var n=this._head;var i=n;var a;var u=Kr(e,true);if(u!==null){if(typeof this._storage[u]!=="undefined"){this._storage[u].value=t;return this}else{a=this._storage[u]=new r(e,t);i=n.prev}}else if(this._map){if(B.call(this._map,e)){V.call(this._map,e).value=t}else{a=new r(e,t);U.call(this._map,e,a);i=n.prev}}while((i=i.next)!==n){if(se.SameValueZero(i.key,e)){i.value=t;return this}}a=a||new r(e,t);if(se.SameValue(-0,e)){a.key=+0}a.next=this._head;a.prev=this._head.prev;a.prev.next=a;a.next.prev=a;this._size+=1;return this},delete:function(t){o(this,"delete");var r=this._head;var n=r;var i=Kr(t,true);if(i!==null){if(typeof this._storage[i]==="undefined"){return false}n=this._storage[i].prev;delete this._storage[i]}else if(this._map){if(!B.call(this._map,t)){return false}n=V.call(this._map,t).prev;H.call(this._map,t)}while((n=n.next)!==r){if(se.SameValueZero(n.key,t)){n.key=e;n.value=e;n.prev.next=n.next;n.next.prev=n.prev;this._size-=1;return true}}return false},clear:function clear(){o(this,"clear");this._map=G?new G:null;this._size=0;this._storage=Zr();var t=this._head;var r=t;var n=r.next;while((r=n)!==t){r.key=e;r.value=e;n=r.next;r.next=r.prev=t}t.next=t.prev=t},keys:function keys(){o(this,"keys");return new i(this,"key")},values:function values(){o(this,"values");return new i(this,"value")},entries:function entries(){o(this,"entries");return new i(this,"key+value")},forEach:function forEach(e){o(this,"forEach");var r=arguments.length>1?arguments[1]:null;var n=this.entries();for(var i=n.next();!i.done;i=n.next()){if(r){t(e,r,i.value[1],i.value[0],this)}else{e(i.value[1],i.value[0],this)}}}});Ce(a,a.entries);return u}(),Set:function(){var e=function isSet(e){return e._es6set&&typeof e._storage!=="undefined"};var r=function requireSetSlot(t,r){if(!se.TypeIsObject(t)||!e(t)){throw new TypeError("Set.prototype."+r+" called on incompatible receiver "+se.ToString(t))}};var o;var i=function Set(){if(!(this instanceof Set)){throw new TypeError('Constructor Set requires "new"')}if(this&&this._es6set){throw new TypeError("Bad construction")}var e=Ne(this,Set,o,{_es6set:true,"[[SetData]]":null,_storage:Zr()});if(!e._es6set){throw new TypeError("bad set")}if(arguments.length>0){Qr(Set,e,arguments[0])}return e};o=i.prototype;var a=function(e){var t=e;if(t==="^null"){return null}else if(t==="^undefined"){return void 0}else{var r=t.charAt(0);if(r==="$"){return C(t,1)}else if(r==="n"){return+C(t,1)}else if(r==="b"){return t==="btrue"}}return+t};var u=function ensureMap(e){if(!e["[[SetData]]"]){var t=new en.Map;e["[[SetData]]"]=t;l(n(e._storage),function(e){var r=a(e);t.set(r,r)});e["[[SetData]]"]=t}e._storage=null};m.getter(i.prototype,"size",function(){r(this,"size");if(this._storage){return n(this._storage).length}u(this);return this["[[SetData]]"].size});b(i.prototype,{has:function has(e){r(this,"has");var t;if(this._storage&&(t=Kr(e))!==null){return!!this._storage[t]}u(this);return this["[[SetData]]"].has(e)},add:function add(e){r(this,"add");var t;if(this._storage&&(t=Kr(e))!==null){this._storage[t]=true;return this}u(this);this["[[SetData]]"].set(e,e);return this},delete:function(e){r(this,"delete");var t;if(this._storage&&(t=Kr(e))!==null){var n=z(this._storage,t);return delete this._storage[t]&&n}u(this);return this["[[SetData]]"]["delete"](e)},clear:function clear(){r(this,"clear");if(this._storage){this._storage=Zr()}if(this["[[SetData]]"]){this["[[SetData]]"].clear()}},values:function values(){r(this,"values");u(this);return this["[[SetData]]"].values()},entries:function entries(){r(this,"entries");u(this);return this["[[SetData]]"].entries()},forEach:function forEach(e){r(this,"forEach");var n=arguments.length>1?arguments[1]:null;var o=this;u(o);this["[[SetData]]"].forEach(function(r,i){if(n){t(e,n,i,i,o)}else{e(i,i,o)}})}});h(i.prototype,"keys",i.prototype.values,true);Ce(i.prototype,i.prototype.values);return i}()};if(S.Map||S.Set){var tn=a(function(){return new Map([[1,2]]).get(1)===2});if(!tn){S.Map=function Map(){if(!(this instanceof Map)){throw new TypeError('Constructor Map requires "new"')}var e=new G;if(arguments.length>0){Yr(Map,e,arguments[0])}delete e.constructor;Object.setPrototypeOf(e,S.Map.prototype);return e};S.Map.prototype=O(G.prototype);h(S.Map.prototype,"constructor",S.Map,true);m.preserveToString(S.Map,G)}var rn=new Map;var nn=function(){var e=new Map([[1,0],[2,0],[3,0],[4,0]]);e.set(-0,e);return e.get(0)===e&&e.get(-0)===e&&e.has(0)&&e.has(-0)}();var on=rn.set(1,2)===rn;if(!nn||!on){re(Map.prototype,"set",function set(e,r){t(U,this,e===0?0:e,r);return this})}if(!nn){b(Map.prototype,{get:function get(e){return t(V,this,e===0?0:e)},has:function has(e){return t(B,this,e===0?0:e)}},true);m.preserveToString(Map.prototype.get,V);m.preserveToString(Map.prototype.has,B)}var an=new Set;var un=function(e){e["delete"](0);e.add(-0);return!e.has(0)}(an);var fn=an.add(1)===an;if(!un||!fn){var sn=Set.prototype.add;Set.prototype.add=function add(e){t(sn,this,e===0?0:e);return this};m.preserveToString(Set.prototype.add,sn)}if(!un){var cn=Set.prototype.has;Set.prototype.has=function has(e){return t(cn,this,e===0?0:e)};m.preserveToString(Set.prototype.has,cn);var ln=Set.prototype["delete"];Set.prototype["delete"]=function SetDelete(e){return t(ln,this,e===0?0:e)};m.preserveToString(Set.prototype["delete"],ln)}var pn=w(S.Map,function(e){var t=new e([]);t.set(42,42);return t instanceof e});var vn=Object.setPrototypeOf&&!pn;var yn=function(){try{return!(S.Map()instanceof S.Map)}catch(e){return e instanceof TypeError}}();if(S.Map.length!==0||vn||!yn){S.Map=function Map(){if(!(this instanceof Map)){throw new TypeError('Constructor Map requires "new"')}var e=new G;if(arguments.length>0){Yr(Map,e,arguments[0])}delete e.constructor;Object.setPrototypeOf(e,Map.prototype);return e};S.Map.prototype=G.prototype;h(S.Map.prototype,"constructor",S.Map,true);m.preserveToString(S.Map,G)}var hn=w(S.Set,function(e){var t=new e([]);t.add(42,42);return t instanceof e});var bn=Object.setPrototypeOf&&!hn;var gn=function(){try{return!(S.Set()instanceof S.Set)}catch(e){return e instanceof TypeError}}();if(S.Set.length!==0||bn||!gn){var dn=S.Set;S.Set=function Set(){if(!(this instanceof Set)){throw new TypeError('Constructor Set requires "new"')}var e=new dn;if(arguments.length>0){Qr(Set,e,arguments[0])}delete e.constructor;Object.setPrototypeOf(e,Set.prototype);return e};S.Set.prototype=dn.prototype;h(S.Set.prototype,"constructor",S.Set,true);m.preserveToString(S.Set,dn)}var mn=new S.Map;var On=!a(function(){return mn.keys().next().done});if(typeof S.Map.prototype.clear!=="function"||(new S.Set).size!==0||mn.size!==0||typeof S.Map.prototype.keys!=="function"||typeof S.Set.prototype.keys!=="function"||typeof S.Map.prototype.forEach!=="function"||typeof S.Set.prototype.forEach!=="function"||u(S.Map)||u(S.Set)||typeof mn.keys().next!=="function"||On||!pn){b(S,{Map:en.Map,Set:en.Set},true)}if(S.Set.prototype.keys!==S.Set.prototype.values){h(S.Set.prototype,"keys",S.Set.prototype.values,true)}Ce(Object.getPrototypeOf((new S.Map).keys()));Ce(Object.getPrototypeOf((new S.Set).keys()));if(c&&S.Set.prototype.has.name!=="has"){var wn=S.Set.prototype.has;re(S.Set.prototype,"has",function has(e){return t(wn,this,e)})}}b(S,en);Pe(S.Map);Pe(S.Set)}var jn=function throwUnlessTargetIsObject(e){if(!se.TypeIsObject(e)){throw new TypeError("target must be an object")}};var Sn={apply:function apply(){return se.Call(se.Call,null,arguments)},construct:function construct(e,t){if(!se.IsConstructor(e)){throw new TypeError("First argument must be a constructor.")}var r=arguments.length>2?arguments[2]:e;if(!se.IsConstructor(r)){throw new TypeError("new.target must be a constructor.")}return se.Construct(e,t,r,"internal")},deleteProperty:function deleteProperty(e,t){jn(e);if(s){var r=Object.getOwnPropertyDescriptor(e,t);if(r&&!r.configurable){return false}}return delete e[t]},has:function has(e,t){jn(e);return t in e}};if(Object.getOwnPropertyNames){Object.assign(Sn,{ownKeys:function ownKeys(e){jn(e);var t=Object.getOwnPropertyNames(e);if(se.IsCallable(Object.getOwnPropertySymbols)){x(t,Object.getOwnPropertySymbols(e))}return t}})}var Tn=function ConvertExceptionToBoolean(e){return!i(e)};if(Object.preventExtensions){Object.assign(Sn,{isExtensible:function isExtensible(e){jn(e);return Object.isExtensible(e)},preventExtensions:function preventExtensions(e){jn(e);return Tn(function(){Object.preventExtensions(e)})}})}if(s){var In=function get(e,t,r){var n=Object.getOwnPropertyDescriptor(e,t);if(!n){var o=Object.getPrototypeOf(e);if(o===null){return void 0}return In(o,t,r)}if("value"in n){return n.value}if(n.get){return se.Call(n.get,r)}return void 0};var En=function set(e,r,n,o){var i=Object.getOwnPropertyDescriptor(e,r);if(!i){var a=Object.getPrototypeOf(e);if(a!==null){return En(a,r,n,o)}i={value:void 0,writable:true,enumerable:true,configurable:true}}if("value"in i){if(!i.writable){return false}if(!se.TypeIsObject(o)){return false}var u=Object.getOwnPropertyDescriptor(o,r);if(u){return ie.defineProperty(o,r,{value:n})}else{return ie.defineProperty(o,r,{value:n,writable:true,enumerable:true,configurable:true})}}if(i.set){t(i.set,o,n);return true}return false};Object.assign(Sn,{defineProperty:function defineProperty(e,t,r){jn(e);return Tn(function(){Object.defineProperty(e,t,r)})},getOwnPropertyDescriptor:function getOwnPropertyDescriptor(e,t){jn(e);return Object.getOwnPropertyDescriptor(e,t)},get:function get(e,t){jn(e);var r=arguments.length>2?arguments[2]:e;return In(e,t,r)},set:function set(e,t,r){jn(e);var n=arguments.length>3?arguments[3]:e;return En(e,t,r,n)}})}if(Object.getPrototypeOf){var Pn=Object.getPrototypeOf;Sn.getPrototypeOf=function getPrototypeOf(e){jn(e);return Pn(e)}}if(Object.setPrototypeOf&&Sn.getPrototypeOf){var Cn=function(e,t){var r=t;while(r){if(e===r){return true}r=Sn.getPrototypeOf(r)}return false};Object.assign(Sn,{setPrototypeOf:function setPrototypeOf(e,t){jn(e);if(t!==null&&!se.TypeIsObject(t)){throw new TypeError("proto must be an object or null")}if(t===ie.getPrototypeOf(e)){return true}if(ie.isExtensible&&!ie.isExtensible(e)){return false}if(Cn(e,t)){return false}Object.setPrototypeOf(e,t);return true}})}var Mn=function(e,t){if(!se.IsCallable(S.Reflect[e])){h(S.Reflect,e,t)}else{var r=a(function(){S.Reflect[e](1);S.Reflect[e](NaN);S.Reflect[e](true);return true});if(r){re(S.Reflect,e,t)}}};Object.keys(Sn).forEach(function(e){Mn(e,Sn[e])});var xn=S.Reflect.getPrototypeOf;if(c&&xn&&xn.name!=="getPrototypeOf"){re(S.Reflect,"getPrototypeOf",function getPrototypeOf(e){return t(xn,S.Reflect,e)})}if(S.Reflect.setPrototypeOf){if(a(function(){S.Reflect.setPrototypeOf(1,{});return true})){re(S.Reflect,"setPrototypeOf",Sn.setPrototypeOf)}}if(S.Reflect.defineProperty){if(!a(function(){var e=!S.Reflect.defineProperty(1,"test",{value:1});var t=typeof Object.preventExtensions!=="function"||!S.Reflect.defineProperty(Object.preventExtensions({}),"test",{});return e&&t})){re(S.Reflect,"defineProperty",Sn.defineProperty)}}if(S.Reflect.construct){if(!a(function(){var e=function F(){};return S.Reflect.construct(function(){},[],e)instanceof e})){re(S.Reflect,"construct",Sn.construct)}}if(String(new Date(NaN))!=="Invalid Date"){var Nn=Date.prototype.toString;var An=function toString(){var e=+this;if(e!==e){return"Invalid Date"}return se.Call(Nn,this)};re(Date.prototype,"toString",An)}var Rn={anchor:function anchor(e){return se.CreateHTML(this,"a","name",e)},big:function big(){return se.CreateHTML(this,"big","","")},blink:function blink(){return se.CreateHTML(this,"blink","","")},bold:function bold(){return se.CreateHTML(this,"b","","")},fixed:function fixed(){return se.CreateHTML(this,"tt","","")},fontcolor:function fontcolor(e){return se.CreateHTML(this,"font","color",e)},fontsize:function fontsize(e){return se.CreateHTML(this,"font","size",e)},italics:function italics(){return se.CreateHTML(this,"i","","")},link:function link(e){return se.CreateHTML(this,"a","href",e)},small:function small(){return se.CreateHTML(this,"small","","")},strike:function strike(){return se.CreateHTML(this,"strike","","")},sub:function sub(){return se.CreateHTML(this,"sub","","")},sup:function sub(){return se.CreateHTML(this,"sup","","")}};l(Object.keys(Rn),function(e){var r=String.prototype[e];var n=false;if(se.IsCallable(r)){var o=t(r,"",' " ');var i=P([],o.match(/"/g)).length;n=o!==o.toLowerCase()||i>2}else{n=true}if(n){re(String.prototype,e,Rn[e])}});var _n=function(){if(!ne){return false}var e=typeof JSON==="object"&&typeof JSON.stringify==="function"?JSON.stringify:null;if(!e){return false}if(typeof e($())!=="undefined"){return true}if(e([$()])!=="[null]"){return true}var t={a:$()};t[$()]=true;if(e(t)!=="{}"){return true}return false}();var kn=a(function(){if(!ne){return true}return JSON.stringify(Object($()))==="{}"&&JSON.stringify([Object($())])==="[{}]"});if(_n||!kn){var Fn=JSON.stringify;re(JSON,"stringify",function stringify(e){if(typeof e==="symbol"){return}var n;if(arguments.length>1){n=arguments[1]}var o=[e];if(!r(n)){var i=se.IsCallable(n)?n:null;var a=function(e,r){var n=i?t(i,this,e,r):r;if(typeof n!=="symbol"){if(te.symbol(n)){return xt({})(n)}else{return n}}};o.push(a)}else{o.push(n)}if(arguments.length>2){o.push(arguments[2])}return Fn.apply(this,o)})}return S});
//# sourceMappingURL=es6-shim.map
;(function (i, s, o, g, r, a, m) {
	i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function ()
	{ (i[r].q = i[r].q || []).push(arguments) }
	, i[r].l = 1 * new Date(); a = s.createElement(o),
	m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)
})(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
ga('create', 'UA-41127645-1', 'auto');
ga('send', 'pageview', { 'page': location.pathname + location.search + location.hash });;//
// Copyright Kamil Pękala http://github.com/kamilkp
// Angular Virtual Scroll Repeat v1.1.7 2016/03/08
//
!function(a,b){"use strict";function c(){if("pageYOffset"in a)return{scrollTop:pageYOffset,scrollLeft:pageXOffset};var b,c,d=document,e=d.documentElement,f=d.body;return b=e.scrollLeft||f.scrollLeft||0,c=e.scrollTop||f.scrollTop||0,{scrollTop:c,scrollLeft:b}}function d(b,c){return b===a?"clientWidth"===c?a.innerWidth:a.innerHeight:b[c]}function e(b,d){return b===a?c()[d]:b[d]}function f(b,d,e){return b.getBoundingClientRect()[e?"left":"top"]-(d===a?0:d.getBoundingClientRect()[e?"left":"top"])+(d===a?c():d)[e?"scrollLeft":"scrollTop"]}var g=document.documentElement,h=g.matches?"matches":g.matchesSelector?"matchesSelector":g.webkitMatches?"webkitMatches":g.webkitMatchesSelector?"webkitMatchesSelector":g.msMatches?"msMatches":g.msMatchesSelector?"msMatchesSelector":g.mozMatches?"mozMatches":g.mozMatchesSelector?"mozMatchesSelector":null,i=b.element.prototype.closest||function(a){for(var c=this[0].parentNode;c!==document.documentElement&&null!=c&&!c[h](a);)c=c.parentNode;return c&&c[h](a)?b.element(c):b.element()},j=b.module("vs-repeat",[]).directive("vsRepeat",["$compile","$parse",function(c,g){return{restrict:"A",scope:!0,compile:function(h,j){var k,l,m,n,o,p,q=b.isDefined(j.vsRepeatContainer)?b.element(h[0].querySelector(j.vsRepeatContainer)):h,r=q.children().eq(0),s=r[0].outerHTML,t="$vs_collection",u=!1,v={vsRepeat:"elementSize",vsOffsetBefore:"offsetBefore",vsOffsetAfter:"offsetAfter",vsScrolledToEndOffset:"scrolledToEndOffset",vsScrolledToBeginningOffset:"scrolledToBeginningOffset",vsExcess:"excess"};if(r.attr("ng-repeat"))p="ng-repeat",k=r.attr("ng-repeat");else if(r.attr("data-ng-repeat"))p="data-ng-repeat",k=r.attr("data-ng-repeat");else if(r.attr("ng-repeat-start"))u=!0,p="ng-repeat-start",k=r.attr("ng-repeat-start");else{if(!r.attr("data-ng-repeat-start"))throw new Error("angular-vs-repeat: no ng-repeat directive on a child element");u=!0,p="data-ng-repeat-start",k=r.attr("data-ng-repeat-start")}if(l=/^\s*(\S+)\s+in\s+([\S\s]+?)(track\s+by\s+\S+)?$/.exec(k),m=l[1],n=l[2],o=l[3],u)for(var w=0,x=q.children().eq(0);null==x.attr("ng-repeat-end")&&null==x.attr("data-ng-repeat-end");)w++,x=q.children().eq(w),s+=x[0].outerHTML;return q.empty(),{pre:function(h,j,k){function l(){if(!G||G.length<1)h[t]=[],C=0,h.sizesCumulative=[0];else if(C=G.length,L){h.sizes=G.map(function(a){var c=h.$new(!1);b.extend(c,a),c[m]=a;var d=k.vsSize||k.vsSizeProperty?c.$eval(k.vsSize||k.vsSizeProperty):h.elementSize;return c.$destroy(),d});var a=0;h.sizesCumulative=h.sizes.map(function(b){var c=a;return a+=b,c}),h.sizesCumulative.push(a)}else q();y()}function q(){K&&h.$$postDigest(function(){if(D[0].offsetHeight||D[0].offsetWidth){for(var a=D.children(),b=0,c=!1,d=!1;b<a.length;){if(null!=a[b].attributes[p]||d){if(c||(h.elementSize=0),c=!0,a[b][P]&&(h.elementSize+=a[b][P]),!u)break;if(null!=a[b].attributes["ng-repeat-end"]||null!=a[b].attributes["data-ng-repeat-end"])break;d=!0}b++}c&&(y(),K=!1,h.$root&&!h.$root.$$phase&&h.$apply())}else var e=h.$watch(function(){(D[0].offsetHeight||D[0].offsetWidth)&&(e(),q())})})}function r(){var a="tr"===F?"":"min-";return H?a+"width":a+"height"}function w(){B()&&h.$digest()}function x(){void 0!==k.vsAutoresize&&(K=!0,q(),h.$root&&!h.$root.$$phase&&h.$apply()),B()&&h.$apply()}function y(){R=void 0,S=void 0,T=C,U=0,z(L?h.sizesCumulative[C]:h.elementSize*C),B(),h.$emit("vsRepeatReinitialized",h.startIndex,h.endIndex)}function z(a){h.totalSize=h.offsetBefore+a+h.offsetAfter}function A(){var a=d(M[0],O);a!==V&&(y(),h.$root&&!h.$root.$$phase&&h.$apply()),V=a}function B(){var a=e(M[0],Q),b=d(M[0],O),c=D[0]===M[0]?0:f(D[0],M[0],H),i=h.startIndex,j=h.endIndex;if(L){for(i=0;h.sizesCumulative[i]<a-h.offsetBefore-c;)i++;for(i>0&&i--,i=Math.max(Math.floor(i-h.excess/2),0),j=i;h.sizesCumulative[j]<a-h.offsetBefore-c+b;)j++;j=Math.min(Math.ceil(j+h.excess/2),C)}else i=Math.max(Math.floor((a-h.offsetBefore-c)/h.elementSize)-h.excess/2,0),j=Math.min(i+Math.ceil(b/h.elementSize)+h.excess,C);T=Math.min(i,T),U=Math.max(j,U),h.startIndex=N.latch?T:i,h.endIndex=N.latch?U:j,U<h.startIndex&&(h.startIndex=U);var l=!1;if(null==R?l=!0:null==S&&(l=!0),l||(N.hunked?Math.abs(h.startIndex-R)>=h.excess/2||0===h.startIndex&&0!==R?l=!0:(Math.abs(h.endIndex-S)>=h.excess/2||h.endIndex===C&&S!==C)&&(l=!0):l=h.startIndex!==R||h.endIndex!==S),l){h[t]=G.slice(h.startIndex,h.endIndex),h.$emit("vsRepeatInnerCollectionUpdated",h.startIndex,h.endIndex,R,S);var m;k.vsScrolledToEnd&&(m=G.length-(h.scrolledToEndOffset||0),(h.endIndex>=m&&S<m||G.length&&h.endIndex===G.length)&&h.$eval(k.vsScrolledToEnd)),k.vsScrolledToBeginning&&(m=h.scrolledToBeginningOffset||0,h.startIndex<=m&&R>h.startIndex&&h.$eval(k.vsScrolledToBeginning)),R=h.startIndex,S=h.endIndex;var n=L?"(sizesCumulative[$index + startIndex] + offsetBefore)":"(($index + startIndex) * elementSize + offsetBefore)",o=g(n),p=o(h,{$index:0}),q=o(h,{$index:h[t].length}),s=h.totalSize;I.css(r(),p+"px"),J.css(r(),s-q+"px")}return l}var C,D=b.isDefined(k.vsRepeatContainer)?b.element(j[0].querySelector(k.vsRepeatContainer)):j,E=b.element(s),F=E[0].tagName.toLowerCase(),G=[],H=void 0!==k.vsHorizontal,I=b.element("<"+F+' class="vs-repeat-before-content"></'+F+">"),J=b.element("<"+F+' class="vs-repeat-after-content"></'+F+">"),K=!k.vsRepeat,L=!!k.vsSize||!!k.vsSizeProperty,M=k.vsScrollParent?"window"===k.vsScrollParent?b.element(a):i.call(D,k.vsScrollParent):D,N="vsOptions"in k?h.$eval(k.vsOptions):{},O=H?"clientWidth":"clientHeight",P=H?"offsetWidth":"offsetHeight",Q=H?"scrollLeft":"scrollTop";if(h.totalSize=0,!("vsSize"in k)&&"vsSizeProperty"in k&&console.warn("vs-size-property attribute is deprecated. Please use vs-size attribute which also accepts angular expressions."),0===M.length)throw"Specified scroll parent selector did not match any element";h.$scrollParent=M,L&&(h.sizesCumulative=[]),h.elementSize=+k.vsRepeat||d(M[0],O)||50,h.offsetBefore=0,h.offsetAfter=0,h.excess=2,H?(I.css("height","100%"),J.css("height","100%")):(I.css("width","100%"),J.css("width","100%")),Object.keys(v).forEach(function(a){k[a]&&k.$observe(a,function(b){h[v[a]]=+b,y()})}),h.$watchCollection(n,function(a){G=a||[],l()}),E.eq(0).attr(p,m+" in "+t+(o?" "+o:"")),E.addClass("vs-repeat-repeated-element"),D.append(I),D.append(E),c(E)(h),D.append(J),h.startIndex=0,h.endIndex=0,M.on("scroll",w),b.element(a).on("resize",x),h.$on("$destroy",function(){b.element(a).off("resize",x),M.off("scroll",w)}),h.$on("vsRepeatTrigger",l),h.$on("vsRepeatResize",function(){K=!0,q()});var R,S,T,U;h.$on("vsRenderAll",function(){N.latch&&setTimeout(function(){var a=C;U=Math.max(a,U),h.endIndex=N.latch?U:a,h[t]=G.slice(h.startIndex,h.endIndex),S=h.endIndex,h.$$postDigest(function(){I.css(r(),0),J.css(r(),0)}),h.$apply(function(){h.$emit("vsRenderAllDone")})})});var V;h.$watch(function(){"function"==typeof a.requestAnimationFrame?a.requestAnimationFrame(A):A()})}}}}}]);"undefined"!=typeof module&&module.exports&&(module.exports=j.name)}(window,window.angular);;angular.module('localeze', ['ui.bootstrap', 'localezeFilters', 'localezeDirectives', 'localezeServices', 'localezeControllers', 'ngRoute', 'ngSanitize', 'ngAnimate', 'chart.js', 'vs-repeat'])
	.provider('httpErrorInterceptor', function HttpErrorInterceptor() {
		this.$get = [function httpErrorInterceptorFactory() {
			// register an http interceptor to message non-success http responses
			return ['$q', '$window', 'AlertQueue', function ($q, $window, AlertQueue) {
				function enqueue(e) {
					// ensure one notification per type at a time
					if (!AlertQueue.some(function (a) { return a.status == e.status; }))
						AlertQueue.push(e);
				}

				return {
					'responseError': function (rejection) {
						// don't decorate HEAD requests, which are inherently look-before-you-leap-y
						if (rejection.config.method != "HEAD")
						{
							if (rejection.status == 401) {
								if ($window.location.hash) {
									$window.location.href = "/manage/login?uri=" + encodeURIComponent($window.location.hash);
								}
								else {
									$window.location.href = "/manage/login?uri=" + encodeURIComponent($window.location.pathname);
								}
							} else if (rejection.status == 500) {
								enqueue({ status: 500, type: 'danger', title: '500', message: 'An error occurred.' });
							} else if (rejection.status == 404) {
								enqueue({ status: 404, type: 'warning', title: '404', message: "We couldn't find what you're looking for." });
							} else if (rejection.status == 403) {
								enqueue({ status: 403, type: 'warning', title: '403', message: "You don't have access to that resource." });
							}
						}

						return $q ? $q.reject(rejection) : rejection;
					}
				};
			}
			];
		}];
	})
	.config(['$httpProvider', 'httpErrorInterceptorProvider', '$locationProvider', function ($httpProvider, httpErrorInterceptorProvider, $locationProvider) {

		$locationProvider.html5Mode(false).hashPrefix('');

		httpErrorInterceptorProvider.$get.forEach(function (interceptorFn) {
			$httpProvider.interceptors.push(interceptorFn());
		});

		// register an http interceptor to indicate unresolved $http promises
		$httpProvider.interceptors.push([
			'$q',
			function ($q) {
				var counter = {
					element: $(document.getElementById('promise-indicator')),
					requestCount: 0,
					increment: function () {
						this.requestCount++;
						this.element.show();
					},
					decrement: function () {
						this.requestCount--;
						if (this.requestCount == 0)
							this.element.hide();
					}
				};

				return {
					'request': function (config) {
						counter.increment();
						return config;
					},
					'response': function (response) {
						counter.decrement();
						return response;
					},
					'responseError': function (rejection) {
						counter.decrement();
						return $q ? $q.reject(rejection) : rejection;
					}
				};
			}
		]);
	}])
	.config(['$modalProvider', function ($modalProvider) {
		var modalFactory = $modalProvider.$get[$modalProvider.$get.length - 1];

		$modalProvider.$get[$modalProvider.$get.length - 1] = function ($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack) {
			// $modal.open() likes you to supply a controller like
			//
			// {
			//		controller : function($foo, $bar) { ... }
			// }
			//
			// however script minification minifies the parameter names, breaking the injector.
			// we need to configure the injecor for each controller function as documented here:
			// 
			// https://docs.angularjs.org/tutorial/step_05
			//
			// instead of the above, use this syntax sugar to configure controllers just like you
			// would with any module (which is minification-safe):
			//
			// {
			//		controller: $modal.controller(['$scope', function($scope) { ... }])
			// }

			var modalInstance = modalFactory($injector, $rootScope, $q, $http, $templateCache, $controller, $modalStack);

			modalInstance.controller = function (_args) {
				var controllerFn = _args.pop();
				controllerFn.$inject = _args;
				return controllerFn;
			}

			return modalInstance;
		};
	}]);

angular.module('localezeServices', ['localezeResources'])
	.factory('QueryString', ['$window', function ($window) {
		var query = {
			all: function () {
				var q = {};
				window.location.search
					.replace('?', '')
					.split('&')
					.map(function (p) { return p.split('='); })
					.forEach(function (p) { q[p[0]] = p[1]; });
				return q;
			},
			hasKey: function (key) {
				return query.all().hasOwnProperty(key);
			},
			valueAt: function (key) {
				var value = query.all()[key];
				return value ? unescape(value) : value;
			}
		};
		return query;
	}]);

angular.module('localezeFilters', ['ngResource'])
	// test an object used as an angular filter to see if it would actually filter anything, i.e. has any truthy properties, e.g.
	//		{} | isEmpty => true
	//		{ foo : null } | isEmpty => true
	//		{ name : '' } | isEmpty => true
	//		{ isBlue : false } | isEmpty => false
	.filter('isEmpty', function () {
		var p;
		return function (filter) {
			for (p in filter) {
				if (
					filter[p] !== undefined &&
					filter[p] !== null &&
					filter[p] !== ''
				)
					return false;
			}
			return true;
		};
	})
	// when bound to a profile, computes its completeness score */
	.filter('score', ['ProfileScore', function(ProfileScore) {
		return function (profile) {
			return ProfileScore.get(profile);
		};
	}])
	// display filter for large numbers, e.g. 3015 => 3k
	.filter('shortcount', function () {
		return function (n) {
			var isNumeric = !isNaN(parseFloat(n)) && isFinite(n);
			if (!isNumeric) return n;
			if (n > 1000) return Math.floor(n / 1000) + "k";
			return n;
		}
	})
	.filter('tel', function () {
		// duplication here on directive tel
		var TEL_REGEXP = /^1?[\-\s]?\(?([2-9]\d{2})\)?[\-\s]?([2-9]\d{2})[\-\s]?(\d{4})$/;

		return function (tel) {
			if (!tel) { return ''; }

			var value = tel.toString().trim();

			if (!TEL_REGEXP.test(value)) {
				return value;
			}

			var matches = value.match(TEL_REGEXP);

			return "(" + matches[1] + ") " + matches[2] + "-" + matches[3];
		};
	})
	.filter('zip', function () {
		// duplication here on directive zip
		var ZIP_REGEXP = /^(\d{5})([\s\-]?(\d{4}))?$/;

		return function (zip) {
			if (!zip) { return ''; }

			var value = zip.toString().trim();

			if (!ZIP_REGEXP.test(value)) {
				return value;
			}

			var matches = value.match(ZIP_REGEXP);

			return matches[1] + (matches.length > 3 && matches[3] ? "-" + matches[3] : "");
		};
	})
	// {{ foo | truncate:3 }} -> limit foo to 3 characters
	.filter('truncate', function () {
		return function (text, length, end) {
			if (isNaN(length))
				length = 10;

			if (end === undefined)
				end = "…";

			if (!text) return text;

			if (text.length <= length || text.length - end.length <= length) {
				return text;
			}
			else {
				return String(text).substring(0, length - end.length) + end;
			}
		};
	})
	/* apply casing rules (sentence, title, or lower case) to an input string */
	.filter('changeCase', function() {
		return function (input, format) {
			input = input || '';
			switch (format) {
				case 'sentence': return input.charAt(0).toUpperCase() + input.slice(1);
				case 'lower': return input.toLowerCase();
				case 'title': return input.toLowerCase().split(' ').map(function (s) { return s.charAt(0).toUpperCase() + s.slice(1); }).join(' ');
				default: return input;
			}
		}
	})
	// [ 'a', 'b', 'c' ] | skip : 1 => [ 'b', 'c' ]
	.filter('skip', function () {
		return function (input, skipCount) {
			return (input || []).slice(skipCount);
		};
	});

angular.module('localezeDirectives', ['ngResource'])
	// counts from one number to another over a configured duration
	// see : https://github.com/sparkalow/angular-count-to
	.directive('countTo', ['$timeout', function ($timeout) {
		return {
			replace: false,
			scope: true,
			link: function (scope, element, attrs) {

				var e = element[0];
				var num, refreshInterval, duration, steps, step, countTo, value, increment;

				var calculate = function () {
					refreshInterval = 30;
					step = 0;
					scope.timoutId = null;
					countTo = parseInt(attrs.countTo) || 0;
					scope.value = parseInt(attrs.value, 10) || 0;
					duration = (parseFloat(attrs.duration) * 1000) || 0;

					steps = Math.ceil(duration / refreshInterval);
					num = parseInt(e.innerHTML) || 0;
					increment = ((countTo - num) / steps);
				}

				var tick = function () {
					scope.timoutId = $timeout(function () {
						num += increment;
						step++;
						if (step >= steps) {
							$timeout.cancel(scope.timoutId);
							num = countTo;
							e.innerHTML = countTo;
						} else {
							e.innerHTML = Math.round(num);
							tick();
						}
					}, refreshInterval);
				}

				var start = function () {
					if (scope.timoutId) {
						$timeout.cancel(scope.timoutId);
					}
					calculate();
					tick();
				}

				attrs.$observe('countTo', function (val) {
					if (val) start();
				});

				attrs.$observe('value', function (val) {
					start();
				});

				return true;
			}
		}
	}])
	// input is valid if its value is in the configured collection
	// syntax <input ng-model="model" in="collection" /> if the collection is strings
	// or
	// syntax <input ng-model="model" in="collection:keyName" /> if the collection is objects, with unique key named keyName
	.directive('in', function () {
		return {
			require: 'ngModel',
			link: function (scope, elm, attrs, ctrl) {
				var collection = attrs['in'].split(':')[0];
				var key = attrs['in'].split(":")[1];

				// make membership case-insensitive for strings (otherwise do nothing)
				function standardize(value) {
					if (typeof (value) == "string")
						return value.toLowerCase();
					return value;
				}

				function getItems() {
					if (!scope[collection]) return [];

					return scope[collection].map(function (e) {
						return standardize((key ? e[key] : e) || "");
					});
				}

				// in list, but not required means an empty value is valid
				var isRequired = elm[0].required;

				function onValueChanged(v) {
					var viewValue;
					switch (typeof (v)) {
						case "number": viewValue = v; break;
						case "string": viewValue = v; break;
						case "object": viewValue = v[key]; break;
						default: viewValue = ""; break;
					}

					if (getItems().indexOf(standardize(viewValue)) >= 0) {
						// it is valid
						ctrl.$setValidity('in', true);
						return viewValue;
					} else {
						// it is invalid
						ctrl.$setValidity('in', !viewValue && !isRequired);
						return viewValue;
					}
				}

				ctrl.$parsers.unshift(onValueChanged);

				// revalidate when the viewmodel updates
				scope.$watch(
					function () { return ctrl.$viewValue; },
					function (viewValue) { onValueChanged(viewValue); }
				);

				// ...or when the collection updates (e.g. REST data loads)
				scope.$watch(
					function () { return scope[collection]; },
					function (viewValue) { onValueChanged(ctrl.$viewValue); }
				);
			}
		};
	})
	/* prevent dropdown menus from toggling (closing) when clicked */
	.directive('noToggle', function () {
		return {
			restrict: 'A',
			link: function (scope, el, attrs, ctrl) {
				el.click(function (e) {
					e.stopPropagation();
				});
			}
		};
	})
	/* prevent click events from propagating beyond the decorated element */
	.directive('noPropagate', function () {
		return {
			restrict: 'A',
			link: function (scope, element, attr) {
				element.bind('click', function (e) {
					e.stopPropagation();
				});
			}
		};
	})
	.directive('zip', ['$filter', function ($filter) {
		var ZIP_REGEXP = /^(\d{5})([\s\-]?(\d{4}))?$/;
		return {
			require: 'ngModel',
			link: function (scope, elm, attrs, ctrl) {
				var isRequired = elm[0].required;	// is value required

				function onValueChanged(viewValue) {
					if (ZIP_REGEXP.test(viewValue)) {
						// it is valid
						ctrl.$setValidity('zip', true);
						return viewValue;
					} else {
						ctrl.$setValidity('zip', !viewValue && !isRequired);
						return '';	// return empty here to prevent the model from updating
					}
				}

				ctrl.$parsers.unshift(onValueChanged);

				// revalidate when the model updates
				scope.$watch(
					function () { return ctrl.$viewValue; },
					function (viewValue) { onValueChanged(viewValue); }
				);
			}
		};
	}])
	/* waitingOn : watches a resource associated with a view, such that
	   the 'loading' css class is appended to the view while the resource
	   is in an unresolved state.  this also means that subsequent requests
	   for the resource will reinvoke this directive's behavior. */
	.directive('waitingOn', function () {
		return {
			restrict: "A",
			link: function (scope, element, attrs) {

				// evaluates the property of obj indicated by an accessor string
				// e.g. _eval(foo, 'bar.baz') yields foo.bar.baz
				function _eval(obj, accessor) {
					if (obj == null) return null;
					var plist = accessor.split('.');
					if (plist.length == 1)
						return obj[accessor];
					else
						return _eval(
							obj[plist[0]],
							plist.slice(1).join('.')
						);
				};

				var css = 'loading';

				var target = _eval(scope, attrs.waitingOn);

				if (target != null && !target.$resolved) {
					element.addClass(css); // assume we're loading, initially
				}

				// kludgey work-around for resource.$resolved not being watchable for some reason
				var isPendingFn = function () {
					var resource = _eval(scope, attrs.waitingOn);
					var isPending =
						resource != null &&
						resource.$resolved == false;

					return isPending;
				}

				scope.$watch(isPendingFn, function (value) {
					var isPending = value;
					if (isPending) {
						element.addClass(css);
					} else {
						element.removeClass(css);
					}
				});
			}
		};
	})
	/* apply casing rules (sentence, title, or lower case) to an input's value */
	.directive('changeCase', ['changeCaseFilter', function (changeCaseFilter) {
		return {
			require: 'ngModel',
			link: function (scope, elem, attrs, ctrl) {
				var format = attrs['changeCase'];

				scope.$watch(
					function () { return ctrl.$viewValue; },
					function (viewValue) {
						var formatted = changeCaseFilter(viewValue, format);
						ctrl.$setViewValue(formatted);
						ctrl.$render();
					}
				);
			}
		};
	}])

	/* create an SVC arg representing a percentage 0..1 */
	.directive('arc', function () {
		return {
			restrict: "A",
			link: function (scope, element, attrs) {
				var value = scope.$eval(attrs['arcAmount']);

				if (value != undefined) {
					var width = 20,
						height = 20,
						τ = 2 * Math.PI; // http://tauday.com/tau-manifesto

					// An arc function with all values bound except the endAngle. So, to compute an
					// SVG path string for a given angle, we pass an object with an endAngle
					// property to the `arc` function, and it will return the corresponding string.
					var arc = d3.svg.arc()
						.innerRadius(4)
						.outerRadius(10)
						.startAngle(0);

					// Create the SVG container, and apply a transform such that the origin is the
					// center of the canvas. This way, we don't need to position arcs individually.
					var svg = d3.select(element[0]).append("svg")
						.attr("width", width)
						.attr("height", height)
					.append("g")
						.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");

					// Add the background arc, from 0 to 100% (τ).
					var background = svg.append("path")
						.datum({ endAngle: τ })
						.style("fill", "#ddd")
						.attr("d", arc);

					// Add the foreground arc in orange, currently showing 12.7%.
					var foreground = svg.append("path")
						.datum({ endAngle: value * τ })
					//	.style("fill",  value > .75 ? "#9fc75e" : "#fd8d00")
						.attr("class", "arc")
						.attr("d", arc);
				}
			}
		};
	})
	/* value of the bound input must be a csv list which is a subset of the supplied collection */
	.directive('subsetOf', function () {
		return {
			require: 'ngModel',
			link: function (scope, elm, attrs, ctrl) {
				// in list, but not required means an empty value is valid
				var isRequired = elm[0].required;

				function onValueChanged(viewValue) {
					var expected = (attrs['subsetOf'] || "").split(",").map(function (e) { return (e || "").trim().toLowerCase(); });
					var actual = (viewValue || "").split(",").map(function (e) { return (e || "").trim().toLowerCase(); });

					var isValid = true;
					actual.forEach(function (e) {
						if (e && expected.indexOf(e) == -1)
							isValid = false;
					});

					if (isValid) {
						ctrl.$setValidity('subsetOf', true);
						return viewValue;
					} else {
						ctrl.$setValidity('subsetOf', !viewValue && !isRequired);
						return viewValue;
					}
				}

				ctrl.$parsers.unshift(onValueChanged);

				// revalidate when the viewmodel updates
				scope.$watch(
					function () { return ctrl.$modelValue; },
					function (viewValue) { onValueChanged(viewValue); }
				);
			}
		};
	})
	/* output dynamic messaging about the completenss of a scored profile component (see ProfileScore) */
	.directive('profileComponentScore', ['ProfileScore', function (ProfileScore) {
		return {
			restrict: 'E',
			template:
				'<p ng-show="format != \'short\' && !hasComponent">\
					<i class="fa fa-minus-circle text-danger"></i>\
					Include your business\' {{ component }} <strong class="text-warning">to add {{ score }}% completeness</strong>\
				</p>\
				<p ng-show="format != \'short\' && hasComponent">\
					<i class="fa fa-check-circle text-primary"></i>\
					{{ component }} added for {{ score }}% completeness.\
				</p>\
				<span ng-show="format == \'short\' && !hasComponent" class="text-warning">\
					{{ score }}% Available\
				</span>\
				<span ng-show="format == \'short\' && hasComponent" class="text-success">\
					Complete\
				</span>',
			scope: {
				profile: '=profile'
			},
			link: function (scope, element, attrs) {
				scope.component = attrs['component'];
				scope.format = attrs['format'];
				//	var profile = scope[attrs['profile']];
				scope.score = ProfileScore[scope.component].value;
				scope.hasComponent = ProfileScore[scope.component].hasProperty(scope.profile);
				scope.$watch(
					function () { return ProfileScore[scope.component].hasProperty(scope.profile); },
					function (value) { scope.hasComponent = value; }
				);
			}
		};
	}])
	.directive('equals', function () {
		return {
			require: 'ngModel',
			link: function (scope, elem, attrs, ctrl) {
				var targetInput = attrs['equals'];

				// evaluates the property of obj indicated by an accessor string
				// e.g. _eval(foo, 'bar.baz') yields foo.bar.baz
				function _eval(obj, accessor) {
					if (obj == null) return null;
					var plist = accessor.split('.');
					if (plist.length == 1)
						return obj[accessor];
					else
						return _eval(
							obj[plist[0]],
							plist.slice(1).join('.')
						);
				};

				scope.$watch(
					function () { return ctrl.$viewValue; },
					function (viewValue) {
						var comparand = _eval(scope, targetInput + ".$viewValue");
						var areEqual = viewValue == comparand;
						ctrl.$setValidity('equals', areEqual);
						return areEqual ? viewValue : undefined;
					}
				);
			}
		};
	})
	// this value must compareTo some other value by way of an operation, e.g. <input ng-model="foo" compareTo="bar" op=">" />
	// TO DO : this is the super-set of the functionality of the 'equals' directive; consolidate
	.directive('compareTo', function () {
		return {
			require: 'ngModel',
			link: function (scope, elem, attrs, ctrl) {
				var targetInput = attrs['compareTo'];

				// evaluates the property of obj indicated by an accessor string
				// e.g. _eval(foo, 'bar.baz') yields foo.bar.baz
				function _eval(obj, accessor) {
					if (obj == null) return null;
					var plist = accessor.split('.');
					if (plist.length == 1)
						return obj[accessor];
					else
						return _eval(
							obj[plist[0]],
							plist.slice(1).join('.')
						);
				};

				scope.$watch(
					function () { return ctrl.$viewValue; },
					function (viewValue) {
						var comparand = _eval(scope, targetInput + ".$viewValue");
						if (!comparand) return undefined; // nothing to compare to

						var isValid = false;
						if (attrs['op'] == ">")
							isValid = viewValue > comparand;
						else if (attrs['op'] == ">=")
							isValid = viewValue >= comparand;
						else if (attrs['op'] == "<")
							isValid = viewValue < comparand;
						else if (attrs['op'] == "<=")
							isValid = viewValue <= comparand;
						else
							isValid = viewValue == comparand;
						ctrl.$setValidity('compareTo', isValid);
						return isValid ? viewValue : undefined;
					}
				);
			}
		};
	})
	/* color <input maxlength="n" /> with angular validation (different than ng-maxlength, which will empty the field if its too long */
	.directive('maxlength', function ($filter) {
		return {
			//require: 'ngModel',
			link: function (scope, elm, attrs, ctrl) {
				var len = Number(attrs['maxlength']);

				function onValueChanged(viewValue) {
					if (viewValue && viewValue.length > len) {
						// it is invalid: truncate
						elm[0].value = elm[0].value.slice(0, len);
					}
				}

				// revalidate when the model updates
				scope.$watch(
					function () { return elm[0].value; },
					function (viewValue) { onValueChanged(viewValue); }
				);
			}
		};
	});

angular.module('localezeControllers', ['localezeResources', 'localezeServices', 'ui.bootstrap', 'ngCookies'])
	.controller('CategoryCtrl', ['$scope', 'Categories', function ($scope, Categories) {
		$scope.categories = Categories.query();
	}])
	.controller('DefaultCtrl', ['$scope', '$location', '$anchorScroll', function ($scope, $location, $anchorScroll) {
		$scope.$on("$routeChangeStart", function (event, next, current) {
			$scope.routeChanging = true;
		});

		$scope.$on("$routeChangeSuccess", function (event, next, current) {
			$scope.routeChanging = false;
			$anchorScroll();	// reposition view to top on route change
			$scope.tab = $location.path().split("/")[1];
		});

		$scope.$on("$routeChangeError", function (event, next, current) {
			$scope.routeChanging = false;
		});
	}])
	.controller('DashboardCtrl', ['$scope', 'SubscriptionsAnalytics', 'Messages', 'DomainEvents', '$modal', function ($scope, SubscriptionsAnalytics, Messages, DomainEvents, $modal) {

	    $scope.stats = SubscriptionsAnalytics.get();
		$scope.messages = Messages.query();
		$scope.events = DomainEvents.query();
		
		$scope.showScoreInfo = function () {
			$modal.open({
				templateUrl: 'completeness-score.html',
				controller: $modal.controller(['$scope', '$modalInstance', function ($scope, $modalInstance) { $scope.dismiss = function () { $modalInstance.dismiss('cancel'); }; }]),
				resolve: {}
			});
		};
	}])
	.controller('ErrorCtrl', ['$scope', '$routeParams', '$location', function ($scope, $routeParams, $location) {
		$scope.code = $routeParams.code;
		$scope.uri = $location.search().uri;
	}])
	.controller('AggregatedEventsCtrl', ['$scope', 'AggregatedEvents', '$http', '$location', function ($scope, AggregatedEvents, $http, $location) {
	    $scope.orderByDate = true;
		$scope.filter = {
			machine: null,
			isError: true
		};

		$scope.setPage = function (page) {
			$scope.filter.page = page;
			$scope.matching = AggregatedEvents.query($scope.filter)
		}

		$scope.view = function (e) {
		    $scope.event = AggregatedEvents
				.query({ hash: e.hash }, function (data) { $scope.event = data[0]; });
		}

		// get event hash value from location hash, if any
		var eventHash;
		if ($location.$$absUrl.indexOf('#') != -1)
		    eventHash = $location.$$absUrl.split('#')[1].substr(1);

		if (eventHash && /^-?[0-9a-f]+$/.test(eventHash)) // test for valid event hash, which may be negative
			$scope.view({ hash: eventHash });
		else
			$scope.setPage(1);
	}])
	.filter('orderObjectBy', function () {
	    return function(items, field, reverse) {
	        var filtered = [];
	        angular.forEach(items, function(item) {
	            filtered.push(item);
	        });
	        filtered.sort(function (a, b) {
	            return (a[field] > b[field] ? 1 : -1);
	        });
	        if(reverse) filtered.reverse();
	        return filtered;
	    };
	})
	.controller('InboxCtrl', ['$scope', '$routeParams', 'Messages', function ($scope, $routeParams, Messages) {
		$scope.messageId = $routeParams.id;
		if ($scope.messageId)
			$scope.message = Messages.get({ id: $scope.messageId });
		else
			$scope.messages = Messages.query();
	}])
	.controller('SearchMenuCtrl', ['$scope', function($scope) { }])
	.controller('DirectorySearchCtrl', ['$scope', function ($scope) { }])
	// a locality (city, state) typeahead, attachable to any <input /> like
	//
	// <input ng-model="locality" locality-typeahead locality-typeahead-onselect="myCallback">
	//
	// the onselect callback is optional, but if supplied, must be defined in the parent context
	.directive('localityTypeahead', ['Localities', function (Localities) {
		return {
			restrict: 'A',
			replace: true,
			template: '<input type="text" typeahead="(item.Locality + \', \' + item.Region) for item in getLocation($viewValue)" typeahead-on-select="select($item, $model, $label, $event)" ng-model-options="{ debounce: 100 }" autocomplete="off">',
			link: function (scope, elm, attrs, ctrl) {
				scope.getLocation = function (prefix) {
					if (!prefix || /\d/.test(prefix)) return [];
					return Localities.query({ 'id': prefix }).$promise;
				};

				scope.select = null;
				if (typeof attrs['localityTypeaheadOnselect'] != "undefined") {
					scope.select = function ($item, $model, $label, $event) {
						scope[attrs['localityTypeaheadOnselect']]($item, $model, $label, $event);
					}
				}
			}
		};
	}])
	.controller('FormCtrl', ['$scope', '$window', function ($scope, $window) {
		// apply angular form validation to posting forms (as opposed to in-client forms)
		$scope.submit = function ($event, formName) {
			$event.preventDefault();
			var form = $scope[formName];
			form.$dirty = true;
			if (form.$valid)
				$window.document.forms[formName].submit();

			return form.$valid;
		}
	}])
	.controller('SubscriptionRenewalCtrl', ['$scope', function($scope) {
		$scope.setAutoRenew = function (subscription, isAutoRenew) {
			if (isAutoRenew)
				subscription.Renewal.$put({ id: subscription.id || subscription.ID }, function () {
					if( subscription.hasOwnProperty('IsAutoRenew') )
						subscription.IsAutoRenew = true;
					else
						subscription.claim.isAutoRenew = true;
				});
			else
				subscription.Renewal.$delete({ id: subscription.id || subscription.ID }, function () {
					if (subscription.hasOwnProperty('IsAutoRenew'))
						subscription.IsAutoRenew = false;
					else
						subscription.claim.isAutoRenew = false;
				});
		};
	}])
	.controller('AccountCtrl', ['$scope', 'Account', 'States', 'Users', function ($scope, Account, States, Users) {
	    $scope.states = States.query();
	    $scope.account = Account.get({ id: 'current' }, function (data)
	    {
	        $scope.account = data;
	        $scope.user = Users.get({ accountId: $scope.account.AccountId, userId: $scope.account.Username }, function (data)
	        {
	            $scope.user = data;
	        });
	    });

	    $scope.update = function () {
	        var postData = angular.copy($scope.user);
	        postData.companyId = $scope.account.CompanyId;
	        postData.contextId = $scope.account.AccountId;
	        postData.accountId = $scope.account.AccountId;
	        postData.userId = $scope.account.Username;

	        Users.put(
				postData,
				function (data, headers) {
				    if ($scope.account.Username != $scope.user.UserName) {
				        window.location = window.location.origin + "/manage/login/?end=";
				    } else {
				        location.reload();
				    }
				});
	    };

	    $scope.options = function () {
	        if ($scope.user.UserName) {
	            var result = Users.options({ companyId: $scope.account.CompanyId, accountId: $scope.account.AccountId, userId: $scope.user.UserName },
                    function (data, headers) {
                        var redirect = headers('allow');
                        var canPost = redirect && redirect.toLowerCase().search("post") != -1;
                        var canPut = redirect && redirect.toLowerCase().search("put") != -1;
                        if (canPost) {
                            $scope.user.UsernameExists = false;
                            $scope.accountForm.$valid = true;
                        }
                        else if (canPut) {
                            $scope.user.UsernameExists = false;
                            $scope.accountForm.$valid = true;
                        }
                        else {
                            $scope.user.UsernameExists = true;
                            $scope.accountForm.$valid = false;
                        }
                    },
                function (data, headers) {
                    $scope.user.UsernameExists = true;
                    $scope.accountForm.$valid = false;
                });
	        }
	        else {
	            $scope.accountForm.$valid = false;
	        }
	    };
	}])
	.controller('HelpCtrl', ['$scope', '$routeParams', '$http', function ($scope, $routeParams, $http) {
	    $http(
			{ method: 'GET', url: '/directory/articles' }
		).success(function (data, status, headers, config) {
		    $scope.index = data;

		    var id = $routeParams.id;

		    if (id) {
		        $scope.index.categories.forEach(function (e, i) {
		            e.items.forEach(function (n) {
		                if (n.id == id) $scope.item = n;
		            })
		        });
		    }

		    $scope.toUri = function (item) {
		    	if (item.id)
		    		return "#/help/" + item.id;
		    	else
		    		return item.url;
		    }
		});
	}]);
;angular.module('localeze')
    .directive('toggle', function () {
        return {
            scope: {
                value: '='
            },
            restrict: 'E',
            template: '<a href class="list-group-item" ng-click="click()">' +
                '<i class="fa fa-fw" ng-class="{ \'fa-square-o\' : !value, \'fa-check-square-o\' : value }"></i>' +
                '{{ label }}' +
                '</a>',
            link: function (scope, element, attr) {
                scope.label = attr['label'] || attr['value'].split('.').pop(); // use label if you've got it
                scope.click = function () { scope.value = !scope.value; };
            }
        };
	})
	// dynamically add classes 'has-error' to inputs contained in elements with this attribute
	.directive('showFeedback', function () {
		return {
			restrict: 'A',
			link: function (scope, el, attrs, ctrl) {
				scope = scope.$parent;
				
				scope.$watch(
					function () {
						// find the form field to watch (if any)
						if (!attrs.showFeedback)
							return;

						var form = attrs.showFeedback.split('.')[0];
						var field = attrs.showFeedback.split('.')[1];

						if (!scope[form] || !field)
							return; // nothing to watch

						// show warning feedback for invalid fields unless:
						// - new empty form
						// - field you haven't gotten around to interacting with yet
						var isIndicateError =
							scope[form][field].$invalid &&
							(scope[form][field].$dirty || !!scope[form][field].$viewValue);

						return isIndicateError;
					},
					function (hasError) {
						if (hasError)
							el.addClass('has-error');
						else
							el.removeClass('has-error');
					}
				);
			}
		}
	})
	/* listen to load events on an <img /> tag, adding a 'broken' class if the src points to a non-200 resource */
	.directive('validateImage', ['$filter', function ($filter) {
		return {
			link: function (scope, element, attrs) {
				element
					.bind('load', function () {
						$(element).parent().removeClass('broken');
					})
					.bind('error', function () {
						// note error() will fire for inappropriate MIME types
						$(element).parent().addClass('broken');
					});
			}
		};
	}])
    .directive('openingHours', function () {
        return {
            restrict: 'E',
            require: 'ngModel',
            scope: {
                // the model must be an object containing an array of schemaOrg.openingHours in a property named .openingHours
                // and an optional .dateRange = { startDate = ISO-8601, endDate = ISO-8601 }
                value: '=ngModel',
                form: '=',
                component: '='
            },
            template:
                '<div class="row" ng-repeat="day in daysOfWeek" ng-show="daysOfWeek[$index].isVisible">' +
                '<div class="col-xs-2">{{ day.name }}</div>' +

                '<div class="col-xs-10">' +
                '<div class="form-group" ng-show="value.openingHours[$parent.$index].length" ng-repeat="block in value.openingHours[$index]">' +
                '<div class="row">' +
                '<div class="col-xs-4"><time-of-day ng-model="block.starts" ng-change="onChanged()" /></div>' +
                '<div class="col-xs-4"><time-of-day ng-model="block.ends" ng-change="onChanged()" /></div>' +
                '<div class="col-xs-2"><button class="btn btn-link btn-sm" title="Remove Hours" ng-click="removeAt($parent.$index, $index)" type="button">Remove</button></div>' +
                '<div class="col-xs-1"><button class="btn btn-link btn-sm" title="Add Hours" ng-click="push($parent.$index)" ng-show="$index == 0 && value.openingHours[$parent.$index].length < 3" type="button">Add</button></div>' +
                '</div>' +
                '</div>' +
                '<div class="form-group" ng-show="!value.openingHours[$index].length">' +
                '<div class="row">' +
                '<div class="col-xs-8"><span class="text-muted">Closed</span></div>' +
                '<div class="col-xs-2"><button class="btn btn-link btn-sm" ng-if="value.openingHours[$index - 1].length" title="Copy previous day\'s hours" ng-click="copyPrevious($index)" type="button">Copy Last</button></div>' +
                '<div class="col-xs-1"><button class="btn btn-link btn-sm" title="Add Hours" ng-click="push($index)" type="button">Add</button></div>' +
                '</div>' +
                '</div>' +
                '</div>' +
                '</div>' +
                '<p class="text-danger" ng-show="form.$error[component]">{{ errorMessage }}</p>',
            link: function ($scope, element, attr, ngModelCtrl) {
                $scope.daysOfWeek = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"].map(function (e) {
                    return { name: e, isVisible: !$scope.value.dateRange }; // no date range? all are visible
                });

                // specify which days of week apply to the provided dateRange (if any)
                if ($scope.value.dateRange) {
                    // we convert this to utc day because with out knowing the timezone the date picker will incorrectly set the day; 
                    // Also we calculate the day is because on our display we are monday indexed and get utc day is sunday indexed
                    // also worth mentioning is the daterange returned by in the json is a string while the daterange generated by adding special hours a date object
                    var start = (new Date($scope.value.dateRange.startDate).getUTCDay() + 6) % 7;
                    var len = (new Date($scope.value.dateRange.endDate) - new Date($scope.value.dateRange.startDate)) / (24 * 60 * 60 * 1000);
                    for (var i = start; i <= start + len; i++)
                        $scope.daysOfWeek[i % 7].isVisible = 1;
                }

                // normalize the data, s.t. all days have hours (even if empty)
                for (var i = 0; $scope.value && i < 7; i++)
                    $scope.value.openingHours[i] = $scope.value.openingHours[i] || [];

                $scope.copyPrevious = function (day) {
                    if (day > 0 && $scope.value.openingHours[day - 1].length) {
                        $scope.value.openingHours[day] = angular.copy($scope.value.openingHours[day - 1]);
                    }
                    $scope.onChanged();
                }

                $scope.push = function (day) {
                    $scope.value.openingHours[day].push({
                        starts: { hour: "12:00", period: "am" },
                        ends: { hour: "12:00", period: "am" }
                    });
                    $scope.onChanged();
                };

                $scope.removeAt = function (day, index) {
                    $scope.value.openingHours[day].splice(index, 1);
                    $scope.onChanged();
                };

                // get the sort value of a time of day (i.e. 1pm > 12am etc)
                function getSortValue(t) {
                    var value = Number(t.hour.substr(0, 2));
                    if (value == 12) {
                        if (t.period == "am")
                            value = 0;
                    }
                    else if (t.period == "pm")
                        value += 12;
                    if (t.hour.substr(3, 2) == "30")
                        value += .5;

                    return value;
                }

                // compare two time blocks by start time
                function compareByStartTime(a, b) {
                    var a_value = getSortValue(a.starts);
                    var b_value = getSortValue(b.starts);

                    var order =
                        a_value < b_value ? -1 :
                            a_value > b_value ? 1 :
                                0;

                    return order;
                };

                $scope.onChanged = function () {
                    // map opening hours into a collection in which:
                    // - each day has an hours array (even if empty)
                    // - the blocks in each day are sorted by starting time
                    // - the hours are expressed as sort value, s.t. closing times after midnight are > 24.0
                    var sorted = [0, 1, 2, 3, 4, 5, 6]
                        // sort blocks in each day
                        .map(function (day) {
                            return angular.copy($scope.value.openingHours[day] || []).sort(compareByStartTime);
                        })
                        .map(function (blocks) {
                            return blocks.map(function (range) {
                                var starts = getSortValue(range.starts);
                                var ends = getSortValue(range.ends);
                                // map after-midnight hours to be >= 24
                                if (ends < starts || (ends == 0 && starts == 0)) {
                                    ends += 24; // same value, but next day
                                }
                                return {
                                    starts: starts,
                                    ends: ends
                                };
                            });
                        });

                    // can't overlap in a day, or between days
                    var hasOverlap = [0, 1, 2, 3, 4, 5, 6].find(function (day) {
                        var thisDaysBlocks = sorted[day] || [];
                        var prevDaysBlocks = sorted[(day + 6) % 7] || [];

                        if (!thisDaysBlocks.length) return false;

                        var hasOverlapInDay = thisDaysBlocks.find(function (range, index) {
                            if (index == 0) return false;
                            return range.starts < thisDaysBlocks[index - 1].ends;
                        });

                        if (hasOverlapInDay) {
                            $scope.errorMessage = $scope.daysOfWeek[day].name + " has an overlap in its hours";
                            return true;
                        }

                        if (!prevDaysBlocks.length) return false;

                        var hasOverlapBetweenDays =
                            prevDaysBlocks[prevDaysBlocks.length - 1].ends > 24 &&
                            (prevDaysBlocks[prevDaysBlocks.length - 1].ends - 24) > thisDaysBlocks[0].starts;

                        if (hasOverlapBetweenDays) {
                            $scope.errorMessage = $scope.daysOfWeek[day].name + " has an overlap with the previous day's hours";
                            return true;
                        }
                    });

                    var isValid = hasOverlap == null;
                    $scope.form.$setValidity($scope.component, isValid);
                }
            }
        };
    })
    .directive('timeOfDay', function () {
        return {
            restrict: 'E',
            require: 'ngModel',
            scope: {
                value: '=ngModel'
            },
            template:
                '<div class="input-group">' +
                '<select name="hour" class="form-control input-sm" ng-model="value.hour" ng-options="time for time in timesOfDay" ng-change="valueChanged()"></select>' +
                '<span class="input-group-btn">' +
                '<button name="period" class="btn btn-default btn-sm" type="button" ng-click="togglePeriod(); valueChanged()">' +
                '{{ value.period }}' +
                '</button>' +
                '</span>' +
                '</div>',
            link: function (scope, element, attr, ngModelCtrl) {
                scope.timesOfDay = [12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
                    .map(function (e) {
                        function pad(h) { return String("00" + h).slice(-2); }
                        return [pad(e) + ':00', pad(e) + ':30'];
                    })
                    .reduce(function (prev, curr) { return prev.concat(curr); });

                scope.togglePeriod = function () {
                    scope.value.period = scope.value.period == "am" ? "pm" : "am";
                };

                scope.valueChanged = function () {
                    scope.$parent.$eval(attr['ngChange']);
                }
            }
        };
    })
    .directive('paymentsStandard', function () {
        return {
            restrict: 'E',
            scope: { item: '=item' },
            templateUrl: 'payments-standard.html'
        };
    })
    .directive('paymentsMobile', function () {
        return {
            restrict: 'E',
            scope: { item: '=item' },
            templateUrl: 'payments-mobile.html'
        };
	})
	.factory('specialtyHoursCtrl', function () {
		return function (scope, element, attrs, ctrls) {
			// sets date to be of form: "yyyy-MM-dd"
			function toShortISOString(date) {
				return date.toISOString().slice(0, 10);
			}

			scope.addSpecialtyHours = function (range) {
				if (!range.startDate) return;

				if (scope.component == 'holidayHours')
					range.endDate = range.startDate; // holiday hours can (presently) only be one day long

				scope.items = scope.items || [];

				scope.items.push({
					dateRange: {
						startDate: toShortISOString(range.startDate),
						endDate: toShortISOString(range.endDate)
					},
					openingHours: []
				});

				scope.specialHoursChanged(scope.items);
			}

			scope.removeSpecialtyHours = function (index) {
				scope.items.splice(index, 1);
				scope.specialHoursChanged(scope.items);
			}

			scope.specialHoursChanged = function (hours) {
				var sortedByStartDate = angular.copy(hours).sort(function (a, b) {
					return (a.dateRange.startDate < b.dateRange.startDate) ? -1 : (a.dateRange.startDate > b.dateRange.startDate) ? 1 : 0;
				});

				var overlap = sortedByStartDate.find(function (item, index) {
					if (index == 0) return false;

					if (item.dateRange.startDate <= sortedByStartDate[index - 1].dateRange.endDate)
						return true;
				});

				var isValid = overlap == null;
				scope.form.$setValidity(scope.component, isValid);
			}
		}
	})
	.directive('seasonalHours', ['specialtyHoursCtrl', function (specialtyHoursCtrl) {
		return {
			restrict: 'E',
			scope: {
				items: '=items',
				form: '=form',
				component: '=component'
			},
			templateUrl: 'seasonal-hours.html',
			link: specialtyHoursCtrl
		};
	}])
	.directive('holidayHours', ['specialtyHoursCtrl', function (specialtyHoursCtrl) {
		return {
			restrict: 'E',
			scope: {
				items: '=items',
				form: '=form',
				component: '=component'
			},
			templateUrl: 'holiday-hours.html',
			link: specialtyHoursCtrl
		};
	}])
	.directive('storefrontPhotos', function () {
		return {
			restrict: 'E',
			scope: { items: '=items' },
			templateUrl: 'storefront-photos.html',
			link: function (scope, element, attrs, ctrls) {
				scope.addExteriorPhoto = function () {
					scope.items.push(scope.newExteriorPhoto);
					scope.newExteriorPhoto = null;
				}

				scope.removePhoto = function (index) {
					scope.items.splice(index, 1);
				}
			}
		};
	})
    .directive('contactInfo', function () {
        return {
            restrict: 'E',
            scope: { item: '=item' },
            templateUrl: 'contact-info.html'
        };
    })
    .directive('socialMedia', function () {
        return {
            restrict: 'E',
            scope: { item: '=item' },
            templateUrl: 'social-media.html'
        };
    })
    .directive('mapImage', ['cfg', function (cfg) {
        return {
            restrict: 'E',
            scope: { position: '=position' },
            template: '<img ng-src="//image.maps.cit.api.here.com/mia/1.6/mapview?c={{ position.Latitude }}%2C{{ position.Longitude }}&z=14&w=295&h=295&f=1&app_id={{ cfg.maps.app_id }}&app_code={{ cfg.maps.app_code }}&nodot" style="width: 295px; height: 295px;" />',
            link: function (scope, element, attr) {
                scope.cfg = cfg;
            }
        };
    }])
    .directive('photoUrl', function () {
        return {
            restrict: 'E',
            require: 'ngModel',
            scope: {
                value: '=ngModel',
                name: '@',
                form: '=',
                allowsGif: '='
            },
			templateUrl: 'photo-url.html',
            link: function (scope, element, attrs, ctrls) {
				if (!scope.form) return;

				scope.feedbackModel = scope.form.$name + "." + attrs.name;
            }
        };
	})
	.directive('yearFounded', function () {
		return {
			restrict: 'E',
			scope: { item: '=item' },
			templateUrl: 'year-founded.html'
		};
	})
	.directive('tagline', function () {
		return {
			restrict: 'E',
			scope: { item: '=item' },
			templateUrl: 'tagline.html'
		};
	})
	.directive('languagesSpoken', function () {
		return {
			restrict: 'E',
			scope: { item: '=item' },
			templateUrl: 'languages-spoken.html',
			link: function (scope, element, attrs, ctrls) {
				scope.addLanguage = function ($item, $model, $label) {
					scope.languageToAdd = null;
					if (scope.item.languages.indexOf($item) == -1)
						scope.item.languages.push($item);
				}

				scope.removeLanguage = function (lang) {
					var index = scope.item.languages.indexOf(lang);
					if (index != -1) scope.item.languages.splice(index, 1);
				};
			}
		};
	})
	.directive('alertGroup', ['AlertQueue', function (AlertQueue) {
		return {
			restrict: 'E',
			scope: { items: '=items' },
			template: '<alert type="{{ item.type }}" close="dismiss($index)" dismiss-on-timeout="5000" ng-repeat="item in alerts">{{ item.message }}</alert>',
			link: function (scope, element, attr) {
				// watch bound items (which may be loaded asynchronously) and enqueue them should they appear
				scope.$watch(
					function () { return scope.items; },
					function () {
						(scope.items || []).forEach(function (i) { AlertQueue.push(i); });
					}
				);
				
				scope.alerts = AlertQueue;
				
				scope.dismiss = function (index) {
					scope.alerts.splice(index, 1);
				}
			}
		};
	}]);

angular.module('localezeServices')
	// a queue of alerts (e.g. to be displayed in <alert-group />); this serves as a singleton
	// which can be written to from anywhere in the application.
	.factory('AlertQueue', [function () {
		var alerts = [];
		return alerts;
	}])
	// encapsulates logic about scoring a business profile for completeness
	.factory('ProfileScore', [function () {
		var cfg = {
			"Name": { value: 15, hasProperty: function (s) { return !!s.name; } },
			"Address": { value: 15, hasProperty: function (s) { return !!(s.city && s.state) || !!s.zip; } },
			"Phone": { value: 15, hasProperty: function (s) { return !!s.phone; } },
			"Categories": { value: 15, hasProperty: function (s) { return s.categories && s.categories.length && s.categories[0].Name; } },
			"Keywords": {
				value: 10,
				hasProperty: function (s) {
					// has unstructured keywords? count as complete
					if (s.keywords && s.keywords.length)
						return true;

					// has structured keywords? count as complete
					return s.categories && s.categories.some(function (c) {
						return c.Groups && c.Groups.some(function (g) {
							return g.Keywords && g.Keywords.length;
						});
					});
				}
			},
			"Web Site": { value: 10, hasProperty: function (s) { return !!s.website; } },
			"Payment Methods": { value: 5, hasProperty: function (s) { return s.paymentMethods && Object.keys(s.paymentMethods).filter(function (k) { return s.paymentMethods[k]; }).length; } },
			"Hours of Operation": { value: 10, hasProperty: function (s) { return s.openingHours && s.openingHours.length && s.openingHours.find(function(e) { return e && e.length; }); } },
			"Social Media": { value: 5, hasProperty: function (s) { return [s.facebook, s.twitter, s.linkedin, s.googleplus, s.yelp, s.foursquare].join('').length; } },
			get: function (s) {
				if (!s) return;
				var score = 0;
				var components = Object.keys(cfg).filter(function (p) { return typeof (cfg[p]) == "object" });
				for (var i = 0; i < components.length; i++) {
					if (cfg[components[i]].hasProperty(s)) {
						score += cfg[components[i]].value;
					}
				}
				return score;
			}
		};

		return cfg;
	}]);;angular.module('localezeControllers')
	.controller('ActivityCtrl', ['$scope', '$filter', 'DomainEvents', function ($scope, $filter, DomainEvents)
	{
	    /* Returns the date of the first of the month N months from now
           N = 0 - current month
	       N = 1 - next month
	       N = -1 - previous month */
		function getDateNMonthsFromNow(n) {
			var now = new Date();
			var monthWithIncrementOrDecrement = now.getMonth() + n; // now.getMonth() returns months in range 0-11
			var addOrSubtractYear = (monthWithIncrementOrDecrement < 0 ? -1
                                       : monthWithIncrementOrDecrement >= 12 ? 1
                                       : 0);
			var year = now.getFullYear() + addOrSubtractYear; 

			function pad(s) { return ("00" + s).slice(-2); } // for formatting

            // js modulo of a negative number returns a negative, so we have to add 12
			var month = pad((monthWithIncrementOrDecrement + 12) % 12 + 1); 
			return [year, month, '01'].join('-');
		}

		$scope.dateRanges = [];
		for (var i = 0; i < 12; i++)
			$scope.dateRanges.push({
			    start: getDateNMonthsFromNow(-1 * i),
			    end: getDateNMonthsFromNow(-1 * (i - 1))
			});

		$scope.defaultRange = { start: getDateNMonthsFromNow(0), end: getDateNMonthsFromNow(1) }; // this calendar month
		$scope.range = $scope.defaultRange;

		$scope.setRange = function (range) {
		    $scope.page = 1;
			$scope.range = range;
			loadEvents();
		}

		$scope.setFilter = function (filter) {
		    $scope.page = 1;
		    $scope.filter = filter;
		    loadEvents();
		}

		$scope.onViewChanged = function () {
			$scope.view = $filter('limitTo')(
				$filter('filter')(
					$scope.events.Items,
					$scope.filter
				)
			);
		}

		$scope.formatDate = function (date) {
			var components = date.split('-');
			return components[1] + "/" + components[0];
		}

		$scope.typeLabels = {
			0 : 'File Imported',
			1 : 'File Processed',
			2 : 'Listing Added',
			3 : 'Listing Edited',
			4 : 'Claim Requested',
			5: 'Listing Closed',
            6: 'Listing Released'
		};

		$scope.count = 250;

		$scope.first = function () {
		    $scope.page = 1;
		    loadEvents();
		    goToTopOfPage();
		}

		$scope.prev = function () {
		    $scope.page--;
		    loadEvents();
		    goToTopOfPage();
		}

		$scope.next = function () {
		    $scope.page++;
		    loadEvents();
		    goToTopOfPage();
		}

		goToTopOfPage = function () {
		    $('document').ready(function () {
		        $(window).scrollTop(0);
		    });
		}

		$scope.nextPageHasItems = true;
		nextPageExists = function () {
		    var nextEvents = DomainEvents.query({
		        count: $scope.count,
		        page: $scope.page + 1,
		        after: $scope.range.start,
		        before: $scope.range.end,
		    }, function (nextEvents) {
		        $scope.nextPageHasItems = nextEvents.Items.length > 0;
		    });
		}

		loadEvents = function() {
			$scope.events = DomainEvents.query({
			    count: $scope.count,
			    page: $scope.page,
				after: $scope.range.start,
				before: $scope.range.end,
			}, function (events) {
				$scope.onViewChanged();
			});
			nextPageExists();
		}

		$scope.first();
	}]);;angular.module('localezeControllers')
	.controller('CategoriesCtrl', ['$scope', 'Categories', function ($scope, Categories) {
		Categories.query({}, function (data) {
			$scope.categories = data.Categories; // unwrap this object into an array
		});

		$scope.addCategory = function ($item, $model, $label) {
			$scope.categoryToAdd = null;
			var isAlreadyContained = false;
			$scope.subscription.categories.forEach(function (c) { if (c.Name == $model.Name) isAlreadyContained = true; });
			if (!isAlreadyContained)
				$scope.subscription.categories.push({ Id: $model.Id, Name: $model.Name });
		}

		$scope.removeCategory = function (index) {
			$scope.subscription.categories.splice(index, 1);
		}
	}])
	.controller('GroupCtrl', ['$scope', '$filter', 'Groups', function ($scope, $filter, Groups) {
		if( $scope.category.Id) 
			$scope.groupsForCategory = Groups.query({ Id: $scope.$parent.category.Id });

		$scope.getGroups = function (categoryId, filterString) {
			return Groups.query({ Id: categoryId }).$promise.then(function (result) {
				return $filter('filter')(result, { Name: filterString })
			});
		};

		$scope.addGroup = function ($index, $item, $model, $label) {
			$scope.subscription.categories[$index].Groups = $scope.subscription.categories[$index].Groups || [];
			var isAlreadyContained = false;
			$scope.subscription.categories[$index].Groups.forEach(function (g) { if (g.Name == $model.Name) isAlreadyContained = true; });
			if (!isAlreadyContained)
				$scope.subscription.categories[$index].Groups.push({ Id: $model.Id, Name: $model.Name });
		}

		$scope.removeGroup = function (parentIndex, index) {
			$scope.subscription.categories[parentIndex].Groups.splice(index, 1);
		}
	}])
	.controller('KeywordCtrl', ['$scope', '$filter', 'Groups', function ($scope, $filter, Groups) {
		Groups.query({ id: $scope.category.Id }).$promise.then(function (result) {
			var matching = { Children: [] };
			for (var i = 0; i < result.length; i++)
				if (result[i].Name == $scope.group.Name)
					matching = result[i];
			$scope.keywordsForGroup = matching.Children;
		});

		$scope.getKeywords = function (categoryId, groupName, filterString) {
			return Groups.query({ id: $scope.category.Id }).$promise.then(function (result) {
				var matching = { Children: [] };
				for (var i = 0; i < result.length; i++)
					if (result[i].Name == $scope.group.Name)
						matching = result[i];
				return $filter('filter')(matching.Children, { Name: filterString });
			});
		}

		$scope.addKeyword = function (group, $item, $model, $label) {
			group.Keywords = group.Keywords || [];
			if (!group.Keywords.some(function (element) { return element.Name == $label; }))
				group.Keywords.push({ Id: $item.Id, Name: $label });
		}

		$scope.removeKeyword = function (group, index) {
			group.Keywords.splice(index, 1);
		}
	}]);;angular.module('localezeControllers')
	.controller('CheckoutCtrl', ['$scope', '$route', '$modal', 'Token', 'Checkout', 'PaymentCommands', function ($scope, $route, $modal, Token, Checkout, PaymentCommands)
	{
		// enter this flow only for self-reg users, for available publishable listings
		if (
			$scope.profile.credit &&
			!$scope.profile.credit.isOpen && // self-reg
			$scope.subscription.interpretationStatus.pid && // has pid
			!$scope.subscription.claim && // not already subscribed
			$scope.subscription.claimConflictType == null && // available
			!$scope.subscription.errors.some(function (e) { return e.code == 4; }) // publishable
		) {
            $scope.token = Token.get({});
            $scope.scenario = Checkout.get({});

            $scope.open = function () {
                $modal.open({
                    templateUrl: 'checkout.html',
                    size: 'lg',
                    controller: $modal.controller(['$scope', '$modalInstance', '$location', 'subscription', 'token', 'scenario', function ($scope, $modalInstance, $location, subscription, token, scenario) {
                        $scope.subscription = subscription;
                        $scope.token = token;
                        $scope.scenario = scenario;

                        $scope.paymentMethods = function (id) {
                            switch (id) {
                                case 1: return "Visa";
                                case 2: return "MasterCard";
                                case 3: return "American Express";
                                case 4: return "Discover";
                                default: return "Unknown";
                            }
                        }

                        $scope.tokenIsExpired = function (input) {
                            var current = new Date();
                            var parts = input.split(/\s*\-\s*/);
                            // if year is greater it's never expired
                            if (current.getFullYear() < parts[1]) return false;
                            // if year is less it's always expired
                            if (current.getFullYear() > parts[1]) return true;
                            // on equal years not expired when month is same or greater
                            if (current.getMonth() + 1 <= parts[0]) return false;
                            // everything else expired
                            return true;
                        }

                        $scope.checkout = function () {
                            $modalInstance.dismiss();
                            PaymentCommands.put(
                                { id: subscription.id },
                                { Token: token.Token },
                                function (response) {
                                    $location.path("/receipt/" + subscription.id + "/" + response.Outcome + "/" + (response.Code || ''));
                                }
                            );
                        };

                        $scope.dismiss = function () {
                            $modalInstance.dismiss('cancel');
                        };
                    }]),
                    resolve: {
                        subscription: function () { return $scope.subscription; },
                        token: function () { return $scope.token; },
                        scenario: function () { return $scope.scenario; }
                    }
                });
            }

			// auto-show checkout view only if listing was just added
			var isNewListing = $route.current.originalPath.split('/').pop() === "review";
			if(isNewListing)
	            $scope.open();
		}
	}]);;angular.module('localezeControllers')
	.controller('ConflictCtrl', ['$scope', 'Conflicts', 'AlertQueue', function ($scope, Conflicts, AlertQueue) {
		$scope.requestToManage = function () {
			Conflicts.post({ id: $scope.subscription.interpretationStatus.pid }, {}, function () {
				AlertQueue.push({
					status: 200,
					type: 'success',
					title: 'Request Sent',
					message: 'Your request has been sent.'
				});
			});
		};
	}]);;angular.module('localeze')
	// <a ng-confirm="continue with {{ action }}?" on-confirmed="callback()">click</a>
	.directive('ngConfirm', [function () {
		return {
			restrict: "A",
			scope: {
				message: "@ngConfirm",
				clickAction: "&onConfirmed"
			},
			link: function (scope, element, attr) {
				element.bind('click', function (event) {
					if (window.confirm(scope.message)) {
						scope.$eval(scope.clickAction)
					}
				});
			}
		};
	}])
	/* handles drag-and-drop of csv/excel fies */
	.directive('csvParser', function () {
		return function (scope, elem, attrs) {
			function handleDrop(e) {
				e.stopPropagation();
				e.preventDefault();

				var file;
				if (e.dataTransfer) {
					if (e.dataTransfer.files.length == 0) return;
					file = e.dataTransfer.files[0];
				} else {
					if (e.target.files.length == 0) return;
					file = e.target.files[0];
				}

				var extension = file.name.split(".").pop();
				
				var dropRegion = elem[0];
				dropRegion.setAttribute("aria-busy", "true");

				var formdata = new FormData();
				formdata.append('file', file);

                // Reset the input element's value to allow re-upload after discard
                e.target.value = null;

				$.ajax({
					url: '/manage/api/changesets/import',  //Server script to process data
					type: 'POST',
					//Ajax events
					//beforeSend: beforeSendHandler,
					uploadProgress: function (event, position, total, percentComplete) {

					},
					success: function (data) {
						scope.$broadcast("csvParsed", { file: file, items: data });
						dropRegion.setAttribute("aria-busy", "false");
					},
					error: function (xhr, status, error) {
						alert("Invalid file format: " + xhr.responseJSON.Message + ".");
						dropRegion.setAttribute("aria-busy", "false");
					},
					// Form data
					data: formdata,
					//Options to tell jQuery not to process data or worry about content-type.
					cache: false,
					contentType: false,
					processData: false
				});
			}

			// drag & drop : 

			function handleDragover(e) {
				e.stopPropagation();
				e.preventDefault();
				e.dataTransfer.dropEffect = 'copy';
			}

			if (elem[0].addEventListener) {
				elem[0].addEventListener('dragenter', handleDragover, false);
				elem[0].addEventListener('dragover', handleDragover, false);
				elem[0].addEventListener('drop', handleDrop, false);
			}

			// file input

			$(elem[0]).find("input[type='file']").each(function (index, element) {
				element.addEventListener('change', function (e) { handleDrop(e); }, false);
			});
		};
	});

angular.module('localezeControllers')
	.controller('ImportSidebarCtrl', ['$scope', 'AlertQueue', function ($scope, AlertQueue) {
		$scope.onExport = function () {
			AlertQueue.push({
				status: 200, type: 'info', title: 'Listings Exported', message: 'Your listing export file is now downloading.'
			});
		};
	}])
	// i do set operations
	.factory('Sets', [function () {
		return {
			// things in either a or b
			union : function(a, b) {
				var set = new Set();
				var result = [];
				a.forEach(function (value) { set.add(value); });
				b.forEach(function (value) { set.add(value); });
				set.forEach(function (item) {
					result.push(item);
				});
				return result;
			},
			// things in both a and b
			intersection : function(a, b) {
				var result = [];
				var u = this.union(a, b);
				var left = new Set(a);
				var right = new Set(b);
				u.forEach(function (value) {
					if (left.has(value) && right.has(value))
						result.push(value);
				});
				return result;
			},
			// things in a not in b
			difference : function(a, b) {
				var result = [];
				var left = new Set(a);
				var right = new Set(b);
				left.forEach(function (value) {
					if (!right.has(value))
						result.push(value);
				});
				return result;
			}
		};
	}])
	.controller('ImportCtrl', ['$scope', '$location', 'Sets', 'Subscriptions', 'ChangeSets', function ($scope, $location, Sets, Subscriptions, ChangeSets)
	{
		$scope.fileSet = {};
		$scope.accountSet = {};
		$scope.viewCache = {}; // caches of the various view options
		$scope.view = []; // the document ids being viewed
		$scope.syncDisposition = 2; // deleted by default

		// when the data in the view model has changed, update counts and caches
		function onViewModelChanged()
		{
			$scope.viewCache = {
				'in-file': Object.keys($scope.fileSet),
				'in-account': Object.keys($scope.accountSet),
				'in-either': Sets.union(Object.keys($scope.fileSet), Object.keys($scope.accountSet)),
				'in-both': Sets.intersection(Object.keys($scope.fileSet), Object.keys($scope.accountSet)),
				'only-in-account': Sets.difference(Object.keys($scope.accountSet), Object.keys($scope.fileSet)),
				'only-in-file': Sets.difference(Object.keys($scope.fileSet), Object.keys($scope.accountSet))
			};

			$scope.counts = {
				'in-file': $scope.viewCache['in-file'].length,
				'in-account': $scope.viewCache['in-account'].length,
				'in-either': $scope.viewCache['in-either'].length,
				'in-both': $scope.viewCache['in-both'].length,
				'only-in-account': $scope.viewCache['only-in-account'].length,
				'only-in-file': $scope.viewCache['only-in-file'].length
			};

			// repopulate the view from the (updated) view cache
			$scope.setView($scope.viewType);
		};

		Subscriptions.queryAll($scope, function (data) {
			// map the response into the account set
			data.forEach(function (item) {
				// closed listings do not appear in feeds, and can potentially be very
				// numerous for some large clients. we omit them so that the UI doesn't
				// show "these closed listings aren't in your file, are you sure you
				// want to close them?"
				if (item.Closure === 1 /* permanent dissolution */) return;
				$scope.accountSet[item.ID] = {
					Name: item.Name,
					Location: {
						StreetAddress: item.Street,
						Locality: item.City,
						Region: item.State
                    },
                    Telephone: item.Phone
				};
			});
			onViewModelChanged();
		});

		$scope.setView = function (type)
		{
			$scope.viewType = type;

			if ($scope.viewCache[$scope.viewType])
				$scope.view = angular.copy($scope.viewCache[$scope.viewType]);
			else
				$scope.view = [];
		}
		$scope.setView('only-in-file'); // default

		$scope.$on("csvParsed", function (event, args) {
			$scope.file = args.file;
			args.items.some(function (item) {
				// check for collisions on listing id
				if ($scope.fileSet[item.ListingId])
				{
					alert("Your file has duplicate or missing listingIDs. Please supply a unique listingID for each listing in your file and resubmit.");
					$scope.reset();
					return true; // collision - exit
				}
				$scope.fileSet[item.ListingId] = item.Body;
			});
			onViewModelChanged();

			// since this event originates from outside of angular, we need
			// to notify the scope. KLUDGE: notifying immediately, in some
			// circumstances, results in an incomplete render, so we do it twice.
			setTimeout(function () { $scope.$apply(); }, 500);
			$scope.$apply();
		});

		function createChangeset(behavior, messageIds)
		{
			$scope.isProcessing = true; // the data is being (or has been) processed
			var batchSize = 100;

			// create a change set
			ChangeSets.post({}, {
				Name: $scope.file.name,
				Behavior: behavior,
				CreatedDate: new Date(),
				Length: messageIds.length
			}).$promise.then(function (changeSet) {
				// enqueue the commands batchfully
				(function handleBatch(batchIndex) {
					$scope.progressPercent = Number((batchIndex / (messageIds.length / batchSize) * 100).toFixed(1));

					var nextBatch = messageIds
						.slice(batchSize * batchIndex, batchSize * (batchIndex + 1))
						.map(function (key, index) {
							return {
								ChangeSetId: changeSet.Id,
								ListingId: key,
								Body: $scope.fileSet[key], // may be null if this id is not in the file
								Index: batchSize * batchIndex + index,
								Disposition: $scope.fileSet[key] ?
									// see whether this is a modification or a creation
									$scope.accountSet[key] ?
										0 : // modified
										3 : // created
									// if the key isn't in the file set, it's a removal
									$scope.syncDisposition
							};
						});

					if (nextBatch.length) {
						ChangeSets
							.enqueue({ id: changeSet.Id }, nextBatch)
							.$promise.then(function () { handleBatch(batchIndex + 1); });
					} else {
						$location.path('/review');
					}
				})(0);
			});
		}

		$scope.merge = function () {
			createChangeset(
				0, // merge
				Object.keys($scope.fileSet)
			);
		};

		$scope.sync = function () {
			createChangeset(
				1, // replace
				// heads up - deletes must precede everything else, because they presently do not create
				// new revisions, and changes are processed in revision order. let us strongly consider
                // adding a surrogate PK to changesetmember, so that monotonicity of processing change messages
				// can be achieved independently of the values of the revision numbers.
				Sets.union(
					Sets.difference(Object.keys($scope.accountSet), Object.keys($scope.fileSet)),
					Object.keys($scope.fileSet)
				)
			);
		}

		$scope.reset = function () {
			$scope.file = null;
			$scope.fileSet = {};
			$scope.view = [];
		};
	}]);;angular.module('localezeControllers')
	.controller('LoginCtrl', ['$scope', '$window', 'QueryString', 'Session', 'PasswordReset', 'AlertQueue', function ($scope, $window, QueryString, Session, PasswordReset, AlertQueue) {
		$scope.isReset = QueryString.hasKey("reset");
		$scope.token = QueryString.valueAt("pin");

		$scope.login = function () {
			Session.put({}, $scope.user, function (response) {
				$scope.lastLoginResponse = response;

				if (response.IsSuccess) {
				    if (QueryString.hasKey("returnUrl") && QueryString.valueAt("returnUrl")) {
				        var partial = decodeURIComponent(escape(QueryString.valueAt("returnUrl")));
				        RedirectPartial(partial);
				    } else if (QueryString.hasKey("ReturnUrl") && QueryString.valueAt("ReturnUrl")) {
				        var partial = decodeURIComponent(escape(QueryString.valueAt("ReturnUrl")));
				        RedirectPartial(partial);
				    } else if (QueryString.hasKey("uri") && QueryString.valueAt("uri")) {
				        var partial = decodeURIComponent(escape(QueryString.valueAt("uri")));
				        RedirectPartial(partial);
				    }
				    else {
						$window.location.href = '/manage/';
					}
				}
			});

			function RedirectPartial(partial) {
			    if (partial.indexOf("#") == 0)
			        $window.location.href = "/manage/" + partial;
			    else
			        $window.location.href = partial;
			}
		};

		$scope.beginReset = function () {
			PasswordReset.post(
				{ username: $scope.user.UserName },
				{
					Password: $scope.user.Password,
					Token: QueryString.valueAt('pin')
				},
				function (response) {
					$scope.lastResetResponse = response;
					if (response.IsSuccess) {
						if ($scope.token) {
							AlertQueue.push({
								status: 202,
								type: 'success',
								title: 'Reset Complete',
								message: 'Your password has been reset.'
							});
						} else {
							AlertQueue.push({
								status: 202,
								type: 'success',
								title: 'Reset Notification Sent',
								message: 'An email to reset your password has been sent to ' + response.NotifyAddress + '. Please follow the authorization instructions in the email.'
							});
						}
					}
				}
			);
		};
	}]);;angular.module('localezeControllers')
	.controller('NavCtrl', ['$scope', '$window', 'Profile', 'Messages', '$location', function ($scope, $window, Profile, Messages, $location) {
		var mobileWidth = 768;
		$scope.escape = $window.escape;

		// add the profile to the parent (document) scope, so that user information is globally available
		$scope.$parent.profile = Profile.get({}, function (data, headers) {
			var isAnonymous = !$scope.profile.name;
			
            if(!isAnonymous)
				$scope.messages = Messages.query({ status: "unread" });
		});

		$scope.path = $window.location.pathname;
		$scope.$on("$routeChangeSuccess", function (event, current) {
			if(current.$$route) {
				$scope.route = $window.location.hash.replace(/\/$/, "") || "/";
				$scope.path = $window.location.pathname;
			}
		});

		$scope.nav = { isCollapsed: $window.innerWidth < mobileWidth };

		$scope.toggle = function ($event) {
			$scope.nav.isCollapsed = !$scope.nav.isCollapsed;
		};

		$scope.onclick = function ($event) {
			if ($window.innerWidth < mobileWidth)
				$scope.nav.isCollapsed = true;
		};

		angular.element($window).bind('resize', function () {
			$scope.nav.isCollapsed = $window.innerWidth < mobileWidth;
			return $scope.$apply();
		});
	}]);;angular.module('localezeControllers')
	.controller('ReceiptCtrl', ['$scope', '$routeParams', function ($scope, $routeParams) {
		$scope.receipt = {
			listingId: $routeParams.listingId,
			outcome: $routeParams.outcome,
			code: $routeParams.code
		};

		$scope.getDeclineReason = function (code) {
			switch (code) {
				case "202":
					return "Card Expired. Please correct payment information and resubmit, or try a different card.";
				case "209":
				case "211":
					return "Invalid CVN. Please correct payment information and resubmit.";
				case "203":
				case "204":
				case "208":
				case "210":
				case "220":
				case "222":
				case "231":
				case "248":
				case "250":
				case "251":
					return "Please contact your financial institution for more information, or try a different card.";
				case "450":
				case "451":
				case "452":
				case "453":
				case "454":
				case "455":
				case "456":
				case "457":
				case "458":
				case "459":
				case "460":
					return "Please verify your address and resubmit. Contact support if the address provided is accurate. ";
				default:
					return "Please contact support for more information.";
			}
		}
	}]);;angular.module('localezeControllers')
    .controller('ReviewCtrl', ['$scope', '$routeParams', '$filter', 'ChangeSets', 'ChangeMessages', 'ConflictMessages', 'AlertQueue',
        function ($scope, $routeParams, $filter, ChangeSets, ChangeMessages, ConflictMessages, AlertQueue) {

		if ($routeParams.id) {
			$scope.changeSetId = $routeParams.id;

			ChangeSets.get({ id: $scope.changeSetId }, function (set) {
				$scope.changeSet = set;
				loadPage(0); // filtering requires changeSet metadata, so these requests can't be asynchronous
			});

			$scope.items = [];
			$scope.view = [];
            $scope.filteredViews = {};

            // set view items
            $scope.updateView = function (filter) {
                if (!!filter)
                    $scope.currentFilter = filter;
                $scope.view = $scope.filteredViews[$scope.currentFilter.name];
			};

			// filter presets for the UI
			$scope.filters = [{
				name: "All",
				criteria: function (e) { return true; },
				isVisible: function () { return true; }
			}, {
				name: "Added",
                criteria: function (e) {
                    // show Adds that are resolvable, not errors, conflicts or duplicates
                    return e.Disposition == 3 && !!e.Pid && e.ValidityState != 0 && e.Owner != 1 && !e.DuplicatesListingId;
				},
				isVisible: function () { return true; }
			}, {
				name: "Updated",
                criteria: function (e) {
                    // show Updates that are resolvable, not errors, conflicts or duplicates
                    return e.Disposition == 0 && !!e.Pid && e.ValidityState != 0 && e.Owner != 1 && !e.DuplicatesListingId;
				},
				isVisible: function () { return true; }
			}, {
				name: "Closed",
				criteria: function (e) { return e.Disposition == 1; },
				isVisible: function () {
					return $scope.changeSet.IsComplete &&
						$scope.changeSet.Behavior == 1 &&
						$scope.filteredViews[this.name].length;
				}
			}, {
				name: "Removed",
				criteria: function (e) { return e.Disposition == 2; },
				isVisible: function () {
					return $scope.changeSet.IsComplete &&
						$scope.changeSet.Behavior == 1 &&
						$scope.filteredViews[this.name].length;
				}
			}, {
				name: "Errors",
				criteria: function (e) {
					// show errors only for adds/edits that are not resolvable or validation errors that are also not conflicts or duplicates
                    return (!e.Pid || e.ValidityState == 0) && e.Owner != 1 && !e.DuplicatesListingId  && (e.Disposition == 3 || e.Disposition == 0);
				},
				isVisible: function () { return true; }
			}, {
				name: "Conflicts",
				criteria: function (e) {
					// show conflicts (owned by someone else) only for adds/edits
					return e.Owner == 1 && (e.Disposition == 3 || e.Disposition == 0);
				},
				isVisible: function () { return true; }
            }, {
                name: "Duplicates",
                criteria: function (e) {
                    // show conflicts only for adds/edits
                    return !!e.DuplicatesListingId && (e.Disposition == 3 || e.Disposition == 0);
				},
				isVisible: function () { return true; }
			}];

			// given a filter object, returns true iff it describes itself as visible
			$scope.whereVisible = function (filter) { return filter.isVisible(); }

            // set collection to default state of empty arrays
			$scope.filters.forEach(function (f) {
				$scope.filteredViews[f.name] = [];
			});

            // the default/current filter
            $scope.updateView($scope.filters[0]); 

            // called for each fetch of data
            function fillFilteredViews(items) {
                if (!items)
                    return;
                // build collection of items per view filter
                items.forEach(function (e) {
                    $scope.filters.forEach(function (filter) {
                        if (filter.criteria(e)) {
                            $scope.filteredViews[filter.name].push(e);
                        }
                    });
                });
            }

			var isCancelLoad = false;	// recursive loader will continue fetching data on route change unless you tell it otherwise
			function loadPage(partitionId) {
				var batchSize = 2500;

				if (isCancelLoad) return;

				ChangeMessages.get(
					{
						id: $scope.changeSetId,
						partitionId: partitionId,
						take: batchSize
					},
					function (data, headers) {
						$scope.items = $scope.items.concat(data);
						fillFilteredViews(data)
						$scope.updateView();

						// fetch more data, if it looks like there might be more
						if (data.length == batchSize)
							loadPage(data[data.length - 1].Id);
					}
				);
            };
            $scope.$on("$routeChangeStart", function (event, next, current) {
                isCancelLoad = true;
            });

            $scope.sendConflictReport = function () {
                ConflictMessages.post({ id: $scope.changeSetId }, function (data, headers) {
                    AlertQueue.push({
                        type: 'success',
                        title: 'Conflict Request',
                        message: 'Support has been notified of your request.'
                    });
                });
            }
		}
		else
		{
			$scope.changeSetId = null;
			$scope.items = ChangeSets.query();
		}
	}]);;angular.module('localezeControllers')
    .controller('SubscriptionCtrl', SubscriptionCtrl)
    .controller('BaseSubscriptionCtrl', BaseSubscriptionCtrl)
    .controller('SubscriptionClaimCtrl', SubscriptionClaimCtrl)
    .controller('SubscriptionDetailsCtrl', ['$scope', '$route', '$routeParams', '$location', '$modal', 'Subscriptions', 'States', function ($scope, $route, $routeParams, $location, $modal, Subscriptions, States) {
        function getModel() {
            $scope.subscription = $routeParams.id ?
                Subscriptions.get({
                    id: $routeParams.id
                }, function (data) {
                    // init categories list
                    data.categories[0] = data.categories[0] || { Name: '' };

                    // set up hours view model
                    if (!data.openingHours)
                        data.openingHours = [];
                    if (data.openingHours.every(function (e) { return e.length == 0; }))
                        data.openingHours.type = 'none';
                    else if (data.openingHours.every(function (r) { return r.length == 1 && r[0].starts.hour == "12:00" && r[0].starts.period == "am" && r[0].ends.hour == "12:00" && r[0].ends.period == "pm"; }))
                        data.openingHours.type = '24hr';
                    else
                        data.openingHours.type = 'weekly';

                    // Set up claim conflict - 0/1 were arbitrarily assigned to self/other conflict on the server
                    if (data.claimConflictType != null) {
                        data.isSelfConflict = data.claimConflictType == 0;
                        data.isOtherConflict = data.claimConflictType == 1;
					}

					// read operation status changes into the view model flag
					if (data.operations)
						data.isSuspendedOperation = data.operations.Status == 1; // temp closed
                }) :
                // init empty subscription instance, even though it shouldn't hit this controller without an id
                { categories: [{ Name: '' }] };
        }
        getModel();

        $scope.states = States.query();

        $scope.setHoursType = function (type) {
            function createAlwaysOpen() {
                return [0, 1, 2, 3, 4, 5, 6].map(function () { return [{ starts: { hour: "12:00", period: "am" }, ends: { hour: "12:00", period: "am" } }]; });
            }
            function createAlwaysClosed() {
                return [0, 1, 2, 3, 4, 5, 6].map(function () { return []; });
            }

            switch (type) {
                case 'none':
                    $scope.subscription.openingHours = [];
                    break;
                case '24hr':
                    $scope.subscription.openingHours = createAlwaysOpen();
                    break;
                default:
                    $scope.subscription.openingHours = createAlwaysClosed();
                    break;
            }
            $scope.subscription.openingHours.type = type;
        }
		
        // call this function *only* in use cases in which the identifying information has not changed
		$scope.updateDetails = function () {
			var args = angular.copy($scope.subscription);
			if (args.isSuspendedOperation)
				args.operations = { Status: 1 }; // temp closed
			else
				args.operations = null;
            Subscriptions.post({}, args, function (data, headers) {
                getModel();
            });
        };

        $scope.open = function () {
            var args = angular.copy($scope.subscription);
            args.isClosed = false;
            Subscriptions.post({ id: $scope.subscription.id }, args, function (data, headers) {
                getModel();
            });
        }

        $scope.close = function () {
            $modal.open({
                templateUrl: 'confirm-close.html',
                controller: $modal.controller(['$scope', '$modalInstance', 'subscription', function ($scope, $modalInstance, subscription) {
                    $scope.cancel = function () {
                        $modalInstance.dismiss('cancel');
                    }
                    $scope.confirm = function () {
                        Subscriptions.post({ id: subscription.id }, { id: subscription.id, isClosed: true }, function (data, headers) { subscription.isClosed = true; });
                        $modalInstance.dismiss('cancel');
                    }
                }]),
                resolve: {
                    subscription: function () { return $scope.subscription; }
                }
            });
        }

		// show a confirmation dialog, calling release on confirmation
        $scope.confirmRelease = function () {
            $modal.open({
                templateUrl: 'confirm-release.html',
				controller: $modal.controller(['$scope', '$modalInstance', 'callback', function ($scope, $modalInstance, callback) {
                    $scope.cancel = function () {
                        $modalInstance.dismiss('cancel');
                    }
                    $scope.confirm = function () {
						callback();
                        $modalInstance.dismiss('cancel');
                    }
                }]),
                resolve: {
					callback: function () { return $scope.release; }
                }
            });
		}

		// release is implemented as delete, which releases the claim (but also sets the document delete flag)
		$scope.release = function () {
			Subscriptions.delete({ id: $scope.subscription.id }, function (data, headers) {
				$location.path('/subscriptions');
			});
		}

        $scope.validate = function (fieldset) {
            return fieldset.every(function (field) {
                return $scope.listingForm[field].$valid;
            });
        };

        $scope.isErrorState = function (errors) {
            var highestErrorCode = 1;
            angular.forEach(errors, function (obj) {
                highestErrorCode = obj.code > highestErrorCode ? obj.code : highestErrorCode;
            });
            return highestErrorCode == 4; //validity states: 4 - error, 3 - warning, 2 - informational
        };
    }])
    .controller('SubscriptionScoreCtrl', ['$scope', '$modal', 'ProfileScore', function ($scope, $modal, ProfileScore) {
        $scope.showScoreInfo = function () {
            $modal.open({
                templateUrl: 'completeness-score.html',
                controller: $modal.controller(['$scope', '$modalInstance', function ($scope, $modalInstance) { $scope.dismiss = function () { $modalInstance.dismiss('cancel'); } }]),
                resolve: {}
            });
        }

        $scope.getScore = function (s) {
            return ProfileScore.get(s);
        }
    }])
    .controller('SubscriptionsDistributionCtrl', ['$scope', '$modal', function ($scope, $modal) {
        $scope.showDistributionCountInfo = function () {
            $modal.open({
                templateUrl: 'distribution-counts.html',
                controller: $modal.controller(['$scope', '$modalInstance', function ($scope, $modalInstance) { $scope.dismiss = function () { $modalInstance.dismiss('cancel'); } }]),
                resolve: {}
            });
        }
    }])
    .controller('SubscriptionWhyClaimCtrl', ['$scope', '$modal', '$window', function ($scope, $modal, $window) {
        $scope.showWhyClaimDialog = function (subscription, profile) {
            $modal.open({
                templateUrl: 'why-claim.html',
				controller: $modal.controller(['$scope', '$modalInstance', '$controller', function ($scope, $modalInstance, $controller) {
                    $scope.subscription = subscription;
                    $scope.profile = profile;
                    $scope.claim = function () {
						$modalInstance.close();
                    };
                }])
			}).result.then(function () {
				// on success, invoke the .open() function from the parent scope, which
				// is assumed to be the scope defined by CheckoutCtrl
				$scope.open();
            });
        };
    }])
    .controller('MoveListingCtrl', ['$scope', '$routeParams', '$location', 'MoveListing', 'AlertQueue', function($scope, $routeParams, $location, MoveListing, AlertQueue) {
        $scope.listingId = $routeParams.id;
        $scope.originalListingId = $routeParams.id;

        $scope.move = function () {
            MoveListing.put(
            { id : $routeParams.id }, 
            JSON.stringify($scope.listingId)) // Read as a raw JSON string on the server
            .$promise.then(
                function(data) {
                    if (!data.error) {
                        $location.path('/subscriptions/' + $scope.listingId + '/details');
                    }
                    else {
                        GenerateErrorMessage(data.error);
                    }
                });
        };
        
        var GenerateErrorMessage = function(error) {
            var message = '';
            if (error === 'Duplicate') {
                message = 'The specified listing id already exists in your account.';
            }
            // InvalidFormat is already handled by angularjs regex validation
            else if (error === 'NotFound') {
                message = 'The original listing id does not exist.';
            }

            AlertQueue.push( { type: 'danger', title: 'Edit listing id', message: message });
        };

	}]);

angular.module('localezeDirectives')
	// render a hyperlink to a listing's public profile (if it exists)
	.directive('publicLink', ['$http', function ($http) {
		return {
			restrict: 'E',
			scope: { interpretation: '=interpretation' },
			template:
				'<a ng-disabled="!hasPublicVersion" class="btn btn-sm btn-default" ng-class="{ \'btn-info\' : hasPublicVersion }" ng-href="/directory/us/?id={{ interpretation.pid }}" target="_blank">' +
				'<i class="fa fa-fw fa-share"></i>' +
				'View Public Profile' +
				'</a>',
			link: function (scope, element, attrs) {
				// watch the interpretation, which may be lazy-loaded
				function updateLink() {
					if (scope.interpretation && scope.interpretation.pid) {
						// get the response code the directory would return if we requested this; 200 means it's there
						$http({ method: 'head', url: '/directory/us/?id=' + scope.interpretation.pid })
							.success(function (data) { scope.hasPublicVersion = true; });
					}
				}

				scope.$watch(
					function () { return scope.interpretation; },
					function () { updateLink(); }
				);
			}
		}
	}]);

BaseSubscriptionCtrl.$inject = ['Subscriptions', 'States', '$scope', '$modal', '$location'];
/**
 * Controls the base functionality of submitting a listing's primary details
 */
function BaseSubscriptionCtrl(Subscriptions, States, $scope, $modal, $location) {
    $scope.states = States.query();
	
    // Call this function in *any* use case in which the identifying information may have changed
    $scope.update = function () {

        if ($scope.listingForm.$invalid) {
            // mark the fields that are invalid so user knows what to correct
            angular.forEach($scope.listingForm.$error, function (field) {
                angular.forEach(field, function (errorField) {
                    errorField.$setDirty();
                })
            });
            return;
        }

		function upsertListing(listing)
		{
			var isAddOperation = !listing.id; // listing is new if it doesn't have a system-provided id

			function getRedirectUrl(response)
			{
				if (isAddOperation)
					return "/subscriptions/" + response.id + "/review";
				else
					return "/subscriptions/" + response.id + "/details";
			}

			Subscriptions.post($scope.subscription, function (response) {
				$location.path(getRedirectUrl(response));
			});
		}

        // Trying to add a new listing 
        // we require clientIds (listingIds) for all adds EXCEPT if the user is ecommerce (the client id is generated server side in that case)
        if (!$scope.subscription.id && $scope.subscription.clientId) {
            $scope.query = Subscriptions.options({ id: $scope.subscription.clientId }, function (data, headers) {
                var allow = headers('Allow');

                // Already own the listing, so not allowed to add
                if (allow != null && (allow.indexOf('GET') !== -1 || allow.indexOf('DELETE') !== -1)) {
                    $modal.open({
                        templateUrl: 'duplicate-listing-id-on-add.html',
                        controller: $modal.controller(['$scope', '$modalInstance', 'subscription', function ($scope, $modalInstance, subscription) {
                            $scope.dismiss = function () {
                                $modalInstance.dismiss('cancel');
                            };
                            $scope.edit = function () {
                                $modalInstance.dismiss('cancel');
                                $location.path("/subscriptions/" + subscription.clientId + "/details");
                            };
                        }]),
                        resolve: {
                            subscription: function () { return $scope.subscription; }
                        }
                    });
                }
				else
				{
					upsertListing($scope.subscription);
                }
            });
        }
        // Update existing listing or add listing w/o id (only allowed if the user is an ecommerce user)
		else
		{
			upsertListing($scope.subscription);
        }
    }
}

SubscriptionCtrl.$inject = ['$scope', '$controller', '$modal', '$routeParams', 'Subscriptions'];
/**
 * Controls adding/editing a listing's primary details through the managed listings page
 */
function SubscriptionCtrl($scope, $controller, $modal, $routeParams, Subscriptions) {
    $controller('BaseSubscriptionCtrl', { $scope: $scope, Subscriptions : Subscriptions });

    function getModel() {
        $scope.subscription = $routeParams.id ?
            Subscriptions.get({ id: $routeParams.id }, function (data) {
                // init categories list
                data.categories[0] = data.categories[0] || { Name: '' };
            }) :
            {
                categories: [{ Name: '' }],
            };	// init empty subscription instance
    }
    getModel();

    $scope.localitySelected = function ($item, $model, $label, $event) {
    	$scope.subscription.city = $item.Locality;
    	$scope.subscription.state = $item.Region;
    }
}

SubscriptionClaimCtrl.$inject = ['$scope', '$controller', '$routeParams', 'Listings'];
/**
 * Controls adding a new listing's primary details through the directory
 */
function SubscriptionClaimCtrl($scope, $controller, $routeParams, Listings) {
    $controller('BaseSubscriptionCtrl', { $scope: $scope });

    $scope.getModel = function (){
        if ($routeParams.id) {
            Listings.get({
                id: $routeParams.id
            }).$promise.then(
                function (data) {
                    data.categories[0] = data.categories[0] || { Name: '' };
                    data.id = null; // this service returns the pid as the listing id, which we don't want
                    $scope.subscription = data;
                }
           );
        }
        else {
            $scope.subscription = {
                categories: [{ Name: '' }]
            };
        };	// init empty subscription instance
    }
    $scope.getModel();
};angular.module('localezeControllers')
	.controller('SubscriptionsCtrl', ['$scope', '$filter', '$location', 'Subscriptions', 'Claims', 'AlertQueue', function ($scope, $filter, $location, Subscriptions, Claims, AlertQueue) {
		$scope.listings = [];
		$scope.view = [];

		$scope.predicate = 'PartitionId';
		$scope.reverse = true;
		$scope.take = 25;
		$scope.skip = 0;
		
		function getEmptyFilter() {
			return {
				isClaimed: null,
				isClosed: null,
				isValid: null,
				isConflict: null,
				isDuplicate: null,
				isAvailable: null,
				predicate: function (e) {
					var filterText = $scope.filter.query;
					if (filterText)
						filterText = filterText.toLowerCase();

					return ($scope.filter.isClaimed == null || e.IsClaimed == $scope.filter.isClaimed) &&
						/* consider permanently closed listings in a warning state to be valid for the purposes of this UX */
						($scope.filter.isValid == null || ((e.ValidationState == 1 || !!(e.Closure == 1)) == $scope.filter.isValid)) &&
						($scope.filter.isClosed == null || (!!e.Closure == $scope.filter.isClosed)) &&
						($scope.filter.isConflict == null || (e.ClaimantKind == 1) == $scope.filter.isConflict) &&
						($scope.filter.isDuplicate != true || e.DuplicatesId) &&
						($scope.filter.isAvailable == null || e.IsAvailable == $scope.filter.isAvailable) &&
						(
							!filterText ||
							(
								(e.Name || '').toLowerCase().indexOf(filterText) >= 0 ||
								(e.Street || '').toLowerCase().indexOf(filterText) >= 0 ||
								(e.City || '').toLowerCase().indexOf(filterText) >= 0 ||
								(e.Zip || '').indexOf(filterText) >= 0 ||
								(e.Phone || '').indexOf(filterText) >= 0 ||
								(e.ID + '').toLowerCase().indexOf(filterText) >= 0
							)
						);
				}
			};
		}

		$scope.filterPresets = [{
			name: "All Listings",
			cfg: function () {
				$scope.filter = getEmptyFilter();
			}
		}, {
			name: "Claimed Listings",
			cfg: function () {
				$scope.filter = getEmptyFilter();
				$scope.filter.isClaimed = true;
			}
		}, {
			name: "Closed Listings",
			cfg: function () {
				$scope.filter = getEmptyFilter();
				$scope.filter.isClosed = true;
			}
		}, {
			name: "Require Action",
			cfg: function () {
				$scope.filter = getEmptyFilter();
				$scope.filter.isValid = false;
				$scope.filter.isClosed = null;
				$scope.predicate = 'ValidationState'; // sort by severity, desc
				$scope.reverse = true;
			}
		}, {
			name: "Conflicts",
			cfg: function () {
				$scope.filter = getEmptyFilter();
				$scope.filter.isConflict = true;
			}
		}, {
			name: "Duplicates",
			cfg: function () {
				$scope.filter = getEmptyFilter();
				$scope.filter.isDuplicate = true;
			}
		}, {
			name: "Available Listings",
			cfg: function () {
				$scope.filter = getEmptyFilter();
				$scope.filter.isAvailable = true;
			}
		}];

		$scope.filterChanged = function (preset) {
			if (preset) {
				// sort alphabetically on name on filter changed (by default)
				$scope.predicate = 'Name';
				$scope.reverse = false;

				preset.cfg();
				$scope.filterPreset = preset;
			}

			$scope.skip = 0;

			$scope.view = $filter('orderBy')(
				$filter('filter')($scope.listings, $scope.filter.predicate),
				$scope.predicate,
				$scope.reverse
			);
		}

		// accept a filter from the URI, if one is specified
		var defaultFilter = $scope.filterPresets[1]; // open/claimed
		if (typeof ($location.search().filter) == "string")
			defaultFilter = $scope.filterPresets.find(function (p) { return p.name == $location.search().filter; }) || defaultFilter;

		$scope.filterChanged(defaultFilter);

		$scope.sortChanged = function ()
		{
			$scope.skip = 0;

			$scope.view = $filter('orderBy')(
				$scope.view,
				$scope.predicate,
				$scope.reverse
			);
		}

		$scope.nextPage = function () {
			$scope.skip += $scope.take;
		}

		$scope.prevPage = function () {
			$scope.skip -= $scope.take;
		}

		$scope.min = function (a, b) {
			return a < b ? a : b;
		}

		Subscriptions.queryAll($scope, function (data) {
			$scope.listings = $scope.listings.concat(data);

			$scope.filterChanged(); // update the view
		});

		$scope.onExport = function () {
			AlertQueue.push({
				status: 200, type: 'info', title: 'Listings Exported', message: 'Your listing export file is now downloading.'
			});
		};

		$scope.select = function (id) {
			$location.path("/subscriptions/" + id + "/details");
		};

		$scope.claimAllInView = function () {
			Claims.post({}, $scope.view.map(function (v) { return v.ID }), function (data, headers) {
				AlertQueue.push({
					type: 'info',
					title: 'Accepted',
					message: 'Your request for claims has been sent.'
				});
			});
		};
	}]);

angular.module('localezeDirectives')
	/* a toggle on e.g. table headers that sorts on some field, with optional reverse */
	.directive('sortToggle', [function () {
		return {
			restrict: 'E',
			transclude: true,
			template:
			'<span class="no-select" ng-click="toggle()" ng-transclude></span>' +
			' <i ng-show="$parent.predicate == field" ng-click="toggle()" class="fa" ng-class="{ \'true\' : \'fa-caret-down\', \'false\' : \'fa-caret-up\'}[$parent.reverse]"></i>',
			scope: {

			},
			link: function (scope, element, attrs) {
				scope.field = attrs['field'];

				scope.toggle = function () {
					if (scope.$parent.predicate === scope.field) {
						scope.$parent.reverse = !scope.$parent.reverse;
					}
					else {
						scope.$parent.predicate = scope.field;
						scope.$parent.reverse = false;
					}
				}
			}
		};
	}])
	/* a three-way switch, like a checkbox for bool? */
	.directive('threeWaySwitch', [function () {
		return {
			restrict: 'E',
			template: '<span class="no-select" ng-click="next()"><i class="fa fa-fw" ng-class="class"></i> {{ label }}</span>',
			scope: {
				value: '='
			},
			link: function (scope, element, attrs) {
				scope.label = attrs['label'];
				
				scope.updateClass = function () {
					if (scope.value == null) {
						scope.class = "fa-minus-square-o";
					} else if (scope.value == true) {
						scope.class = "fa-check-square-o";
					} else {
						scope.class = "fa-square-o";
					}
				}

				scope.next = function () {
					if (scope.value == null) {
						scope.value = true;
					} else if (scope.value == true) {
						scope.value = false;
					} else {
						scope.value = null;
					}
					scope.updateClass();
				}

				scope.updateClass();

				scope.$watch(
					function () { return scope.value; },
					function () { scope.updateClass(); }
				);
			}
		};
	}]);;angular.module('localezeControllers')
    .controller('SuggestEditCtrl', ['$scope', '$window', '$routeParams', 'States', 'Listings', 'SuggestedEdits', 'VerifyPhone', 'AlertQueue',
        function ($scope, $window, $routeParams, States, Listings, SuggestedEdits, VerifyPhone, AlertQueue) {
		    $scope.states = States.query();

		    $scope.listing =
			    Listings.get({
				    id: $routeParams.id
			    }, function (data) {
				    data.categories[0] = data.categories[0] || { Name: '' };
				    data.id = null; // this service returns the pid as the listing id, which we don't want
			    }, function (response) {
				    if(response.status === 404)
					    $window.location.href = '/directory/us'; // no such listing
			    });

		    $scope.submit = function () {
                SuggestedEdits.post({}, $scope.listing, function (response) {
                    if (!response.success)
                        AlertQueue.push({ type: 'danger', message: response.message });
                    else 
						$window.location.href = '/manage/#/subscriptions';
			    });
            };

            $scope.callMe = function () {
                VerifyPhone.post({ phone: $scope.listing.phone }, function (response) {
                    if (!response.success) {
                        AlertQueue.push({ type: 'danger', message: response.message });
                    }
                    else {
                        AlertQueue.push({ type: 'info', message: response.message })
                    }
                });
            };
        }]
    );;angular.module('localezeControllers')
	.controller('SuperUserCtrl', ['$scope', '$window', '$location', 'Companies', 'Accounts', function ($scope, $window, $location, Companies, Accounts)
	{
		$scope.init = function () {
			if ($scope.canChangeCompany) {
				// load the impersonated company and its accounts
				Companies.get({ id: $scope.impersonatedUser.Company.CompanyID }).$promise.then(function (company) {
					$scope.company = company;
				});
				$scope.accounts = Accounts.query({ companyid: $scope.impersonatedUser.Company.CompanyID });
			} else {
				// just load the accounts for the current company
				$scope.accounts = Accounts.query({ companyid: 'current' });
			}
		}

		// find companies matching a name filter
		$scope.findCompaniesLike = function (filter) {
			return Companies.query({ name: filter, serviceStatus: 1 }).$promise;
		}

		// when a new company is selected, load its accounts
		$scope.companySelected = function () {
			$scope.impersonatedUser = null;
			$scope.accounts = Accounts.query({ companyid: $scope.company.CompanyID }).$promise.then(function (accounts) {
				$scope.impersonatedUser = accounts[0];
				$scope.accountSelected();
			});
		}

		// when an account is selected, set the impersonation cookie and reload the page (as them)
		$scope.accountSelected = function () {
			document.cookie = "superuseraccountid=" + $scope.impersonatedUser.AccountId + ";" + "Secure;Path=/; Domain=" + $location.host();
			$window.location.reload();
		};
	}]);;angular.module('localezeFilters')
	// {{ somePastDate | timeAgo }} -> "4 hours ago"
	.filter('timeAgo', function () {
		/*
		 * JavaScript Pretty Date
		 * Copyright (c) 2011 John Resig (ejohn.org)
		 * Licensed under the MIT and GPL licenses.
		 */
		return function (time) {
			var date = new Date(time),
				diff = (((new Date()).getTime() - date.getTime()) / 1000);

			var isFuture = diff < 0;
			var postposition = isFuture ? "from now" : "ago";
			diff = Math.abs(diff);
			day_diff = Math.floor(diff / 86400);

			if (isNaN(day_diff)) {
				return;
			}

			return day_diff == 0 && (
					diff < 60 && "just now" ||
					diff < 120 && "1 minute " + postposition ||
					diff < 3600 && Math.floor(diff / 60) + " minutes " + postposition ||
					diff < 7200 && "1 hour " + postposition ||
					diff < 86400 && Math.floor(diff / 3600) + " hours " + postposition
				) ||
				day_diff == 1 && (isFuture ? "tomorrow" : "yesterday") ||
				day_diff < 7 && day_diff + " days " + postposition ||
				Math.ceil(day_diff / 7) + " weeks " + postposition;
		};
	});;angular.module('localezeResources', ['ngResource'])
	.factory('Accounts', ['$resource', function ($resource) {
	    return $resource('/api/companies/:companyid/accounts',
            {
                companyid: '@companyid'
            },
            { 'query': { method: 'get', isArray: true } }
        );
	}])
	// account 'names' are actually '{company name} {account name} {account id}'.  this strips off the account id for display :/
	.filter('accountName', function () {
	    return function (n) {
	        return (n || '').replace(/\s\d+$/, '');
	    }
	})
	.factory('Account', ['$resource', 'AlertQueue', function ($resource, AlertQueue) {
	    var Account = $resource('/manage/api/accounts/:id', { id: "@id" }, { get: { method: 'GET' }, put: { method: "PUT" } });

	    var p = Account.put;
	    Account.put = function (params, payload, success, error) {
	        return p(
				params,
				payload,
				function (data, headers) {
				    AlertQueue.push({
				        status: 200, type: 'success', title: 'Changes Saved', message: 'Your account information has been updated.'
				    });
				    if (success) return success(data, headers);
				},
				error
			);
	    };

	    return Account;
	}])
	.factory('Companies', function ($resource) {
		return $resource(
			'/api/companies/?name=:name',
			{
				name: '@name'
			},
			{
				query: { method: 'get', isArray: true }
			}
		);
	})
    .factory('Token', ['$resource', function ($resource) {
        return $resource('/manage/api/tokens/', {  }, { get: { method: 'GET' } });
    }])
    .factory('Checkout', ['$resource', function ($resource) {
        return $resource('/api/checkout/', {}, { get: { method: 'GET' } });
    }])
	.factory('Categories', ['$resource', 'Groups', function ($resource, Groups) {
	    var Categories = $resource('/api/categories', {}, { query: { method: 'GET', isArray: false, cache: true } });

	    function addGroupResource(categories) {
	        for (var i = 0; i < categories.length; i++) {
	            categories[i].id = categories[i].Id; // to do: pick a casing!
	            categories[i].name = categories[i].Name;
	            var index = categories[i].Id; // must close over this for the following:
	            categories[i].getGroups = function () { return Groups.query({ id: index }); }
	        }
	    }

	    var query = Categories.query;
	    Categories.query = function (params, success, error) {
	        return query(
				params,
				function (data, headers) {
				    addGroupResource(data.Categories);
				    if (typeof (Storage) !== "undefined")
				        try { localStorage.setItem("categories", JSON.stringify(data)); } catch (e) { }
				    if (success) success(data, headers);
				},
				error
			)
	    };

	    return Categories;
	}])
	.factory('Claims', ['$resource', function ($resource) {
		var claims = $resource('/manage/api/claims',
			{ /* none yet. stub for claim-by-id endpoint. */ },
			{ post: { method: 'POST' } }
		);

		return claims;
	}])
	.factory('Conflicts', ['$resource', function ($resource) {
	    return $resource('/manage/api/subscriptions/:id/conflicts', { id: '@id' }, { post: { method: 'POST' } });
	}])
	.factory('DomainEvents', ['$resource', function ($resource) {
	    return $resource('/api/domainevents/', {}, {
	        query: { method: 'GET', isArray: false }
	    });
	}])
    .factory('AggregatedEvents', ['$resource', function ($resource) {
        var AggregatedEvents = $resource(
			// REST service URI prototype
			'/api/aggregatedevents/:id?page=:page&machine=:machine',
			// parameter binding
			{
			    id: '@id',
			    page: '@page',
			    machine: '@machine'
			},
			{
			    'query': { method: 'GET', isArray: true }
			}
		);

        return AggregatedEvents;
    }])
	.factory('Groups', ['$resource', function($resource) {
	    return $resource('/api/categories/:id/groups', { id : '@id' }, { query: { method: 'GET', isArray: true, cache: true } });
	}])
    .factory('Listings', ['$resource', function ($resource) {
    	return $resource('/manage/api/listings/:id/', { id: '@id' }, { query: { method: 'GET' } });
    }])
    .factory('Localities', ['$resource', function ($resource) {
    	return $resource('/api/localities/?id=:id', { id: '@id' }, { query: { method: 'GET', isArray : true } });
    }])
	.factory('Messages', ['$resource', function ($resource) {
	    return $resource('/api/messages/:id',
			{ id: "@id" },
			{
			    query: { method: 'GET', cache: false, isArray: false }
			});
	}])
	.factory('PasswordReset', ['$resource', function ($resource) {
		var PasswordReset = $resource('/manage/api/accounts/:username/password',
			{
				username : '@username'
			}, {
				post: { method: 'POST', isArray: false }
			});

		return PasswordReset;
	}])
    .factory('Validation', ['$resource', function ($resource) {
        var result = $resource('/manage/api/validate?type=:type&input=:url',
        {
            type: '@type',
            url: '@url'
        },
        {
            options: {
                method: 'options',
            }
        });
        return result;
    }])
	.factory('Profile', ['$resource', function ($resource) {
		var profile = $resource('/api/users/current');
		var get = profile.get;
		profile.get = function (params, success, error) {
			return get(
				params,
				function (p, headers) {
					// true iff the given profile has a feature with the given id
					p.hasFeature = function (featureId)
					{
						return (p.features || []).some(function (f) { return f.id == featureId; });
					};
					if (success) success(p, headers);
				},
				error
			);
		};
		return profile;
	}])
	/* renewal resource on a given subscription */
	.factory('Renewal', ['$resource', function ($resource) {
		var Renewal = $resource('/manage/api/subscriptions/:id/renewal', { id: '@id' }, { put: { method: "PUT" }, 'delete': { method: "DELETE" } });
		return Renewal;
	}])
	.factory('States', ['$resource', function ($resource) {
	    var States = {};

	    States.query = function (args, success, error) {
	    	return ["AK", "AL", "AR", "AZ", "CA", "CO", "CT", "DC", "DE", "FL", "GA", "HI", "IA", "ID", "IL", "IN", "KS", "KY", "LA", "MA", "MD", "ME", "MI", "MN", "MO", "MS", "MT", "NC", "ND", "NE", "NH", "NJ", "NM", "NV", "NY", "OH", "OK", "OR", "PA", "RI", "SC", "SD", "TN", "TX", "UT", "VA", "VT", "WA", "WI", "WV", "WY"];
	    };

	    return States;
	}])
	.factory('Session', ['$resource', function ($resource) {
		return $resource('/manage/api/session', {}, {
			'put': { method: 'put' },
			'delete': { method: 'delete' },
		});
	}])
    // moves a listing from one resource identifier to another
    .factory('MoveListing', ['$resource', function ($resource) {
        return $resource('/manage/api/listings/:id/move', 
            { id: '@id' },
            { put : { method: 'put' } });
    }])
	.factory('Subscriptions', ['$resource', 'AlertQueue', 'Renewal', function ($resource, AlertQueue, Renewal) {
		var Subscriptions = $resource('/manage/api/subscriptions/:id?partitionId=:partitionId&take=:take',
			{
			    id: "@id",
			    partitionId: "@partitionId",
				take: "@take"
			},
			{
			    post: { method: 'POST' },
			    query: { method: 'GET', isArray: true },
			    'delete': { method: 'DELETE' },
			    options: { method: 'OPTIONS' }
			});
            
		var g = Subscriptions.get;
		Subscriptions.get = function (params, success, error) {
			return g(
				params,
				function (data, headers) {
					if (data) {
						data.Renewal = new Renewal();
						if (data.errors) {
							// report only severe errors for closed listings
							var toReport = data.errors.filter(function (e) { return !data.isClosed || e.code === 4; });

							// push listing validation errors into the alert queue
							for (var i = 0; i < toReport.length; i++) {
								AlertQueue.push({
									type: 'warning',
									title: 'There is a problem with this listing',
									message: toReport[i].message
								});
							}
						}
					}
					if (success) return success(data, headers);
				},
				error
			);
		};

		var q = Subscriptions.query;
		Subscriptions.query = function (params, success, error) {
			return q(
				params,
				function (data, headers) {
					var subscriptions = [];
				    if (data && data.length) {
				    	for (var i = 0; i < data.length; i++) {
				    	    // explode the csv data into objects; see IListingViewService for format information
				    		var fieldDelimiter = '^~';
							var csv = data[i].split(fieldDelimiter);
							var subscription = {
								PartitionId: csv[0], // this value is numeric, but too large to be accurately represented in javascript
								ID: csv[1],
								Name: csv[2],
								Street: csv[3],
								City: csv[4],
								State: csv[5],
								Zip: csv[6],
								Phone: csv[7],
								IsPaid: csv[8] == "1",
								IsClaimed: csv[8] != "",
								SubscriptionEnd: csv[9],
								IsAutoRenew: csv[10] == "1",
								Closure: Number(csv[11]), /* 0 = open, 1 = permanent dissolution, 2 = temporary closure */
								ValidationState: Number(csv[11]) === 1 && Number(csv[12]) === 3 ? /* if permanently closed and in a warning state... */
									1 : /* redact warnings for closed listings */
									Number(csv[12]), /* otherwise, use the stored validation state */
								Score: Number(csv[13]),
								// remediations (unused in this implementation)
								IsResolved: csv[15] == "1",
								ClaimantKind: csv[16] != '' ? csv[16] : null,
								DuplicatesId: csv[17]
							}

							subscription.IsAvailable = subscription.IsResolved &&
								subscription.ClaimantKind == null &&
								subscription.ValidationState != 4 && // 4 = error
								!subscription.Closure;

							subscription.Renewal = new Renewal();

							subscriptions.push(subscription);
						}
					}
					if (success) return success(subscriptions, headers);
				},
				error
			);
		};

		var p = Subscriptions.post;
		Subscriptions.post = function (params, payload, success, error) {
			return p(
				params,
				payload,
				function (data, headers) {
					AlertQueue.push({
						status: 200, type: 'success', title: 'Changes Saved', message: 'Your edits to your listing have been saved.'
					});
					if (success) return success(data, headers);
				},
				error
			);
		};

		// a loader which will query all pages of data, sending them to callback, until the route is changed
		Subscriptions.queryAll = function (scope, callback) {
			var isCancelLoad = false;
			var firstPartitionId = '18446744073709551615'; // max unsigned int64
			(function loadPage(partitionId) {
				var batchSize = partitionId == firstPartitionId ? 1000 : 5000; // optimize initial load time

				if (isCancelLoad) return;

				Subscriptions.query({
					partitionId: partitionId,
					take: batchSize
				}, function (data, headers) {
					callback(data);

					if (data.length == batchSize)
						loadPage(data[data.length - 1].PartitionId);
				});
			})(firstPartitionId);

			scope.$on("$routeChangeStart", function (event, next, current) {
				isCancelLoad = true;
			});
		};

		return Subscriptions;
	}])
	.factory('SubscriptionsAnalytics', ['$resource', function ($resource) {
	    return $resource('/manage/api/subscriptions/analytics');
	}])
    .factory('Users', ['$resource', function ($resource) {
    	return $resource('/api/userprofile/?accountId=:accountId&userId=:userId',
            {
                accountId: '@accountId',
                userId: '@userId'
            },
			{
			    'put': { method: 'put' },
			    'options': {
			        method: 'options',
			        debounceMS: 250,
			        abortPending: true
			    }
			});
    }])
    .factory('UserPermissions', ['$resource', function ($resource) {
    	return $resource('/api/userpermissions/:policy',
            {
                policy: '@policy'
            },
            {
                'post': { method: 'post' }
            });
    }])
	.factory('SuggestedEdits', ['$resource', function ($resource) {
		return $resource(
			'/manage/api/suggestededits/',
			{},
			{
				post: { method: 'POST' }
			}
		);
    }])
    .factory('VerifyPhone', ['$resource', function ($resource) {
        return $resource(
            '/manage/api/phone/:phone/verify',
            {
                phone: '@phone'
            },
            {
                post: { method: 'POST' }
            }
        );
    }])
    .factory('ChangeMessages', ['$resource', function ($resource) {
        return $resource('/manage/api/changesets/:id/members',
            {
                id: '@id'
            },
            {
                'get': { method: 'get', isArray: true }
            }
        );
    }])
	.factory('ChangeSets', ['$resource', function ($resource) {
		return $resource('/manage/api/changesets/:id',
			{
				id: '@id'
			},
			{
				'post': { method: 'post' },
				'enqueue': { method: 'post', url: '/manage/api/changesets/:id/members' }
			}
		);
	}])
	.factory('PaymentCommands', ['$resource', function ($resource) {
		return $resource(
			'/manage/api/listings/:id/payment',
			{
				id: '@id'
			},
			{
				put: { method: 'PUT' }
			}
		);
	}]);
;angular.module('localezeResources')
	.factory('ConflictMessages', ['$resource', function ($resource) {
		var ConflictMessages = $resource(
			'/manage/api/queues/:id/conflicts',
			{
				id: '@id'
			},
			{
			    'query': { method: 'get', isArray: true },
                'post': { method: 'post' }
			}
		);

		return ConflictMessages;
	}]);;angular.module('localezeControllers')
	.controller('AuthorInfoCtrl', ['$scope', '$location', 'AuthorInfo', function ($scope, $location, AuthorInfo) {
		$scope.pid = /(\d+)\/?\s*$/.exec($location.absUrl())[1];

		$scope.authorInfo = AuthorInfo.get({ id: $scope.pid });
	}]);

angular.module('localezeResources')
	.factory('AuthorInfo', ['$resource', function ($resource) {
		return $resource(
			'/directory/api/authorInfo/:id',
			{
				id: '@id'
			},
			{
				get: { method: 'GET', isArray: false }
			}
		);
	}]);;angular.module('localezeControllers')
	.controller('OwnerInfoCtrl', ['$scope', '$location', 'OwnerInfo', function ($scope, $location, OwnerInfo) {
		$scope.pid = /(\d+)\/?\s*$/.exec($location.absUrl())[1];

		$scope.ownerInfo = OwnerInfo.get({ id: $scope.pid });
	}]);

angular.module('localezeResources')
	.factory('OwnerInfo', ['$resource', function ($resource) {
		return $resource(
			'/directory/api/ownerInfo/:id',
			{
				id: '@id'
			},
			{
				get: { method: 'GET', isArray: false }
			}
		);
	}]);;angular.module('localezeResources')
	.factory('CloseByPid', ['$resource', function ($resource) {
		return $resource('/directory/api/close/:id',
			{
				id: '@id'
			},
			{
				post: { method: 'post' }
			}
		);
	}]);

angular.module('localezeDirectives')
	// e.g. <close-button pid="123">Close</close-button>
	.directive('closeButton', ['$window', 'CloseByPid', function ($window, CloseByPid) {
		return {
			replace: true,
			scope: { pid: '=pid' },
			link: function (scope, element, attrs) {
				element.click(function () {
					CloseByPid.post({ id: scope.pid }, {})
						.$promise.then(function (response) {
							$window.location.href = '/manage/#/subscriptions/' + response.Id + '/details'
						});
				});
			}
		};
	}]);

angular.module('localezeControllers')
	.controller('DirectorySubscriptionCtrl', ['$scope', '$http', '$location', '$modal', function ($scope, $http, $location, $modal) {
		$scope.pid = /(\d+)\/?\s*$/.exec($location.absUrl())[1];

		$scope.subscription = { $resolved: false };

		$http({ method: 'get', url: '/directory/api/subscriptions/' + $scope.pid })
			.success(function (data) {
				$scope.subscription = data;
				$scope.subscription.$resolved = true;
			});

		$http({ method: 'get', url: '/api/users/current/' })
			.success(function (data) {
				if (data) $scope.user = data;
			});
	}])
    .controller('WhyClaimInformationCtrl', ['$scope', '$modal', '$window', function ($scope, $modal, $window) {
        $scope.showWhyClaimDialog = function (id) {
            $modal.open({
                templateUrl: 'why-claim.html',
                controller: $modal.controller(['$scope', '$modalInstance', function ($scope, $modalInstance) {
                    $scope.dismiss = function () { $modalInstance.dismiss('cancel'); }
                    $scope.id = id;
                    $scope.claim = function () {
                        $scope.dismiss();
                        $window.location.href = '/manage/#/subscriptions/add/' + id;
                    };
                }]),
                resolve: {}
            });
        };
	}]);;