var ASSET2 = (function() {
  
  // mutate src strings that we know to be cyc uploads (change id or dim part).
  // we can't create URLs from scratch since we don't know the UPLOAD_SERVER
  // server uri may vary, by api part must be consistent:
  /*
  //myserve/abc/10x10_crop.png
  //otherserve/10x10.png
  //otherserve/10x10.png?b=000
  //wowserve/xyz/file.png
  */
  var SERV_NAME_FORMAT = /(\/\/[^\/]+\/)([^\.]+)\.([^\?]+)/;
  var W_H_METHOD = /^([^x]+)x([^_]+)_(.+)/;
  
  function s2o(s) {
    var o = {};
    var m;
    if (m = s.match(SERV_NAME_FORMAT)) {
      o.serv = m[1];
      var wh, nm = m[2].split('/');
      o.format = m[3];
      
      if (nm.length == 1) { // ph
        wh = nm[0].split('x');
      } else {
        o.id = nm[0];
        if (m = nm[1].match(W_H_METHOD)) {
          wh = [m[1], m[2]];
          o.method = m[3];
        }
      }
      if (wh) o.d = { w: parseInt(wh[0]), h: parseInt(wh[1]) };
    }
    return o;
  }
  
  function o2s(o) {
    //return o.id ? o.serv + o.id + '/' + (o.d ? o.d.w + 'x' + o.d.h + '_' + o.method : 'file') + '.' + o.format :
    return o.id ? o.serv + o.id + '/' + (o.d ? o.d.w + 'x' + o.d.h + '_' + o.method : 'file') + '.jpg' :
      o.serv + o.d.w + 'x' + o.d.h + '.png?b=ddd&f=aaa'; // ph only supports png, so don't use o.format here 
  }
  
  return {
    
    id: function(s) {
      return s2o(s).id;
    },
    
    rpId: function(s, id) {
      var o = s2o(s);
      o.id = id;
      o.method = o.method || 'crop'; // if s is a ph, no method will be set
      return o2s(o);
    },

    rpDim: function(s, d) {
      var o = s2o(s);
      o.d = d;
      return o2s(o);
    },
    
    ph: function(s, d) {
      var o = s2o(s);
      o.id = false;
      o.d = d;
      return o2s(o);
    }
  }
})();

(function() {
  
  addListener(document, 'DOMContentLoaded', init);
  addListener(window, 'load', init);
  
  function init(e, ev) {
    if (init.done != (init.done = true)) {

      for (var nl = gebCN(document.body, 'div', 'gallery'), i = 0; i < nl.length; i++)
        Gal.ld(nl[i]);
      
      for (var nl = gebCN(document.body, 'div', 'slides'), i = 0; i < nl.length; i++)
        Gal.ld(nl[i]);
      
      for (var nl = gebCN(document.body, 'sp', 'sumPane'), i = 0; i < nl.length; i++)
        initPane(nl[i]);
    }
  }

  // ----------------------------------------------------------------------------------------------
  // rollover summary panels:
  // in use on: oxdi, eagle, speedster2 (available, nut used live)

  function initPane(sp) {
    var o = {};
    o.sp = sp;
    o.ex = sp.nextSibling;
    o.an = sp.parentNode;
    o.im = sp.parentNode.firstChild;
    o.r = { w: o.im.width, h: o.im.height };
    o.i = -o.r.h;
    o.t = 7;
    o.k = o.r.h / 15; // px increment to cover height in 10 steps
    
    addEnterListener(o.an, true, function() {
      runTrans(o.sp, o.i, 0, 200, function(e, i) {
        o.sp.style.bottom = Math.round(o.i = i) + 'px';
      })
    });
    addEnterListener(o.an, false, function() {
      runTrans(o.sp, o.i, -o.r.h, 200, function(e, i) {
        o.sp.style.bottom = Math.round(o.i = i) + 'px';
      })
    });
    
    o.an.style.position = 'relative';
    o.an.style.overflow = 'hidden';
    o.sp.style.position = 'absolute';
    o.sp.style.left = '0';
    o.sp.style.bottom = o.i + 'px';
    o.sp.style.padding = '10px';
    o.sp.style.width = (o.r.w - 20) + 'px';
    o.sp.style.height = (o.r.h - 20) + 'px';
    o.sp.style.display = 'block';
    return o;
  }
  
  function upSumPane(o, on) {
    // if mid-way through another transition, use the current value, not the argument:
    var j = o.j = on ? 0 : -o.r.h;
    var d1 = +new Date();
    var id = setInterval(function () {
      if (j != o.j) // new transition tookover
        return clearInterval(id);
      var d = +new Date();
      var n = 15 - Math.round((d - d1) / o.t); // steps remaining
      o.i = o.j - Math.round(o.k * n * (on ? 1 : -1));
      if ((on && o.i > o.j) || (!on && o.i < o.j))
        o.i = o.j
      o.sp.style.bottom = o.i + 'px';
      if (o.i == o.j)
        clearInterval(id);
    }, o.t);
  }


  // ----------------------------------------------------------------------------------------------
  // galleries:

  window.Gal = function(e) {
    this.init(e);
    Gal.n = (Gal.n || 0) + 1;
    Gal.h = (Gal.h || {});
    Gal.h[e.g = Gal.n] = this;
  }

  Gal.ld = function(e) {
    return e.g ? Gal.h[e.g] : new Gal(e);
  }

  cp(Gal.prototype, {
    
    init: function(e) {
      this.e = e;
      this.initLists();
      this.initInfo();
      this.initButton('prev');
      this.initButton('next');
      this.initButton('togg');
    
      this.t = this.tns ? 'gallery' : 'slides';
      this.gal = true;
       
      if (this.zm) { // preload future srcs
        var s = this.zm.src;
        (new Image()).src = ASSET2.ph(s, this.zmR);
        for (var i = 0; i < this.n; i++)
          if (this.ids[i])
            (new Image()).src = ASSET2.rpId(s, this.ids[i]);
      }  
      this.addListeners();
      this.show(0, true); // so template needn't do overlay/url etc init
      this.n > 1 && this.togg && !Gal.noAutoPlay && this.start();
    },
        
    initLists: function() {
      var ims = this.e.getElementsByTagName('img');
      var imA = ims[0];
      var imX = ims[ims.length - 1];
      var cA = imA.parentNode.parentNode;
      var cB = imX.parentNode.parentNode;
      var o;

      this.N = ims.length;
    
      if (imA.parentNode.tagName != 'A') // non-save image must be zoom
        this.zm = ims[0];
      else {
        this.zms = cA.getElementsByTagName('a');
        this.zmE = cA;
      }

      this.zmR = getImgDim(imA);

      if (this.zm || cA != cB) {
        this.tns = cB.getElementsByTagName('a');
        this.tnE = cB;
        this.tnR = getImgDim(imX);
      }
      this.n = (this.tns || this.zms).length;
      
      var scList = gebCN(this.e, 'div', 'section');
      if (scList.length > 0) {
        this.scs = scList[0].parentNode;
        if (scList.length == this.n && scList.length == this.scs.childNodes.length) {
          //lg('found ' + scList.length + ' overlays');
          this.scs.style.zIndex = 1; // assume after zm, so just match it's stacking
        } else {
          this.scs = false;
          //lg('found section but it does not appear to be an overlay');
        }
      }
    },
  
    initInfo: function() {
      this.hasLink = [];
      this.hasText = [];
      this.ids = [];
      this.alts = [];
    
      for (var e, i = 0; i < this.n; i++) {
        e = (this.tns || this.zms)[i];
        this.hasLink[i] = e.getAttribute('data-url');
        this.hasText[i] = e.getAttribute('data-overlay') == 'true';
        e = e.firstChild || e;
        this.ids[i] = ASSET2.id(e.src) || ''; // only if this.scs?
        this.alts[i] = e.alt || 
          (this.tns && this.zms && this.zms[i].firstChild.alt) || ''; // if this a tn with no alt, but there is also zm, check it's alt also:
      }
      if (this.tns && this.n > 1 && !this.rowN) {
        // item spacing:
        this.k = this.tns[1].firstChild.getBoundingClientRect().left - 
                 this.tns[0].firstChild.getBoundingClientRect().right;

        // how many items could fit in a row if the last item has no right margin?
        this.rowN = Math.floor(
          (this.zmR.w + this.k) / 
          (this.tnR.w + this.k));
      }
    },
    
    initButton: function(cN) {
      var e;
      if (e = gebCN(this.e, 'a', cN)[0])
        this[cN] = this.upButton(e);
    },
  
    upButtons: function() {
      this.prev && this.upButton(this.prev);
      this.next && this.upButton(this.next);
      this.togg && this.upButton(this.togg);
    },
  
    upButton: function(e) {
      e.style.display = this.n < 2 ?
        'none' :
        (e.offsetHeight ? e.style.display : 'block'); // if already rendered no need to make block (avoid issue of static inline buttons)
    
      return e;
    },
  
    addListeners: function() {
      var isEdit;
      try { isEdit = !!(window.top && window.top.relocate) }
      catch (err) { isEdit = false }
      var o = this;
      addListener(o.e, 'click', function(e) {
        o.fire(e, true, !isEdit);
        return false;
      });
      isEdit && addListener(o.e, 'dblclick', function(e) {
        o.fire(e, false, true);
        return false;
      }); 
      o.tnE && addEnterListener(o.e, true, function() {
        if (o.doTnFade === undefined) {
          if (o.doTnFade = !o.tns[0].offsetHeight) {
            setOpacity(o.tnE, 0);
            o.tnE.style.display = 'block';
            addEnterListener(o.e, false, function() {
              runTrans(o.tnE, 1, 0, 200, setOpacity);
            });
          }
        }
        if (o.doTnFade)
          runTrans(o.tnE, 0, 1, 200, setOpacity);
      });  
    },
  
    fire: function(e, incButt, incReloc) {
      var cTN = this.tns && this.tns[0].parentNode;
      for (var pre; e != this.e; (pre = e) && (e = e.parentNode))
        if (e == cTN && pre)
          return incButt && this.show(pre) && this.stop();
        else if (e == this.prev)
          return incButt && this.show(this.i - 1) && this.stop();
        else if (e == this.next)
          return incButt && this.show(this.i + 1) && this.stop();
        else if (e == this.togg)
          return incButt && (this.stop() || this.start());

      // non thumb, non button:
      if (incReloc && this.hasLink[this.i] && this.hasLink[this.i] != '#')
        if (window.top && window.top.relocate)
          window.top.relocate(this.hasLink[this.i], true);
        else
          window.location.href = this.hasLink[this.i];
    },
  
    stop: function() {
      if (!this.stopId) return false;
      clearInterval(this.stopId);
      addCN(this.e, 'paused');
      this.stopId = false;
      return true;
    },
  
    start: function() {
      var o = this;
      if (o.stopId) return false;
      rmCN(o.e, 'paused');
      o.stopId = setInterval(function() { o.show(o.i + 1); }, 6000);
      return true;
    },
    
    tgText: function(i) {
      (this.tns || this.zms)[i].setAttribute('data-overlay', 
        (this.hasText[i] = !this.hasText[i]) ? 'true' : 'false');
      this.show(i, true);
      return this.hasText[i];
    },
  
    upHREF: function(i, s) {
      (this.tns || this.zms)[i].setAttribute('data-url', this.hasLink[i] = s);
      this.show(i, true)
    },
  
    upAlt: function(i, s) {
      this.alts[i] = s;
      if (this.tns) getIm(this.tns[i]).alt = s;
      if (this.zms) getIm(this.zms[i]).alt = s;
      if (i == this.i) this.show(true);
    },
  
    upFileId: function(i, v) {
      var id = v.id || v;
      var e;
      if (e = this.tns && getIm(this.tns[i]))
        e.src = ASSET2.rpId(e.src, id);
      if (e = this.zms && getIm(this.zms[i]))
        e.src = ASSET2.rpId(e.src, id);
    
      this.ids[i] = id;
    
      if (this.i == i)
        this.show(true);
    },
  
    upTnCns: function() {
      if (this.n > 1 && this.tns) {
        // base thumb class name:
        var cN = (' ' + this.tns[0].className + ' ').replace(/ (first|last|on) /g, ' ').replace(/^(\s)+|(\s)+$/g, '');
        for (var k, i = 0; i < this.n; i++) {
          // 'last' is used to mean 'at the end of a row' i.e. has no right margin
          switch ((i + 1) % this.rowN) {
            case 0:
              this.tns[i].className = 'last ' + cN;
              break;
            case 1:
              this.tns[i].className = 'first ' + cN;
              break;
            default:
              this.tns[i].className = cN;
          }
        }
      }
    },
  
    show: function(v, imm) {
      // imm: no transition, and force redraw even if this is the current index (because there may have been an edit)
      var i;
      if (v === true) { // shortcut to current index, immediate
        i = this.i % this.n; // i may be to great if it is the last item and it was just removed
        imm = true;
      } else if (typeof v != 'number') { // use i for this thumb
        i = 0;
        while (this.tns[i] != v) i++;
      } else if (v < 0) {
        i = this.n + v;
      } else {
        i = v % this.n;
      }
      if (imm || this.i != i) {
        var o = this;
        var i1 = this.i || 0;
        var i2 = i;
        var cb = function() { o.upI(i2) };
        if (this.zm)
          this.zm = this.repTop(this.zm, ASSET2.rpId(this.zm.src, this.ids[i2]), imm, cb);
        else
          this.setTop(this.zms[i1], this.zms[i2], imm, cb);
        return true;
      }
      return false;
    },
      
    upI: function(i) {
      var i1 = this.i || 0;
      var i2 = this.i = i;

      Gal.up && Gal.up(this, i1, i2);

      if (this.zm)
        this.zm.alt = this.alts[i2];

      (this.zm || this.zms[0].parentNode).style.cursor = 
        this.hasLink[i2] ? 'pointer' : '';

      if (this.scs) {
        this.scs.style.cursor = 
          this.hasLink[i2] ? 'pointer' : '';
        this.scs.style.display = 
          this.hasText[i2] ? 'block' : 'none';
        this.scs.childNodes[i1].style.display = 'none';
        this.scs.childNodes[i2].style.display = 
          this.hasText[i2] ? 'block' : 'none';
      }
      
      try {
        if (window.top && window.top.showGalItem) {
          window.top.hideGalItem(this, i1);
          window.top.showGalItem(this, i2);
        } else {
          //window.top && window.top.on && window.top.on === this && window.top.OS.insp.up('gal.upI');
        }
      } catch(err) {}
    },
  
    repTop: function(e, src, imm, f) {
      if (imm) {
        e.src = src;
        f && f();
        return e;
      } else { // create a new hidden image over e. fade in, then remove e.
        e.style.zIndex = 0;
        var e2 = e.cloneNode(true);
        e2.style.position = 'absolute';
        e2.style.top = '0px';
        e2.style.left = '0px';
        e2.style.zIndex = 1;
        getIm(e2).src = src;
        setOpacity(e2, 0);
        insertAfter(e, e2);
        runTrans(e2, 0, 1, 400, setOpacity, function() {
          rm(e);
          f && f();      
        });
        return e2;
      }  
    },
  
    setTop: function(e, e2, imm, f) {
      // najad, groupc, floral, cwo
      // zms are abs position in stylesheet within a fixed height box
      // all but first is display:none
      e.style.zIndex = 0;
      e2.style.zIndex = 1;
      e2.style.display = 'block'; // might be hidden by stylesheet
      if (imm) {
        setOpacity(e2, 1);
        if (e != e2) e.style.display = 'none';
        f && f();
      } else {
        runTrans(e2, 0, 1, 400, setOpacity, function() {
          setOpacity(e, 0);
          f && f();          
        });
      }
    },
  
    rm: function(i) {
      // are we removing the current item?
      // show next, before removal, while the current info is still valid:
      var isTop = this.i == i;
      if (isTop) this.show(i + 1, true);
      if (this.tns) rm(this.tns[i]);
      if (this.zms) rm(this.zms[i]);
      if (this.scs) rm(this.scs.childNodes[i]);
      this.hasText.splice(i, 1);
      this.hasLink.splice(i, 1);
      this.alts.splice(i, 1);
      this.ids.splice(i, 1);
      this.n--;
      // if we moved to a later item before removal, that index is now 1 place to high:
      //if (isTop && this.i > 0) this.i--;
      if (this.i > i) this.i--; // we removed an item at a lower index to the selected one - so move our index down one
      this.upButtons();
    
    },
  
    add: function() {
      // remove .last from last tns. create new with first classname, minus first, and last
      if (this.tns) appendPH(this.tns, this.tnR);
      if (this.zms) appendPH(this.zms, this.zmR);
      if (this.scs) this.scs.appendChild(createNode('<div class="section" style="display:none"><p>Overlay</p></div>'));
      this.hasText[this.n] = false;
      this.hasLink[this.n] = false;
      this.ids[this.n] = '';
      this.alts[this.n] = '';
      this.n++;
      this.upTnCns();
      this.upButtons();
      this.show(this.n - 1);
    },
    
    mv: function(i, iR) {
      // put item at i before item at iR
      var e = (this.tns || this.zms)[i]; // remember node reference for current i in case it moves
      if (this.tns) this.tns[0].parentNode.insertBefore(this.tns[i], this.tns[iR] || null);
      if (this.zms) this.zms[0].parentNode.insertBefore(this.zms[i], this.zms[iR] || null);
      if (this.scs) this.scs.insertBefore(this.scs.childNodes[i], this.scs.childNodes[iR] || null);
      this.initInfo();
      this.i = nodeIndex(e); // this.i may have if an item was inserted before it, or if i itself was dragged, so update according to new position
      this.upTnCns();
    },
  
    getSRC: function(i, w, h) {
      if (this.ids[i]) return ASSET2.rpDim(getIm((this.zms || this.tns)[i]).src, { w: w, h: h });
      if (this.tns) return getIm(this.tns[i]).src; // using s3 - no resize
      return '';
    }
  });

  function appendPH(nl, d) {
    rmCN(nl[nl.length - 1], 'last');
    return insertAfter(nl[nl.length - 1],
      rmCN(addCN(createNode('<a class="' + nl[0].className + '"><img width="' + d.w + '" height="' + d.h + '" src="' + window.top.getSRC(d) + '" /></a>'), 'last'), 'first'));
  }

  function insertAfter(a, b) {
    return a.parentNode.insertBefore(b, a.nextSibling);
  }

  function createNode(s) {
    var e = createNode.e = createNode.e || document.createElement('div');
    e.innerHTML = s;
    return e.removeChild(e.firstChild);
  }
  
  function getImgDim(img) {
    var img;
    var o = {};
    if ((o.w = img.width) && (o.h = img.height)) return o;
    // IE reports props/attributes as 0 when image is hidden - but can use currentStyle instead:
    var s;
    if ((s = img.currentStyle) && (o.w = parseInt(s.width)) && (o.h = parseInt(s.height))) return o;
    else lg('Failed to read dims', img, img.width);
  }

  function getIm(e) {
    return e.firstChild || e;
  }

  function getIt(e) {
    return e.parentNode.tagName == 'A' ? e.parentNode : e;
  }



  // ----------------------------------------------------------------------------------------------

  function addEnterListener(e, isEnter, f) {
    return IE ?
      addListener(e, isEnter ? 'mouseenter' : 'mouseleave', f) :
      addListener(e, isEnter ? 'mouseover' : 'mouseout', function(e2, ev) {
        if (isNotDesc(ev.relatedTarget, e)) return f(e, ev);
      });
  }


  // ----------------------------------------------------------------------------------------------
  // tools:

  var IE = !!document.expando;

  function addListener(e, t, f, useCap) {
    if (e.addEventListener)
      e.addEventListener(t, function(ev) {
        if (f(ev.target, ev) === false)
          ev.preventDefault();
      }, !!useCap);
    else if (e.attachEvent)
      e.attachEvent('on' + t, function() {
        window.event.returnValue = f(window.event.srcElement, window.event);
      });
    return f;
  }

  function xhr(a, b, c) {
    var s, h, f;
    if (typeof a == 'object') {
      s = '/api';
      h = a;
      f = b;
    } else if (c) {
      s = a;
      h = b;
      f = c;
    } else {
      s = a;
      h = null;
      f = b;
    }  
    return xhr2(h ? 'POST' : 'GET', s, h, f);
  }
  
  function xhr2(v, path, h, f) {
    var o;
    
    try {
      o = new XMLHttpRequest();
    } catch (er) {
      try {
        o = new ActiveXObject('Microsoft.XMLHTTP')
      } catch (er) {
        try {
          o = new ActiveXObject('Msxml2.XMLHTTP') 
        } catch (er) {
          return false;
        }
      }
    }
    
    o.open(v, 
      path + 
      (path.indexOf('?') == -1 ? 
        '?' : '&') + 
      new Date().getTime());
      
    o.onreadystatechange = function() {
      try {
        if (o.status && o.readyState == 4) {
          o.onreadystatechange = function() {};
          if (f)
            try {
              f(JSON.parse(o.responseText));
            } catch (er2) {
              f({ error: true, reason: 'The server response was badly formatted (JSON parse error).' })
            }
        }
        
      } catch (er) { }
    }
    o.send(h ? JSON.stringify(h) : null);
  }

  function gebCN(e, tN, cN) {
    // ignoring tag...
    if (e.getElementsByClassName)
      return e.getElementsByClassName(cN);
  
    var vs = new Array();
    var nl = e.getElementsByTagName(tN);
    var n = nl.length;
    var rx = new RegExp("(^|\\s)" + cN + "(\\s|$)");
    for (i = 0, j = 0; i < n; i++) {
      if (rx.test(nl[i].className)) {
        vs[j] = nl[i];
        j++;
      }
    }
    return vs;
  }

  function addCN(e, cN) {
    if (!hasCN(e, cN))
      e.className ?
        (e.className += ' ' + cN) :
        (e.className = cN);
    return e;
  }

  function rmCN(e, cN) {
    e.className = (' ' + e.className + ' ').replace(' ' + cN + ' ', ' ').replace(/^(\s)+|(\s)+$/g, '');
    return e;
  }

  function hasCN(e, cN) {
    return (' ' + e.className + ' ').indexOf(' ' + cN + ' ') > -1;
  }

  function nodeIndex(nd) {
    var i = 0;
    while (nd = nd.previousSibling) i++;
    return i;
  }

  function getNext(nd) {
    var v;
    if (v = nd.firstChild) return v;
    do
      if (v = nd.nextSibling) return v;
    while (nd = nd.parentNode);    
  }

  function getPrev(nd) {
    return nd.previousSibling ?
      getLast(nd.previousSibling) : nd.parentNode;
  }

  function getLast(nd) {
    var n2;
    while (n2 = nd.lastChild) nd = n2;
    return nd;
  }

  function isNotDesc(c, e) {
    while (c !== null && (c = c.parentNode) !== null) if (c === e) return false;
    return true;
  }

  function rm(nd) {
    return nd.parentNode.removeChild(nd);
  }
  
  function runTrans(e, i, j, n, f, f2) {
    e.tid && clearInterval(e.tid); // stop any active/incomplete transition
    
    var d1 = +new Date();
    var d2 = d1 + n;

    f(e, i);
    e.tid = setInterval(function() {
      var d = +new Date();
      f(e, i + (j - i) * easing( d > d2 ? 1 : (d - d1) / n ));
      if (d > d2) {
        clearInterval(e.tid);
        e.tid = null;
        f2 && f2();
      }
    }, 13);
  }

  function easing(pos) {
    return (-Math.cos(pos * Math.PI) / 2) + 0.5;
  };

  function setOpacity(e, v) {
    if (IE) {
      e.style.filter = 'alpha(opacity=' + Math.round(v * 100) + ')';
      e.style.zoom = 1;    
    } else {
      e.style.opacity = v.toFixed(3);
    }
  }

  function cp(o) {
    for (var v, i = 1; i < arguments.length; i++)
      if (typeof (v = arguments[i]) == 'object')
        for (var id in v)
          o[id] = v[id];
    return o;
  };

  function map() {
    var h = {};
    for (var i = 0; i < arguments.length; i++) h[arguments[i]] = true;
    return h;
  };

  function isArray(v) {
    return Object.prototype.toString.call(v) === '[object Array]';
  }

  function lg() {
    try {
      var vs = [];
      for (var i = 0; i < arguments.length; i++) vs[i] = arguments[i];
      console.log(vs);
    } catch (err) {}
  }

  // JSON for IE pre-8, from:
  // https://github.com/douglascrockford/JSON-js

  var JSON;
  if (!JSON)
    JSON = {};

  (function () {
      "use strict";

      function f(n) {
          // Format integers to have at least two digits.
          return n < 10 ? '0' + n : n;
      }

      if (typeof Date.prototype.toJSON !== 'function') {

          Date.prototype.toJSON = function (key) {

              return isFinite(this.valueOf()) ?
                  this.getUTCFullYear()     + '-' +
                  f(this.getUTCMonth() + 1) + '-' +
                  f(this.getUTCDate())      + 'T' +
                  f(this.getUTCHours())     + ':' +
                  f(this.getUTCMinutes())   + ':' +
                  f(this.getUTCSeconds())   + 'Z' : null;
          };

          String.prototype.toJSON      =
              Number.prototype.toJSON  =
              Boolean.prototype.toJSON = function (key) {
                  return this.valueOf();
              };
      }

      var cx = /[\u0000\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
          escapable = /[\\\"\x00-\x1f\x7f-\x9f\u00ad\u0600-\u0604\u070f\u17b4\u17b5\u200c-\u200f\u2028-\u202f\u2060-\u206f\ufeff\ufff0-\uffff]/g,
          gap,
          indent,
          meta = {    // table of character substitutions
              '\b': '\\b',
              '\t': '\\t',
              '\n': '\\n',
              '\f': '\\f',
              '\r': '\\r',
              '"' : '\\"',
              '\\': '\\\\'
          },
          rep;


      function quote(string) {

  // If the string contains no control characters, no quote characters, and no
  // backslash characters, then we can safely slap some quotes around it.
  // Otherwise we must also replace the offending characters with safe escape
  // sequences.

          escapable.lastIndex = 0;
          return escapable.test(string) ? '"' + string.replace(escapable, function (a) {
              var c = meta[a];
              return typeof c === 'string' ? c :
                  '\\u' + ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
          }) + '"' : '"' + string + '"';
      }


      function str(key, holder) {

  // Produce a string from holder[key].

          var i,          // The loop counter.
              k,          // The member key.
              v,          // The member value.
              length,
              mind = gap,
              partial,
              value = holder[key];

  // If the value has a toJSON method, call it to obtain a replacement value.

          if (value && typeof value === 'object' &&
                  typeof value.toJSON === 'function') {
              value = value.toJSON(key);
          }

  // If we were called with a replacer function, then call the replacer to
  // obtain a replacement value.

          if (typeof rep === 'function') {
              value = rep.call(holder, key, value);
          }

  // What happens next depends on the value's type.

          switch (typeof value) {
          case 'string':
              return quote(value);

          case 'number':

  // JSON numbers must be finite. Encode non-finite numbers as null.

              return isFinite(value) ? String(value) : 'null';

          case 'boolean':
          case 'null':

  // If the value is a boolean or null, convert it to a string. Note:
  // typeof null does not produce 'null'. The case is included here in
  // the remote chance that this gets fixed someday.

              return String(value);

  // If the type is 'object', we might be dealing with an object or an array or
  // null.

          case 'object':

  // Due to a specification blunder in ECMAScript, typeof null is 'object',
  // so watch out for that case.

              if (!value) {
                  return 'null';
              }

  // Make an array to hold the partial results of stringifying this object value.

              gap += indent;
              partial = [];

  // Is the value an array?

              if (Object.prototype.toString.apply(value) === '[object Array]') {

  // The value is an array. Stringify every element. Use null as a placeholder
  // for non-JSON values.

                  length = value.length;
                  for (i = 0; i < length; i += 1) {
                      partial[i] = str(i, value) || 'null';
                  }

  // Join all of the elements together, separated with commas, and wrap them in
  // brackets.

                  v = partial.length === 0 ? '[]' : gap ?
                      '[\n' + gap + partial.join(',\n' + gap) + '\n' + mind + ']' :
                      '[' + partial.join(',') + ']';
                  gap = mind;
                  return v;
              }

  // If the replacer is an array, use it to select the members to be stringified.

              if (rep && typeof rep === 'object') {
                  length = rep.length;
                  for (i = 0; i < length; i += 1) {
                      if (typeof rep[i] === 'string') {
                          k = rep[i];
                          v = str(k, value);
                          if (v) {
                              partial.push(quote(k) + (gap ? ': ' : ':') + v);
                          }
                      }
                  }
              } else {

  // Otherwise, iterate through all of the keys in the object.

                  for (k in value) {
                      if (Object.prototype.hasOwnProperty.call(value, k)) {
                          v = str(k, value);
                          if (v) {
                              partial.push(quote(k) + (gap ? ': ' : ':') + v);
                          }
                      }
                  }
              }

  // Join all of the member texts together, separated with commas,
  // and wrap them in braces.

              v = partial.length === 0 ? '{}' : gap ?
                  '{\n' + gap + partial.join(',\n' + gap) + '\n' + mind + '}' :
                  '{' + partial.join(',') + '}';
              gap = mind;
              return v;
          }
      }

  // If the JSON object does not yet have a stringify method, give it one.

      if (typeof JSON.stringify !== 'function') {
          JSON.stringify = function (value, replacer, space) {

  // The stringify method takes a value and an optional replacer, and an optional
  // space parameter, and returns a JSON text. The replacer can be a function
  // that can replace values, or an array of strings that will select the keys.
  // A default replacer method can be provided. Use of the space parameter can
  // produce text that is more easily readable.

              var i;
              gap = '';
              indent = '';

  // If the space parameter is a number, make an indent string containing that
  // many spaces.

              if (typeof space === 'number') {
                  for (i = 0; i < space; i += 1) {
                      indent += ' ';
                  }

  // If the space parameter is a string, it will be used as the indent string.

              } else if (typeof space === 'string') {
                  indent = space;
              }

  // If there is a replacer, it must be a function or an array.
  // Otherwise, throw an error.

              rep = replacer;
              if (replacer && typeof replacer !== 'function' &&
                      (typeof replacer !== 'object' ||
                      typeof replacer.length !== 'number')) {
                  throw new Error('JSON.stringify');
              }

  // Make a fake root object containing our value under the key of ''.
  // Return the result of stringifying the value.

              return str('', {'': value});
          };
      }


  // If the JSON object does not yet have a parse method, give it one.

      if (typeof JSON.parse !== 'function') {

          JSON.parse = function (text, reviver) {

  // The parse method takes a text and an optional reviver function, and returns
  // a JavaScript value if the text is a valid JSON text.

              var j;

              function walk(holder, key) {

  // The walk method is used to recursively walk the resulting structure so
  // that modifications can be made.

                  var k, v, value = holder[key];
                  if (value && typeof value === 'object') {
                      for (k in value) {
                          if (Object.prototype.hasOwnProperty.call(value, k)) {
                              v = walk(value, k);
                              if (v !== undefined) {
                                  value[k] = v;
                              } else {
                                  delete value[k];
                              }
                          }
                      }
                  }
                  return reviver.call(holder, key, value);
              }


  // Parsing happens in four stages. In the first stage, we replace certain
  // Unicode characters with escape sequences. JavaScript handles many characters
  // incorrectly, either silently deleting them, or treating them as line endings.

              text = String(text);
              cx.lastIndex = 0;
              if (cx.test(text)) {
                  text = text.replace(cx, function (a) {
                      return '\\u' +
                          ('0000' + a.charCodeAt(0).toString(16)).slice(-4);
                  });
              }

  // In the second stage, we run the text against regular expressions that look
  // for non-JSON patterns. We are especially concerned with '()' and 'new'
  // because they can cause invocation, and '=' because it can cause mutation.
  // But just to be safe, we want to reject all unexpected forms.

  // We split the second stage into 4 regexp operations in order to work around
  // crippling inefficiencies in IE's and Safari's regexp engines. First we
  // replace the JSON backslash pairs with '@' (a non-JSON character). Second, we
  // replace all simple value tokens with ']' characters. Third, we delete all
  // open brackets that follow a colon or comma or that begin the text. Finally,
  // we look to see that the remaining characters are only whitespace or ']' or
  // ',' or ':' or '{' or '}'. If that is so, then the text is safe for eval.

              if (/^[\],:{}\s]*$/
                      .test(text.replace(/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g, '@')
                          .replace(/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g, ']')
                          .replace(/(?:^|:|,)(?:\s*\[)+/g, ''))) {

  // In the third stage we use the eval function to compile the text into a
  // JavaScript structure. The '{' operator is subject to a syntactic ambiguity
  // in JavaScript: it can begin a block or an object literal. We wrap the text
  // in parens to eliminate the ambiguity.

                  j = eval('(' + text + ')');

  // In the optional fourth stage, we recursively walk the new structure, passing
  // each name/value pair to a reviver function for possible transformation.

                  return typeof reviver === 'function' ?
                      walk({'': j}, '') : j;
              }

  // If the text is not JSON parseable, then a SyntaxError is thrown.

              throw new SyntaxError('JSON.parse');
          };
      }
  }());


})()
