/* Minification failed. Returning unminified contents.
(6650,60-61): run-time error JS1195: Expected expression: >
(6651,40-41): run-time error JS1195: Expected expression: >
(6658,18-19): run-time error JS1195: Expected expression: )
(6659,13-14): run-time error JS1002: Syntax error: }
(6662,62-63): run-time error JS1004: Expected ';': {
(6665,14-15): run-time error JS1195: Expected expression: )
(6672,24-25): run-time error JS1004: Expected ';': {
(6700,1-2): run-time error JS1002: Syntax error: }
(6695,5,6698,6): run-time error JS1018: 'return' statement outside of function: return {
        Initialize: initialize,
        Cleanup: cleanup
    }
 */
/* NUGET: BEGIN LICENSE TEXT
 *
 * Microsoft grants you the right to use these script files for the sole
 * purpose of either: (i) interacting through your browser with the Microsoft
 * website or online service, subject to the applicable licensing or use
 * terms; or (ii) using the files as included with a Microsoft product subject
 * to that product's license terms. Microsoft reserves all other rights to the
 * files not expressly granted by Microsoft, whether by implication, estoppel
 * or otherwise. Insofar as a script file is dual licensed under GPL,
 * Microsoft neither took the code under GPL nor distributes it thereunder but
 * under the terms set out in this paragraph. All notices and licenses
 * below are for informational purposes only.
 *
 * Copyright (c) Faruk Ates, Paul Irish, Alex Sexton; http://www.modernizr.com/license/
 *
 * Includes matchMedia polyfill; Copyright (c) 2010 Filament Group, Inc; http://opensource.org/licenses/MIT
 *
 * Includes material adapted from ES5-shim https://github.com/kriskowal/es5-shim/blob/master/es5-shim.js; Copyright 2009-2012 by contributors; http://opensource.org/licenses/MIT
 *
 * Includes material from css-support; Copyright (c) 2005-2012 Diego Perini; https://github.com/dperini/css-support/blob/master/LICENSE
 *
 * NUGET: END LICENSE TEXT */

/*!
 * Modernizr v2.6.2
 * www.modernizr.com
 *
 * Copyright (c) Faruk Ates, Paul Irish, Alex Sexton
 * Available under the BSD and MIT licenses: www.modernizr.com/license/
 */

/*
 * Modernizr tests which native CSS3 and HTML5 features are available in
 * the current UA and makes the results available to you in two ways:
 * as properties on a global Modernizr object, and as classes on the
 * <html> element. This information allows you to progressively enhance
 * your pages with a granular level of control over the experience.
 *
 * Modernizr has an optional (not included) conditional resource loader
 * called Modernizr.load(), based on Yepnope.js (yepnopejs.com).
 * To get a build that includes Modernizr.load(), as well as choosing
 * which tests to include, go to www.modernizr.com/download/
 *
 * Authors        Faruk Ates, Paul Irish, Alex Sexton
 * Contributors   Ryan Seddon, Ben Alman
 */

window.Modernizr = (function( window, document, undefined ) {

    var version = '2.6.2',

    Modernizr = {},

    /*>>cssclasses*/
    // option for enabling the HTML classes to be added
    enableClasses = true,
    /*>>cssclasses*/

    docElement = document.documentElement,

    /**
     * Create our "modernizr" element that we do most feature tests on.
     */
    mod = 'modernizr',
    modElem = document.createElement(mod),
    mStyle = modElem.style,

    /**
     * Create the input element for various Web Forms feature tests.
     */
    inputElem /*>>inputelem*/ = document.createElement('input') /*>>inputelem*/ ,

    /*>>smile*/
    smile = ':)',
    /*>>smile*/

    toString = {}.toString,

    // TODO :: make the prefixes more granular
    /*>>prefixes*/
    // List of property values to set for css tests. See ticket #21
    prefixes = ' -webkit- -moz- -o- -ms- '.split(' '),
    /*>>prefixes*/

    /*>>domprefixes*/
    // Following spec is to expose vendor-specific style properties as:
    //   elem.style.WebkitBorderRadius
    // and the following would be incorrect:
    //   elem.style.webkitBorderRadius

    // Webkit ghosts their properties in lowercase but Opera & Moz do not.
    // Microsoft uses a lowercase `ms` instead of the correct `Ms` in IE8+
    //   erik.eae.net/archives/2008/03/10/21.48.10/

    // More here: github.com/Modernizr/Modernizr/issues/issue/21
    omPrefixes = 'Webkit Moz O ms',

    cssomPrefixes = omPrefixes.split(' '),

    domPrefixes = omPrefixes.toLowerCase().split(' '),
    /*>>domprefixes*/

    /*>>ns*/
    ns = {'svg': 'http://www.w3.org/2000/svg'},
    /*>>ns*/

    tests = {},
    inputs = {},
    attrs = {},

    classes = [],

    slice = classes.slice,

    featureName, // used in testing loop


    /*>>teststyles*/
    // Inject element with style element and some CSS rules
    injectElementWithStyles = function( rule, callback, nodes, testnames ) {

      var style, ret, node, docOverflow,
          div = document.createElement('div'),
          // After page load injecting a fake body doesn't work so check if body exists
          body = document.body,
          // IE6 and 7 won't return offsetWidth or offsetHeight unless it's in the body element, so we fake it.
          fakeBody = body || document.createElement('body');

      if ( parseInt(nodes, 10) ) {
          // In order not to give false positives we create a node for each test
          // This also allows the method to scale for unspecified uses
          while ( nodes-- ) {
              node = document.createElement('div');
              node.id = testnames ? testnames[nodes] : mod + (nodes + 1);
              div.appendChild(node);
          }
      }

      // <style> elements in IE6-9 are considered 'NoScope' elements and therefore will be removed
      // when injected with innerHTML. To get around this you need to prepend the 'NoScope' element
      // with a 'scoped' element, in our case the soft-hyphen entity as it won't mess with our measurements.
      // msdn.microsoft.com/en-us/library/ms533897%28VS.85%29.aspx
      // Documents served as xml will throw if using &shy; so use xml friendly encoded version. See issue #277
      style = ['&#173;','<style id="s', mod, '">', rule, '</style>'].join('');
      div.id = mod;
      // IE6 will false positive on some tests due to the style element inside the test div somehow interfering offsetHeight, so insert it into body or fakebody.
      // Opera will act all quirky when injecting elements in documentElement when page is served as xml, needs fakebody too. #270
      (body ? div : fakeBody).innerHTML += style;
      fakeBody.appendChild(div);
      if ( !body ) {
          //avoid crashing IE8, if background image is used
          fakeBody.style.background = '';
          //Safari 5.13/5.1.4 OSX stops loading if ::-webkit-scrollbar is used and scrollbars are visible
          fakeBody.style.overflow = 'hidden';
          docOverflow = docElement.style.overflow;
          docElement.style.overflow = 'hidden';
          docElement.appendChild(fakeBody);
      }

      ret = callback(div, rule);
      // If this is done after page load we don't want to remove the body so check if body exists
      if ( !body ) {
          fakeBody.parentNode.removeChild(fakeBody);
          docElement.style.overflow = docOverflow;
      } else {
          div.parentNode.removeChild(div);
      }

      return !!ret;

    },
    /*>>teststyles*/

    /*>>mq*/
    // adapted from matchMedia polyfill
    // by Scott Jehl and Paul Irish
    // gist.github.com/786768
    testMediaQuery = function( mq ) {

      var matchMedia = window.matchMedia || window.msMatchMedia;
      if ( matchMedia ) {
        return matchMedia(mq).matches;
      }

      var bool;

      injectElementWithStyles('@media ' + mq + ' { #' + mod + ' { position: absolute; } }', function( node ) {
        bool = (window.getComputedStyle ?
                  getComputedStyle(node, null) :
                  node.currentStyle)['position'] == 'absolute';
      });

      return bool;

     },
     /*>>mq*/


    /*>>hasevent*/
    //
    // isEventSupported determines if a given element supports the given event
    // kangax.github.com/iseventsupported/
    //
    // The following results are known incorrects:
    //   Modernizr.hasEvent("webkitTransitionEnd", elem) // false negative
    //   Modernizr.hasEvent("textInput") // in Webkit. github.com/Modernizr/Modernizr/issues/333
    //   ...
    isEventSupported = (function() {

      var TAGNAMES = {
        'select': 'input', 'change': 'input',
        'submit': 'form', 'reset': 'form',
        'error': 'img', 'load': 'img', 'abort': 'img'
      };

      function isEventSupported( eventName, element ) {

        element = element || document.createElement(TAGNAMES[eventName] || 'div');
        eventName = 'on' + eventName;

        // When using `setAttribute`, IE skips "unload", WebKit skips "unload" and "resize", whereas `in` "catches" those
        var isSupported = eventName in element;

        if ( !isSupported ) {
          // If it has no `setAttribute` (i.e. doesn't implement Node interface), try generic element
          if ( !element.setAttribute ) {
            element = document.createElement('div');
          }
          if ( element.setAttribute && element.removeAttribute ) {
            element.setAttribute(eventName, '');
            isSupported = is(element[eventName], 'function');

            // If property was created, "remove it" (by setting value to `undefined`)
            if ( !is(element[eventName], 'undefined') ) {
              element[eventName] = undefined;
            }
            element.removeAttribute(eventName);
          }
        }

        element = null;
        return isSupported;
      }
      return isEventSupported;
    })(),
    /*>>hasevent*/

    // TODO :: Add flag for hasownprop ? didn't last time

    // hasOwnProperty shim by kangax needed for Safari 2.0 support
    _hasOwnProperty = ({}).hasOwnProperty, hasOwnProp;

    if ( !is(_hasOwnProperty, 'undefined') && !is(_hasOwnProperty.call, 'undefined') ) {
      hasOwnProp = function (object, property) {
        return _hasOwnProperty.call(object, property);
      };
    }
    else {
      hasOwnProp = function (object, property) { /* yes, this can give false positives/negatives, but most of the time we don't care about those */
        return ((property in object) && is(object.constructor.prototype[property], 'undefined'));
      };
    }

    // Adapted from ES5-shim https://github.com/kriskowal/es5-shim/blob/master/es5-shim.js
    // es5.github.com/#x15.3.4.5

    if (!Function.prototype.bind) {
      Function.prototype.bind = function bind(that) {

        var target = this;

        if (typeof target != "function") {
            throw new TypeError();
        }

        var args = slice.call(arguments, 1),
            bound = function () {

            if (this instanceof bound) {

              var F = function(){};
              F.prototype = target.prototype;
              var self = new F();

              var result = target.apply(
                  self,
                  args.concat(slice.call(arguments))
              );
              if (Object(result) === result) {
                  return result;
              }
              return self;

            } else {

              return target.apply(
                  that,
                  args.concat(slice.call(arguments))
              );

            }

        };

        return bound;
      };
    }

    /**
     * setCss applies given styles to the Modernizr DOM node.
     */
    function setCss( str ) {
        mStyle.cssText = str;
    }

    /**
     * setCssAll extrapolates all vendor-specific css strings.
     */
    function setCssAll( str1, str2 ) {
        return setCss(prefixes.join(str1 + ';') + ( str2 || '' ));
    }

    /**
     * is returns a boolean for if typeof obj is exactly type.
     */
    function is( obj, type ) {
        return typeof obj === type;
    }

    /**
     * contains returns a boolean for if substr is found within str.
     */
    function contains( str, substr ) {
        return !!~('' + str).indexOf(substr);
    }

    /*>>testprop*/

    // testProps is a generic CSS / DOM property test.

    // In testing support for a given CSS property, it's legit to test:
    //    `elem.style[styleName] !== undefined`
    // If the property is supported it will return an empty string,
    // if unsupported it will return undefined.

    // We'll take advantage of this quick test and skip setting a style
    // on our modernizr element, but instead just testing undefined vs
    // empty string.

    // Because the testing of the CSS property names (with "-", as
    // opposed to the camelCase DOM properties) is non-portable and
    // non-standard but works in WebKit and IE (but not Gecko or Opera),
    // we explicitly reject properties with dashes so that authors
    // developing in WebKit or IE first don't end up with
    // browser-specific content by accident.

    function testProps( props, prefixed ) {
        for ( var i in props ) {
            var prop = props[i];
            if ( !contains(prop, "-") && mStyle[prop] !== undefined ) {
                return prefixed == 'pfx' ? prop : true;
            }
        }
        return false;
    }
    /*>>testprop*/

    // TODO :: add testDOMProps
    /**
     * testDOMProps is a generic DOM property test; if a browser supports
     *   a certain property, it won't return undefined for it.
     */
    function testDOMProps( props, obj, elem ) {
        for ( var i in props ) {
            var item = obj[props[i]];
            if ( item !== undefined) {

                // return the property name as a string
                if (elem === false) return props[i];

                // let's bind a function
                if (is(item, 'function')){
                  // default to autobind unless override
                  return item.bind(elem || obj);
                }

                // return the unbound function or obj or value
                return item;
            }
        }
        return false;
    }

    /*>>testallprops*/
    /**
     * testPropsAll tests a list of DOM properties we want to check against.
     *   We specify literally ALL possible (known and/or likely) properties on
     *   the element including the non-vendor prefixed one, for forward-
     *   compatibility.
     */
    function testPropsAll( prop, prefixed, elem ) {

        var ucProp  = prop.charAt(0).toUpperCase() + prop.slice(1),
            props   = (prop + ' ' + cssomPrefixes.join(ucProp + ' ') + ucProp).split(' ');

        // did they call .prefixed('boxSizing') or are we just testing a prop?
        if(is(prefixed, "string") || is(prefixed, "undefined")) {
          return testProps(props, prefixed);

        // otherwise, they called .prefixed('requestAnimationFrame', window[, elem])
        } else {
          props = (prop + ' ' + (domPrefixes).join(ucProp + ' ') + ucProp).split(' ');
          return testDOMProps(props, prefixed, elem);
        }
    }
    /*>>testallprops*/


    /**
     * Tests
     * -----
     */

    // The *new* flexbox
    // dev.w3.org/csswg/css3-flexbox

    tests['flexbox'] = function() {
      return testPropsAll('flexWrap');
    };

    // The *old* flexbox
    // www.w3.org/TR/2009/WD-css3-flexbox-20090723/

    tests['flexboxlegacy'] = function() {
        return testPropsAll('boxDirection');
    };

    // On the S60 and BB Storm, getContext exists, but always returns undefined
    // so we actually have to call getContext() to verify
    // github.com/Modernizr/Modernizr/issues/issue/97/

    tests['canvas'] = function() {
        var elem = document.createElement('canvas');
        return !!(elem.getContext && elem.getContext('2d'));
    };

    tests['canvastext'] = function() {
        return !!(Modernizr['canvas'] && is(document.createElement('canvas').getContext('2d').fillText, 'function'));
    };

    // webk.it/70117 is tracking a legit WebGL feature detect proposal

    // We do a soft detect which may false positive in order to avoid
    // an expensive context creation: bugzil.la/732441

    tests['webgl'] = function() {
        return !!window.WebGLRenderingContext;
    };

    /*
     * The Modernizr.touch test only indicates if the browser supports
     *    touch events, which does not necessarily reflect a touchscreen
     *    device, as evidenced by tablets running Windows 7 or, alas,
     *    the Palm Pre / WebOS (touch) phones.
     *
     * Additionally, Chrome (desktop) used to lie about its support on this,
     *    but that has since been rectified: crbug.com/36415
     *
     * We also test for Firefox 4 Multitouch Support.
     *
     * For more info, see: modernizr.github.com/Modernizr/touch.html
     */

    tests['touch'] = function() {
        var bool;

        if(('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch) {
          bool = true;
        } else {
          injectElementWithStyles(['@media (',prefixes.join('touch-enabled),('),mod,')','{#modernizr{top:9px;position:absolute}}'].join(''), function( node ) {
            bool = node.offsetTop === 9;
          });
        }

        return bool;
    };


    // geolocation is often considered a trivial feature detect...
    // Turns out, it's quite tricky to get right:
    //
    // Using !!navigator.geolocation does two things we don't want. It:
    //   1. Leaks memory in IE9: github.com/Modernizr/Modernizr/issues/513
    //   2. Disables page caching in WebKit: webk.it/43956
    //
    // Meanwhile, in Firefox < 8, an about:config setting could expose
    // a false positive that would throw an exception: bugzil.la/688158

    tests['geolocation'] = function() {
        return 'geolocation' in navigator;
    };


    tests['postmessage'] = function() {
      return !!window.postMessage;
    };


    // Chrome incognito mode used to throw an exception when using openDatabase
    // It doesn't anymore.
    tests['websqldatabase'] = function() {
      return !!window.openDatabase;
    };

    // Vendors had inconsistent prefixing with the experimental Indexed DB:
    // - Webkit's implementation is accessible through webkitIndexedDB
    // - Firefox shipped moz_indexedDB before FF4b9, but since then has been mozIndexedDB
    // For speed, we don't test the legacy (and beta-only) indexedDB
    tests['indexedDB'] = function() {
      return !!testPropsAll("indexedDB", window);
    };

    // documentMode logic from YUI to filter out IE8 Compat Mode
    //   which false positives.
    tests['hashchange'] = function() {
      return isEventSupported('hashchange', window) && (document.documentMode === undefined || document.documentMode > 7);
    };

    // Per 1.6:
    // This used to be Modernizr.historymanagement but the longer
    // name has been deprecated in favor of a shorter and property-matching one.
    // The old API is still available in 1.6, but as of 2.0 will throw a warning,
    // and in the first release thereafter disappear entirely.
    tests['history'] = function() {
      return !!(window.history && history.pushState);
    };

    tests['draganddrop'] = function() {
        var div = document.createElement('div');
        return ('draggable' in div) || ('ondragstart' in div && 'ondrop' in div);
    };

    // FF3.6 was EOL'ed on 4/24/12, but the ESR version of FF10
    // will be supported until FF19 (2/12/13), at which time, ESR becomes FF17.
    // FF10 still uses prefixes, so check for it until then.
    // for more ESR info, see: mozilla.org/en-US/firefox/organizations/faq/
    tests['websockets'] = function() {
        return 'WebSocket' in window || 'MozWebSocket' in window;
    };


    // css-tricks.com/rgba-browser-support/
    tests['rgba'] = function() {
        // Set an rgba() color and check the returned value

        setCss('background-color:rgba(150,255,150,.5)');

        return contains(mStyle.backgroundColor, 'rgba');
    };

    tests['hsla'] = function() {
        // Same as rgba(), in fact, browsers re-map hsla() to rgba() internally,
        //   except IE9 who retains it as hsla

        setCss('background-color:hsla(120,40%,100%,.5)');

        return contains(mStyle.backgroundColor, 'rgba') || contains(mStyle.backgroundColor, 'hsla');
    };

    tests['multiplebgs'] = function() {
        // Setting multiple images AND a color on the background shorthand property
        //  and then querying the style.background property value for the number of
        //  occurrences of "url(" is a reliable method for detecting ACTUAL support for this!

        setCss('background:url(https://),url(https://),red url(https://)');

        // If the UA supports multiple backgrounds, there should be three occurrences
        //   of the string "url(" in the return value for elemStyle.background

        return (/(url\s*\(.*?){3}/).test(mStyle.background);
    };



    // this will false positive in Opera Mini
    //   github.com/Modernizr/Modernizr/issues/396

    tests['backgroundsize'] = function() {
        return testPropsAll('backgroundSize');
    };

    tests['borderimage'] = function() {
        return testPropsAll('borderImage');
    };


    // Super comprehensive table about all the unique implementations of
    // border-radius: muddledramblings.com/table-of-css3-border-radius-compliance

    tests['borderradius'] = function() {
        return testPropsAll('borderRadius');
    };

    // WebOS unfortunately false positives on this test.
    tests['boxshadow'] = function() {
        return testPropsAll('boxShadow');
    };

    // FF3.0 will false positive on this test
    tests['textshadow'] = function() {
        return document.createElement('div').style.textShadow === '';
    };


    tests['opacity'] = function() {
        // Browsers that actually have CSS Opacity implemented have done so
        //  according to spec, which means their return values are within the
        //  range of [0.0,1.0] - including the leading zero.

        setCssAll('opacity:.55');

        // The non-literal . in this regex is intentional:
        //   German Chrome returns this value as 0,55
        // github.com/Modernizr/Modernizr/issues/#issue/59/comment/516632
        return (/^0.55$/).test(mStyle.opacity);
    };


    // Note, Android < 4 will pass this test, but can only animate
    //   a single property at a time
    //   daneden.me/2011/12/putting-up-with-androids-bullshit/
    tests['cssanimations'] = function() {
        return testPropsAll('animationName');
    };


    tests['csscolumns'] = function() {
        return testPropsAll('columnCount');
    };


    tests['cssgradients'] = function() {
        /**
         * For CSS Gradients syntax, please see:
         * webkit.org/blog/175/introducing-css-gradients/
         * developer.mozilla.org/en/CSS/-moz-linear-gradient
         * developer.mozilla.org/en/CSS/-moz-radial-gradient
         * dev.w3.org/csswg/css3-images/#gradients-
         */

        var str1 = 'background-image:',
            str2 = 'gradient(linear,left top,right bottom,from(#9f9),to(white));',
            str3 = 'linear-gradient(left top,#9f9, white);';

        setCss(
             // legacy webkit syntax (FIXME: remove when syntax not in use anymore)
              (str1 + '-webkit- '.split(' ').join(str2 + str1) +
             // standard syntax             // trailing 'background-image:'
              prefixes.join(str3 + str1)).slice(0, -str1.length)
        );

        return contains(mStyle.backgroundImage, 'gradient');
    };


    tests['cssreflections'] = function() {
        return testPropsAll('boxReflect');
    };


    tests['csstransforms'] = function() {
        return !!testPropsAll('transform');
    };


    tests['csstransforms3d'] = function() {

        var ret = !!testPropsAll('perspective');

        // Webkit's 3D transforms are passed off to the browser's own graphics renderer.
        //   It works fine in Safari on Leopard and Snow Leopard, but not in Chrome in
        //   some conditions. As a result, Webkit typically recognizes the syntax but
        //   will sometimes throw a false positive, thus we must do a more thorough check:
        if ( ret && 'webkitPerspective' in docElement.style ) {

          // Webkit allows this media query to succeed only if the feature is enabled.
          // `@media (transform-3d),(-webkit-transform-3d){ ... }`
          injectElementWithStyles('@media (transform-3d),(-webkit-transform-3d){#modernizr{left:9px;position:absolute;height:3px;}}', function( node, rule ) {
            ret = node.offsetLeft === 9 && node.offsetHeight === 3;
          });
        }
        return ret;
    };


    tests['csstransitions'] = function() {
        return testPropsAll('transition');
    };


    /*>>fontface*/
    // @font-face detection routine by Diego Perini
    // javascript.nwbox.com/CSSSupport/

    // false positives:
    //   WebOS github.com/Modernizr/Modernizr/issues/342
    //   WP7   github.com/Modernizr/Modernizr/issues/538
    tests['fontface'] = function() {
        var bool;

        injectElementWithStyles('@font-face {font-family:"font";src:url("https://")}', function( node, rule ) {
          var style = document.getElementById('smodernizr'),
              sheet = style.sheet || style.styleSheet,
              cssText = sheet ? (sheet.cssRules && sheet.cssRules[0] ? sheet.cssRules[0].cssText : sheet.cssText || '') : '';

          bool = /src/i.test(cssText) && cssText.indexOf(rule.split(' ')[0]) === 0;
        });

        return bool;
    };
    /*>>fontface*/

    // CSS generated content detection
    tests['generatedcontent'] = function() {
        var bool;

        injectElementWithStyles(['#',mod,'{font:0/0 a}#',mod,':after{content:"',smile,'";visibility:hidden;font:3px/1 a}'].join(''), function( node ) {
          bool = node.offsetHeight >= 3;
        });

        return bool;
    };



    // These tests evaluate support of the video/audio elements, as well as
    // testing what types of content they support.
    //
    // We're using the Boolean constructor here, so that we can extend the value
    // e.g.  Modernizr.video     // true
    //       Modernizr.video.ogg // 'probably'
    //
    // Codec values from : github.com/NielsLeenheer/html5test/blob/9106a8/index.html#L845
    //                     thx to NielsLeenheer and zcorpan

    // Note: in some older browsers, "no" was a return value instead of empty string.
    //   It was live in FF3.5.0 and 3.5.1, but fixed in 3.5.2
    //   It was also live in Safari 4.0.0 - 4.0.4, but fixed in 4.0.5

    tests['video'] = function() {
        var elem = document.createElement('video'),
            bool = false;

        // IE9 Running on Windows Server SKU can cause an exception to be thrown, bug #224
        try {
            if ( bool = !!elem.canPlayType ) {
                bool      = new Boolean(bool);
                bool.ogg  = elem.canPlayType('video/ogg; codecs="theora"')      .replace(/^no$/,'');

                // Without QuickTime, this value will be `undefined`. github.com/Modernizr/Modernizr/issues/546
                bool.h264 = elem.canPlayType('video/mp4; codecs="avc1.42E01E"') .replace(/^no$/,'');

                bool.webm = elem.canPlayType('video/webm; codecs="vp8, vorbis"').replace(/^no$/,'');
            }

        } catch(e) { }

        return bool;
    };

    tests['audio'] = function() {
        var elem = document.createElement('audio'),
            bool = false;

        try {
            if ( bool = !!elem.canPlayType ) {
                bool      = new Boolean(bool);
                bool.ogg  = elem.canPlayType('audio/ogg; codecs="vorbis"').replace(/^no$/,'');
                bool.mp3  = elem.canPlayType('audio/mpeg;')               .replace(/^no$/,'');

                // Mimetypes accepted:
                //   developer.mozilla.org/En/Media_formats_supported_by_the_audio_and_video_elements
                //   bit.ly/iphoneoscodecs
                bool.wav  = elem.canPlayType('audio/wav; codecs="1"')     .replace(/^no$/,'');
                bool.m4a  = ( elem.canPlayType('audio/x-m4a;')            ||
                              elem.canPlayType('audio/aac;'))             .replace(/^no$/,'');
            }
        } catch(e) { }

        return bool;
    };


    // In FF4, if disabled, window.localStorage should === null.

    // Normally, we could not test that directly and need to do a
    //   `('localStorage' in window) && ` test first because otherwise Firefox will
    //   throw bugzil.la/365772 if cookies are disabled

    // Also in iOS5 Private Browsing mode, attempting to use localStorage.setItem
    // will throw the exception:
    //   QUOTA_EXCEEDED_ERRROR DOM Exception 22.
    // Peculiarly, getItem and removeItem calls do not throw.

    // Because we are forced to try/catch this, we'll go aggressive.

    // Just FWIW: IE8 Compat mode supports these features completely:
    //   www.quirksmode.org/dom/html5.html
    // But IE8 doesn't support either with local files

    tests['localstorage'] = function() {
        try {
            localStorage.setItem(mod, mod);
            localStorage.removeItem(mod);
            return true;
        } catch(e) {
            return false;
        }
    };

    tests['sessionstorage'] = function() {
        try {
            sessionStorage.setItem(mod, mod);
            sessionStorage.removeItem(mod);
            return true;
        } catch(e) {
            return false;
        }
    };


    tests['webworkers'] = function() {
        return !!window.Worker;
    };


    tests['applicationcache'] = function() {
        return !!window.applicationCache;
    };


    // Thanks to Erik Dahlstrom
    tests['svg'] = function() {
        return !!document.createElementNS && !!document.createElementNS(ns.svg, 'svg').createSVGRect;
    };

    // specifically for SVG inline in HTML, not within XHTML
    // test page: paulirish.com/demo/inline-svg
    tests['inlinesvg'] = function() {
      var div = document.createElement('div');
      div.innerHTML = '<svg/>';
      return (div.firstChild && div.firstChild.namespaceURI) == ns.svg;
    };

    // SVG SMIL animation
    tests['smil'] = function() {
        return !!document.createElementNS && /SVGAnimate/.test(toString.call(document.createElementNS(ns.svg, 'animate')));
    };

    // This test is only for clip paths in SVG proper, not clip paths on HTML content
    // demo: srufaculty.sru.edu/david.dailey/svg/newstuff/clipPath4.svg

    // However read the comments to dig into applying SVG clippaths to HTML content here:
    //   github.com/Modernizr/Modernizr/issues/213#issuecomment-1149491
    tests['svgclippaths'] = function() {
        return !!document.createElementNS && /SVGClipPath/.test(toString.call(document.createElementNS(ns.svg, 'clipPath')));
    };

    /*>>webforms*/
    // input features and input types go directly onto the ret object, bypassing the tests loop.
    // Hold this guy to execute in a moment.
    function webforms() {
        /*>>input*/
        // Run through HTML5's new input attributes to see if the UA understands any.
        // We're using f which is the <input> element created early on
        // Mike Taylr has created a comprehensive resource for testing these attributes
        //   when applied to all input types:
        //   miketaylr.com/code/input-type-attr.html
        // spec: www.whatwg.org/specs/web-apps/current-work/multipage/the-input-element.html#input-type-attr-summary

        // Only input placeholder is tested while textarea's placeholder is not.
        // Currently Safari 4 and Opera 11 have support only for the input placeholder
        // Both tests are available in feature-detects/forms-placeholder.js
        Modernizr['input'] = (function( props ) {
            for ( var i = 0, len = props.length; i < len; i++ ) {
                attrs[ props[i] ] = !!(props[i] in inputElem);
            }
            if (attrs.list){
              // safari false positive's on datalist: webk.it/74252
              // see also github.com/Modernizr/Modernizr/issues/146
              attrs.list = !!(document.createElement('datalist') && window.HTMLDataListElement);
            }
            return attrs;
        })('autocomplete autofocus list placeholder max min multiple pattern required step'.split(' '));
        /*>>input*/

        /*>>inputtypes*/
        // Run through HTML5's new input types to see if the UA understands any.
        //   This is put behind the tests runloop because it doesn't return a
        //   true/false like all the other tests; instead, it returns an object
        //   containing each input type with its corresponding true/false value

        // Big thanks to @miketaylr for the html5 forms expertise. miketaylr.com/
        Modernizr['inputtypes'] = (function(props) {

            for ( var i = 0, bool, inputElemType, defaultView, len = props.length; i < len; i++ ) {

                inputElem.setAttribute('type', inputElemType = props[i]);
                bool = inputElem.type !== 'text';

                // We first check to see if the type we give it sticks..
                // If the type does, we feed it a textual value, which shouldn't be valid.
                // If the value doesn't stick, we know there's input sanitization which infers a custom UI
                if ( bool ) {

                    inputElem.value         = smile;
                    inputElem.style.cssText = 'position:absolute;visibility:hidden;';

                    if ( /^range$/.test(inputElemType) && inputElem.style.WebkitAppearance !== undefined ) {

                      docElement.appendChild(inputElem);
                      defaultView = document.defaultView;

                      // Safari 2-4 allows the smiley as a value, despite making a slider
                      bool =  defaultView.getComputedStyle &&
                              defaultView.getComputedStyle(inputElem, null).WebkitAppearance !== 'textfield' &&
                              // Mobile android web browser has false positive, so must
                              // check the height to see if the widget is actually there.
                              (inputElem.offsetHeight !== 0);

                      docElement.removeChild(inputElem);

                    } else if ( /^(search|tel)$/.test(inputElemType) ){
                      // Spec doesn't define any special parsing or detectable UI
                      //   behaviors so we pass these through as true

                      // Interestingly, opera fails the earlier test, so it doesn't
                      //  even make it here.

                    } else if ( /^(url|email)$/.test(inputElemType) ) {
                      // Real url and email support comes with prebaked validation.
                      bool = inputElem.checkValidity && inputElem.checkValidity() === false;

                    } else {
                      // If the upgraded input compontent rejects the :) text, we got a winner
                      bool = inputElem.value != smile;
                    }
                }

                inputs[ props[i] ] = !!bool;
            }
            return inputs;
        })('search tel url email datetime date month week time datetime-local number range color'.split(' '));
        /*>>inputtypes*/
    }
    /*>>webforms*/


    // End of test definitions
    // -----------------------



    // Run through all tests and detect their support in the current UA.
    // todo: hypothetically we could be doing an array of tests and use a basic loop here.
    for ( var feature in tests ) {
        if ( hasOwnProp(tests, feature) ) {
            // run the test, throw the return value into the Modernizr,
            //   then based on that boolean, define an appropriate className
            //   and push it into an array of classes we'll join later.
            featureName  = feature.toLowerCase();
            Modernizr[featureName] = tests[feature]();

            classes.push((Modernizr[featureName] ? '' : 'no-') + featureName);
        }
    }

    /*>>webforms*/
    // input tests need to run.
    Modernizr.input || webforms();
    /*>>webforms*/


    /**
     * addTest allows the user to define their own feature tests
     * the result will be added onto the Modernizr object,
     * as well as an appropriate className set on the html element
     *
     * @param feature - String naming the feature
     * @param test - Function returning true if feature is supported, false if not
     */
     Modernizr.addTest = function ( feature, test ) {
       if ( typeof feature == 'object' ) {
         for ( var key in feature ) {
           if ( hasOwnProp( feature, key ) ) {
             Modernizr.addTest( key, feature[ key ] );
           }
         }
       } else {

         feature = feature.toLowerCase();

         if ( Modernizr[feature] !== undefined ) {
           // we're going to quit if you're trying to overwrite an existing test
           // if we were to allow it, we'd do this:
           //   var re = new RegExp("\\b(no-)?" + feature + "\\b");
           //   docElement.className = docElement.className.replace( re, '' );
           // but, no rly, stuff 'em.
           return Modernizr;
         }

         test = typeof test == 'function' ? test() : test;

         if (typeof enableClasses !== "undefined" && enableClasses) {
           docElement.className += ' ' + (test ? '' : 'no-') + feature;
         }
         Modernizr[feature] = test;

       }

       return Modernizr; // allow chaining.
     };


    // Reset modElem.cssText to nothing to reduce memory footprint.
    setCss('');
    modElem = inputElem = null;

    /*>>shiv*/
    /*! HTML5 Shiv v3.6.1 | @afarkas @jdalton @jon_neal @rem | MIT/GPL2 Licensed */
    ;(function(window, document) {
    /*jshint evil:true */
      /** Preset options */
      var options = window.html5 || {};

      /** Used to skip problem elements */
      var reSkip = /^<|^(?:button|map|select|textarea|object|iframe|option|optgroup)$/i;

      /** Not all elements can be cloned in IE **/
      var saveClones = /^(?:a|b|code|div|fieldset|h1|h2|h3|h4|h5|h6|i|label|li|ol|p|q|span|strong|style|table|tbody|td|th|tr|ul)$/i;

      /** Detect whether the browser supports default html5 styles */
      var supportsHtml5Styles;

      /** Name of the expando, to work with multiple documents or to re-shiv one document */
      var expando = '_html5shiv';

      /** The id for the the documents expando */
      var expanID = 0;

      /** Cached data for each document */
      var expandoData = {};

      /** Detect whether the browser supports unknown elements */
      var supportsUnknownElements;

      (function() {
        try {
            var a = document.createElement('a');
            a.innerHTML = '<xyz></xyz>';
            //if the hidden property is implemented we can assume, that the browser supports basic HTML5 Styles
            supportsHtml5Styles = ('hidden' in a);

            supportsUnknownElements = a.childNodes.length == 1 || (function() {
              // assign a false positive if unable to shiv
              (document.createElement)('a');
              var frag = document.createDocumentFragment();
              return (
                typeof frag.cloneNode == 'undefined' ||
                typeof frag.createDocumentFragment == 'undefined' ||
                typeof frag.createElement == 'undefined'
              );
            }());
        } catch(e) {
          supportsHtml5Styles = true;
          supportsUnknownElements = true;
        }

      }());

      /*--------------------------------------------------------------------------*/

      /**
       * Creates a style sheet with the given CSS text and adds it to the document.
       * @private
       * @param {Document} ownerDocument The document.
       * @param {String} cssText The CSS text.
       * @returns {StyleSheet} The style element.
       */
      function addStyleSheet(ownerDocument, cssText) {
        var p = ownerDocument.createElement('p'),
            parent = ownerDocument.getElementsByTagName('head')[0] || ownerDocument.documentElement;

        p.innerHTML = 'x<style>' + cssText + '</style>';
        return parent.insertBefore(p.lastChild, parent.firstChild);
      }

      /**
       * Returns the value of `html5.elements` as an array.
       * @private
       * @returns {Array} An array of shived element node names.
       */
      function getElements() {
        var elements = html5.elements;
        return typeof elements == 'string' ? elements.split(' ') : elements;
      }

        /**
       * Returns the data associated to the given document
       * @private
       * @param {Document} ownerDocument The document.
       * @returns {Object} An object of data.
       */
      function getExpandoData(ownerDocument) {
        var data = expandoData[ownerDocument[expando]];
        if (!data) {
            data = {};
            expanID++;
            ownerDocument[expando] = expanID;
            expandoData[expanID] = data;
        }
        return data;
      }

      /**
       * returns a shived element for the given nodeName and document
       * @memberOf html5
       * @param {String} nodeName name of the element
       * @param {Document} ownerDocument The context document.
       * @returns {Object} The shived element.
       */
      function createElement(nodeName, ownerDocument, data){
        if (!ownerDocument) {
            ownerDocument = document;
        }
        if(supportsUnknownElements){
            return ownerDocument.createElement(nodeName);
        }
        if (!data) {
            data = getExpandoData(ownerDocument);
        }
        var node;

        if (data.cache[nodeName]) {
            node = data.cache[nodeName].cloneNode();
        } else if (saveClones.test(nodeName)) {
            node = (data.cache[nodeName] = data.createElem(nodeName)).cloneNode();
        } else {
            node = data.createElem(nodeName);
        }

        // Avoid adding some elements to fragments in IE < 9 because
        // * Attributes like `name` or `type` cannot be set/changed once an element
        //   is inserted into a document/fragment
        // * Link elements with `src` attributes that are inaccessible, as with
        //   a 403 response, will cause the tab/window to crash
        // * Script elements appended to fragments will execute when their `src`
        //   or `text` property is set
        return node.canHaveChildren && !reSkip.test(nodeName) ? data.frag.appendChild(node) : node;
      }

      /**
       * returns a shived DocumentFragment for the given document
       * @memberOf html5
       * @param {Document} ownerDocument The context document.
       * @returns {Object} The shived DocumentFragment.
       */
      function createDocumentFragment(ownerDocument, data){
        if (!ownerDocument) {
            ownerDocument = document;
        }
        if(supportsUnknownElements){
            return ownerDocument.createDocumentFragment();
        }
        data = data || getExpandoData(ownerDocument);
        var clone = data.frag.cloneNode(),
            i = 0,
            elems = getElements(),
            l = elems.length;
        for(;i<l;i++){
            clone.createElement(elems[i]);
        }
        return clone;
      }

      /**
       * Shivs the `createElement` and `createDocumentFragment` methods of the document.
       * @private
       * @param {Document|DocumentFragment} ownerDocument The document.
       * @param {Object} data of the document.
       */
      function shivMethods(ownerDocument, data) {
        if (!data.cache) {
            data.cache = {};
            data.createElem = ownerDocument.createElement;
            data.createFrag = ownerDocument.createDocumentFragment;
            data.frag = data.createFrag();
        }


        ownerDocument.createElement = function(nodeName) {
          //abort shiv
          if (!html5.shivMethods) {
              return data.createElem(nodeName);
          }
          return createElement(nodeName, ownerDocument, data);
        };

        ownerDocument.createDocumentFragment = Function('h,f', 'return function(){' +
          'var n=f.cloneNode(),c=n.createElement;' +
          'h.shivMethods&&(' +
            // unroll the `createElement` calls
            getElements().join().replace(/\w+/g, function(nodeName) {
              data.createElem(nodeName);
              data.frag.createElement(nodeName);
              return 'c("' + nodeName + '")';
            }) +
          ');return n}'
        )(html5, data.frag);
      }

      /*--------------------------------------------------------------------------*/

      /**
       * Shivs the given document.
       * @memberOf html5
       * @param {Document} ownerDocument The document to shiv.
       * @returns {Document} The shived document.
       */
      function shivDocument(ownerDocument) {
        if (!ownerDocument) {
            ownerDocument = document;
        }
        var data = getExpandoData(ownerDocument);

        if (html5.shivCSS && !supportsHtml5Styles && !data.hasCSS) {
          data.hasCSS = !!addStyleSheet(ownerDocument,
            // corrects block display not defined in IE6/7/8/9
            'article,aside,figcaption,figure,footer,header,hgroup,nav,section{display:block}' +
            // adds styling not present in IE6/7/8/9
            'mark{background:#FF0;color:#000}'
          );
        }
        if (!supportsUnknownElements) {
          shivMethods(ownerDocument, data);
        }
        return ownerDocument;
      }

      /*--------------------------------------------------------------------------*/

      /**
       * The `html5` object is exposed so that more elements can be shived and
       * existing shiving can be detected on iframes.
       * @type Object
       * @example
       *
       * // options can be changed before the script is included
       * html5 = { 'elements': 'mark section', 'shivCSS': false, 'shivMethods': false };
       */
      var html5 = {

        /**
         * An array or space separated string of node names of the elements to shiv.
         * @memberOf html5
         * @type Array|String
         */
        'elements': options.elements || 'abbr article aside audio bdi canvas data datalist details figcaption figure footer header hgroup mark meter nav output progress section summary time video',

        /**
         * A flag to indicate that the HTML5 style sheet should be inserted.
         * @memberOf html5
         * @type Boolean
         */
        'shivCSS': (options.shivCSS !== false),

        /**
         * Is equal to true if a browser supports creating unknown/HTML5 elements
         * @memberOf html5
         * @type boolean
         */
        'supportsUnknownElements': supportsUnknownElements,

        /**
         * A flag to indicate that the document's `createElement` and `createDocumentFragment`
         * methods should be overwritten.
         * @memberOf html5
         * @type Boolean
         */
        'shivMethods': (options.shivMethods !== false),

        /**
         * A string to describe the type of `html5` object ("default" or "default print").
         * @memberOf html5
         * @type String
         */
        'type': 'default',

        // shivs the document according to the specified `html5` object options
        'shivDocument': shivDocument,

        //creates a shived element
        createElement: createElement,

        //creates a shived documentFragment
        createDocumentFragment: createDocumentFragment
      };

      /*--------------------------------------------------------------------------*/

      // expose html5
      window.html5 = html5;

      // shiv the document
      shivDocument(document);

    }(this, document));
    /*>>shiv*/

    // Assign private properties to the return object with prefix
    Modernizr._version      = version;

    // expose these for the plugin API. Look in the source for how to join() them against your input
    /*>>prefixes*/
    Modernizr._prefixes     = prefixes;
    /*>>prefixes*/
    /*>>domprefixes*/
    Modernizr._domPrefixes  = domPrefixes;
    Modernizr._cssomPrefixes  = cssomPrefixes;
    /*>>domprefixes*/

    /*>>mq*/
    // Modernizr.mq tests a given media query, live against the current state of the window
    // A few important notes:
    //   * If a browser does not support media queries at all (eg. oldIE) the mq() will always return false
    //   * A max-width or orientation query will be evaluated against the current state, which may change later.
    //   * You must specify values. Eg. If you are testing support for the min-width media query use:
    //       Modernizr.mq('(min-width:0)')
    // usage:
    // Modernizr.mq('only screen and (max-width:768)')
    Modernizr.mq            = testMediaQuery;
    /*>>mq*/

    /*>>hasevent*/
    // Modernizr.hasEvent() detects support for a given event, with an optional element to test on
    // Modernizr.hasEvent('gesturestart', elem)
    Modernizr.hasEvent      = isEventSupported;
    /*>>hasevent*/

    /*>>testprop*/
    // Modernizr.testProp() investigates whether a given style property is recognized
    // Note that the property names must be provided in the camelCase variant.
    // Modernizr.testProp('pointerEvents')
    Modernizr.testProp      = function(prop){
        return testProps([prop]);
    };
    /*>>testprop*/

    /*>>testallprops*/
    // Modernizr.testAllProps() investigates whether a given style property,
    //   or any of its vendor-prefixed variants, is recognized
    // Note that the property names must be provided in the camelCase variant.
    // Modernizr.testAllProps('boxSizing')
    Modernizr.testAllProps  = testPropsAll;
    /*>>testallprops*/


    /*>>teststyles*/
    // Modernizr.testStyles() allows you to add custom styles to the document and test an element afterwards
    // Modernizr.testStyles('#modernizr { position:absolute }', function(elem, rule){ ... })
    Modernizr.testStyles    = injectElementWithStyles;
    /*>>teststyles*/


    /*>>prefixed*/
    // Modernizr.prefixed() returns the prefixed or nonprefixed property name variant of your input
    // Modernizr.prefixed('boxSizing') // 'MozBoxSizing'

    // Properties must be passed as dom-style camelcase, rather than `box-sizing` hypentated style.
    // Return values will also be the camelCase variant, if you need to translate that to hypenated style use:
    //
    //     str.replace(/([A-Z])/g, function(str,m1){ return '-' + m1.toLowerCase(); }).replace(/^ms-/,'-ms-');

    // If you're trying to ascertain which transition end event to bind to, you might do something like...
    //
    //     var transEndEventNames = {
    //       'WebkitTransition' : 'webkitTransitionEnd',
    //       'MozTransition'    : 'transitionend',
    //       'OTransition'      : 'oTransitionEnd',
    //       'msTransition'     : 'MSTransitionEnd',
    //       'transition'       : 'transitionend'
    //     },
    //     transEndEventName = transEndEventNames[ Modernizr.prefixed('transition') ];

    Modernizr.prefixed      = function(prop, obj, elem){
      if(!obj) {
        return testPropsAll(prop, 'pfx');
      } else {
        // Testing DOM property e.g. Modernizr.prefixed('requestAnimationFrame', window) // 'mozRequestAnimationFrame'
        return testPropsAll(prop, obj, elem);
      }
    };
    /*>>prefixed*/


    /*>>cssclasses*/
    // Remove "no-js" class from <html> element, if it exists:
    docElement.className = docElement.className.replace(/(^|\s)no-js(\s|$)/, '$1$2') +

                            // Add the new classes to the <html> element.
                            (enableClasses ? ' js ' + classes.join(' ') : '');
    /*>>cssclasses*/

    return Modernizr;

})(this, this.document);
;
/*
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 */
/**
 * @preserve http://google-maps-utility-library-v3.googlecode.com
 */
/**
 * @name ArcGIS Server Link for Google Maps JavaScript API V3
 * @version 1.0
 * @author: Nianwei Liu (nianwei at gmail dot com)
 * @fileoverview 
 *  <p><a href="examples.html">Examples</a>
 *   </p> 
 *  <p>This library lets you add map resources accessible via 
 *    <a href = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/index.html'> 
 *    ESRI ArcGIS Server&#0153; REST API</a> into <a 
 *    href='http://code.google.com/apis/maps/documentation/javascript/'>
 *    Google Maps API V3</a> and provide some additional support for map tiles created 
 *    with different spatial reference and tiling scheme.</p>
 *    </p>.
 *    <table>
 *    <tr>
 *    <td style = 'width:200px'>
 *    {@link TileLayer}<br/>
 *    {@link TileLayerOptions}<br/>
 *    {@link MapType}<br/>
 *    {@link MapTypeOptions}<br/>
 *    {@link MapOverlay}<br/>
 *    {@link MapOverlayOptions}<br/>
 *    {@link Projection}<br/>
 *    </td>
 *    <td style = 'width:200px'>
 *    {@link Catalog}<br/>
 *    {@link MapService}<br/></b>
 *    {@link Layer}<br/>
 *    {@link GeocodeService}<br/>
 *    {@link GeometryService}<br/>
 *    {@link GPService}<br/>
 *    {@link GPTask}<br/>
 *    {@link RouteTask}<br/>
 *     <br/></td>
 *     <td style = 'width:200px'>
 *    {@link SpatialReference}<br/>
 *    {@link Geographic}<br/>
 *    {@link LambertConformalConic}<br/>
 *    {@link TransverseMercator}<br/>
 *    {@link SphereMercator}<br/>
 *    {@link Albers}<br/>
 *    {@link SpatialRelationship}<br/>
 *     </td>
 *     <td style = 'width:200px'>
 *    {@link Util} <br/> 
 *    {@link Config} <br/> 
 *    {@link Error} <br/> 
 *     </td>
 *    </tr></table>
 *    <p> There are many objects used in the REST API that do not require 
 *    a constructor and can be
 *    used just as object literal in the operation:</p>
 *    <table><tr>
 *    <td style = 'width:200px'>
 *    {@link Field}<br/>
 *    {@link TileInfo}<br/>
 *    {@link LOD}<br/>
 *    {@link ExportMapOptions}<br/>
 *    {@link MapImage}<br/>
 *    {@link IdentifyOptions}<br/>
 *    {@link IdentifyResults}<br/>
 *    {@link IdentifyResult}<br/>
 *     <br/></td>
 *     <td style = 'width:200px'>
 *    {@link QueryOptions}<br/>
 *    {@link ResultSet}<br/>
 *    {@link FindOptions}<br/>
 *    {@link FindResults}<br/>
 *    {@link FindResult}<br/>
 *    {@link Feature}<br/>
 *     </td>
 *     <td style = 'width:200px'>
 *    {@link GeocodeOptions}<br/>
 *    {@link GeocodeResults}<br/>
 *    {@link GeocodeResult}<br/>
 *    {@link ReverseGeocodeOptions}<br/>
 *    {@link ReverseGeocodeResult}<br/>
 *    {@link BufferOptions}<br/>
 *    {@link BufferResults}<br/> 
 *    {@link ProjectOptions}<br/>
 *    {@link ProjectResults}<br/> 
 *    </td>
 *     <td style = 'width:200px'>
 *    {@link RouteOptions}<br/>
 *    {@link RouteResults}<br/>  
 *    </td>
 *    </tr></table>
 */

/*jslint evil: true, sub: true */
/*global escape ActiveXObject */
var gmaps = gmaps || {};

/** @const */
var RAD_DEG = Math.PI / 180;
var jsonpID_ = 0;
window['ags_jsonp'] = window['ags_jsonp'] || {};


var G = google.maps;
var WGS84, NAD83, WEB_MERCATOR, WEB_MERCATOR_AUX;

/**
 * @name Config
 * @class This is an object literal that sets common configuration values used across the lib.
 * @property {String} [proxyUrl] The URL to the web proxy page used in case the length of the URL request to an ArcGIS Server REST resource exceeds 2000 characters.
 * @property {Boolean} [alwaysUseProxy] whether to always use proxy page when send request to server.
 */
var Config = {
    proxyUrl: null,
    alwaysUseProxy: false
};

/**
 * an internal collection of Spatial Refeneces supported in the application.
 * The key of the collection is the wkid/wkt, and value is an instance of
 * {@link SpatialReference}.
 */
var spatialReferences_ = {};

/**
 * A set of utilities ((<code>Util</code>)
 * for commonly used functions.
 * @name Util
 * @namespace
 */
var Util = {};

/**
 * Extract the substring from full string, between start string and end string
 * @param {String} full
 * @param {String} start
 * @param {String} end
 */
function extractString_(full, start, end) {
    var i = (start === '') ? 0 : full.indexOf(start);
    var e = end === '' ? full.length : full.indexOf(end, i + start.length);
    return full.substring(i + start.length, e);
}

/**
 * Check if the object is String
 * @param {Object} o
 */
function isString_(o) {
    return o && typeof o === 'string';
}

/**
   * Check if the object is array
   * @param {Object} o
   */
function isArray_(o) {
    return o && o.splice;
}

function isNumber_(o) {
    return typeof o === 'number';
}

/**
   * Add the property of the source object to destination object 
   * if not already exists.
   * @param {Object} dest
   * @param {Object} src
   * @param {Boolean} force
   * @return {Object}
   */
function augmentObject_(src, dest, force) {
    if (src && dest) {
        var p;
        for (p in src) {
            if (force || !(p in dest)) {
                dest[p] = src[p];
            }
        }
    }
    return dest;
}

/**
 * Wrapper around google.maps.event.trigger
 * @param {Object} src
 * @param {String} evtName
 * @param {Object} args
 */
function triggerEvent_(src, evtName, args) {
    G.event.trigger.apply(this, arguments);
}

/**
 * handle JSON error
 * @param {Object} errback
 * @param {Object} json
 */
function handleErr_(errback, json) {
    if (errback && json && json.error) {
        errback(json.error);
    }
}

/**
 * get REST format for 2 time
 * @param {Date} time
 * @param {Date} endTime
 */
function formatTimeString_(time, endTime) {
    var ret = '';
    if (time) {
        ret += (time.getTime() - time.getTimezoneOffset() * 60000);
    }
    if (endTime) {
        ret += ', ' + (endTime.getTime() - endTime.getTimezoneOffset() * 60000);
    }
    return ret;
}

/**
 * Set opacity of a node.
 * @param {Node} node
 * @param {Number} 0-1
 */
function setNodeOpacity_(node, op) {
    // closure compiler removed?
    op = Math.min(Math.max(op, 0), 1);
    if (node) {
        var st = node.style;
        if (typeof st.opacity !== 'undefined') {
            st.opacity = op;
        }
        if (typeof st.filters !== 'undefined') {
            st.filters.alpha.opacity = Math.floor(100 * op);
        }
        if (typeof st.filter !== 'undefined') {
            st.filter = "alpha(opacity:" + Math.floor(op * 100) + ")";
        }
    }
}

/**
 * get the layerdef text string from an object literal
 * @param {Object} defs
 */
function getLayerDefsString_(defs) {
    var strDefs = '';
    for (var x in defs) {
        if (defs.hasOwnProperty(x)) {
            if (strDefs.length > 0) {
                strDefs += ';';
            }
            strDefs += (x + ':' + defs[x]);
        }
    }
    return strDefs;
}

function getXmlHttp_() {
    if (typeof XMLHttpRequest === "undefined") {
        try {
            return new ActiveXObject("Msxml2.XMLHTTP.6.0");
        } catch (e) {
        }
        try {
            return new ActiveXObject("Msxml2.XMLHTTP.3.0");
        } catch (e1) {
        }
        try {
            return new ActiveXObject("Msxml2.XMLHTTP");
        } catch (e2) {
        }
        throw new Error("This browser does not support XMLHttpRequest.");
    } else {
        return new XMLHttpRequest();
    }
}

/**
 * @name GeometryType
 * @enum {String}
 * @const
 * @class List of Geometry type supported by ArcGIS server.
 * @property {String} [POINT] esriGeometryPoint
 * @property {String} [MULTIPOINT] esriGeometryMultipoint
 * @property {String} [POLYLINE] esriGeometryPolyline
 * @property {String} [POLYGON] esriGeometryPolygon
 * @property {String} [ENVELOPE] esriGeometryEnvelope
 */
var GeometryType = {
    POINT: 'esriGeometryPoint',
    MULTIPOINT: 'esriGeometryMultipoint',
    POLYLINE: 'esriGeometryPolyline',
    POLYGON: 'esriGeometryPolygon',
    ENVELOPE: 'esriGeometryEnvelope'
};

function getGeometryType_(obj) {
    var o = obj;
    if (isArray_(obj) && obj.length > 0) {
        o = obj[0];
    }
    if (o instanceof G.LatLng || o instanceof G.Marker) {
        if (isArray_(obj) && obj.length > 1) {
            return GeometryType.MULTIPOINT;
        } else {
            return GeometryType.POINT;
        }
    } else if (o instanceof G.Polyline) {
        return GeometryType.POLYLINE;
    } else if (o instanceof G.Polygon) {
        return GeometryType.POLYGON;
    } else if (o instanceof G.LatLngBounds) {
        return GeometryType.ENVELOPE;
    } else if (o.x !== undefined && o.y !== undefined) {
        return GeometryType.POINT;
    } else if (o.points) {
        return GeometryType.MULTIPOINT;
    } else if (o.paths) {
        return GeometryType.POLYLINE;
    } else if (o.rings) {
        return GeometryType.POLYGON;
    }
    return null;
}

/**
 * Is the object an Google Overlay?
 * @param {Object} obj
 * @return {Boolean}
 */
function isOverlay_(obj) {
    var o = obj;
    if (isArray_(obj) && obj.length > 0) {
        o = obj[0];
    }
    if (isArray_(o) && o.length > 0) {
        o = o[0];
    }
    if (o instanceof G.LatLng || o instanceof G.Marker ||
        o instanceof G.Polyline ||
        o instanceof G.Polygon ||
        o instanceof G.LatLngBounds) {
        return true;
    }
    return false;
}

function formatSRParam_(sr) {
    if (!sr) {
        return null;
    }
    // for 9.3 compatibility, return wkid if possible.
    return isNumber_(sr) ? sr : sr.wkid ? sr.wkid : sr.toJSON();
}

/**
 * @param {MVCArrayOfLatLng} pts
 */
function fromLatLngsToJSON_(pts, close) {
    var arr = [];
    var latlng;
    for (var i = 0, c = pts.getLength(); i < c; i++) {
        latlng = pts.getAt(i);
        arr.push('[' + latlng.lng() + ',' + latlng.lat() + ']');
    }
    if (close && arr.length > 0) {
        arr.push('[' + pts.getAt(0).lng() + ',' + pts.getAt(0).lat() + ']');
    }
    return arr.join(',');
}

/**
 * Convert overlays (Marker, Polyline, Polygons) to JSON string in AGS format.
 * @param {OverlayView|Array.OverlayView} geom
 */
function fromOverlaysToJSON_(geom) {
    var gtype = getGeometryType_(geom);
    var g, gs, i, pts;
    var json = '{';
    switch (gtype) {
    case GeometryType.POINT:
        g = isArray_(geom) ? geom[0] : geom;
        if (g instanceof G.Marker) {
            g = g.getPosition();
        }
        json += 'x:' + g.lng() + ',y:' + g.lat();
        break;
    case GeometryType.MULTIPOINT:
        pts = [];
        for (i = 0; i < geom.length; i++) {
            if (geom[i] instanceof G.Marker) {
                g = geom[i].getPosition();
            } else {
                g = geom[i];
            }
            pts.push('[' + g.lng() + ',' + g.lat() + ']');
        }
        json += 'points: [' + pts.join(',') + ']';
        break;
    case GeometryType.POLYLINE:
        // V3 does not support multiple paths yet
        pts = [];
        gs = isArray_(geom) ? geom : [geom];
        for (i = 0; i < gs.length; i++) {
            pts.push('[' + fromLatLngsToJSON_(gs[i].getPath()) + ']');
        }
        json += 'paths:[' + pts.join(',') + ']';
        break;
    case GeometryType.POLYGON:
        pts = [];
        g = isArray_(geom) ? geom[0] : geom;
        var paths = g.getPaths();
        for (i = 0; i < paths.getLength(); i++) {
            pts.push('[' + fromLatLngsToJSON_(paths.getAt(i), true) + ']');
        }
        json += 'rings:[' + pts.join(',') + ']';

        break;
    case GeometryType.ENVELOPE:
        g = isArray_(geom) ? geom[0] : geom;
        json += 'xmin:' + g.getSouthWest().lng() + ',ymin:' + g.getSouthWest().lat() + ',xmax:' + g.getNorthEast().lng() + ',ymax:' + g.getNorthEast().lat();
        break;
    }
    json += ', spatialReference:{wkid:4326}';
    json += '}';
    return json;
}

/**
 * From ESRI geometry format to JSON String, primarily used in Geometry service
 * @param {Object} geom
 */
function fromGeometryToJSON_(geom) {
    function fromPointsToJSON(pts) {
        var arr = [];
        for (var i = 0, c = pts.length; i < c; i++) {
            arr.push('[' + pts[i][0] + ',' + pts[i][1] + ']');
        }
        return '[' + arr.join(',') + ']';
    }

    function fromLinesToJSON(lines) {
        var arr = [];
        for (var i = 0, c = lines.length; i < c; i++) {
            arr.push(fromPointsToJSON(lines[i]));
        }
        return '[' + arr.join(',') + ']';
    }

    var json = '{';
    if (geom.x) {
        json += 'x:' + geom.x + ',y:' + geom.y;
    } else if (geom.xmin) {
        json += 'xmin:' + geom.xmin + ',ymin:' + geom.ymin + ',xmax:' + geom.xmax + ',ymax:' + geom.ymax;
    } else if (geom.points) {
        json += 'points:' + fromPointsToJSON(geom.points);
    } else if (geom.paths) {
        json += 'paths:' + fromLinesToJSON(geom.paths);
    } else if (geom.rings) {
        json += 'rings:' + fromLinesToJSON(geom.rings);
    }
    json += '}';
    return json;
}

/**
 * Helper method to convert an Envelope object to <code>google.maps.LatLngBounds</code>
 * @private
 * @param {Object} extent
 * @return {google.maps.LatLngBounds} gLatLngBounds
 */
function fromEnvelopeToLatLngBounds_(extent) {
    var sr = spatialReferences_[extent.spatialReference.wkid || extent.spatialReference.wkt];
    sr = sr || WGS84;
    var sw = sr.inverse([extent.xmin, extent.ymin]);
    var ne = sr.inverse([extent.xmax, extent.ymax]);
    return new G.LatLngBounds(new G.LatLng(sw[1], sw[0]), new G.LatLng(ne[1], ne[0]));
}

/**
 * Convert a ArcGIS Geometry JSON object to core Google Maps API
 * overlays such as  <code>google.maps.Marker</code>, <code>google.maps.Polyline</code> or <code>google.maps.Polygon</code>
 * Note ArcGIS Geometry may have multiple parts, but the coresponding OverlayView
 * may (Polygon) or may not (Polyline) support multi-parts, so the result is an array for consistency.
 * @param {Object} json geometry
 * @param {OverlayOptions} opts see {@link OverlayOptions}
 * @return {Array.OverlayView}
 */
function fromJSONToOverlays_(geom, opts) {
    var ovs = null;
    var ov;
    var i, ic, j, jc, parts, part, lnglat, latlngs;
    opts = opts || {};
    if (geom) {
        ovs = [];
        if (geom.x) {
            ov = new G.Marker(augmentObject_(opts.markerOptions || opts, {
                'position': new G.LatLng(geom.y, geom.x)
            }));
            ovs.push(ov);
        } else {
            //mulpt, line and poly
            parts = geom.points || geom.paths || geom.rings;
            if (!parts) {
                return ovs;
            }
            var rings = [];
            for (i = 0, ic = parts.length; i < ic; i++) {
                part = parts[i];
                if (geom.points) {
                    // multipoint
                    ov = new G.Marker(augmentObject_(opts.markerOptions || opts, {
                        'position': new G.LatLng(part[1], part[0])
                    }));
                    ovs.push(ov);
                } else {
                    latlngs = [];
                    for (j = 0, jc = part.length; j < jc; j++) {
                        lnglat = part[j];
                        latlngs.push(new G.LatLng(lnglat[1], lnglat[0]));
                    }
                    if (geom.paths) {
                        ov = new G.Polyline(augmentObject_(opts.polylineOptions || opts, {
                            'path': latlngs
                        }));
                        ovs.push(ov);
                    } else if (geom.rings) {
                        // V3 supports multiple rings
                        rings.push(latlngs);
                    }
                }
            }
            if (geom.rings) {
                ov = new G.Polygon(augmentObject_(opts.polygonOptions || opts, {
                    'paths': rings
                }));
                ovs.push(ov);
            }
        }
    }
    return ovs;
}

function parseFeatures_(features, ovOpts) {
    if (features) {
        var i, I, f;
        for (i = 0, I = features.length; i < I; i++) {
            f = features[i];
            if (f.geometry) {
                f.geometry = fromJSONToOverlays_(f.geometry, ovOpts);
            }
        }
    }
}

/**
 * get string as rest parameter
 * @param {Object} o
 */
function formatRequestString_(o) {
    var ret;
    if (typeof o === 'object') {
        if (isArray_(o)) {
            ret = [];
            for (var i = 0, I = o.length; i < I; i++) {
                ret.push(formatRequestString_(o[i]));
            }
            return '[' + ret.join(',') + ']';
        } else if (isOverlay_(o)) {
            return fromOverlaysToJSON_(o);
        } else if (o.toJSON) {
            return o.toJSON();
        } else {
            ret = '';
            for (var x in o) {
                if (o.hasOwnProperty(x)) {
                    if (ret.length > 0) {
                        ret += ', ';
                    }
                    ret += x + ':' + formatRequestString_(o[x]);
                }
            }
            return '{' + ret + '}';
        }
    }
    return o.toString();
}

function fromLatLngsToFeatureSet_(latlngs) {
    var i, I, latlng;
    var features = [];
    for (i = 0, I = latlngs.length; i < I; i++) {
        latlng = latlngs[i];
        if (latlng instanceof G.Marker) {
            latlng = latlng.getPosition();
        }
        features.push({
            'geometry': {
                'x': latlng.lng(),
                'y': latlng.lat(),
                'spatialReference': {
                    'wkid': 4326
                }
            }
        });
    }
    return {
        'type': '"features"',
        'features': features,
        'doNotLocateOnRestrictedElements': false
    };
}

function prepareGeometryParams_(p) {
    var params = {};
    if (!p) {
        return null;
    }
    var json = [];
    var g, isOv;
    if (p.geometries && p.geometries.length > 0) {
        g = p.geometries[0];
        isOv = isOverlay_(g);
        for (var i = 0, c = p.geometries.length; i < c; i++) {
            if (isOv) {
                json.push(fromOverlaysToJSON_(p.geometries[i]));
            } else {
                json.push(fromGeometryToJSON_(p.geometries[i]));
            }
        }
    }
    if (!p.geometryType) {
        p.geometryType = getGeometryType_(g);
    }
    if (isOv) {
        params.inSR = WGS84.wkid;
    } else if (p.inSpatialReference) {
        params.inSR = formatSRParam_(p.inSpatialReference);
    }
    if (p.outSpatialReference) {
        params.outSR = formatSRParam_(p.outSpatialReference);
    }
    params.geometries = '{geometryType:"' + p.geometryType + '", geometries:[' + json.join(',') + ']}';
    return params;
}

function log_(msg) {
    if (window.console) {
        window.console.log(msg);
    } else {
        var l = document.getElementById('_ags_log');
        if (l) {
            l.innerHTML = l.innerHTML + msg + '<br/>';
        }
    }
}

/**
 * Format params to URL string
 * @param {Object} params
 */
function formatParams_(params) {
    var query = '';
    if (params) {
        params.f = params.f || 'json';
        for (var x in params) {
            if (params.hasOwnProperty(x) && params[x] !== null && params[x] !== undefined) { // wont sent undefined.
                //jslint complaint about escape cause NN does not support it.
                var val = formatRequestString_(params[x]);
                query += (query.length > 0 ? '&' : '') + (x + '=' + (escape ? escape(val) : encodeURIComponent(val)));
            }
        }
    }
    return query;
}

/** create a callback closure
 * @private
 * @param {Object} fn
 * @param {Object} obj
 */
function callback_(fn, obj) {
    var args = [];
    for (var i = 2, c = arguments.length; i < c; i++) {
        args.push(arguments[i]);
    }
    return function() {
        fn.apply(obj, args);
    };
}

function addCopyrightInfo_(cpArray, mapService, map) {
    if (mapService.hasLoaded()) {
        cpArray.push(mapService.copyrightText);
    } else {
        G.event.addListenerOnce(mapService, 'load', function() {
            setCopyrightInfo_(map);
        });
    }
}

/**
 * Find copyright control in the map
 * @param {Object} map
 */
function setCopyrightInfo_(map) {
    var div = null;
    if (map) {
        var mvc = map.controls[G.ControlPosition.BOTTOM_RIGHT];
        if (mvc) {
            for (var i = 0, c = mvc.getLength(); i < c; i++) {
                if (mvc.getAt(i).id === 'agsCopyrights') {
                    div = mvc.getAt(i);
                    break;
                }
            }
        }
        //var callback = callback_(setCopyrightInfo_, null, map);
        if (!div) {
            div = document.createElement('div');
            div.style.fontFamily = 'Arial,sans-serif';
            div.style.fontSize = '10px';
            div.style.textAlign = 'right';
            div.id = 'agsCopyrights';
            map.controls[G.ControlPosition.BOTTOM_RIGHT].push(div);
            G.event.addListener(map, 'maptypeid_changed', function() {
                setCopyrightInfo_(map);
            });
        }
        var ovs = map.agsOverlays;
        var cp = [];
        var svc, type;
        if (ovs) {
            for (var i = 0, c = ovs.getLength(); i < c; i++) {
                addCopyrightInfo_(cp, ovs.getAt(i).mapService_, map);
            }
        }
        var ovTypes = map.overlayMapTypes;
        if (ovTypes) {
            for (var i = 0, c = ovTypes.getLength(); i < c; i++) {
                type = ovTypes.getAt(i);
                if (type instanceof MapType) {
                    for (var j = 0, cj = type.tileLayers_.length; j < cj; j++) {
                        addCopyrightInfo_(cp, type.tileLayers_[j].mapService_, map);
                    }
                }
            }
        }
        type = map.mapTypes.get(map.getMapTypeId());
        if (type instanceof MapType) {
            for (var i = 0, c = type.tileLayers_.length; i < c; i++) {
                addCopyrightInfo_(cp, type.tileLayers_[i].mapService_, map);
            }
            if (type.negative) {
                div.style.color = '#ffffff';
            } else {
                div.style.color = '#000000';
            }
        }
        div.innerHTML = cp.join('<br/>');
    }
}

function getJSON_(url, params, callbackName, callbackFn) {

    var sid = 'ags_jsonp_' + (jsonpID_++) + '_' + Math.floor(Math.random() * 1000000);
    var script = null;
    params = params || {};

    // AGS10.1 escapes && so had to take it off.
    params[callbackName || 'callback'] = 'ags_jsonp.' + sid;

    var query = formatParams_(params);

    var head = document.getElementsByTagName("head")[0];
    if (!head) {
        throw new Error("document must have header tag");
    }

    var jsonpcallback = function() {
        if (window['ags_jsonp'][sid]) {
            delete window['ags_jsonp'][sid]; //['ags_jsonp']
        }

        if (script) {
            head.removeChild(script);
        }
        script = null;
        callbackFn.apply(null, arguments);
        /**
     * This event is fired after a REST JSONP response was returned by server.
     * @name Util#jsonpend
     * @param {String} scriptID
     * @event
     */
        triggerEvent_(Util, 'jsonpend', sid);
    };

    window['ags_jsonp'][sid] = jsonpcallback;
    if ((query + url).length < 2000 && !Config.alwaysUseProxy) {
        script = document.createElement("script");
        script.src = url + (url.indexOf('?') === -1 ? '?' : '&') + query;
        script.id = sid;
        head.appendChild(script);
    } else {
        // check if same host
        var loc = window.location;
        var dom = loc.protocol + '//' + loc.hostname + (!loc.port || loc.port === 80 ? '' : ':' + loc.port + '/');
        var useProxy = true;
        if (url.toLowerCase().indexOf(dom.toLowerCase()) !== -1) {
            useProxy = false;
        }
        if (Config.alwaysUseProxy) {
            useProxy = true;
        }
        if (useProxy && !Config.proxyUrl) {
            throw new Error('No proxyUrl property in Config is defined');
        }
        var xmlhttp = getXmlHttp_();
        xmlhttp.onreadystatechange = function() {
            if (xmlhttp.readyState === 4) {
                if (xmlhttp.status === 200) {
                    eval(xmlhttp.responseText);
                } else {
                    throw new Error("Error code " + xmlhttp.status);
                }
            }
        };
        xmlhttp.open('POST', useProxy ? Config.proxyUrl + '?' + url : url, true);
        xmlhttp.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
        xmlhttp.send(query);
    }
    /**
   * This event is fired before a REST request sent to server.
   * @name Util#jsonpstart
   * @param {String} scriptID
   * @event
   */
    triggerEvent_(Util, 'jsonpstart', sid);
    return sid;
}

/**
 * Make Cross Domain Calls. This function returns the
 * script ID which can be used to track the requests. parameters:
 * <ul>
 * <li>url: url of server resource
 * <li>params: an object with name,value pairs. value must be string
 * <li>callbackName: Callback parameter name the server is expecting.e.g:'callback'
 * <li>callbackFn: the actual callback function.
 * </ul>
 * @param {String} url
 * @param {Object} params
 * @param {String} callbackName
 * @param {Function} callbackFn
 * @return {String} scriptID
 */
Util.getJSON = function(url, params, callbackName, callbackFn) {
    getJSON_(url, params, callbackName, callbackFn);
};


/**
 * Add a list of overlays to map
 * @param {google.maps.Map} map
 * @param {Array.OverlayView} overlays
 */
Util.addToMap = function(map, overlays) {
    if (isArray_(overlays)) {
        var ov;
        for (var i = 0, I = overlays.length; i < I; i++) {
            ov = overlays[i];
            if (isArray_(ov)) {
                Util.addToMap(map, ov);
            } else if (isOverlay_(ov)) {
                ov.setMap(map);
            }
        }
    }
};
/**
   * Add a list of overlays to map
   * @param {Array.OverlayView} overlays
   * @param {Boolean} clearArray
   */
Util.removeFromMap = function(overlays, clearArray) {
    Util.addToMap(null, overlays);
    if (clearArray) {
        overlays.length = 0;
    }
};


/**
   * Create A Generic Spatial Reference Object
   * The <code>params </code> passed in constructor is a javascript object literal and depends on
   * the type of Coordinate System to construct.
   * @name SpatialReference
   * @class This  class (<code>SpatialReference</code>) is for coordinate systems that converts value
   * between geographic and real-world coordinates. The following classes extend this class:
   *    {@link Geographic}, {@link SphereMercator}, {@link LambertConformalConic}, and {@link TransverseMercator}.
   * @constructor
   * @property {Number} [wkid] well-known coodinate system id (EPSG code)
   * @property {String} [wkt] well-known coodinate system text
   * @param {Object} params
   */
function SpatialReference(params) {
    params = params || {};
    this.wkid = params.wkid;
    this.wkt = params.wkt;
}

/**
   * Convert Lat Lng to real-world coordinates.
   * Note both input and output are array of [x,y], although their values in different units.
 * @param {Array.number} lnglat
 * @return {Array.number}
   */
SpatialReference.prototype.forward = function(lnglat) {
    return lnglat;
};
/**
   * Convert real-world coordinates  to Lat Lng.
   * Note both input and output are are array of [x,y], although their values are different.
 * @param {Array.number}  coords
 * @return {Array.number}
   */
SpatialReference.prototype.inverse = function(coords) {
    return coords;
};
/**
   * Get the map the periodicity in x-direction, in map units NOT pixels
 * @return {number} periodicity in x-direction
   */
SpatialReference.prototype.getCircum = function() {
    return 360;
};
/**
   * To JSON String
   * @return String
   */
SpatialReference.prototype.toJSON = function() {
    return '{' + (this.wkid ? ' wkid:' + this.wkid : 'wkt: \'' + this.wkt + '\'') + '}';
};

/**
   * Creates a Geographic Coordinate System. e.g.:<br/>
 * <code> var g2 = new Geographic({wkid:4326});
   * </code>
   * @name Geographic
   * @class This class (<code>Geographic</code>) will simply retuns same LatLng as Coordinates. 
   *   The <code>param</code> should have wkid property. Any Geographic Coordinate Systems (eg. WGS84(4326)) can 
   *   use this class As-Is. 
   *   <br/>Note:<b> This class does not support datum transformation</b>.
 * @constructor
   * @extends SpatialReference
   * @param {Object} params
   */
function Geographic(params) {
    params = params || {};
    SpatialReference.call(this, params);
}

Geographic.prototype = new SpatialReference();

/**
 * Create a Lambert Conformal Conic Projection based Spatial Reference. The <code>params</code> passed in construction should
 * include the following properties:<code>
 * <br/>-wkid: well-known id
 * <br/>-semi_major:  ellipsoidal semi-major axis in meter
 * <br/>-unit: meters per unit
 * <br/>-inverse_flattening: inverse of flattening of the ellipsoid where 1/f  =  a/(a - b)
 * <br/>-standard_parallel_1: phi1, latitude of the first standard parallel
 * <br/>-standard_parallel_2: phi2, latitude of the second standard parallel
 * <br/>-latitude_of_origin: phi0, latitude of the false origin
 * <br/>-central_meridian: lamda0, longitude of the false origin  (with respect to the prime meridian)
 * <br/>-false_easting: FE, false easting, the Eastings value assigned to the natural origin
 * <br/>-false_northing: FN, false northing, the Northings value assigned to the natural origin
 * </code>
 * <br/> e.g. North Carolina State Plane NAD83 Feet: <br/>
 * <code> var ncsp82  = new LambertConformalConic({wkid:2264, semi_major: 6378137.0,inverse_flattening: 298.257222101,
 *   standard_parallel_1: 34.33333333333334, standard_parallel_2: 36.16666666666666,
 *   central_meridian: -79.0, latitude_of_origin: 33.75,false_easting: 2000000.002616666,
 *   'false_northing': 0, unit: 0.3048006096012192 }); </code>
 * @name LambertConformalConic
 * @class This class (<code>LambertConformalConic</code>) represents a Spatial Reference System based on <a target  = wiki href  = 'http://en.wikipedia.org/wiki/Lambert_conformal_conic_projection'>Lambert Conformal Conic Projection</a>. 
 * @extends SpatialReference
 * @constructor
 * @param {Object} params
 */
function LambertConformalConic(params) {
    //http://pubs.er.usgs.gov/djvu/PP/PP_1395.pdf
    //for NCSP83: GLatLng(35.102363,-80.5666)<  === > GPoint(1531463.95, 495879.744);
    params = params || {};
    SpatialReference.call(this, params);
    var f_i = params.inverse_flattening;
    var phi1 = params.standard_parallel_1 * RAD_DEG;
    var phi2 = params.standard_parallel_2 * RAD_DEG;
    var phi0 = params.latitude_of_origin * RAD_DEG;
    this.a_ = params.semi_major / params.unit;
    this.lamda0_ = params.central_meridian * RAD_DEG;
    this.FE_ = params.false_easting;
    this.FN_ = params.false_northing;

    var f = 1.0 / f_i; //e: eccentricity of the ellipsoid where e^2  =  2f - f^2 
    var es = 2 * f - f * f;
    this.e_ = Math.sqrt(es);
    var m1 = this.calc_m_(phi1, es);
    var m2 = this.calc_m_(phi2, es);
    var tF = this.calc_t_(phi0, this.e_);
    var t1 = this.calc_t_(phi1, this.e_);
    var t2 = this.calc_t_(phi2, this.e_);
    this.n_ = Math.log(m1 / m2) / Math.log(t1 / t2);
    this.F_ = m1 / (this.n_ * Math.pow(t1, this.n_));
    this.rho0_ = this.calc_rho_(this.a_, this.F_, tF, this.n_);
}

LambertConformalConic.prototype = new SpatialReference();
/**
   * calc_m_
 * @param {number} phi
 * @param {number} es e square
   */
LambertConformalConic.prototype.calc_m_ = function(phi, es) {
    var sinphi = Math.sin(phi);
    return Math.cos(phi) / Math.sqrt(1 - es * sinphi * sinphi);
};
/**
   * calc_t_
   * @param {Object} phi
   * @param {Object} e
   */
LambertConformalConic.prototype.calc_t_ = function(phi, e) {
    var esp = e * Math.sin(phi);
    return Math.tan(Math.PI / 4 - phi / 2) / Math.pow((1 - esp) / (1 + esp), e / 2);
};
/**
   * calc_rho (15-7)_
   * @param {Object} a
   * @param {Object} F
   * @param {Object} t
   * @param {Object} n
   */
LambertConformalConic.prototype.calc_rho_ = function(a, F, t, n) {
    return a * F * Math.pow(t, n);
};
/**
   * calc_phi_
   * @param {Object} t_i
   * @param {Object} e
   * @param {Object} phi
   */
LambertConformalConic.prototype.calc_phi_ = function(t, e, phi) {
    var esp = e * Math.sin(phi);
    return Math.PI / 2 - 2 * Math.atan(t * Math.pow((1 - esp) / (1 + esp), e / 2));
};
/**
   * solve phi iteratively.
   * @param {Object} t_i
   * @param {Object} e
   * @param {Object} init
   */
LambertConformalConic.prototype.solve_phi_ = function(t_i, e, init) {
    // iteration
    var i = 0;
    var phi = init;
    var newphi = this.calc_phi_(t_i, e, phi); //this.
    while (Math.abs(newphi - phi) > 0.000000001 && i < 10) {
        i++;
        phi = newphi;
        newphi = this.calc_phi_(t_i, e, phi); //this.
    }
    return newphi;
};
/** 
   * see {@link SpatialReference}
 * @param {Array.number} lnglat
 * @return {Array.number}
   */
LambertConformalConic.prototype.forward = function(lnglat) {
    var phi = lnglat[1] * RAD_DEG; // (Math.PI / 180);
    var lamda = lnglat[0] * RAD_DEG;
    var t = this.calc_t_(phi, this.e_);
    var rho = this.calc_rho_(this.a_, this.F_, t, this.n_);
    var theta = this.n_ * (lamda - this.lamda0_);
    var E = this.FE_ + rho * Math.sin(theta);
    var N = this.FN_ + this.rho0_ - rho * Math.cos(theta);
    return [E, N];
};
/**
   * see {@link SpatialReference}
 * @param {Array.number}  coords
 * @return {Array.number}
   */
LambertConformalConic.prototype.inverse = function(coords) {
    var E = coords[0] - this.FE_;
    var N = coords[1] - this.FN_;
    var theta = Math.atan(E / (this.rho0_ - N));
    var rho = (this.n_ > 0 ? 1 : -1) * Math.sqrt(E * E + (this.rho0_ - N) * (this.rho0_ - N));
    var t = Math.pow((rho / (this.a_ * this.F_)), 1 / this.n_);
    var init = Math.PI / 2 - 2 * Math.atan(t);
    var phi = this.solve_phi_(t, this.e_, init);
    var lamda = theta / this.n_ + this.lamda0_;
    return [lamda / RAD_DEG, phi / RAD_DEG];
};
/**
   *  see {@link SpatialReference}
 *  @return {number}
   */
LambertConformalConic.prototype.getCircum = function() {
    return Math.PI * 2 * this.a_;
};

/**
 * Create a Transverse Mercator Projection. The <code>params</code> passed in constructor should contain the 
 * following properties: <br/>
 * <code>
 * <br/>-wkid: well-known id
 * <br/>-semi_major:  ellipsoidal semi-major axis in meters
 * <br/>-unit: meters per unit
 * <br/>-inverse_flattening: inverse of flattening of the ellipsoid where 1/f  =  a/(a - b)
 * <br/>-Scale Factor: scale factor at origin
 * <br/>-latitude_of_origin: phi0, latitude of the false origin
 * <br/>-central_meridian: lamda0, longitude of the false origin  (with respect to the prime meridian)
 * <br/>-false_easting: FE, false easting, the Eastings value assigned to the natural origin 
 * <br/>-false_northing: FN, false northing, the Northings value assigned to the natural origin 
 * </code>
 * <br/>e.g. Georgia West State Plane NAD83 Feet:  
 * <br/><code> var gawsp83  = new TransverseMercator({wkid: 102667, semi_major:6378137.0,
 *  inverse_flattening:298.257222101,central_meridian:-84.16666666666667, latitude_of_origin: 30.0,
 *  scale_factor:0.9999, false_easting:2296583.333333333, false_northing:0, unit: 0.3048006096012192});
 *  </code>
 * @param {Object} params 
 * @name TransverseMercator
 * @constructor
 * @class This class (<code>TransverseMercator</code>) represents a Spatial Reference System based on 
 * <a target  = wiki href  = 'http://en.wikipedia.org/wiki/Transverse_Mercator_projection'>Transverse Mercator Projection</a>
 * @extends SpatialReference
 */
function TransverseMercator(params) {
    params = params || {};
    SpatialReference.call(this, params);
    //GLatLng(33.74561,-84.454308)<  === >  GPoint(2209149.07977075, 1362617.71496891);
    this.a_ = params.semi_major / params.unit; //this.
    var f_i = params.inverse_flattening;
    this.k0_ = params.scale_factor;
    var phi0 = params.latitude_of_origin * RAD_DEG; //(Math.PI / 180);
    this.lamda0_ = params.central_meridian * RAD_DEG;
    this.FE_ = params.false_easting; //this.
    this.FN_ = params.false_northing; //this.
    var f = 1.0 / f_i; //this.
    /*e: eccentricity of the ellipsoid where e^2  =  2f - f^2 */
    this.es_ = 2 * f - f * f;
    //var _e  =  Math.sqrt(this.es_);
    /* e^4 */
    this.ep4_ = this.es_ * this.es_;
    /* e^6 */
    this.ep6_ = this.ep4_ * this.es_;
    /* e'  second eccentricity where e'^2  =  e^2 / (1-e^2) */
    this.eas_ = this.es_ / (1 - this.es_);
    this.M0_ = this.calc_m_(phi0, this.a_, this.es_, this.ep4_, this.ep6_);
}

TransverseMercator.prototype = new SpatialReference();
/**
   * calc_m_
   * @param {Object} phi
   * @param {Object} a
   * @param {Object} es
   * @param {Object} ep4
   * @param {Object} ep6
   */
TransverseMercator.prototype.calc_m_ = function(phi, a, es, ep4, ep6) {
    return a * ((1 - es / 4 - 3 * ep4 / 64 - 5 * ep6 / 256) * phi - (3 * es / 8 + 3 * ep4 / 32 + 45 * ep6 / 1024) * Math.sin(2 * phi) + (15 * ep4 / 256 + 45 * ep6 / 1024) * Math.sin(4 * phi) - (35 * ep6 / 3072) * Math.sin(6 * phi));
};
/**
   * see {@link SpatialReference}
 * @param {Array.number} lnglat
 * @return {Array.number}
   */
TransverseMercator.prototype.forward = function(lnglat) {
    var phi = lnglat[1] * RAD_DEG; // (Math.PI / 180);
    var lamda = lnglat[0] * RAD_DEG; //(Math.PI / 180);
    var nu = this.a_ / Math.sqrt(1 - this.es_ * Math.pow(Math.sin(phi), 2));
    var T = Math.pow(Math.tan(phi), 2);
    var C = this.eas_ * Math.pow(Math.cos(phi), 2);
    var A = (lamda - this.lamda0_) * Math.cos(phi);
    var M = this.calc_m_(phi, this.a_, this.es_, this.ep4_, this.ep6_);
    var E = this.FE_ + this.k0_ * nu * (A + (1 - T + C) * Math.pow(A, 3) / 6 + (5 - 18 * T + T * T + 72 * C - 58 * this.eas_) * Math.pow(A, 5) / 120);
    var N = this.FN_ + this.k0_ * (M - this.M0_) + nu * Math.tan(phi) * (A * A / 2 + (5 - T + 9 * C + 4 * C * C) * Math.pow(A, 4) / 120 + (61 - 58 * T + T * T + 600 * C - 330 * this.eas_) * Math.pow(A, 6) / 720);
    return [E, N];
};
/**
   * see {@link SpatialReference}
 * @param {Array.number}  coords
 * @return {Array.number}
   */
TransverseMercator.prototype.inverse = function(coords) {
    var E = coords[0];
    var N = coords[1];
    var e1 = (1 - Math.sqrt(1 - this.es_)) / (1 + Math.sqrt(1 - this.es_));
    var M1 = this.M0_ + (N - this.FN_) / this.k0_;
    var mu1 = M1 / (this.a_ * (1 - this.es_ / 4 - 3 * this.ep4_ / 64 - 5 * this.ep6_ / 256));
    var phi1 = mu1 + (3 * e1 / 2 - 27 * Math.pow(e1, 3) / 32) * Math.sin(2 * mu1) + (21 * e1 * e1 / 16 - 55 * Math.pow(e1, 4) / 32) * Math.sin(4 * mu1) + (151 * Math.pow(e1, 3) / 6) * Math.sin(6 * mu1) + (1097 * Math.pow(e1, 4) / 512) * Math.sin(8 * mu1);
    var C1 = this.eas_ * Math.pow(Math.cos(phi1), 2);
    var T1 = Math.pow(Math.tan(phi1), 2);
    var N1 = this.a_ / Math.sqrt(1 - this.es_ * Math.pow(Math.sin(phi1), 2));
    var R1 = this.a_ * (1 - this.es_) / Math.pow((1 - this.es_ * Math.pow(Math.sin(phi1), 2)), 3 / 2);
    var D = (E - this.FE_) / (N1 * this.k0_);
    var phi = phi1 - (N1 * Math.tan(phi1) / R1) * (D * D / 2 - (5 + 3 * T1 + 10 * C1 - 4 * C1 * C1 - 9 * this.eas_) * Math.pow(D, 4) / 24 + (61 + 90 * T1 + 28 * C1 + 45 * T1 * T1 - 252 * this.eas_ - 3 * C1 * C1) * Math.pow(D, 6) / 720);
    var lamda = this.lamda0_ + (D - (1 + 2 * T1 + C1) * Math.pow(D, 3) / 6 + (5 - 2 * C1 + 28 * T1 - 3 * C1 * C1 + 8 * this.eas_ + 24 * T1 * T1) * Math.pow(D, 5) / 120) / Math.cos(phi1);
    return [lamda / RAD_DEG, phi / RAD_DEG];
};
/**
   * see {@link SpatialReference}
 * @return number
   */
TransverseMercator.prototype.getCircum = function() {
    return Math.PI * 2 * this.a_;
};

/**
 * Creates a Spatial Reference based on Sphere Mercator Projection. 
 * The <code>params</code> passed in constructor should have the following properties:
 * <code><br/>-wkid: wkid
 * <br/>-semi_major:  ellipsoidal semi-major axis 
 * <br/>-unit: meters per unit
 * <br/>-central_meridian: lamda0, longitude of the false origin  (with respect to the prime meridian)
 * </code>
 * <br/>e.g. The "Web Mercator" used in ArcGIS Server:<br/>
 * <code> var web_mercator  = new SphereMercator({wkid: 102113,  semi_major:6378137.0,  central_meridian:0, unit: 1 });
 * </code>
 * @name SphereMercator
 * @class This class (<code>SphereMercator</code>) is the Projection Default Google Maps uses. It is a special form of Mercator.
 * @constructor
 * @param {Object} params 
 * @extends SpatialReference
 */
function SphereMercator(params) {
    /*  =========== parameters  =  ===================== */
    params = params || {};
    SpatialReference.call(this, params);
    this.a_ = (params.semi_major || 6378137.0) / (params.unit || 1);
    this.lamda0_ = (params.central_meridian || 0.0) * RAD_DEG;
}

SphereMercator.prototype = new SpatialReference();

/**
   * See {@link SpatialReference}
 * @param {Array.number} lnglat
 * @return {Array.number}
   */
SphereMercator.prototype.forward = function(lnglat) {
    var phi = lnglat[1] * RAD_DEG;
    var lamda = lnglat[0] * RAD_DEG;
    var E = this.a_ * (lamda - this.lamda0_);
    var N = (this.a_ / 2) * Math.log((1 + Math.sin(phi)) / (1 - Math.sin(phi)));
    return [E, N];
};
/**
   * See {@link SpatialReference}
 * @param {Array.number}  coords
 * @return {Array.number}
   */
SphereMercator.prototype.inverse = function(coords) {
    var E = coords[0];
    var N = coords[1];
    var phi = Math.PI / 2 - 2 * Math.atan(Math.exp(-N / this.a_));
    var lamda = E / this.a_ + this.lamda0_;
    return [lamda / RAD_DEG, phi / RAD_DEG];
};
/**
   * See {@link SpatialReference}
   * @return {Number}
   */
SphereMercator.prototype.getCircum = function() {
    return Math.PI * 2 * this.a_;
};

/**
 * Create a Albers Equal-Area Conic Projection based Spatial Reference. The <code>params</code> passed in construction should
 * include the following properties:<code>
 * <br/>-wkid: well-known id
 * <br/>-semi_major:  ellipsoidal semi-major axis in meter
 * <br/>-unit: meters per unit
 * <br/>-inverse_flattening: inverse of flattening of the ellipsoid where 1/f  =  a/(a - b)
 * <br/>-standard_parallel_1: phi1, latitude of the first standard parallel
 * <br/>-standard_parallel_2: phi2, latitude of the second standard parallel
 * <br/>-latitude_of_origin: phi0, latitude of the false origin
 * <br/>-central_meridian: lamda0, longitude of the false origin  (with respect to the prime meridian)
 * <br/>-false_easting: FE, false easting, the Eastings value assigned to the natural origin
 * <br/>-false_northing: FN, false northing, the Northings value assigned to the natural origin
 * </code>
 * <br/> e.g. 
 * <code> var albers  = new Albers({wkid:9999, semi_major: 6378206.4,inverse_flattening: 294.9786982,
 *   standard_parallel_1: 29.5, standard_parallel_2: 45.5,
 *   central_meridian: -96.0, latitude_of_origin: 23,false_easting: 0,
 *   'false_northing': 0, unit: 1 }); </code>
 * @name Albers
 * @class This class (<code>Albers</code>) represents a Spatial Reference System based on <a target=wiki href  = 'http://en.wikipedia.org/wiki/Albers_projection'>Albers Projection</a>. 
 * @extends SpatialReference
 * @constructor
 * @param {Object} params
 */
function Albers(params) {
    //http://pubs.er.usgs.gov/djvu/PP/PP_1395.pdf, page 101 &  292
    //for NAD_1983_Alaska_Albers: LatLng()<  === > Point();
    params = params || {};
    SpatialReference.call(this, params);
    var f_i = params.inverse_flattening;
    var phi1 = params.standard_parallel_1 * RAD_DEG;
    var phi2 = params.standard_parallel_2 * RAD_DEG;
    var phi0 = params.latitude_of_origin * RAD_DEG;
    this.a_ = params.semi_major / params.unit;
    this.lamda0_ = params.central_meridian * RAD_DEG;
    this.FE_ = params.false_easting;
    this.FN_ = params.false_northing;

    var f = 1.0 / f_i; //e: eccentricity of the ellipsoid where e^2  =  2f - f^2 
    var es = 2 * f - f * f;
    this.e_ = Math.sqrt(es);
    var m1 = this.calc_m_(phi1, es);
    var m2 = this.calc_m_(phi2, es);

    var q1 = this.calc_q_(phi1, this.e_);
    var q2 = this.calc_q_(phi2, this.e_);
    var q0 = this.calc_q_(phi0, this.e_);

    this.n_ = (m1 * m1 - m2 * m2) / (q2 - q1);
    this.C_ = m1 * m1 + this.n_ * q1;
    this.rho0_ = this.calc_rho_(this.a_, this.C_, this.n_, q0);
};

Albers.prototype = new SpatialReference();
/**
   * calc_m_
 * @param {number} phi
 * @param {number} es e square
   */
Albers.prototype.calc_m_ = function(phi, es) {
    var sinphi = Math.sin(phi);
    return Math.cos(phi) / Math.sqrt(1 - es * sinphi * sinphi);
};


/**
   * formular (3-12) page 101
   * @param {Object} phi
   * @param {Object} e
   */
Albers.prototype.calc_q_ = function(phi, e) {
    var esp = e * Math.sin(phi);
    return (1 - e * e) * (Math.sin(phi) / (1 - esp * esp) - (1 / (2 * e)) * Math.log((1 - esp) / (1 + esp)));
};

Albers.prototype.calc_rho_ = function(a, C, n, q) {
    return a * Math.sqrt(C - n * q) / n;
};

Albers.prototype.calc_phi_ = function(q, e, phi) {
    var esp = e * Math.sin(phi);
    return phi + (1 - esp * esp) * (1 - esp * esp) / (2 * Math.cos(phi)) * (q / (1 - e * e) - Math.sin(phi) / (1 - esp * esp) + Math.log((1 - esp) / (1 + esp)) / (2 * e));
};

Albers.prototype.solve_phi_ = function(q, e, init) {
    // iteration
    var i = 0;
    var phi = init;
    var newphi = this.calc_phi_(q, e, phi);
    while (Math.abs(newphi - phi) > 0.00000001 && i < 10) {
        i++;
        phi = newphi;
        newphi = this.calc_phi_(q, e, phi);
    }
    return newphi;
};

/** 
   * see {@link SpatialReference}
 * @param {Array.number} lnglat
 * @return {Array.number}
   */
Albers.prototype.forward = function(lnglat) {
    var phi = lnglat[1] * RAD_DEG;
    var lamda = lnglat[0] * RAD_DEG;
    var q = this.calc_q_(phi, this.e_);
    var rho = this.calc_rho_(this.a_, this.C_, this.n_, q);
    var theta = this.n_ * (lamda - this.lamda0_);
    var E = this.FE_ + rho * Math.sin(theta);
    var N = this.FN_ + this.rho0_ - rho * Math.cos(theta);
    return [E, N];
};
/**
   * see {@link SpatialReference}
 * @param {Array.number}  coords
 * @return {Array.number}
   */
Albers.prototype.inverse = function(coords) {
    var E = coords[0] - this.FE_;
    var N = coords[1] - this.FN_;
    var rho = Math.sqrt(E * E + (this.rho0_ - N) * (this.rho0_ - N));
    var adj = this.n_ > 0 ? 1 : -1;
    var theta = Math.atan(adj * E / (adj * this.rho0_ - adj * N));
    var q = (this.C_ - rho * rho * this.n_ * this.n_ / (this.a_ * this.a_)) / this.n_;
    var init = Math.asin(q / 2);
    var phi = this.solve_phi_(q, this.e_, init);
    var lamda = theta / this.n_ + this.lamda0_;
    return [lamda / RAD_DEG, phi / RAD_DEG];
};
/**
   *  see {@link SpatialReference}
 *  @return number
   */
Albers.prototype.getCircum = function() {
    return Math.PI * 2 * this.a_;
};
/**
   * See {@link SpatialReference}
 * @return {number}
   */
Albers.prototype.getCircum = function() {
    return Math.PI * 2 * this.a_;
};

WGS84 = new Geographic({
    wkid: 4326
});
NAD83 = new Geographic({
    wkid: 4269
});
WEB_MERCATOR = new SphereMercator({
    wkid: 102113,
    semi_major: 6378137.0,
    central_meridian: 0,
    unit: 1
});
WEB_MERCATOR_AUX = new SphereMercator({
    wkid: 102100,
    semi_major: 6378137.0,
    central_meridian: 0,
    unit: 1
});

// declared early but assign here to avoid dependency error by jslint
spatialReferences_ = {
    '4326': WGS84,
    '4269': NAD83,
    '102113': WEB_MERCATOR,
    '102100': WEB_MERCATOR_AUX
};

SpatialReference.WGS84 = WGS84;
SpatialReference.NAD83 = NAD83;
//TODO: check advanced compile impact
SpatialReference.WEB_MERCATOR = WEB_MERCATOR;
SpatialReference.WEB_MERCATOR_AUX = WEB_MERCATOR_AUX;

/**
   * <b> static</b> method. Call with Syntax <code>SpatialReference.register(..)</code>. 
   * Add A Spatial Reference to the internal collection of Spatial References.
   * the <code>wktOrSR</code> parameter can be String format of "well-known text" of the
   * Spatial Reference, or an instance of {@link SpatialReference}.
   * <br/><li> If passes in String WKT format, to be consistent, it should use the same format as listed
   * in <a  href  = 'http://edndoc.esri.com/arcims/9.2/elements/pcs.htm'>
   * ESRI documentation</a>. For example, add NC State Plane NAD83 as String:
   * <br/><code>
   * SpatialReference.register(2264,'PROJCS["NAD_1983_StatePlane_North_Carolina_FIPS_3200_Feet",
   * GEOGCS["GCS_North_American_1983",
   * DATUM["D_North_American_1983",
   * SPHEROID["GRS_1980",6378137.0,298.257222101]],
   * PRIMEM["Greenwich",0.0],
   * UNIT["Degree",0.0174532925199433]],
   * PROJECTION["Lambert_Conformal_Conic"],
   * PARAMETER["False_Easting",2000000.002616666],
   * PARAMETER["False_Northing",0.0],
   * PARAMETER["Central_Meridian",-79.0],
   * PARAMETER["Standard_Parallel_1",34.33333333333334],
   * PARAMETER["Standard_Parallel_2",36.16666666666666],
   * PARAMETER["Latitude_Of_Origin",33.75],
   * UNIT["Foot_US",0.3048006096012192]]');
   * <br/></code>
   * Note: only <b>Lambert Conformal Conic</b> and <b>Transverse Mercator</b> Projection
   * based Spatial References are supported if added via WKT String.
   * <br/><li> If passes in an instance of {@link SpatialReference}, it can be one of the
   * built in classes, or a class that extends SpatialReference. For example, add NC State Plane NAD83 as SR:
   * <br/><code>
   * SpatialReferences.register(2264: new LambertConformalConic({
   * wkid: 2264,
   * semi_major: 6378137.0,
   * inverse_flattening: 298.257222101,
   * standard_parallel_1: 34.33333333333334,
   * standard_parallel_2: 36.16666666666666,
   * central_meridian: -79.0,
   * latitude_of_origin: 33.75,
   * 'false_easting': 2000000.002616666,
   * 'false_northing': 0,
   * unit: 0.3048006096012192
   * });
   * <br/></code>
   * @static
   * @param {Number|String} wkid/wkt
   * @param {Object} wktOrSR
   * @return {SpatialReference} registered SR
   */
Util.registerSR = function(wkidt, wktOrSR) {
    var sr = spatialReferences_['' + wkidt];
    if (sr) {
        return sr;
    }
    if (wktOrSR instanceof SpatialReference) {
        spatialReferences_['' + wkidt] = wktOrSR;
        sr = wktOrSR;

    } else {
        var wkt = wktOrSR || wkidt; // only one param is passed in.
        var params = {
            'wkt': wkidt
        };
        if (wkidt === parseInt(wkidt, 10)) {
            params = {
                'wkid': wkidt
            };
        }
        var prj = extractString_(wkt, "PROJECTION[\"", "\"]");
        var spheroid = extractString_(wkt, "SPHEROID[", "]").split(",");
        if (prj !== "") {
            params.unit = parseFloat(extractString_(extractString_(wkt, "PROJECTION", ""), "UNIT[", "]").split(",")[1]);
            params.semi_major = parseFloat(spheroid[1]);
            params.inverse_flattening = parseFloat(spheroid[2]);
            params.latitude_of_origin = parseFloat(extractString_(wkt, "\"Latitude_Of_Origin\",", "]"));
            params.central_meridian = parseFloat(extractString_(wkt, "\"Central_Meridian\",", "]"));
            params.false_easting = parseFloat(extractString_(wkt, "\"False_Easting\",", "]"));
            params.false_northing = parseFloat(extractString_(wkt, "\"False_Northing\",", "]"));
        }
        switch (prj) {
        case "":
            sr = new SpatialReference(params);
            break;
        case "Lambert_Conformal_Conic":
            params.standard_parallel_1 = parseFloat(extractString_(wkt, "\"Standard_Parallel_1\",", "]"));
            params.standard_parallel_2 = parseFloat(extractString_(wkt, "\"Standard_Parallel_2\",", "]"));
            sr = new LambertConformalConic(params);
            break;
        case "Transverse_Mercator":
            params.scale_factor = parseFloat(extractString_(wkt, "\"Scale_Factor\",", "]"));
            sr = new TransverseMercator(params);
            break;
        case "Albers":
            params.standard_parallel_1 = parseFloat(extractString_(wkt, "\"Standard_Parallel_1\",", "]"));
            params.standard_parallel_2 = parseFloat(extractString_(wkt, "\"Standard_Parallel_2\",", "]"));
            sr = new Albers(params);
            break;
        // more implementations here.
        default:
            throw new Error(prj + "  not supported");
        }
        if (sr) {
            spatialReferences_['' + wkidt] = sr;
        }
    }

    return sr;
};

//end of projection related code//
/**
 * @name Error
   * @class Error returned from Server.
   * Syntax:
   * <pre>
   * {
   "error" : 
  {
    "code" : 500, 
    "message" : "Object reference not set to an instance of an object.", 
    "details" : [
      "'geometry' parameter is invalid"
    ]
  }
  }
  </pre>
   */
/**
   * Create a ArcGIS service catalog instance using it's url:<code> http://&lt;host>/&lt;instance>/rest/services</code>
   * @name Catalog
   * @constructor
   * @class  The catalog resource is the root node and initial entry point into an ArcGIS Server host.
   * This resource represents a catalog of folders and services published on the host.
   *  @param {String} url
   * @property {String} [currentVersion] currentVersion
 * @property {Array.string} [folders] folders list
 * @property {Array.string} [services] list of services. Each has <code>name, type</code> property.
   */
function Catalog(url) {
    this.url = url;
    var me = this;
    getJSON_(url, {}, '', function(json) {
        augmentObject_(json, me);
        /**
       * This event is fired when the catalog info is loaded.
       * @name Catalog#load
       * @event
       */
        triggerEvent_(me, 'load');
    });
}

/**
   * @name Field
   * @class This class represents a field in a {@link Layer}. It is accessed from
   * the <code> fields</code> property. There is no constructor for this class,
   *  use Object Literal.
   * @property {String} [name] field Name
   * @property {String} [type] field type (esriFieldTypeOID|esriFieldTypeString|esriFieldTypeInteger|esriFieldTypeGeometry}.
   * @property {String} [alias] field alias.
   * @property {Domain} [domain] domain
   * @property {Int} [length] length.
   */
/**
   * Create a ArcGIS map Layer using it's url (http://[mapservice-url]/[layerId])
   * @name Layer
   * @class This class (<code>Layer</code>) The layer / table(v10+)
   *  resource represents a single layer / table in a map of a map service 
   *  published by ArcGIS Server.
   * @constructor
   * @param {String} url
   * @property {Number} [id] layer ID
   * @property {String} [name] layer Name
   * @property {String} [type] Feature Layer|Image Layer
   * @property {String} [description] description
   * @property {String} [definitionExpression] Layer definition.
   * @property {String} [geometryType] geometryType type(esriGeometryPoint|..), only available after load.
   * @property {String} [copyrightText] copyrightText, only available after load.
   * @property {Layer} [parentLayer] parent Layer {@link Layer}
   * @property {Boolean} [defaultVisibility] defaultVisibility
 * @property {Array.Layer} [subLayers] sub Layers. {@link Layer}.
   * @property {Boolean} [visibility] Visibility of this layer
   * @property {Number} [minScale] minScale
   * @property {Number} [maxScale] maxScale
   * @property {TimeInfo} [timeInfo] timeInfo
   * @property {DrawingInfo} [drawingInfo] rendering info See {@link DrawingInfo}
   * @property {Boolean} [hasAttachments] hasAttachments
   * @property {String} [typeIdField] typeIdField
 * @property {Array.Field} [fields] fields, only available after load. See {@link Field}
 * @property {Array.String} [types] subtypes: id, name, domains.
 * @property {Array.String} [relationships] relationships (id, name, relatedTableId)
   */
function Layer(url) {
    this.url = url;
    this.definition = null;
}

/**
   * Load extra information such as it's fields from layer resource.
   * If opt_callback function will be called after it is loaded
   */
Layer.prototype.load = function() {
    var me = this;
    if (this.loaded_) {
        return;
    }
    getJSON_(this.url, {}, '', function(json) {
        augmentObject_(json, me);
        me.loaded_ = true;
        /**
       * This event is fired when layer's service info is loaded.
       * @name Layer#load
       * @event
       */
        triggerEvent_(me, 'load');
    });
};


/**
   * Whether the layer is viewable at given scale
   * @param {Number} scale
   * @return {Boolean}
   */
Layer.prototype.isInScale = function(scale) {
    // note if the layer's extra info is not loaded, it will return true
    if (this.maxScale && this.maxScale > scale) {
        return false;
    }
    if (this.minScale && this.minScale < scale) {
        return false;
    }
    return true;
};
/**
   * @name SpatialRelationship
   * @enum
   * @class This is actually a list of constants that represent spatial 
   * relationship types. 
   * @property {String} [INTERSECTS] esriSpatialRelIntersects 
   * @property {String} [CONTAINS] esriSpatialRelContains
   * @property {String} [CROSSES] esriSpatialRelCrosses
   * @property {String} [ENVELOPE_INTERSECTS] esriSpatialRelEnvelopeIntersects
   * @property {String} [INDEX_INTERSECTS] esriSpatialRelIndexIntersects
   * @property {String} [OVERLAPS] esriSpatialRelOverlaps
   * @property {String} [TOUCHES] esriSpatialRelTouches
   * @property {String} [WITHIN] esriSpatialRelWithin
  */
var SpatialRelationship = {
    INTERSECTS: 'esriSpatialRelIntersects',
    CONTAINS: 'esriSpatialRelContains',
    CROSSES: 'esriSpatialRelCrosses',
    ENVELOPE_INTERSECTS: 'esriSpatialRelEnvelopeIntersects',
    INDEX_INTERSECTS: 'esriSpatialRelIndexIntersects',
    OVERLAPS: 'esriSpatialRelOverlaps',
    TOUCHES: 'esriSpatialRelTouches',
    WITHIN: 'esriSpatialRelWithin'
};
/**
   * @name QueryOptions
   * @class This class represent the parameters needed in an query operation for a {@link Layer}.
   *   There is no constructor, use JavaScript object literal.
   * <br/>For more info see <a  href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/query.html'>Query Operation</a>.
   * @property {String} [text]  A literal search text. If the layer has a display field
   *   associated with it, the server searches for this text in this field.
   *   This parameter is a short hand for a where clause of:
   *   where [displayField]like '%[text]%'. The text is case sensitive.
   *   This parameter is ignored if the where parameter is specified.
   * @property {OverlayView|Array.OverlayView} [geometry] The geometry to apply as the spatial filter.
   * @property {SpatialRelationship} [spatialRelationship] The spatial relationship to be applied on the
   *    input geometry while performing the query. The supported spatial relationships
   *    include intersects, contains, envelope intersects, within, etc.
   *    The default spatial relationship is intersects. See {@link SpatialRelationship}
   * @property {String} [where] A where clause for the query filter. Any legal SQL where clause operating on the fields in the layer is allowed.
   * @property {Array.string} [outFields] The list of fields to be included in the returned resultset.
   * @property {Boolean} [returnGeometry] If true, If true, the resultset will include the geometries associated with each result.
   * @property {Array.number} [objectIds] The object IDs of this layer / table to be queried
   * @property {Number} [maxAllowableOffset] This option can be used to specify the maximum allowable offset  to be used for generalizing geometries returned by the query operation
   * @property {Boolean} [returnIdsOnly] If true, the response only includes an array of object IDs. Otherwise the response is a feature set. The default is false.
   * @property {OverlayOptions} [overlayOptions] See {@link OverlayOptions}
   */
/**
   * @name ResultSet
   * @class This class represent the results of an query operation for a {@link Layer}.
   *   There is no constructor, use JavaScript object literal.
   * <br/>For more info see <a  href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/query.html'>Query Operation</a>.
   * @property {String} [displayFieldName] display Field Name for layer
   * @property {Object} [fieldAliases] Field Name's Aliases. key is field name, value is alias.
   * @property {GemetryType} [geometryType] esriGeometryPoint | esriGeometryMultipoint | esriGeometryPolygon | esriGeometryPolyline
   * @property {Array.feature} [features] result as array of {@link Feature}
   * @property {String} [objectIdFieldName] objectIdFieldName when returnIdsOnly=true
   * @property {Array.int} [objectIds] objectIds when returnIdsOnly=true
   */
/**
   * The query operation is performed on a layer resource. The result of this operation is a resultset resource that will be
   * passed in the callback function. param is an instance of {@link QueryOptions}
   * <br/>For more info see <a href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/query.html'>Query Operation</a>.
   * @param {QueryOptions} params
   * @param {Function} callback
   * @param {Function} errback
   */
Layer.prototype.query = function(p, callback, errback) {
    if (!p) {
        return;
    }
    // handle text, where, relationParam, objectIds, maxAllowableOffset
    var params = augmentObject_(p, {});
    if (p.geometry && !isString_(p.geometry)) {
        params.geometry = fromOverlaysToJSON_(p.geometry);
        params.geometryType = getGeometryType_(p.geometry);
        params.inSR = 4326;
    }
    if (p.spatialRelationship) {
        params.spatialRel = p.spatialRelationship;
        delete params.spatialRelationship;
    }
    if (p.outFields && isArray_(p.outFields)) {
        params.outFields = p.outFields.join(',');
    }
    if (p.objectIds) {
        params.objectIds = p.objectIds.join(',');
    }
    if (p.time) {
        params.time = formatTimeString_(p.time, p.endTime);
    }
    if (p.returnDistinctValues)
        params.returnDistinctValues = true;
    params.outSR = 4326;
    params.returnGeometry = p.returnGeometry === false ? false : true;
    params.returnIdsOnly = p.returnIdsOnly === true ? true : false;
    params.returnCountOnly = p.returnCountOnly === true ? true : false;
    delete params.overlayOptions;
    getJSON_(this.url + '/query', params, '', function(json) {
        parseFeatures_(json.features, p.overlayOptions);
        callback(json, json.error);
        handleErr_(errback, json);
    });
};
/**
   * @name QueryRelatedRecordsOptions
   * @class This class represent the parameters needed in an query related records operation for a {@link Layer}.
   * <br/>For more info see <a  href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/queryrelatedrecords.html'>Query Related Records Operation</a>.
 * @property {Array.number} [objectIds] The object IDs of this layer / table to be queried
   * @property {Int} [relatioshipId] The ID of the relationship to be queried
 * @property {Array.string} [outFields] The list of fields to be included in the returned resultset. This list is a comma delimited list of field names.
   * @property {String} [definitionExpression]  The definition expression to be applied to the related table / layer. From the list of objectIds, only those records that conform to this expression will be returned.
   * @property {Boolean} [returnGeometry  = true] If true, the resultset will include the geometries associated with each result.
   * @property [Number] [maxAllowableOffset] This option can be used to specify the maximum allowable offset  to be used for generalizing geometries returned by the query operation
   * @property {Number} [outSR] The well-known ID of or the {@link SpatialReference} of the output geometries
   */
/**
   * @name RelatedRecords
   * @class This class represent the results of an query related records operation for a {@link Layer}.
   * <br/>For more info see <a  href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/queryrelatedrecords.html'>Query Operation</a>.
   * @property {String} [geometryType] esriGeometryPoint | esriGeometryMultipoint | esriGeometryPolygon | esriGeometryPolyline
   * @property {Object} [spatialReference] {@link SpatialReference}
   * @property {String} [displayFieldName] display Field Name for layer
 * @property {Array.object} [relatedRecordGroups] list of related records
   */
/**
   * @name RelatedRecord
   * @class This class represent the result of an query related records operation for a {@link Layer}.
   *   There is no constructor, use JavaScript object literal.
   * <br/>For more info see <a  href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/queryrelatedrecords.html'>Query Operation</a>.
   * @property {int} [objectId] objectid of original record
 * @property {Array.feature} [relatedRecords] list of {@link Feature}s.
   */
/**
   * The query related records operation is performed on a layer / table resource. 
   * The result of this operation are featuresets grouped by source layer / table 
   * object IDs. Each featureset contains Feature objects including the values for 
   * the fields requested by the user. For related layers, if you request geometry 
   * information, the geometry of each feature is also returned in the featureset. 
   * For related tables, the featureset does not include geometries. 
   * @param {QueryRelatedRecordsParameters} params
   * @param {Function} callback
   * @param {Function} errback
   */
Layer.prototype.queryRelatedRecords = function(qparams, callback, errback) {
    if (!qparams) {
        return;
    }
    var params = augmentObject_(qparams, {});
    params.f = params.f || 'json';
    if (params.outFields && !isString_(params.outFields)) {
        params.outFields = params.outFields.join(',');
    }
    params.returnGeometry = params.returnGeometry === false ? false : true;
    getJSON_(this.url + '/query', params, '', function(json) {
        handleErr_(errback, json);
        callback(json);
    });
};

/**
   * @name MapSerivceOptions
   * @class provides options to construct a {@link MapService}
   * @property {Number} delayLoad number of seconds to delay loading meta data on construction.
   */
/**
   * Creates a MapService objects that can be used by UI components.
   * <ul><li> <code> url</code> (required) is the URL of the map servive, e.g. <code>
   * http://sampleserver1.arcgisonline.com/ArcGIS/rest/services/Specialty/ESRI_StateCityHighway_USA/MapServer</code>.
   * <ul/> Note the spatial reference of the map service must already exists
   * in the {@link spatialReferences_} if actual coordinates transformation is needed.
   * @name MapService
   * @class This class (<code>MapService</code>) is the core class for all map service operations.
   * It represents an ArcGIS Server map service that offer access to map and layer content
   * @constructor
   * @param {String} url
   * @property {String} [url] map service URL
   * @property {String} [serviceDescription] serviceDescription
   * @property {String} [mapName] map frame Name inside the map document
   * @property {String} [description] description
   * @property {String} [copyrightText] copyrightText
   * @property {Array.Layer} [layers] array of {@link Layer}s.
   * @property {Array.Layer} [tables] array of Tables of type {@link Layer}.
   * @property {SpatialReference} [spatialReference] see {@link SpatialReference}
   * @property {Boolean} [singleFusedMapCache] if map cache is singleFused
   * @property {TileInfo} [tileInfo] See {@link TileInfo}
   * @property {TimeInfo} [timeInfo] see {@link TimeInfo}
   * @property {String} [units] unit
   * @property {String} [supportedImageFormatTypes] supportedImageFormatTypes, comma delimited list.
   * @property {Object} [documentInfo] Object with the folloing properties: <code>Title, Author,Comments,Subject,Category,Keywords</code>
   */
function MapService(url, opts) {
    this.url = url;
    this.loaded_ = false;
    var tks = url.split("/");
    this.name = tks[tks.length - 2].replace(/_/g, ' ');
    opts = opts || {};
    if (opts.delayLoad) {
        var me = this;
        window.setTimeout(function() {
            me.loadServiceInfo();
        }, opts.delayLoad * 1000);
    } else {
        this.loadServiceInfo();
    }
}

/**
   * Load serviceInfo
   */
MapService.prototype.loadServiceInfo = function() {
    var me = this;
    getJSON_(this.url, {}, '', function(json) {
        me.init_(json);
    });
};
/**
   * initialize an ArcGIS Map Service from the meta data information.
   * The <code>json</code> parameter is the json object returned by Map Service.
   * @private
   * @param {Object} json
   */
MapService.prototype.init_ = function(json) {
    var me = this;
    if (json.error) {
        me.errorLoading_();
        return;
    }
    //if (json.error) {
    //  throw new Error(json.error.message);
    //}
    augmentObject_(json, this);
    if (json.spatialReference.wkt) {
        this.spatialReference = Util.registerSR(json.spatialReference.wkt);
    } else {
        this.spatialReference = spatialReferences_[json.spatialReference.wkid];
    }
    if (json.tables !== undefined) {
        // v10.0 +
        getJSON_(this.url + '/layers', {}, '', function(json2) {
            me.initLayers_(json2);
            // V10 SP1 
            getJSON_(me.url + '/legend', {}, '', function(json3) {
                me.initLegend_(json3);
                me.setLoaded_();
            });
        });
    } else {
        // v9.3
        me.initLayers_(json);
        me.setLoaded_();
    }
};

MapService.prototype.setLoaded_ = function() {
    this.loaded_ = true;
    /**
     * This event is fired when the service and it's service info is loaded.
     * @name MapService#load
     * @event
     */
    triggerEvent_(this, "load");
};

MapService.prototype.errorLoading_ = function() {
    /**
       * This event is fired when the service loaded incorrectly
       * @name MapService#error
       * @event
       */

    triggerEvent_(this, "error");
};

/**
   * initialize an Layers.
   * The <code>json</code> parameter is the json object returned by Map Service or layers operation(v10+).
   * @private
   * @param {Object} json2
   */
MapService.prototype.initLayers_ = function(json2) {
    var layers = [];
    var tables = [];
    this.layers = layers;
    if (json2.tables) {
        this.tables = tables;
    }
    var layer, i, c, info;
    for (i = 0, c = json2.layers.length; i < c; i++) {
        info = json2.layers[i];
        layer = new Layer(this.url + '/' + info.id);
        augmentObject_(info, layer);
        layer.visible = layer.defaultVisibility;
        layers.push(layer);
    }
    if (json2.tables) {
        for (i = 0, c = json2.tables.length; i < c; i++) {
            info = json2.tables[i];
            layer = new Layer(this.url + '/' + info.id);
            augmentObject_(info, layer);
            tables.push(layer);
        }
    }
    for (i = 0, c = layers.length; i < c; i++) {
        layer = layers[i];
        if (layer.subLayerIds) {
            layer.subLayers = [];
            for (var j = 0, jc = layer.subLayerIds.length; j < jc; j++) {
                var subLayer = this.getLayer(layer.subLayerIds[j]);
                layer.subLayers.push(subLayer);
                subLayer.parentLayer = layer;
            }
        }
    }

};
/**
   * initialize an Layers.
   * The <code>json</code> parameter is the json object returned by Map Service or layers operation(v10+).
   * @private
   * @param {Object} json2
   */
MapService.prototype.initLegend_ = function(json3) {
    // if not AGS10 SP1, server will return error.
    var layers = this.layers;
    if (json3.layers) {
        var layer, i, c, info;
        for (i = 0, c = json3.layers.length; i < c; i++) {
            info = json3.layers[i];
            layer = layers[info.layerId]; // layers id should same as index.
            augmentObject_(info, layer);
        }
    }
};
/**
   * Get a map layer by it's name(String) or id (Number), return {@link Layer}.
   * @param {String|Number} nameOrId
   * @return {Layer}
   */
MapService.prototype.getLayer = function(nameOrId) {
    var layers = this.layers;
    if (layers) {
        for (var i = 0, c = layers.length; i < c; i++) {
            if (nameOrId === layers[i].id) {
                return layers[i];
            }
            if (isString_(nameOrId) && layers[i].name.toLowerCase() === nameOrId.toLowerCase()) {
                return layers[i];
            }
        }
    }
    return null;
};

/**
   * Get the layer definitions.
   * @return {Object} key as id, value as string of definition expression.
   */
MapService.prototype.getLayerDefs_ = function() {
    var ret = {};
    if (this.layers) {
        for (var i = 0, c = this.layers.length; i < c; i++) {
            var layer = this.layers[i];
            if (layer.definition) {
                ret[String(layer.id)] = layer.definition;
            }
        }
    }
    return ret;
};
/**
   * If the map service meta info is loaded
   * @return {Boolean}
   */
MapService.prototype.hasLoaded = function() {
    return this.loaded_;
}; /**
   * get a  list of visible layer's Ids
   * @return {Array.number} null if not initialized
   */
MapService.prototype.getVisibleLayerIds_ = function(layerIds) {
    var ret = [];
    if (this.layers) { // in case service not loaded_
        var layer;
        // a special behavior of REST (as of 9.3.1): 
        // if partial group then parent must be off
        var i, c;
        for (i = 0, c = this.layers.length; i < c; i++) {
            layer = this.layers[i];
            if (layer.subLayers) {
                for (var j = 0, jc = layer.subLayers.length; j < jc; j++) {
                    if (layer.subLayers[j].visible === false) {
                        layer.visible = false;
                        break;
                    }
                }
            }
        }
        for (i = 0, c = this.layers.length; i < c; i++) {
            layer = this.layers[i];
            //2010-10-26: in AGS10, group layer behavior is opposite of 9.3.1. And UNDOUMENTED in REST API!
            if (layer.subLayers && layer.subLayers.length > 0) {
                continue;
            }
            if (layer.visible === true && (!layerIds || layerIds.indexOf(layer.id) !== -1)) {
                ret.push(layer.id);
            }
        }
    }

    return ret;
};
/**
   * get initial bounds of the map serivce
   * @return {google.maps.LatLngBounds}
   */
MapService.prototype.getInitialBounds = function() {
    if (this.initialExtent) {
        this.initBounds_ = this.initBounds_ || fromEnvelopeToLatLngBounds_(this.initialExtent);
        return this.initBounds_;
    }
    return null;
};
/**
   * get full bounds of the map serivce
   * @return {google.maps.LatLngBounds}
   */
MapService.prototype.getFullBounds = function() {
    if (this.fullExtent) {
        this.fullBounds_ = this.fullBounds_ || fromEnvelopeToLatLngBounds_(this.fullExtent);
        return this.fullBounds_;
    }
    return null;
};
/**
 * @name ExportMapOptions
 * @class This class represent the parameters needed in an exportMap operation for a {@link MapService}.
  * <br/>For more info see <a  href='http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/export.html'>Export Operation</a>.
 * @property {Number} [width] width of image, in pixel;
 * @property {Number} [height] height of image, in pixel;
 * @property {SpatialReference} [imageSR] The well-known ID of the spatial reference of the exported image or instance of {@link SpatialReference}.
 * @property {String} [format  = png] The format of the exported image. png | png8 | png24 | jpg | pdf | bmp | gif | svg
 * @property {Number} [dpi] The dpi of the exported image, default 96
 * @property {Object} [layerDefinitions] Allows you to filter the features of individual layers in the exported map by specifying 
 *   definition expressions for those layers. Syntax: { "&lt;layerId1>" : "&lt;layerDef1>" , "&lt;layerId2>" : "&lt;layerDef2>" }
 *   key is layerId returned by server, value is definition for that layer.
 * @property {Array.number} [layerIds] list of layer ids. If not specified along with layerOptions, show list of visible layers. 
 * @property {String} [layerOptions] show | hide | include | exclude. If not specified with along layerIds, show list of visible layers. 
 * @property {Boolean} [transparent  = true] If true, the image will be exported with 
 *  the background color of the map set as its transparent color. note the REST API default value is false.
 * @property {google.maps.LatLngBounds} [bounds] bounds of map
 * @property {Date} [time] The time instant the exported map image if the service supports time (since AGS10).
 * @property {Date} [endTime] The end time instant the exported map image if the service supports time (since AGS10).
 *  time=&lt;timeInstant> or time=&lt;startTime>, &lt;endTime>, e.g. time=1199145600000, 1230768000000 (1 Jan 2008 00:00:00 GMT to 1 Jan 2009 00:00:00 GMT) 
 * @property {Object} [layerTimeOptions] layerTimeOptions The time options per layer. Users can indicate whether or not the layer should use the time extent
 *  specified by the time parameter or not, whether to draw the layer 
 *  features cumulatively or not and the time offsets for the layer. Syntax: <pre>
 *  {
  "&lt;layerId1>" : {
    //If true, use the time extent specified by the time parameter
    "useTime" : &lt; true | false >,
    //If true, draw all the features from the beginning of time for that data
    "timeDataCumulative" : &lt; true | false >,
    //Time offset for this layer so that it can be overlaid on the top of a previous or future time period
    "timeOffset" : &lt;timeOffset1>,
    "timeOffsetUnits" : "&lt;esriTimeUnitsCenturies | esriTimeUnitsDays | esriTimeUnitsDecades | 
                             esriTimeUnitsHours | esriTimeUnitsMilliseconds | esriTimeUnitsMinutes | 
                             esriTimeUnitsMonths | esriTimeUnitsSeconds | esriTimeUnitsWeeks | esriTimeUnitsYears |
                             esriTimeUnitsUnknown>"
  },
  "&lt;layerId2>" : {
    "useTime" : &lt; true | false >,
    "timeDataCumulative" : &lt; true | false >,
    "timeOffsetOffset" : &lt;timeOffset2>,
    "timeOffsetUnits" : "&lt;timeOffsetUnits2>"
  }
}
</pre>
 */

/**
 * @name MapImage
 * @class This is the result of {@link MapService}.exportMap operation.
 *   There is no constructor, use as JavaScript object literal.
 * @property {String} [href] URL of image
 * @property {google.maps.LatLngBounds} [bounds] The bounding box of the exported image. 
 * @property {Number} [width] width of the exported image.
 * @property {Number} [height] height of the exported image.
 * @property {Number} [scale] scale of the exported image.
 */

/**
   * Export an image with given parameters.
   * For more info see <a  href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/export.html'>Export Operation</a>.
   * <br/> The <code>params</code> is an instance of {@link ExportMapOptions}.
   * The following properties will be set automatically if not specified:...
   * <br/> The <code>callback</code> is the callback function with argument of
   * an instance of {@link MapImage}.
   * @param {ExportMapOptions} params
   * @param {Function} callback
   * @param {Function} errback
   * @return {String|None} url of image if f=image, none if f=json
   */
MapService.prototype.exportMap = function(p, callback, errback) {
    if (!p || !p.bounds) {
        return;
    }
    // note: dynamic map may overlay on top of maptypes with different projection
    var params = {}; // augmentObject_(p, );
    params.f = p.f;

    var bnds = p.bounds;
    var swx = bnds.getSouthWest().lng();
    var nex = bnds.getNorthEast().lng();
    if (swx > nex) {
        swx = swx - 180;
        //    nex = nex +180;
    }
    params.bbox = '' + swx + ',' + '' + bnds.getSouthWest().lat() + ',' +
        nex +
        ',' +
        '' +
        bnds.getNorthEast().lat();
    //delete params.bounds;
    //log_('send '+bnds.toUrlValue());
    params.size = '' + p.width + ',' + p.height;
    params.dpi = p.dpi;

    if (p.imageSR) {
        if (p.imageSR.wkid) {
            params.imageSR = p.imageSR.wkid;
        } else {
            params.imageSR = '{wkt:' + p.imageSR.wkt + '}';
        }
    }
    params.bboxSR = '4326';
    params.format = p.format;

    var defs = p.layerDefinitions;
    // there is a slightly difference between {} and undefined
    // if do not want use def at all, pass in {}, if want to use 
    // in service, do not pass in anything.
    if (defs === undefined) {
        defs = this.getLayerDefs_();
    }
    // for 9.3 compatibility:
    params.layerDefs = getLayerDefsString_(defs);
    var vlayers = (!this.loaded_ && p.layerIds) ? p.layerIds : this.getVisibleLayerIds_(p.layerIds || null);
    var layerOpt = p.layerOption || 'show';
    // REMOVED CODE: EIS 9/8/14
    // It does not make sense to not properly update layer visibility as we may have user interaction
    //if (vlayers === undefined) {
    //  vlayers = this.getVisibleLayerIds_();
    //}
    if (vlayers.length > 0) {
        params.layers = layerOpt + ':' + vlayers.join(',');
    } else {
        // no layers visible, no need to go to server, note if vlayers is null means not init yet in which case do not send layers 
        if (this.loaded_ && callback) {
            callback({
                href: null
            });
            return;
        }

    }
    params.transparent = (p.transparent === false ? false : true);
    if (p.time) {
        params.time = formatTimeString_(p.time, p.endTime);
    }
    //TODO: finish once v10 released
    params.layerTimeOptions = p.layerTimeOptions;


    //params.f = 'image';

    if (params.f === 'image') {
        return this.url + '/export?' + formatParams_(params);
    } else {
        getJSON_(this.url + '/export', params, '', function(json) {
            if (json.extent) {
                json.bounds = fromEnvelopeToLatLngBounds_(json.extent);
                //log_('got '+json.bounds.toUrlValue());
                delete json.extent;
                callback(json);
            } else {
                handleErr_(errback, json.error);
            }
        });
    }
};
/**
 * @name Feature
 * @class This class represent JSON feature object as returned by the REST API.
 *   There is no constructor, use JavaScript object literal.
 * <br/>For more info see <a  href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/feature.html'>Feature Object</a>.
 * Syntax:
 * <pre>
{
  "geometry" : &lt;overlays>,
  "attributes" : {
    "name1" : &lt;value1>,
    "name2" : &lt;value2>,
  }
}
 * </pre>
 * @property {Array.OverlayView} [geometry] geometries. Array of Marker, Polyline or Polygon.
 * @property {Object} [attributes] attributes as name-value JSON object.
 */
/**
   * @name IdentifyOptions
   * @class This class represent the parameters needed in an identify operation for a {@link MapService}.
   *   There is no constructor, use JavaScript object literal.
   * <br/>For more info see <a  href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/identify.html'>Identify Operation</a>.
   * @property {Geometry} [geometry] The geometry to identify on, <code>google.maps.LatLng</code>, <code>Polyline</code>, or <code>Polygon</code>.
   * @property {Array.number} [layerIds] The layers to perform the identify operation on. 
   * @property {String} [layerOption] The layers to perform the identify operation on. 'top|visible|all'. 
   * @property {Number} [tolerance] The distance in screen pixels from the specified geometry within which the identify should be performed
   * @property {google.maps.LatLngBounds} [bounds] The bounding box of the map currently being viewed.
   * @property {Number} [width] width of image in pixel
   * @property {Number} [height] height of image in pixel
   * @property {Number} [dpi] dpi of image, default 96;
   * @property {Boolean} [returnGeometry  = true] If true, the resultset will include the geometries associated with each result.
   * @property {Number} [maxAllowableOffset] This option can be used to specify the maximum allowable offset  to be used for generalizing geometries returned by the identify operation
   * @property {OverlayOptions} [overlayOptions] how results should be rendered. See {@link OverlayOptions}
   */
/**
   * @name IdentifyResults
   * @class This class represent the results of an identify operation for
   * a {@link MapService}.
   *   There is no constructor, use JavaScript object literal.
   * <br/>For more info see <a  href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/identify.html'>Identify Operation</a>.
   * @property {Array.IdentifyResult} [results] The identify results as an array of {@link IdentifyResult}
   */
/**
   * @name IdentifyResult
   * @class This class represent one entry in the results of an identify operation for a {@link MapService}.
   *   There is no constructor, use JavaScript object literal.
   * <br/>For more info see <a  href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/identify.html'>Identify Operation</a>.
   * @property {Number} [layerId] layerId
   * @property {String} [layerName] layerName
   * @property {String} [value] value of the display field
   * @property {String} [displayFieldName] displayFieldName
   * @property {Feature} [feature] {@link Feature}
   */
/**
   * Identify features on a particular Geographic location, using {@link IdentifyOptions} and
   * process {@link IdentifyResults} using the <code>callback</code> function.
   * For more info see <a
   * href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/identify.html'>Identify Operation</a>.
   * @param {IdentifyOptions} params
   * @param {Function} callback
   * @param {Function} errback
   */
MapService.prototype.identify = function(p, callback, errback) {
    if (!p) {
        return;
    }
    var params = {}; //augmentObject_(p, );
    params.geometry = fromOverlaysToJSON_(p.geometry);
    params.geometryType = getGeometryType_(p.geometry);
    params.mapExtent = fromOverlaysToJSON_(p.bounds);
    params.tolerance = p.tolerance || 2;
    params.sr = 4326;
    params.imageDisplay = '' + p.width + ',' + p.height + ',' + (p.dpi || 96);
    params.layers = (p.layerOption || 'all');
    if (p.layerIds) {
        params.layers += ':' + p.layerIds.join(',');
    }
    if (p.layerDefs) {
        params.layerDefs = getLayerDefsString_(p.layerDefs);
    }
    params.maxAllowableOffset = p.maxAllowableOffset;
    params.returnGeometry = (p.returnGeometry === false ? false : true);

    getJSON_(this.url + '/identify', params, '', function(json) {
        // process results;
        var rets = null;
        var i, js, g;
        if (json.results) {
            rets = [];
            for (i = 0; i < json.results.length; i++) {
                js = json.results[i];
                g = fromJSONToOverlays_(js.geometry, p.overlayOptions);
                js.feature = {
                    geometry: g,
                    attributes: js.attributes
                };
                delete js.attributes;
            }
        }
        callback(json);
        handleErr_(errback, json);
    });
};
/**
   * @name FindOptions
   * @class This class represent the parameters needed in an find operation for a {@link MapService}.
   *   There is no constructor, use JavaScript object literal.
   * <br/>For more info see <a  href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/find.html'>Find Operation</a>.
   * @property {String} [searchText] The search string. This is the text that is searched across the layers and the fields that the user specifies.
   * @property {Boolean} [contains  = true] If false, the operation searches for an exact match of
   *   the searchText string. An exact match is case sensitive.
   *   Otherwise, it searches for a value that contains the searchText provided.
   *    This search is not case sensitive. The default is true.
   * @property {Array.string} [searchFields] The names of the fields to search. 
   *    If this parameter is not specified, all fields are searched.
   * @property {Array.number} [layerIds] The layer Ids to perform the find operation on. The layers to perform the find operation on.
   * @property {Boolean} [returnGeometry  = true] If true, the resultset will include the geometries associated with each result.
   * @property {Number} [maxAllowableOffset] This option can be used to specify the maximum allowable offset  to be used for generalizing
   *             geometries returned by the find operation 
   */
/**
   * @name FindResults
   * @class This class represent the results of a find operation for a {@link MapService}.
   *   There is no constructor, use JavaScript object literal.
   * <br/>For more info see <a  href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/find.html'>Find Operation</a>.
   * @property {Array.FindResult} [results] The find results as an array of {@link FindResult}
   */
/**
   * @name FindResult
   * @class This class represent one entry in the results of a find operation for a {@link MapService}.
   *   There is no constructor, use JavaScript object literal.
   * <br/>For more info see <a  href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/find.html'>Find Operation</a>.
   * @property {Number} [layerId] layerId
   * @property {String} [layerName] layerName
   * @property {String} [value] value of the display field
   * @property {String} [displayFieldName] displayFieldName
   * @property {String} [foundFieldName] foundFieldName
   * @property {String} [geometryType] esriGeometryPoint | esriGeometryPolyline | esriGeometryPolygon | esriGeometryEnvelope
   * @property {Feature} [feature] {@link Feature}
   */
/**
   * Find features using the {@link FindOptions} and process {@link FindResults}
   * using the <code>callback</code> function.
   * For more info see <a
   * href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/find.html'>Find Operation</a>.
   * @param {FindOptions} opts
   * @param {Function} callback
   * @param {Function} errback
   */
MapService.prototype.find = function(opts, callback, errback) {
    if (!opts) {
        return;
    }
    // handle searchText, contains, maxAllowableOffset
    var params = augmentObject_(opts, {});
    if (opts.layerIds) {
        params.layers = opts.layerIds.join(',');
        delete params.layerIds;
    }
    if (opts.searchFields) {
        params.searchFields = opts.searchFields.join(',');
    }
    params.contains = (opts.contains === false ? false : true);
    if (opts.layerDefinitions) {
        params.layerDefs = getLayerDefsString_(opts.layerDefinitions);
        delete params.layerDefinitions;
    }
    params.sr = 4326;
    params.returnGeometry = (opts.returnGeometry === false ? false : true);
    getJSON_(this.url + '/find', params, '', function(json) {
        var rets = null;
        var i, js, g;
        if (json.results) {
            rets = [];
            for (i = 0; i < json.results.length; i++) {
                js = json.results[i];
                g = fromJSONToOverlays_(js.geometry, opts.overlayOptions);
                js.feature = {
                    'geometry': g,
                    'attributes': js.attributes
                };
                delete js.attributes;
            }
        }
        callback(json);
        handleErr_(errback, json);
    });
};

/**
   * Query a layer with given id or name using the {@link QueryOptions} and process {@link ResultSet}
   * using the <code>callback</code> function.
   * See {@link Layer}.
   * For more info see <a  href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/query.html'>Query Layer Operation</a>.
   * @param {Number|String} layerNameOrId
   * @param {QueryOptions} params
   * @param {Function} callback
   * @param {Function} errback
   */
MapService.prototype.queryLayer = function(layerNameOrId, params, callback, errback) {
    var layer = this.getLayer(layerNameOrId);
    if (layer) {
        layer.query(params, callback, errback);
    }
};

/**
 * Creates a GeocodeService class.
 * Params:<li><code>url</code>: URL of service, syntax:<code>	http://{catalogurl}/{serviceName}/GeocodeServer</code>
 * @name GeocodeService
 * @class This class (<code>GeocodeService</code>) represent an ArcGIS <a href="http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/geocodeserver.html">GeocodeServer</a>
 *  service.
 * @constructor
 * @param {String} url
 * @property {String} [serviceDescription] serviceDescription
 * @property {Array.Field} [addressFields] input fields. 
 *    Each entry is an object of type {@link Field}, plus <code>required(true|false)</code>
 * @property {Array.Field} [candidateFields] candidate Fields. 
 *    Each entry is an object of type {@link Field}
 * @property {Array.Field} [intersectionCandidateFields] intersectionCandidateFields
 *    Each entry is an object of type {@link Field}
 * @property {SpatialReference} [spatialReference] spatialReference
 * @property {Object} [locatorProperties] an object with key-value pair that is specific to Locator type.
 */
function GeocodeService(url) {
    this.url = url;
    this.loaded_ = false;
    var me = this;
    getJSON_(url, {}, '', function(json) {
        me.init_(json);
    });
}

/**
   * init
   * @param {Object} json
   */
GeocodeService.prototype.init_ = function(json) {
    augmentObject_(json, this);
    if (json.spatialReference) {
        this.spatialReference = spatialReferences_[json.spatialReference.wkid || json.spatialReference.wkt] || WGS84;
    }
    this.loaded_ = true;
    /**
     * This event is fired when the service and it's service info is loaded.
     * @name GeocodeService#load
     * @event
     */
    triggerEvent_(this, 'load');
};


/**
 * @name GeocodeOptions
 * @class This class represent the parameters needed in a find address candidate operation
 *  on a {@link GeocodeService}.
 *   There is no constructor, use JavaScript object literal.
 * <br/>For more info see <a  href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/candidates.html'>Find Adddress Candidates Operation</a>.
 * @property {Object} [inputs] an object literal with name-value pair of input values.
 * @property {Array.string} [outFields] The list of fields to be included in the returned resultset. 
 * @property {int|SpatialReference} [outSR] output SR, see {@link SpatialReference}
 */
/**
 * @name GeocodeResults
 * @class This class represent the results of an find address candidate operation for a 
 *  {@link GeocodeService}.
 *   There is no constructor, use JavaScript object literal.
 * <br/>For more info see <a  href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/candidates.html'>Find Adddress Candidates Operation</a>.
 * @property {Array.GeocodeResult} [candidates] The find address results as 
 * an array of {@link GeocodeResult}
 */
/**
 * @name GeocodeResult
 * @class This class represent one entry in the results of a find address operation for a
 *  {@link GeocodeService}.
 *   There is no constructor, use JavaScript object literal.
 * <br/>For more info see <a  href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/candidates.html'>Find Adddress Candidates Operation</a>.
 * @property {String} [address] matched address
 * @property {google.maps.LatLng} [location] matched location
 * @property {Number} [score] matching score
 * @property {Object} [attributes] attributes as name-value JSON object. 
 */
/**
 * The findAddressCandidates operation is performed on a geocode service
 *  resource. The result of this operation is a resource representing 
 *  the list of address candidates. This resource provides information 
 *  about candidates including the address, location, and score.
 *  param is an instance of {@link GeocodeOptions}. An instance of
 *  {@link GeocodeResults} will be passed into callback function.
 * @param {GeocodeOptions} params
 * @param {Function} callback
 * @param {Function} errback
 */
GeocodeService.prototype.findAddressCandidates = function(gparams, callback, errback) {
    var params = augmentObject_(gparams, {});
    if (params.inputs) {
        augmentObject_(params.inputs, params);
        delete params.inputs;
    }
    if (isArray_(params.outFields)) {
        params.outFields = params.outFields.join(',');
    }
    //params.outSR = 4326;
    var me = this;
    getJSON_(this.url + '/findAddressCandidates', params, '', function(json) {
        if (json.candidates) {
            var res, loc;
            var cands = [];
            for (var i = 0; i < json.candidates.length; i++) {
                res = json.candidates[i];
                loc = res.location;
                if (!isNaN(loc.x) && !isNaN(loc.y)) {
                    var ll = [loc.x, loc.y];
                    // problem: AGS9.31 does not support outSR, so it wil be ignored.
                    // however 10.0 does not return wkid in the result.
                    // as compromise, use outSR in 10's request, not included in 9.3.
                    var sr = me.spatialReference;
                    if (gparams.outSR) {
                        sr = spatialReferences_[gparams.outSR];
                    }
                    if (sr) ll = sr.inverse(ll);
                    res.location = new G.LatLng(ll[1], ll[0]);
                    cands[cands.length] = res;
                }
            }
        }
        callback({
            candidates: cands
        });
        handleErr_(errback, json);
    });
};
/**
   * Alias of <code>GeocodeService.findAddressCandidates</code>;
   * @param {GeocodeOptions} params
   * @param {Function} callback
   */
GeocodeService.prototype.geocode = function(params, callback) {
    this.findAddressCandidates(params, callback);
};

/**
 * @name ReverseGeocodeOptions
 * @class This class represent the parameters needed in a reverseGeocode operation
 *  on a {@link GeocodeService}.
 *   There is no constructor, use JavaScript object literal.
 * <br/>For more info see <a  href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/inverse.html'>Reverse Geocode Operation</a>.
 * @property {google.maps.LatLng} [location] an object literal of LatLng. 
 * @property {Number} [distance] The distance in meters from the given location within which 
 *  a matching address should be searched.
 */
/**
 * @name ReverseGeocodeResult
 * @class This class represent one entry in the results of a find address operation for a
 *  {@link GeocodeService}.
 *   There is no constructor, use JavaScript object literal.
 * <br/>For more info see <a  href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/inverse.html'>Reverse Geocode Operation</a>.
 * @property {Object} [address] matched address, object literal with name-value address parts. 
 * @property {google.maps.LatLng} [location] matched location
 */
/**
 * The reverseGeocode operation is The reverseGeocode operation is performed on a geocode service resource. 
 * The result of this operation is a inverse geocoded address resource.
 *  param is an instance of {@link ReverseGeocodeOptions}. An instance of
 *  {@link ReverseGeocodeResult} will be passed into callback function.
 * @param {ReverseGeocodeOptions} params
 * @param {Function} callback
 * @param {Function} errback
 */
GeocodeService.prototype.reverseGeocode = function(params, callback, errback) {
    if (!isString_(params.location)) {
        params.location = fromOverlaysToJSON_(params.location);
    }
    params.outSR = 4326;
    var me = this;
    getJSON_(this.url + '/reverseGeocode', params, '', function(json) {
        if (json.location) {
            var loc = json.location;
            if (!isNaN(loc.x) && !isNaN(loc.y)) {
                var ll = [loc.x, loc.y];
                if (me.spatialReference) {
                    ll = me.spatialReference.inverse(ll);
                }
                json.location = new G.LatLng(ll[1], ll[0]);
            }
        }
        callback(json);
        handleErr_(errback, json);
    });
};

//TODO: implement more Geometry operations
/**
 * Creates an GeometryService class.
 * Params:<li><code>url</code>: URL of service, syntax:<code>	http://{catalogurl}/{serviceName}/GeometryServer</code>
 * @name GeometryService
 * @constructor
 * @class This class (<code>GeometryService</code>) represent an ArcGIS 
 * <a href="http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/geometryserver.html">Geometry</a>
 *  service.
 * @param {String} url
 */
function GeometryService(url) {
    this.url = url;
    this.t = 'geocodeservice';
}

/**
   * @name ProjectOptions
   * @class This class represent the parameters needed in an project operation
   *  for a {@link GeometryService}.
   *   There is no constructor, use JavaScript object literal.
   * <br/>For more info see <a  href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/project.html'>Project Operation</a>.
 * @property {Array.OverlayView|Array.object} [geometries] Array of <code>google.maps.LatLng, Polyline, Polygon</code>, or ESRI Geometry format to project.
   * @property {GeometryType} [geometryType] esriGeometryPoint | esriGeometryPolyline | esriGeometryPolygon | esriGeometryEnvelope
   * @property {SpatialReference} [inSpatialReference] The well-known ID of or the {@link SpatialReference} of the input geometries
   * @property {SpatialReference} [outSpatialReference] The well-known ID of or the {@link SpatialReference} of the out geometries
   */
/**
   * @name ProjectResults
   * @class This class represent the parameters needed in an project operation
   *  for a {@link GeometryService}.
   *   There is no constructor, use JavaScript object literal.
   * <br/>For more info see <a  href  = 'http://sampleserver3.arcgisonline.com/ArcGIS/SDK/REST/project.html'>Project Operation</a>.
   * @property {Array.OverlayView|Array.object} [geometries] Array of <code>google.maps.LatLng, Polyline, Polygon<code>, or ESRI Geometry format to project. 
    */
/**
   * This resource projects an array of input geometries from an input spatial reference
   * to an output spatial reference. Result of type {@link ProjectResults} is passed in callback function.
   * @param {ProjectOptions} params
   * @param {Function} callback
   * @param {Function} errback
   */
GeometryService.prototype.project = function(p, callback, errback) {
    var params = prepareGeometryParams_(p);
    getJSON_(this.url + '/project', params, "callback", function(json) {
        var geom = [];
        if (p.outSpatialReference === 4326 || p.outSpatialReference.wkid === 4326) {
            for (var i = 0, c = json.geometries.length; i < c; i++) {
                geom.push(fromJSONToOverlays_(json.geometries[i]));
            }
            json.geometries = geom;
        }
        callback(json);
        handleErr_(errback, json);
    });
};


/**
   * This resource unions an array of input geometries
   * @param {UnionOptions} params
   * @param {Function} callback
   * @param {Function} errback
   */
GeometryService.prototype.union = function(p, callback, errback) {
    var params = prepareGeometryParams_(p);
    params.sr = 4326;
    p.outSpatialReference = 4326;
    getJSON_(this.url + '/union', params, "callback", function(json) {
        var geom = [];
        console.log(json);
        json.geometry = fromJSONToOverlays_(json.geometry);
        callback(json);
        handleErr_(errback, json);
    });
};


/**
  * Common units code in spatialReferences. Used in buffer operation.
  * This only has the most common units, for a full list of supported units, see 
  * <a href=http://resources.esri.com/help/9.3/ArcGISDesktop/ArcObjects/esriGeometry/esriSRUnitType.htm>esriSRUnitType</a>
  * and <a href=http://resources.esri.com/help/9.3/ArcGISDesktop/ArcObjects/esriGeometry/esriSRUnit2Type.htm>esriSRUnit2Type</a>
   * @enum {Number}
  * @property {Number} [METER] 9001 International meter.
  * @property {Number} [FOOT] 9002 International meter.
  * @property {Number} [SURVEY_FOOT] 9003 US survey foot.
  * @property {Number} [SURVEY_MILE] 9035 US survey mile.
  * @property {Number} [KILLOMETER] 9036 killometer.
  * @property {Number} [RADIAN] 9101 radian.
  * @property {Number} [DEGREE] 9102 degree.
  */
var SRUnit = {
    METER: 9001,
    FOOT: 9002,
    SURVEY_FOOT: 9003,
    SURVEY_MILE: 9035,
    KILLOMETER: 9036,
    RADIAN: 9101,
    DEGREE: 9102
};
/**
   * @name BufferOptions
   * @class This class represent the parameters needed in an buffer operation
   *  for a {@link GeometryService}.
   * @property {Array.OverlayView|Array.object} [geometries] Array of <code>google.maps.LatLng</code>, <code>Polyline</code>, <code>Polygon</code>, or ESRI Geometry format to buffer. 
   * @property {SpatialReference} [bufferSpatialReference] The well-known ID of or the {@link SpatialReference} of the buffer geometries
   * @property {Array.number} [distances] The distances the input geometries are buffered.
   * @property {Number} [unit] see <a href='http://resources.esri.com/help/9.3/ArcGISDesktop/ArcObjects/esriGeometry/esriSRUnitType.htm'>esriSRUnitType Constants </a> .
   * @property {Boolean} [unionResults] If true, all geometries buffered at a given distance are unioned into a single (possibly multipart) polygon, and the unioned geometry is placed in the output array.
   * @property {OverlayOptions} [overlayOptions] how to render result overlay. See {@link OverlayOptions}
   */
/**
   * @name BufferResults
   * @class This class represent the parameters needed in an project operation
   *  for a {@link GeometryService}.
   *   There is no constructor, use JavaScript object literal.
   * @property {Array.OverlayView|Array.object} [geometries] Array of <code>google.maps.LatLng, Polyline, Polygon</code>, or ESRI Geometry format to project. 
   */
/**
   * This resource projects an array of input geometries from an input spatial reference
   * to an output spatial reference. Result of type {@link BufferResults} is passed in callback function.
   * @param {BufferOptions} params
   * @param {Function} callback. 
   * @param {Function} errback
   */
GeometryService.prototype.buffer = function(p, callback, errback) {
    var params = prepareGeometryParams_(p);
    if (p.bufferSpatialReference) {
        params.bufferSR = formatSRParam_(p.bufferSpatialReference);
    }
    params.outSR = 4326;
    params.distances = p.distances.join(',');
    if (p.unit) {
        params.unit = p.unit;
    }
    getJSON_(this.url + '/buffer', params, "callback", function(json) {
        var geom = [];
        if (json.geometries) {
            for (var i = 0, c = json.geometries.length; i < c; i++) {
                geom.push(fromJSONToOverlays_(json.geometries[i], p['overlayOptions']));
            }
        }
        json.geometries = geom;
        callback(json);
        handleErr_(errback, json);
    });
};

/**
   * @name GPService
   * @class GPService
   * @constructor
   * @property {String} [serviceDescription]
   * @property {Array.string} [tasks]
   * @property {String} [executionType]
   * @property {String} [resultMapServerName]
   * @param {String} url http://[catalog-url]/[serviceName]/GPServer 
   */
function GPService(url) {
    this.url = url;
    this.loaded_ = false;
    var me = this;
    getJSON_(url, {}, '', function(json) {
        augmentObject_(json, me);
        me.loaded_ = true;
        /**
     * This event is fired when the service and it's service info is loaded.
     * @name GPService#load
     * @event
     */
        triggerEvent_(me, 'load');
    });
}

/**
   * @name GPParameter
   * @property {String} [name]
   * @property {String} [dataType]
   * @property {String} [displayName]
   * @property {String} [direction]
   * @property {Object} [defaultValue]
   * @property {Object} [parameterType]
   * @property {String} [category]
   * @property {Array.object} [choiceList]
   */
/**
   * @name GPTask
   * @class GPTask
   * @constructor
   * @property {String} [name]
   * @property {String} [displayName]
   * @property {String} [category]
   * @property {String} [helpUrl]
   * @property {String} [executionType]
   * @property {Array.GPParameter} [parameters] see {@link GPParameter}
   * @property {String} [name]
   * @property {String} [name]
   * @property {Array.string} [tasks]
   * @property {String} [resultMapServerName]
   * @param {String} url http://[catalog-url]/[serviceName]/GPServer 
   */
function GPTask(url) {
    this.url = url;
    this.loaded_ = false;
    var me = this;
    getJSON_(url, {}, '', function(json) {
        augmentObject_(json, me);
        me.loaded_ = true;
        /**
     * This event is fired when the service and it's service info is loaded.
     * @name GPService#load
     * @event
     */
        triggerEvent_(me, 'load');
    });
}

/**
   * @name GPOptions
   * @property {Object} [parameters] name-value pair of params. 
   * @property {Number|SpatialReference} [outSpatialReference] 
   * @property {Number|SpatialReference} [processSpatialReference] 
   */
/**
   * execute a GeoProcessing task
   * @param {GPOptions} p
   * @param {Function} callback will pass {@link GPResults} 
   * @param {Function} errback pass in {@link Error}
   */
GPTask.prototype.execute = function(p, callback, errback) {
    var params = {};
    if (p.parameters) {
        augmentObject_(p.parameters, params);
    }
    if (p.outSpatialReference) {
        params['env:outSR'] = formatSRParam_(p.outSpatialReference);
    } else {
        params['env:outSR'] = 4326;
    }
    if (p.processSpatialReference) {
        params['env:processSR'] = formatSRParam_(p.processSpatialReference);
    }
    getJSON_(this.url + '/execute', params, '', function(json) {
        if (json.results) {
            var res, f;
            for (var i = 0; i < json.results.length; i++) {
                res = json.results[i];
                if (res.dataType === 'GPFeatureRecordSetLayer') {
                    for (var j = 0, J = res.value.features.length; j < J; j++) {
                        f = res.value.features[j];
                        if (f.geometry) {
                            f.geometry = fromJSONToOverlays_(f.geometry, p.overlayOptions);
                        }
                    }
                }
            }
        }
        callback(json);
        handleErr_(errback, json);
    });
};

/**
   * @name GPResults
   * @property {Array.string} messages
   * @property {Array.GPResult} results
   */
/**
   * @name GPResult
   * @property {String} paramName
   * @property {String} dataType
   * @property {Object} value
   */
/**
   * @name NetworkService
   * @class NetworkService
   * @constructor
   * @property {String} serviceDescription
   * @property {Array.string} routeLayers
   * @property {Array.string} serviceAreaLayers
   * @property {Array.string} closestFacilityLayers
   * @param {String} url http://[catalog-url]/[serviceName]/NAServer 
   */
function NetworkService(url) {
    this.url = url;
    this.loaded_ = false;
    var me = this;
    getJSON_(url, {}, '', function(json) {
        augmentObject_(json, me);
        me.loaded_ = true;
        /**
     * This event is fired when the service and it's service info is loaded.
     * @name NetworkService#load
     * @event
     */
        triggerEvent_(me, 'load');
    });
}

/**
   * @name RouteOptions
   * @class intance that specify how a route should be solved.
   * @property {Array.google.maps.LatLng|Array.Marker} [stops] the locations the route must pass
   * @property {Array.google.maps.LatLng|Array.Marker} [barriers] the locations the route must avoid
   * @property {Boolean} [returnDirections] If true, directions will be generated and returned with the analysis results. Default is true
   * @property {Boolean} [returnRoutes] If true, routes will be returned with the analysis results. Default is true. 
   * @property {Boolean} [findBestSequence] If true, the solver should resequence the route in the optimal order. The default is as defined in the network layer. 
   * @property {Boolean} [preserveFirstStop] If true, the solver should resequence the route in the optimal order. The default is as defined in the network layer. 
   * @property {Boolean} [preserveLastStop] If true, the solver should resequence the route in the optimal order. The default is as defined in the network layer. 
   */
/**
   * @name RouteResults
   * @class intance that specify the results of the solve operation.
   * @property {Array.google.maps.LatLng} [stops]
   */
/**
   * Create a route task with the URL of the routing server resource.
   * @name RouteTask 
   * @class This class (<code>RouteTask</code>) represent a Network Layer resource deployed in a NetWorkService.
   * It can solve a route based on stops, barrier
   * @constructor
   * @param {String} url
   */
function RouteTask(url) {
    this.url = url;
}

/**
   * Solve a route based on inputs such as stops and barriers. Result of type {@link RouteResults} 
   * is passed to Function callback, and error of type {@link Error} is passed to Function errback.
   * @param {RouteOptions} opt_Route
   * @param {Function} callback
   * @param {Function} errback
   */
RouteTask.prototype.solve = function(opts, callback, errback) {
    if (!opts) {
        return;
    }
    // handle many other fields
    var params = augmentObject_(opts, {});
    //params['outSR'] = WGS84.wkid;
    if (isArray_(opts.stops)) {
        params.stops = fromLatLngsToFeatureSet_(opts.stops);
    }
    if (isArray_(opts.barriers)) {
        if (opts.barriers.length > 0) {
            params.barriers = fromLatLngsToFeatureSet_(opts.barriers);
        } else {
            delete params.barriers;
        }
    }
    params.returnRoutes = (opts.returnRoutes === false ? false : true);
    params.returnDirections = (opts.returnDirections === true ? true : false);
    params.returnBarriers = (opts.returnBarriers === true ? true : false);
    params.returnStops = (opts.returnStops === true ? true : false);

    getJSON_(this.url + '/solve', params, '', function(json) {
        if (json.routes) {
            parseFeatures_(json.routes.features, opts.overlayOptions);
        }
        callback(json);
        handleErr_(errback, json);
    });
};


/**
   * @name OverlayOptions
   * @class Instance of this classes that specify how
   *   the geometry features returned by ArcGIS server should be rendered in the browser.
   * @property {google.maps.MarkerOptions} [markerOptions] style option for points.
   * @property {google.maps.PolylineOptions} [polylineOptions] style option for polylines. <a href=http://code.google.com/apis/maps/documentation/javascript/reference.html#PolylineOptions>PolylineOptions</a>
   * @property {google.maps.PolygonOptions} [polygonOptions] style option for polygons. <a href=http://code.google.com/apis/maps/documentation/javascript/reference.html#PolygonOptions>PolygonOptions</a>
   * @property {Number} [strokeOpacity] The stroke opacity between 0.0 and 1.0
   * @property {Number} [fillOpacity] The fill opacity between 0.0 and 1.0
   * @property {String} [strokeColor] The stroke color in HTML hex style, ie. "#FFAA00"
   * @property {String} [fillColor] The fill color in HTML hex style, ie. "#FFAA00"
   * @property {Number} [strokeWeight] The stroke width in pixels.
   * @property {Number} [zIndex] The zIndex compared to other overlays.
   * @property {String|google.maps.MarkerImage} [icon] Icon for the foreground
   * @property {String|google.maps.MarkerImage} [shadow] Shadow image
   */
/**
   * @name TileInfo
   * @class This class contains information about map tile infornation for a cached map service.
   *    <br/>There is no constructor for this class.
   * @property {Number} [rows] tile row size,  e.g. 512, must be same as cols
   * @property {Number} [cols] tile cols size,  e.g. 512, must be same as rows
   * @property {Number} [dpi] dot per inch for map tiles.
   * @property {String} [format] PNG8 | PNG24 | PNG32 | GIF | JPEG
   * @property {Number} [compressionQuality] JPEG only.0-100.
   * @property {Point} [origin] origin of tile system of type
   * @property {SpatialReference} [spatialReference] spatial reference.  <b>wkid info only</b>.
   * @property {Array.LOD} [lods] Array of Level of Details. See {@link LOD}
   */
/**
   * @name LOD
   * @class This class contains information about one "Level Of Detail" for a cached map service.
   *   It is the type of {@link lods} property of {@link TileInfo}
   *   <br/>There is no constructor for this class. Use as object literal.
   * @property {Number} [level] zoom level.
   * @property {Number} [resolution] map unit per pixel
   * @property {Number} [scale] actual map scale. e.g a value of 5000 means 1:5000 scale.
   */
/**
   * Creates an ArcGIS Map Tiling Reference System.
   * <ul>
   * <li><code>tileInfo</code> tiling information. An instance of {@link TileInfo}
   * </ul>Applications normally do not create instances of this class directly.
   * @name Projection
   * @implements {google.maps.Projection}
   * @constructor
   * @class This class (<code>Projection</code>) implements a custom
   * <a href  = 'http://code.google.com/apis/maps/documentation/javascript/reference.html#Projection'>google.maps.Projection</a>
   * from the core Google Maps API.
   *   It includes a real {@link SpatialReference} object to convert LatLng from/to
   *   map coordinates, and tiling scheme informations to convert
   *   map coordinates from/to pixel coordinates.
   * @param {TileInfo} tileInfo
   */
function Projection(tileInfo) {
    //if (!tileInfo) {
    //  throw new Error('map service is not tiled');
    //}
    this.lods_ = tileInfo ? tileInfo.lods : null;
    this.spatialReference_ = tileInfo ? spatialReferences_[tileInfo.spatialReference.wkid || tileInfo.spatialReference.wkt] : WEB_MERCATOR;
    if (!this.spatialReference_) {
        throw new Error('unsupported Spatial Reference');
    }
    // resolution (unit/pixel) at lod level 0. Due to changes from V2-V3, 
    // zoom is no longer defined in Projection. It is assumed that level's zoom factor is 2. 
    this.resolution0_ = tileInfo ? tileInfo.lods[0].resolution : 156543.033928;
    // zoom offset of this tileinfo's zoom 0 to Google's zoom0
    this.minZoom = Math.floor(Math.log(this.spatialReference_.getCircum() / this.resolution0_ / 256) / Math.LN2 + 0.5);
    this.maxZoom = tileInfo ? this.minZoom + this.lods_.length - 1 : 20;
    if (G.Size) {
        this.tileSize_ = tileInfo ? new G.Size(tileInfo.cols, tileInfo.rows) : new G.Size(256, 256);
    }
    // Find out how the map units scaled to 1 tile at zoom 0. 
    // from V2-V3, coords must scaled to 256 pixel under Mercator at zoom 0.
    // scale can be considered under this SR, what's the actual pixel number to 256 to cover whole earth?
    this.scale_ = Math.pow(2, this.minZoom) * this.resolution0_;
    this.originX_ = tileInfo ? tileInfo.origin.x : -20037508.342787;
    this.originY_ = tileInfo ? tileInfo.origin.y : 20037508.342787;
    // validation check
    if (tileInfo) {
        var ratio;
        for (var i = 0; i < tileInfo.lods.length - 1; i++) {
            ratio = tileInfo.lods[i].resolution / tileInfo.lods[i + 1].resolution;
            if (ratio > 2.001 || ratio < 1.999) {
                throw new Error('This type of map cache is not supported in V3. \nScale ratio between zoom levels must be 2');
            }
        }
    }
}

/**
   * See <a href  = 'http://code.google.com/apis/maps/documentation/javascript/reference.html#Projection'>google.maps.Projection</a>.
   * @param {LatLng} gLatLng
   * @param {Point} opt_point
   * @return {Point} pixel
   */
Projection.prototype.fromLatLngToPoint = function(latlng, opt_point) {
    if (!latlng || isNaN(latlng.lat()) || isNaN(latlng.lng())) {
        return null;
    }
    var coords = this.spatialReference_.forward([latlng.lng(), latlng.lat()]);
    var point = opt_point || new G.Point(0, 0);
    point.x = (coords[0] - this.originX_) / this.scale_;
    point.y = (this.originY_ - coords[1]) / this.scale_;
    return point;
};
// somehow externs was ignored in adv mode.
Projection.prototype['fromLatLngToPoint'] = Projection.prototype.fromLatLngToPoint;
/**
   * See <a href  = 'http://code.google.com/apis/maps/documentation/javascript/reference.html#Projection'>google.maps.Projection</a>.
   * @param {Point} pixel
   * @param {Boolean} opt_nowrap
   * @return {LatLng}
   */
Projection.prototype.fromPointToLatLng = function(pixel, opt_nowrap) {
    //TODO: handle nowrap
    if (pixel === null) {
        return null;
    }
    var x = pixel.x * this.scale_ + this.originX_;
    var y = this.originY_ - pixel.y * this.scale_;
    var geo = this.spatialReference_.inverse([x, y]);
    return new G.LatLng(geo[1], geo[0]);
};
//Projection.prototype['fromLatLngToPoint'] = Projection.prototype.fromLatLngToPoint;
/**
   * Get the scale at given level;
   * @param {Number} zoom
   * @return {Number}
   */
Projection.prototype.getScale = function(zoom) {
    var zoomIdx = zoom - this.minZoom;
    var res = 0;
    if (this.lods_[zoomIdx]) {
        res = this.lods_[zoomIdx].scale;
    }
    return res;
};

Projection.WEB_MECATOR = new Projection();

/**
   * @name TileLayerOptions
   * @class Instances of this class are used in the {@link opt_layerOpts} argument
   *   to the constructor of the {@link TileLayer} class.
   * @property {String} [hosts] host pattern of tile servers if they are numbered. Most browser
   *   has default restrictions on how many concurrent connections can be made to
   *   a single host. One technique to workaround this is to create multiple hosts and rotate them when
   *   loading tiles.
   *   The syntax is <code>prefix[<i>numberOfHosts</i>]suffix</code>, for example, <code>"mt[4].google.com"</code> means
   *   rotate hosts in <code>mt0.google.com, mt1.google.com, mt2.google.com, mt3.google.com</code> (4 hosts).
   * @property {Number} [minZoom] min zoom level.
   * @property {Number} [maxZoom] max zoom level.
   * @property {Number} [opacity] opacity (0-1).
   */
/** Creates a tile layer from a cached by ArcGIS map service. 
   * <br/> <code> service</code> (required) is the underline {@link MapService}
   * <br/> <code>opt_layerOpts</code> (optional) is an instance of {@link TileLayerOptions}.
   * @name TileLayer
   * @constructor
   * @class This class (<code>TileLayer</code>) provides access to a cached ArcGIS Server
   * map service. There is no <code>GTileLayer</code> class in Google Maps API V3, this class is kept to allow
   * finer control of zoom levels for each individual tile sets within a map type, such as zoom level range and opacity.
   * @param {MapService} service
   * @param {TileLayerOptions} opt_layerOpts
   */
function TileLayer(service, opt_layerOpts) {
    opt_layerOpts = opt_layerOpts || {};
    if (opt_layerOpts.opacity) {
        this.opacity_ = opt_layerOpts.opacity;
        delete opt_layerOpts.opacity;
    }
    augmentObject_(opt_layerOpts, this);
    this.mapService_ = (service instanceof MapService) ? service : new MapService(service);
    //In the format of mt[number].domain.com
    if (opt_layerOpts.hosts) {
        var pro = extractString_(this.mapService_.url, '', '://');
        var host = extractString_(this.mapService_.url, '://', '/');
        var path = extractString_(this.mapService_.url, pro + '://' + host, '');
        this.urlTemplate_ = pro + '://' + opt_layerOpts.hosts + path;
        this.numOfHosts_ = parseInt(extractString_(opt_layerOpts.hosts, '[', ']'), 10);
    }
    this.name = opt_layerOpts.name || this.mapService_.name;
    this.maxZoom = opt_layerOpts.maxZoom || 19;
    this.minZoom = opt_layerOpts.minZoom || 0;
    this.dynaZoom = opt_layerOpts.dynaZoom || this.maxZoom;
    if (this.mapService_.loaded_) {
        this.init_(opt_layerOpts);
    } else {
        var me = this;
        G.event.addListenerOnce(this.mapService_, 'load', function() {
            me.init_(opt_layerOpts);
        });
    }
    this.tiles_ = {};
    this.map_ = opt_layerOpts.map;
}

/**
   * Initialize the tile layer from a loaded map service
   * @param {Object} opt_layerOpts
   */
TileLayer.prototype.init_ = function(opt_layerOpts) {
    if (this.mapService_.tileInfo) {
        this.projection_ = new Projection(this.mapService_.tileInfo);
        this.minZoom = opt_layerOpts.minZoom || this.projection_.minZoom;
        this.maxZoom = opt_layerOpts.maxZoom || this.projection_.maxZoom;
    }
};


/**
   * Returns a string (URL) for given tile coordinate (x, y) and zoom level
   * @private not meant to be called by client
   * @param {Object} tile
   * @param {Number} zoom
   * @return {String} url
   */
TileLayer.prototype.getTileUrl = function(tile, zoom) {
    var z = zoom - (this.projection_ ? this.projection_.minZoom : this.minZoom);
    var url = '';
    if (!isNaN(tile.x) && !isNaN(tile.y) && z >= 0 && tile.x >= 0 && tile.y >= 0) {
        var u = this.mapService_.url;
        if (this.urlTemplate_) {
            u = this.urlTemplate_.replace('[' + this.numOfHosts_ + ']', '' + ((tile.y + tile.x) % this.numOfHosts_));
        }
        var prj = this.projection_ || (this.map_ ? this.map_.getProjection() : Projection.WEB_MECATOR);
        if (!prj instanceof Projection) {
            // if use Google's image 
            prj = Projection.WEB_MECATOR;
        }
        var size = prj.tileSize_;
        var numOfTiles = 1 << zoom;
        var gworldsw = new G.Point(tile.x * size.width / numOfTiles, (tile.y + 1) * size.height / numOfTiles);
        var gworldne = new G.Point((tile.x + 1) * size.width / numOfTiles, tile.y * size.height / numOfTiles);
        var bnds = new G.LatLngBounds(prj.fromPointToLatLng(gworldsw), prj.fromPointToLatLng(gworldne));
        var fullBounds = this.mapService_.getFullBounds();
        if (this.mapService_.singleFusedMapCache === false || zoom > this.dynaZoom) {
            // dynamic map service
            var params = {
                'f': 'image'
            };
            params.bounds = bnds;
            params.format = 'png32';
            params.width = size.width;
            params.height = size.height;
            params.imageSR = prj.spatialReference_;
            url = this.mapService_.exportMap(params);
        } else if (fullBounds && !fullBounds.intersects(bnds)) {
            url = '';
        } else {
            url = u + '/tile/' + z + '/' + tile.y + '/' + tile.x;
        }
    }
    //log_('url=' + url);
    return url;
};
/**
   * set Opacity
   * @param {Number} op (0-1)
   */
TileLayer.prototype.setOpacity = function(op) {
    this.opacity_ = op;
    var tiles = this.tiles_;
    for (var x in tiles) {
        if (tiles.hasOwnProperty(x)) {
            setNodeOpacity_(tiles[x], op);
        }
    }
};
/**
   * get the opacity (0-1) of the tile layer
   * @return {Number}
   */
TileLayer.prototype.getOpacity = function() {
    return this.opacity_;
};
/**
   * get the underline {@link MapService}
   * @return {MapService}
   */
TileLayer.prototype.getMapService = function() {
    return this.mapService_;
};

/**
   * @name MapTypeOptions
   * @class Instance of this class are used in the {@link opt_typeOpts} argument
   *  to the constructor of the {@link MapType} class. See
   *  <a href=http://code.google.com/apis/maps/documentation/javascript/reference.html#MapType>google.maps.MapType</a>.
   * @property {String} [name] map type name
   * @property {Projection} [projection] an instance of {@link Projection}.
   * @property {String} [alt] Alt text to display when this MapType's button is hovered over in the MapTypeControl. Optional.
   * @property {Number} [maxZoom] The maximum zoom level for the map when displaying this MapType. Required for base MapTypes, ignored for overlay MapTypes.
   * @property {Number} [minZoom] The minimum zoom level for the map when displaying this MapType. Optional; defaults to 0.
   * @property {google.maps.Size} [tileSize] The dimensions of each tile.
   */
// * @property {Number} [radius] Radius of the planet for the map, in meters. Optional; defaults to Earth's equatorial radius of 6378137 meters.
/**
   * Creates a MapType, with a array of {@link TileLayer}s, or a single URL as shortcut.
   * @name MapType
   * @constructor
   * @class This class implements the Google Maps API's
   * <a href  = http://code.google.com/apis/maps/documentation/javascript/reference.html#MapType>GMapType</a>.
   * It holds a list of {@link TileLayer}s.
   * <p> Note: all tiled layer in the same map type must use same spatial reference and tile scheme.</p>
   * @param {Array.TileLayer|String} tileLayers
   * @param {MapTypeOptions} opt_typeOpts
   */
function MapType(tileLayers, opt_typeOpts) {

    opt_typeOpts = opt_typeOpts || {};
    var i;
    if (opt_typeOpts.opacity) {
        this.opacity_ = opt_typeOpts.opacity;
        delete opt_typeOpts.opacity;
    }
    augmentObject_(opt_typeOpts, this);
    var layers = tileLayers;
    if (isString_(tileLayers)) {
        layers = [new TileLayer(tileLayers, opt_typeOpts)];
    } else if (tileLayers instanceof MapService) {
        layers = [new TileLayer(tileLayers, opt_typeOpts)];
    } else if (tileLayers instanceof TileLayer) {
        layers = [tileLayers];
    } else if (tileLayers.length > 0 && isString_(tileLayers[0])) {
        layers = [];
        for (i = 0; i < tileLayers.length; i++) {
            layers[i] = new TileLayer(tileLayers[i], opt_typeOpts);
        }
    }
    this.tileLayers_ = layers;
    this.tiles_ = {};
    if (opt_typeOpts.maxZoom !== undefined) {
        this.maxZoom = opt_typeOpts.maxZoom;
    } else {
        var maxZ = 0;
        for (i = 0; i < layers.length; i++) {
            maxZ = Math.max(maxZ, layers[i].maxZoom);
        }
        this.maxZoom = maxZ;
    }
    if (layers[0].projection_) {
        this.tileSize = layers[0].projection_.tileSize_;
        this.projection = layers[0].projection_;
    } else {
        this.tileSize = new G.Size(256, 256);
    }
    if (!this.name) {
        this.name = layers[0].name;
    }

}

/**
   * Get a tile for given tile coordinates Returns a tile for the given tile coordinate (x, y) and zoom level.
   * This tile will be appended to the given ownerDocument.
   * @private not meant to be called directly.
   * @param {Point} tileCoord
   * @param {Number} zoom
   * @return {Node}
   */
MapType.prototype.getTile = function(tileCoord, zoom, ownerDocument) {
    var div = ownerDocument.createElement('div');
    var tileId = '_' + tileCoord.x + '_' + tileCoord.y + '_' + zoom;
    for (var i = 0; i < this.tileLayers_.length; i++) {
        var t = this.tileLayers_[i];
        if (zoom <= t.maxZoom && zoom >= t.minZoom) {
            var url = t.getTileUrl(tileCoord, zoom);
            if (url) {
                var img = ownerDocument.createElement(document.all ? 'img' : 'div'); //IE does not like img
                img.style.border = '0px none';
                img.style.margin = '0px';
                img.style.padding = '0px';
                img.style.overflow = 'hidden';
                img.style.position = 'absolute';
                img.style.top = '0px';
                img.style.left = '0px';
                img.style.width = '' + this.tileSize.width + 'px';
                img.style.height = '' + this.tileSize.height + 'px';
                //log_(url);
                if (document.all) {
                    img.src = url;
                } else {
                    img.style.backgroundImage = 'url(' + url + ')';
                }
                div.appendChild(img);
                t.tiles_[tileId] = img;
                if (t.opacity_ !== undefined) {
                    setNodeOpacity_(img, t.opacity_);
                } else if (this.opacity_ !== undefined) {
                    // in FF it's OK to set parent div just once but IE does not like it.
                    setNodeOpacity_(img, this.opacity_);
                }
            } else {
                // TODO: use a div to display NoData
            }
        }
    }
    this.tiles_[tileId] = div;
    div.setAttribute('tid', tileId);
    return div;
};
MapType.prototype['getTile'] = MapType.prototype.getTile;
/**
   * Release tile and cleanup
   * @private not meant to be called directly.
   * @param {Node} node
   */
MapType.prototype.releaseTile = function(node) {
    if (node.getAttribute('tid')) {
        var tileId = node.getAttribute('tid');
        if (this.tiles_[tileId]) {
            delete this.tiles_[tileId];
        }
        for (var i = 0; i < this.tileLayers_.length; i++) {
            var t = this.tileLayers_[i];
            if (t.tiles_[tileId]) {
                delete t.tiles_[tileId];
            }
        }
    }
};
MapType.prototype['releaseTile'] = MapType.prototype.releaseTile;
/**
   * Set Opactity
   * @param {Number} op
   */
MapType.prototype.setOpacity = function(op) {
    this.opacity_ = op;
    var tiles = this.tiles_;
    for (var x in tiles) {
        if (tiles.hasOwnProperty(x)) {
            var nodes = tiles[x].childNodes;
            for (var i = 0; i < nodes.length; i++) {
                setNodeOpacity_(nodes[i], op);
            }
        }
    }
};
/**
   * get opacity
   * @return {Number}
   */
MapType.prototype.getOpacity = function() {
    return this.opacity_;
};
/**
   * get list of {@link TileLayer} in this map type
   * @return {Array.TileLayer}
   */
MapType.prototype.getTileLayers = function() {
    return this.tileLayers_;
};


/**
   * @name MapOverlayOptions
   * @class Instance of this class are used in the {@link opt_ovelayOpts} argument
   *  to the constructor of the {@link MapOverlay} class.
   * @property {Number} [opacity  = 1.0] Opacity of map image from 0.0 (invisible) to 1.0 (opaque)
   * @property {ExportMapOptions} [exportOptions] See {@link ExportMapOptions}
   * @property {google.maps.Map} [map] map to attach to.
   */
/**
   * Creates an Map Overlay using <code>url</code> of the map service and optional {@link MapOverlayOptions}.
   * <li/> <code> service</code> (required) is url of the underline {@link MapService} or the MapService itself.
   * <li/> <code>opt_overlayOpts</code> (optional) is an instance of {@link MapOverlayOptions}.
   * @name MapOverlay
   * @class This class (<code>MapOverlay</code>) extends the Google Maps API's
   * <a href  = http://code.google.com/apis/maps/documentation/reference.html#OverlayView>OverlayView</a>
   * that draws map images from data source on the fly. It is also known as "<b>Dynamic Maps</b>".
   * It can be added to the map via <code>setMap(map) </code> method.
   * The similar class in the core Map API is <a href  = http://code.google.com/apis/maps/documentation/javascript/reference.html#GroundOverlay>google.maps.GroundOverlay</a>,
   * however, the instance of this class always cover the viewport exactly, and will redraw itself as map moves.
   * @constructor
   * @param {String|MapService} service
   * @param {MapOverlayOptions} opt_overlayOpts
   */
function MapOverlay(service, opt_overlayOpts) {
    opt_overlayOpts = opt_overlayOpts || {};
    this.mapService_ = (service instanceof MapService) ? service : new MapService(service);

    if (opt_overlayOpts.onError) {
        google.maps.event.addListenerOnce(this.mapService_, 'error', opt_overlayOpts.onError);
    }


    this.minZoom = opt_overlayOpts.minZoom;
    this.maxZoom = opt_overlayOpts.maxZoom;
    this.opacity_ = opt_overlayOpts.opacity || 1;
    this.exportOptions_ = opt_overlayOpts.exportOptions || {};
    this.visible_ = true;
    this.drawing_ = false;
    // do we need another refresh. Normally happens bounds changed before server returns image.
    this.needsNewRefresh_ = false;
    this.overlay_ = null;
    this.div_ = null;
    // Once the LatLng and text are set, add the overlay to the map.  This will
    // trigger a call to panes_changed which should in turn call draw.
    if (opt_overlayOpts.map) {
        this.setMap(opt_overlayOpts.map);
    }
    this.map_ = null;
    this.listeners_ = [];
}

MapOverlay.prototype = new G.OverlayView();
/**
   * Called by API not by app code.
   * Handler when overlay is added. Interface method.
   * This will be called after setMap(map) is called.
   */
MapOverlay.prototype.onAdd = function() {
    var me = this;
    this.listeners_.push(G.event.addListener(this.getMap(), 'bounds_changed', callback_(this.refresh, this)));
    this.listeners_.push(G.event.addListener(this.getMap(), 'dragstart', function() {
        me.dragging = true;
    }));
    this.listeners_.push(G.event.addListener(this.getMap(), 'dragend', function() {
        me.dragging = false;
    }));
    var map = this.getMap();
    map.agsOverlays = map.agsOverlays || new G.MVCArray();
    map.agsOverlays.push(this);
    setCopyrightInfo_(map);
    this.map_ = map;
};
MapOverlay.prototype['onAdd'] = MapOverlay.prototype.onAdd;
/** 
   * Called by API not by app code.
   * Handler when overlay is removed.
   */
MapOverlay.prototype.onRemove = function() {
    for (var i = 0, j = this.listeners_.length; i < j; i++) {
        G.event.removeListener(this.listeners_[i]);
    }
    //G.event.removeListener(this.zoomChangedListener_);
    //this.div_.parentNode.removeChild(this.div_);
    //this.div_ = null;
    if (this.overlay_) this.overlay_.setMap(null);
    var map = this.map_; // getMap();
    var agsOvs = map.agsOverlays;
    if (agsOvs) {
        for (var i = 0, c = agsOvs.getLength(); i < c; i++) {
            if (agsOvs.getAt(i) == this) {
                agsOvs.removeAt(i);
                break;
            }
        }
    }
    setCopyrightInfo_(map);
    this.map_ = null;
};
MapOverlay.prototype['onRemove'] = MapOverlay.prototype.onRemove;
/**
   * Called by API not by app code.
   * See OverlayView.draw in core API docs.
   */
MapOverlay.prototype.draw = function() {
    if (!this.drawing_ || this.needsNewRefresh_ === true) {
        this.refresh();
    }
};

MapOverlay.prototype['draw'] = MapOverlay.prototype.draw;
/**
   * Gets Image Opacity. return <code>opacity</code> between 0-1.
   * @return {Number} opacity
   */
MapOverlay.prototype.getOpacity = function() {
    return this.opacity_;
};
/**
   * Sets Image Opacity. parameter <code>opacity</code> between 0-1.
   * @param {Number} opacity
   */
MapOverlay.prototype.setOpacity = function(opacity) {
    var op = Math.min(Math.max(opacity, 0), 1);
    this.opacity_ = op;
    if (this.overlay_) {
        setNodeOpacity_(this.overlay_.div_, op);
    }
};
/**
   * Gets underline {@link MapService}.
   * @return {MapService} MapService
   */
MapOverlay.prototype.getMapService = function() {
    return this.mapService_;
};
/**
   * Refresh the map image in current view port.
   */
MapOverlay.prototype.refresh = function() {
    if (this.isHidden())
        return;
    //if (this.drawing_ === true) {
    //  this.needsNewRefresh_ = true;
    //  return;
    //}
    var m = this.getMap();
    var bnds = m ? m.getBounds() : null;
    if (!bnds) {
        return;
    }
    //this.drawTimeout_ = setTimeout(
    var params = this.exportOptions_;
    params.bounds = bnds;
    var sr = WEB_MERCATOR;
    // V3 no map.getSize()
    var s = m.getDiv();
    params.width = s.offsetWidth;
    params.height = s.offsetHeight;
    if (s.offsetWidth == 0 || s.offsetHeight == 0) {
        return;
    }
    var prj = m.getProjection(); // note this is not same as this.getProjection which returns MapCanvasProjection
    if (prj && prj instanceof Projection) {
        sr = prj.spatialReference_;
    }
    params.imageSR = sr;
    var me = this;
    if (this.overlay_ && this.overlay_.div_)
        this.overlay_.div_.style.visibility = 'hidden';

    if (this.drawTimeout_) {
        clearTimeout(this.drawTimeout_);
        this.drawTimeout_ = null;
    }

    this.drawTimeout_ = setTimeout(function() {
        /**
         * This event is fired before the the drawing request was sent to server.
         * @name MapOverlay#drawstart
         * @event
         */
        triggerEvent_(me, 'drawstart');
        me.drawing_ = true;

        if (!me.dragging && me.overlay_) {
            me.overlay_.setMap(null);
            me.overlay_ = null;
        }

        //csc proxy server issue fix attempt
        params.f = "image";

        //this.div_.style.backgroundImage = '';
        //query actual image (GET)
        if (params.f === 'image') {

            var useProxy = false;
            if (Config.alwaysUseProxy) {
                useProxy = true;
            }
            if (useProxy && !Config.proxyUrl) {
                throw new Error('No proxyUrl property in Config is defined');
            }

            var url = me.mapService_.exportMap(params);
            me.overlay_ = new ImageOverlay(me.map_.getBounds(), useProxy ? Config.proxyUrl + '?' + url : url, me.map_, me.opacity_);

            // EIS: Addition
            // drawend should only be triggered once we have successfully fetched the image from the server.
            // otherwise, there is a potential delay between drawend and the actual displaying of the image
            G.event.addDomListenerOnce(me.overlay_, "load", function () {
                /**
                 * This event is fired after the the drawing request was returned by server.
                 * @name MapOverlay#drawend
                 * @event
                 */
                triggerEvent_(me, 'drawend');
                me.drawing_ = false;
            });
        }
        //query json (POST)
        else {
            me.mapService_.exportMap(params, function (json) {
                clearTimeout(me.drawTimeout_);
                me.drawTimeout_ = null;
                //if (me.needsNewRefresh_ === true) {
                //    me.needsNewRefresh_ = false;
                //    me.refresh();
                //    return;
                //}
                if (json.href) {
                    if (me.overlay_) {
                        me.overlay_.setMap(null);
                        me.overlay_ = null;
                    }
                    me.overlay_ = new ImageOverlay(json.bounds, json.href, me.map_, me.opacity_);

                    // EIS: Addition
                    // drawend should only be triggered once we have successfully fetched the image from the server.
                    // otherwise, there is a potential delay between drawend and the actual displaying of the image
                    G.event.addDomListenerOnce(me.overlay_, "load", function () {
                        /**
                         * This event is fired after the the drawing request was returned by server.
                         * @name MapOverlay#drawend
                         * @event
                         */
                        triggerEvent_(me, 'drawend');
                        me.drawing_ = false;
                    });

                }

            });
        }
    }, 500);
};


/**
   * Check if the overlay is visible, and within zoomzoom range and current map bounds intersects with it's fullbounds.
   * @return {Boolean} visible
   */
MapOverlay.prototype.isHidden = function() {
    return !this.visible_ || !this.isInZoomRange_();
};
/**
   * If this in zoom range
   * @private
   * @return {Boolean}
   */
MapOverlay.prototype.isInZoomRange_ = function() {
    var z = this.getMap().getZoom();
    if ((this.minZoom !== undefined && z < this.minZoom) ||
    (this.maxZoom !== undefined && z > this.maxZoom)) {
        return false;
    }
    return true;
};

/**
   * Makes the overlay visible.
   */
MapOverlay.prototype.show = function() {
    this.visible_ = true;
    if (this.overlay_ && this.overlay_.div_)
        this.overlay_.div_.style.visibility = 'visible';
    this.refresh();
};
/**
   * Hide the overlay
   */
MapOverlay.prototype.hide = function() {
    this.visible_ = false;
    if (this.overlay_ && this.overlay_.div_)
        this.overlay_.div_.style.visibility = 'hidden';
    //this.div_.style.visibility = 'hidden';
};

/**
   * @class simply an image overaly. Added due to some unknown problems related to 
   * overlayLayer pane after bounds change since gmaps API v3.4. 
   * this class is based on sample code USGSOverlay
   * @constructor
   * @param {Object} bounds
   * @param {Object} url
   * @param {Object} map
   */
function ImageOverlay(bounds, url, map, op) {
    this.bounds_ = bounds;
    this.url_ = url;
    this.map_ = map;
    this.div_ = null;
    this.op_ = op;
    this.setMap(map);
}

ImageOverlay.prototype = new G.OverlayView();
ImageOverlay.prototype.onAdd = function() {
    var me = this;
    var div = document.createElement('DIV');
    div.style.border = "none";
    div.style.borderWidth = "0px";
    div.style.position = "absolute";
    var s = this.map_.getDiv();
    div.style.width = s.offsetWidth + 'px';
    div.style.height = s.offsetHeight + 'px';
    // EIS: Adding in a loaded event to better inform draw-start and draw-end events
    var bgImage = new Image();
    bgImage.onload = function() {
        div.style.backgroundImage = 'url(' + this.src + ')';
        triggerEvent_(me, "load");
    };
    bgImage.src = this.url_;


    // Set the overlay's div_ property to this DIV
    this.div_ = div;

    // We add an overlay to a map via one of the map's panes.
    // We'll add this overlay to the overlayImage pane.
    var panes = this.getPanes();
    setNodeOpacity_(div, this.op_);
    panes.overlayLayer.appendChild(div);
};
ImageOverlay.prototype.draw = function() {

    // Size and position the overlay. We use a southwest and northeast
    // position of the overlay to peg it to the correct position and size.
    // We need to retrieve the projection from this overlay to do this.
    var overlayProjection = this.getProjection();

    // Retrieve the southwest and northeast coordinates of this overlay
    // in latlngs and convert them to pixels coordinates.
    // We'll use these coordinates to resize the DIV.
    var sw = overlayProjection.fromLatLngToDivPixel(this.bounds_.getSouthWest());
    var ne = overlayProjection.fromLatLngToDivPixel(this.bounds_.getNorthEast());

    // Resize the image's DIV to fit the indicated dimensions.
    var div = this.div_;
    div.style.left = sw.x + 'px';
    div.style.top = ne.y + 'px';
    //div.style.width = (ne.x - sw.x) + 'px';
    //div.style.height = (sw.y - ne.y) + 'px';
};
ImageOverlay.prototype.onRemove = function() {
    this.div_.parentNode.removeChild(this.div_);
    this.div_ = null;
}; /**
 * Creates a copyright control
 * @name CopyrightControl
 * @class put a copyright notice at bottom rigth corner.
 * @constructor
 * @param {google.maps.Map} map
 */
function CopyrightControl(map) {
    // reason to put div creation out is allow MapOverlay tigger it if this control is not created.
    this.map_ = map;
    setCopyrightInfo_(map);
}

/**
   * refresh copyright text 
   */
CopyrightControl.prototype.refresh = function() {
    setCopyrightInfo_(this.map_);
};

gmaps.ags = {
    SpatialReference: SpatialReference,
    Geographic: Geographic,
    LambertConformalConic: LambertConformalConic,
    SphereMercator: SphereMercator,
    TransverseMercator: TransverseMercator,
    SpatialRelationship: SpatialRelationship,
    GeometryType: GeometryType,
    SRUnit: SRUnit,
    Catalog: Catalog,
    MapService: MapService,
    Layer: Layer,
    GeocodeService: GeocodeService,
    GeometryService: GeometryService,
    GPService: GPService,
    GPTask: GPTask,
    RouteTask: RouteTask,
    Util: Util,
    Config: Config,
    Projection: Projection,
    TileLayer: TileLayer,
    MapOverlay: MapOverlay,
    MapType: MapType,
    CopyrightControl: CopyrightControl
};;
var CSP = CSP || {};

//override arcgislink defaults
var Config = {
    proxyUrl: "proxy.ashx",
    alwaysUseProxy: false
};

//TODO: add production link
function getMapServerURL() {

  var _env = document.location.hostname;

  //if ((_env.toLowerCase().indexOf("-dev") > -1) || (_env.toLowerCase() == "localhost") ) {
  //  return "https://vdotgisnfd.cov.virginia.gov/arcgis/rest/services/CSPD/ServiceRequests/MapServer";
  //}
  //else if (_env.toLowerCase().indexOf("-tst") > -1) {
  //  return "https://vdotgisnft.cov.virginia.gov/arcgis/rest/services/CSP/ServiceRequests/MapServer";
  //}
  //else if (_env.toLowerCase().indexOf("-uat") > -1) {
  //  return "https://vdotgisnfu.cov.virginia.gov/arcgis/rest/services/CSP/ServiceRequests/MapServer";
  //}
  //else {
  //  return "https://vdotgisnf.cov.virginia.gov/arcgis/rest/services/CSP/ServiceRequests/MapServer";
  //}

  if ((_env.toLowerCase().indexOf("-dev") > -1) || (_env.toLowerCase() == "localhost") ) {
    return "https://vdotgisuportal.vdot.virginia.gov/env/rest/services/MyVDOT_Dev/ServiceRequests/MapServer";
  }
  else if (_env.toLowerCase().indexOf("-tst") > -1) {
    return "https://vdotgisuportal.vdot.virginia.gov/env/rest/services/MyVDOT_Tst/ServiceRequests/MapServer";
  }
  else if (_env.toLowerCase().indexOf("-uat") > -1) {
    return "https://vdotgisportal.vdot.virginia.gov/env/rest/services/MyVDOT_Uat/ServiceRequests/MapServer";
  }
  else {
    return "https://vdotgisportal.vdot.virginia.gov/env/rest/services/MyVDOT/ServiceRequests/MapServer";
  }
}

//function getHostName() {

//        var _env = document.location.hostname;

//        if (_env.toLowerCase().indexOf("cspdev") > -1) {
//            return "cscmapdev.cov.virginia.gov";
//        }

//        if (_env.toLowerCase().indexOf("cspuat") > -1) {
//            return "cscmapuat.cov.virginia.gov";
//        }

//        if (_env.toLowerCase().indexOf("csptst") > -1) {
//            return "cscmaptst.cov.virginia.gov";
//        }

//        //Default link for now
//        //if (_env.toLowerCase().indexOf("cspdev") < 0) {
//            return "cscmap.cov.virginia.gov";
//        //}
//};

CSP.Map = function($) {
    var map;
    var marker;
    var mouseIsDown = false;
    var sr = {};
    var geocoder = null;
    var autocomplete = null;
    var serviceRequestLayer = null;
    var serviceRequestsUrl = null;
    var infoWindow = null;

    function extractGoogleAddressComponents(place) {
        var address_components = place.address_components;
        var components={}; 
        jQuery.each(address_components, function(k,v1) {jQuery.each(v1.types, function(k2, v2){components[v2]=v1.long_name});});

        return components;
    };

    function getGoogleLocationInfo() {

        var latLng = marker.getPosition();

        sr.latLng = latLng;

        if (geocoder == null) {
            geocoder = new google.maps.Geocoder();
        }

        geocoder.geocode({ 'latLng': latLng }, function (results, status) {
            if (status == google.maps.GeocoderStatus.OK) {
                if (results[0]) {

                    console.log(results[0]);

                    //map.setZoom(11);
                    //marker = new google.maps.Marker({
                    //    position: latlng,
                    //    map: map
                    //});

                    sr.googleData = extractGoogleAddressComponents(results[0]);
                    
                    publishLocationChanged();

                    //return results[0];

                    //infowindow.setContent(results[1].formatted_address);
                    //infowindow.open(map, marker);
                } else {
                    console.log('No google results found');
                }
            } else {
                console.log('Geocoder failed due to: ' + status);
            }
        });
    };

    function publishLocationChanged() {

        var latLng = sr.latLng;

        console.log("getting google info...");
        var g = sr.googleData;

        console.log(g);

        console.log("...google info received");

        var loc = g.locality || "";

        console.log(1);
        var pc = g.postal_code || "";
        var streetNumber = g.street_number || "";
        var street = g.route || "";
        var fullStreet = streetNumber + " " + street;


        console.log("publishing...");
        PubSub.publish("locationChanged", { lat: latLng.lat(), lon: latLng.lng(), address: fullStreet, city: loc, zip: pc });
        console.log("...success");
    };

    function positionMarker(latLng) {
        if (marker == null) {
            marker = new google.maps.Marker({
                position: latLng,
                draggable: true,
                map: map,
                title: 'Here is the issue'
            });

            google.maps.event.addListener(marker, 'mousedown', function (event) {
                mouseIsDown = true;
                console.log("mouse is down");
            });

            google.maps.event.addListener(marker, 'mouseup', function (event) {
                mouseIsDown = false;
                console.log("mouse is up");

                getGoogleLocationInfo();
            });
        } else {
            marker.setPosition(latLng);
        }


        // zoom the map if needed
        if (map.getZoom() < 17) {
            map.setZoom(17);
        }

        // Pan to the point if not visible
        var bounds = map.getBounds();
        if (!bounds.contains(latLng)) {
            map.panTo(latLng);
        }
    };

    function getLocation() {
        if (navigator.geolocation) {
            navigator.geolocation.getCurrentPosition(function (position) {
                var pos = new google.maps.LatLng(position.coords.latitude, position.coords.longitude);
                //var infowindow = new google.maps.InfoWindow({
                //    map: map,
                //    zoom: 14,
                //    position: pos,
                //    content: 'You are here'
                //});

                positionMarker(pos);
                getGoogleLocationInfo();

                map.setCenter(pos);
                map.setZoom(14);
            }, function () {
                handleNoGeolocation(true);
            });
        } else {
            // Browser doesn't support Geolocation
            handleNoGeolocation(false);
        }
        //initMap();
    };

    function handleNoGeolocation(errorFlag) {
        var content;
        if (errorFlag) {
            content = 'Error: The Geolocation service failed.';
        } else {
            content = 'Error: Your browser doesn\'t support geolocation.';
        }

        var options = {
            map: map,
            position: new google.maps.LatLng(60, 105),
            content: content
        };

        var infowindow = new google.maps.InfoWindow(options);
        map.setCenter(options.position);
    };

    
    function autocompletePlaceChanged() {
        var place = autocomplete.getPlace();
        if (!place.geometry) {
            return;
        }
        
        sr.googleData = extractGoogleAddressComponents(place);

        //HACK: There is a bug in Google's Place API where street numbers don't always get returned so work aroun it.
        // Google Defect https://code.google.com/p/gmaps-api-issues/issues/detail?id=6715
        if (sr.googleData.street_number === undefined) {
            var matches = $("#map-autocomplete").val().match(/^\d+/);
            if (matches) {
                sr.googleData.street_number = matches[0];
            }
        }

        // If the place has a geometry, then present it on a map.
        //we have NO lat/lng but we do have a viewport
        //we probably will never hit this
        if (place.geometry.viewport && place.geometry.location == null) {
            var bounds = new google.maps.LatLngBounds();
            //sr.latLng = place.geometry.getCenter();
            map.fitBounds(place.geometry.viewport);
            //positionMarker(place.geometry.viewport.getCenter());
        }
        //we have an actual lat/lng
        //looks like we will ALWAYS hit this now with Google
        else {
            sr.latLng = place.geometry.location;
            map.setCenter(place.geometry.location);
            map.setZoom(17);  // Why 17? Because it looks good.
            positionMarker(place.geometry.location);
            publishLocationChanged();
        }
        
    };

    //todo:  need to confirm this dropdown is visible
    function getCurrentProblemType() {
        var result = "";
        //alert(pt);

        /*
DeadAnimal             "Dead Animal"
Pothole                "Pothole"
BushesOrTallGrass      "Vegetation"
Debris                 "Debris"
DrainageProblem        "Drainage Problem"
Guardrails             "GR End Assessment"
PedestrianSignal       "Signal Problem, Other"
StopSign               "Sign"
YieldSign              "Sign"
TreesLimbs             "Tree/Limbs Down"

UnpavedRoadProblem  Depends on RoadRepairType
                NeedsGravel = "Need Gravel Added"
                NeedsGrading = "Road Needs To Be Graded"
                NeedsDustControl = "Dust Control"
                Other = "Other"

SignOrSignal depends on FixSignOrSignalType value
                StopSign, YieldSign = "Sign Down"
                Other, TrafficSignal, PedestrianSignal, it also depends on SignalProblem value.
                                NotDisplaying = "Signal Completely Out"
                                Stuck = "Signal Not Changing/Stuck"
                                BadTiming, Other = "Signal Problem, Other"
        */
        
        // TODO need to add weather problem types here
        var pt;
        switch ($('label.checked #ProblemTypeCategory').val()) {
            case "signOrSafety":
                pt = $('#SignOrSafetyProblemType').val();
                break;

            case "newSignsOrSafety":
                result = "Traffic Study Request";
                break;

            case "removal":
                pt = $('#RemovalProblemType').val();
                break;

            case "maintenance":
                pt = $('#MaintenanceProblemType').val();
                break;
        }

        switch (pt) {
            case "DeadAnimal":
                result = "Dead Animal";
                break;
            case "Pothole":
                result = "Pothole";
                break;
            case "BushesOrTallGrass":
                result = "Vegetation";
                break;
            case "Debris":
                result = "Debris";
                break;
            case "DrainageProblem":
                result = "Drainage Problem";
                break;
            case "Guardrails":
                result = "GR End Assessment";
                break;
            case "PedestrianSignal":
                result = "Signal Problem, Other";
                break;
            case "StopSign":
                // fall through to next
            case "YieldSign":
                result = "Sign";
                break;
            case "TreesLimbs":
                //var vl = jQuery('input[id=VegetationLocation]:checked').val();

                //if (vl == "InRoad") {
                //    result = "Low Hanging Tree Branches";
                //} else {
                //    result = "Tree/Limbs Down";
                //}
                result = "Tree/Limbs Down";
                break;
            case "UnpavedRoadProblem":
                var rrt = jQuery('input[id=RoadRepairType]:checked').val();

                switch (rrt) {
                    case "NeedsGravel":
                        result = "Need Gravel Added";
                        break;
                    case "NeedsGrading":
                        result = "Road Needs To Be Graded";
                        break;
                    case "NeedsDustControl":
                        result = "Dust Control";
                        break;
                    case "Other":
                        result = "Other";
                    break;
                }
                break;
            case "TrafficSignal":
                var fsst= jQuery('input[id=FixSignOrSignalType]:checked').val();
                var sp = jQuery('input[id=SignalProblem]:checked').val();

                switch (fsst) {
                    case "StopSign":
                        //fall through to next
                    case "YieldSign":
                        result = "Sign Down";
                        break;
                    default:
                        //Other, TrafficSignal, PedestrianSignal
                        //NotDisplaying = "Signal Completely Out"
                        //Stuck = "Signal Not Changing/Stuck"
                        //BadTiming, Other = "Signal Problem, Other"
                        switch (sp) {
                            case "NotDisplaying":
                                result = "Signal Completely Out";
                                break;
                            case "Stuck":
                                result = "Signal Not Changing/Stuck";
                                break;
                            default:
                                result = "Signal Problem, Other";
                                break;
                        }
                        break;
                }
                break;
        }

        console.log("problem type is now:" + result);

        return result;
    };

    //Bug causing "SR's of this type" to not appear related to css % based h/w. doing this programmatically to fix bug
    var setMapCanvasSize = function() {
        var heightFactor = .9;
        var widthFactor = 1;

        if (CSP.Main.IsMobileView()) {
            heightFactor = .72;
            widthFactor = .95;
        }

        var $mapFrame = $('.map-frame');
        $('#map-canvas').height($mapFrame.css('height').replace('px', '') * heightFactor).width($('.map-frame').width() * widthFactor);
    }

    function initMap() {
      //we will add a definition query based on the value that comes back from the function below
      //we also need to only show maintenance requests...
      var pt = getCurrentProblemType();

      //layer.definition = "STATE_NAME='Kansas' and POP2007>25000";
      var layerDef = "vdot_ProblemTypeName = '" + pt + "'";
      console.log(layerDef);

      var firstCall = false;

      if (map == null) {
          firstCall = true;

          console.log("map is null.  initializing");

          var mapOptions = {
              zoom: 7,
              center: new google.maps.LatLng(37.5429251, -79),
              mapTypeId: google.maps.MapTypeId.ROADMAP,
              draggableCursor: "default",
              streetViewControl: false,
              panControl: false,
              zoomControlOptions: {
                  style: google.maps.ZoomControlStyle.DEFAULT
              }
          };

          map = new google.maps.Map(document.getElementById('map-canvas'), mapOptions);

          // EIS: 9/22/14
          // Create Autocomplete

          // Add the autocomplete to the TOP_LEFT of the map
          var autocomplete_input = (document.getElementById('map-autocomplete'));
          map.controls[google.maps.ControlPosition.TOP_LEFT].push(autocomplete_input);

          // Bind the input to the Autocomplete widget
          autocomplete = new google.maps.places.Autocomplete(autocomplete_input);

          // Have the bounds property of the Autocomplete listen to the map
          autocomplete.bindTo('bounds', map);

          // prevent the enter key from submitting the form if th autocomplete has focus
          $("#map-autocomplete").on('keypress', function (e) {
              var keycode = (e.keyCode ? e.keyCode : e.which);
              if (keycode == "13") {
                  e.stopImmediatePropagation();
                  e.preventDefault();
              }
          });

          // bind our place_changed event
          google.maps.event.addListener(autocomplete, 'place_changed', autocompletePlaceChanged);


          // Add our dynamic service for service requests to the map

          // uncomment for dev
          //var serviceRequestUrl = 'http://' + location.hostname + '/arcgis/rest/services/CSC/ServiceRequests/MapServer';
          //serviceRequestsUrl = 'http://' + getHostName() + '/arcgis/rest/services/CSP/CSP_ServiceRequests/MapServer';
          serviceRequestsUrl = getMapServerURL();
          serviceRequestLayer = new gmaps.ags.MapOverlay(serviceRequestsUrl, {
              exportOptions: {
                  layerIds: [0],
                  layerOption: 'show'
                  //layerDefinitions: {
                  //    '3': layerDef
                  //}
              }
          });

          serviceRequestLayer.setMap(map);

          // bind our click event for identify on service requests
          // currently disabled intentionally
          //google.maps.event.addListener(map, 'click', function (event) {
          //    identify(event.latLng);
          //});

          // bind our click event for marker position and geocode
          google.maps.event.addListener(map, 'click', function (event) {
              positionMarker(event.latLng);
              getGoogleLocationInfo();
          });
      }

        //change def query
        var ms = serviceRequestLayer.getMapService();

        //TODO: more elegantly address this race condition
        window.setTimeout(function () {
            console.log("setting layer definition to " + layerDef);
            var layer = ms.getLayer(0);
            console.log("layer retrieved");
            if (layer == null || typeof layer === 'undefined') {
                return;
            }
            layer.definition = layerDef;
            serviceRequestLayer.refresh();
            console.log("layer Def application success");

            //this is necessary because something happening before this is 
            //causing the map frame to resize, this is the only place where i can reliably call
            //something to set a non percentage based height based on .map-frame
            setMapCanvasSize();
        }, firstCall ? 3000 : 1000);

    }

    var resize = function() {
        setMapCanvasSize();
        google.maps.event.trigger(map, "resize");
    }

    return {
        GetLocation: getLocation,
        InitMap: initMap,
        Resize: resize
    };

}(jQuery);;
// Generated by CoffeeScript 1.6.2
/*!
jQuery Waypoints - v2.0.5
Copyright (c) 2011-2014 Caleb Troughton
Licensed under the MIT license.
https://github.com/imakewebthings/jquery-waypoints/blob/master/licenses.txt
*/
(function(){var t=[].indexOf||function(t){for(var e=0,n=this.length;e<n;e++){if(e in this&&this[e]===t)return e}return-1},e=[].slice;(function(t,e){if(typeof define==="function"&&define.amd){return define("waypoints",["jquery"],function(n){return e(n,t)})}else{return e(t.jQuery,t)}})(window,function(n,r){var i,o,l,s,f,u,c,a,h,d,p,y,v,w,g,m;i=n(r);a=t.call(r,"ontouchstart")>=0;s={horizontal:{},vertical:{}};f=1;c={};u="waypoints-context-id";p="resize.waypoints";y="scroll.waypoints";v=1;w="waypoints-waypoint-ids";g="waypoint";m="waypoints";o=function(){function t(t){var e=this;this.$element=t;this.element=t[0];this.didResize=false;this.didScroll=false;this.id="context"+f++;this.oldScroll={x:t.scrollLeft(),y:t.scrollTop()};this.waypoints={horizontal:{},vertical:{}};this.element[u]=this.id;c[this.id]=this;t.bind(y,function(){var t;if(!(e.didScroll||a)){e.didScroll=true;t=function(){e.doScroll();return e.didScroll=false};return r.setTimeout(t,n[m].settings.scrollThrottle)}});t.bind(p,function(){var t;if(!e.didResize){e.didResize=true;t=function(){n[m]("refresh");return e.didResize=false};return r.setTimeout(t,n[m].settings.resizeThrottle)}})}t.prototype.doScroll=function(){var t,e=this;t={horizontal:{newScroll:this.$element.scrollLeft(),oldScroll:this.oldScroll.x,forward:"right",backward:"left"},vertical:{newScroll:this.$element.scrollTop(),oldScroll:this.oldScroll.y,forward:"down",backward:"up"}};if(a&&(!t.vertical.oldScroll||!t.vertical.newScroll)){n[m]("refresh")}n.each(t,function(t,r){var i,o,l;l=[];o=r.newScroll>r.oldScroll;i=o?r.forward:r.backward;n.each(e.waypoints[t],function(t,e){var n,i;if(r.oldScroll<(n=e.offset)&&n<=r.newScroll){return l.push(e)}else if(r.newScroll<(i=e.offset)&&i<=r.oldScroll){return l.push(e)}});l.sort(function(t,e){return t.offset-e.offset});if(!o){l.reverse()}return n.each(l,function(t,e){if(e.options.continuous||t===l.length-1){return e.trigger([i])}})});return this.oldScroll={x:t.horizontal.newScroll,y:t.vertical.newScroll}};t.prototype.refresh=function(){var t,e,r,i=this;r=n.isWindow(this.element);e=this.$element.offset();this.doScroll();t={horizontal:{contextOffset:r?0:e.left,contextScroll:r?0:this.oldScroll.x,contextDimension:this.$element.width(),oldScroll:this.oldScroll.x,forward:"right",backward:"left",offsetProp:"left"},vertical:{contextOffset:r?0:e.top,contextScroll:r?0:this.oldScroll.y,contextDimension:r?n[m]("viewportHeight"):this.$element.height(),oldScroll:this.oldScroll.y,forward:"down",backward:"up",offsetProp:"top"}};return n.each(t,function(t,e){return n.each(i.waypoints[t],function(t,r){var i,o,l,s,f;i=r.options.offset;l=r.offset;o=n.isWindow(r.element)?0:r.$element.offset()[e.offsetProp];if(n.isFunction(i)){i=i.apply(r.element)}else if(typeof i==="string"){i=parseFloat(i);if(r.options.offset.indexOf("%")>-1){i=Math.ceil(e.contextDimension*i/100)}}r.offset=o-e.contextOffset+e.contextScroll-i;if(r.options.onlyOnScroll&&l!=null||!r.enabled){return}if(l!==null&&l<(s=e.oldScroll)&&s<=r.offset){return r.trigger([e.backward])}else if(l!==null&&l>(f=e.oldScroll)&&f>=r.offset){return r.trigger([e.forward])}else if(l===null&&e.oldScroll>=r.offset){return r.trigger([e.forward])}})})};t.prototype.checkEmpty=function(){if(n.isEmptyObject(this.waypoints.horizontal)&&n.isEmptyObject(this.waypoints.vertical)){this.$element.unbind([p,y].join(" "));return delete c[this.id]}};return t}();l=function(){function t(t,e,r){var i,o;if(r.offset==="bottom-in-view"){r.offset=function(){var t;t=n[m]("viewportHeight");if(!n.isWindow(e.element)){t=e.$element.height()}return t-n(this).outerHeight()}}this.$element=t;this.element=t[0];this.axis=r.horizontal?"horizontal":"vertical";this.callback=r.handler;this.context=e;this.enabled=r.enabled;this.id="waypoints"+v++;this.offset=null;this.options=r;e.waypoints[this.axis][this.id]=this;s[this.axis][this.id]=this;i=(o=this.element[w])!=null?o:[];i.push(this.id);this.element[w]=i}t.prototype.trigger=function(t){if(!this.enabled){return}if(this.callback!=null){this.callback.apply(this.element,t)}if(this.options.triggerOnce){return this.destroy()}};t.prototype.disable=function(){return this.enabled=false};t.prototype.enable=function(){this.context.refresh();return this.enabled=true};t.prototype.destroy=function(){delete s[this.axis][this.id];delete this.context.waypoints[this.axis][this.id];return this.context.checkEmpty()};t.getWaypointsByElement=function(t){var e,r;r=t[w];if(!r){return[]}e=n.extend({},s.horizontal,s.vertical);return n.map(r,function(t){return e[t]})};return t}();d={init:function(t,e){var r;e=n.extend({},n.fn[g].defaults,e);if((r=e.handler)==null){e.handler=t}this.each(function(){var t,r,i,s;t=n(this);i=(s=e.context)!=null?s:n.fn[g].defaults.context;if(!n.isWindow(i)){i=t.closest(i)}i=n(i);r=c[i[0][u]];if(!r){r=new o(i)}return new l(t,r,e)});n[m]("refresh");return this},disable:function(){return d._invoke.call(this,"disable")},enable:function(){return d._invoke.call(this,"enable")},destroy:function(){return d._invoke.call(this,"destroy")},prev:function(t,e){return d._traverse.call(this,t,e,function(t,e,n){if(e>0){return t.push(n[e-1])}})},next:function(t,e){return d._traverse.call(this,t,e,function(t,e,n){if(e<n.length-1){return t.push(n[e+1])}})},_traverse:function(t,e,i){var o,l;if(t==null){t="vertical"}if(e==null){e=r}l=h.aggregate(e);o=[];this.each(function(){var e;e=n.inArray(this,l[t]);return i(o,e,l[t])});return this.pushStack(o)},_invoke:function(t){this.each(function(){var e;e=l.getWaypointsByElement(this);return n.each(e,function(e,n){n[t]();return true})});return this}};n.fn[g]=function(){var t,r;r=arguments[0],t=2<=arguments.length?e.call(arguments,1):[];if(d[r]){return d[r].apply(this,t)}else if(n.isFunction(r)){return d.init.apply(this,arguments)}else if(n.isPlainObject(r)){return d.init.apply(this,[null,r])}else if(!r){return n.error("jQuery Waypoints needs a callback function or handler option.")}else{return n.error("The "+r+" method does not exist in jQuery Waypoints.")}};n.fn[g].defaults={context:r,continuous:true,enabled:true,horizontal:false,offset:0,triggerOnce:false};h={refresh:function(){return n.each(c,function(t,e){return e.refresh()})},viewportHeight:function(){var t;return(t=r.innerHeight)!=null?t:i.height()},aggregate:function(t){var e,r,i;e=s;if(t){e=(i=c[n(t)[0][u]])!=null?i.waypoints:void 0}if(!e){return[]}r={horizontal:[],vertical:[]};n.each(r,function(t,i){n.each(e[t],function(t,e){return i.push(e)});i.sort(function(t,e){return t.offset-e.offset});r[t]=n.map(i,function(t){return t.element});return r[t]=n.unique(r[t])});return r},above:function(t){if(t==null){t=r}return h._filter(t,"vertical",function(t,e){return e.offset<=t.oldScroll.y})},below:function(t){if(t==null){t=r}return h._filter(t,"vertical",function(t,e){return e.offset>t.oldScroll.y})},left:function(t){if(t==null){t=r}return h._filter(t,"horizontal",function(t,e){return e.offset<=t.oldScroll.x})},right:function(t){if(t==null){t=r}return h._filter(t,"horizontal",function(t,e){return e.offset>t.oldScroll.x})},enable:function(){return h._invoke("enable")},disable:function(){return h._invoke("disable")},destroy:function(){return h._invoke("destroy")},extendFn:function(t,e){return d[t]=e},_invoke:function(t){var e;e=n.extend({},s.vertical,s.horizontal);return n.each(e,function(e,n){n[t]();return true})},_filter:function(t,e,r){var i,o;i=c[n(t)[0][u]];if(!i){return[]}o=[];n.each(i.waypoints[e],function(t,e){if(r(i,e)){return o.push(e)}});o.sort(function(t,e){return t.offset-e.offset});return n.map(o,function(t){return t.element})}};n[m]=function(){var t,n;n=arguments[0],t=2<=arguments.length?e.call(arguments,1):[];if(h[n]){return h[n].apply(null,t)}else{return h.aggregate.call(null,n)}};n[m].settings={resizeThrottle:100,scrollThrottle:30};return i.on("load.waypoints",function(){return n[m]("refresh")})})}).call(this);;
var CSP = CSP || {};

CSP.Modal = function ($) {

    CSP.modal = {
        init: initializeModal,
        open: openModal,
        close: closeModal
    };

    function sequence(beforeAction, action, afterAction, afterClose) {
        return function () {
            if (beforeAction) beforeAction();
            if (action) action();
            if (afterAction) afterAction();
            closeModal();
            if (afterClose) afterClose();
        }
    }

    /**
     * Valid Options:
     * id:              id on modal,
     * title:           Title on modal,
     * message:         Message in modal body,
     * hasActions:      Renders action buttons if true,
     * actionText1:      Text of the first action button,
     * actionText2:      Text of the second action button,
     * actionCallback1:  Callback function to execute when the first action button is clicked
     * actionCallback2:  Callback function to execute when the second action button is clicked
     * beforeActionCallback: Executed before Callback
     * afterActionCallback: Executed after Callback
     * afterCloseCallback: Executed after Callback
     * titleColor: Sets title text color
     * messageColor: Sets the body text color
     */

    function initializeModal(options) {
        if (options.id) $('.modal').attr("id", options.id);

        if (options.beforeOpen) options.beforeOpen();

        if (options.title) {
            $('.modal .modal-heading').text(options.title);

            if (options.titleColor) {
                $('.modal .modal-heading').css({ 'color': options.titleColor });
            }
        }

        if (options.message) {
            $('.modal .modal-message').text(options.message);

            if (options.messageColor) {
                $('.modal .modal-heading').css({ 'color': options.messageColor });
            }
        }


        if (options.hasActions) {
            if (options.actionText1) {
                $('.modal .modal-body .button.actionCallback1').text(options.actionText1);
                $('.modal .modal-body .button.actionCallback1').on('click', sequence(options.beforeActionCallback, options.actionCallback1, options.afterActionCallback, options.afterCloseCallback));
            }

            if (options.actionText2) {
                $('.modal .modal-body .button.actionCallback2').text(options.actionText2);
                $('.modal .modal-body .button.actionCallback2').on('click', sequence(options.beforeActionCallback, options.actionCallback2, options.afterActionCallback, options.afterCloseCallback));
            }
        }
        else {
            $('.modal .modal-body .button').hide();
            $('.modal .modal-body .button').off('click');
        }
    }

    function openModal() {      
        $('.modal').show();
        $('.modal .modal-body .button').on('click');
    }

    function closeModal() {
        $('.modal').hide();
        $('.modal .modal-body .button').off('click');
    }

    function handleActionClick(callback) {
        return function () {
            callback();
            closeModal();
        };
    }
}(jQuery);;
var CSP = CSP || {};

CSP.ServiceRequest = function($) {

    var user;
    var res;
    let observer; // Store the IntersectionObserver instance

    function Customer(id) {
        this.id = id;
        this.signedOn = id ? true : false;
    }

    /*
    ||  initializePageEvents() - Run when the page first loads; Wire up event listeners
    */
    function initialize(options) {
        $('#frmCreateSr input').on ("keypress", function(e) {
            var keycode = (e.keyCode ? e.keyCode : e.which);
            if (keycode == "13") {
                e.stopImmediatePropagation();
                e.preventDefault();
            }
        });
    
        var settings = $.extend({}, options);
        user = new Customer(settings.userId);
        res = settings.createClaimAction;

        if (settings.userId) {
            $('#divStatusUpdates').show();
            $('#aUpdateAccount').off('click.updateUser');
            $('#aUpdateAccount').on('click.updateUser', function() {
                $(window).on('focus.updateUser', function() {
                    var interval = window.setInterval(function() {
                            $.ajax({
                                url: "/IdentityProviderAccount/GetCustomer/" + settings.userId,
                                type: 'POST',
                                success: function(data) {
                                    $('#CustomerFirstName').val(data.firstName);
                                    $('#CustomerLastName').val(data.lastName);
                                    $('#CustomerDaytimePhoneNumber').val(data.daytimePhoneNumber);
                                    $('#CustomerDaytimePhoneNumberExtension').val(data.daytimePhoneNumberExtension);
                                    $('#CustomerHomePhoneNumber').val(data.homePhoneNumber);
                                    $('#CustomerHomePhoneNumberExtension').val(data.homePhoneNumberExtension);
                                    $('#CustomerEmailAddress').val(data.emailAddress);

                                    if (data.daytimePhoneNumber || data.homePhoneNumber) {
                                        $('[name="CallbackType"]').removeAttr('disabled');
                                        $('#divUpdateTypes .mute').hide();
                                    } else if (!data.daytimePhoneNumber && !data.homePhoneNumber) {
                                        $('[name="CallbackType"]').attr('disabled', 'disabled');
                                        $('#divUpdateTypes .mute').show();
                                    }

                                    if (data.emailAddressChangePending) {
                                        $('#email-change-pending').show();
                                    } else {
                                        $('#email-change-pending').hide();
                                    }
                                }
                            });
                            window.clearInterval(interval);
                        },
                        2000);
                });
            });
        }
        else {
            // may need to display status update options if user enters a valid email
            $('#CustomerEmailAddress').on('blur', function () {
                var email = $('#CustomerEmailAddress').val();
                if (email != null && email != "") {
                    $.ajax({
                        url: "/IdentityProviderAccount/EmailAddressIsValid?emailAddress=" + encodeURIComponent(email),
                        type: 'POST',
                        success: function (data) {
                            if (data == true) {
                                $('#divStatusUpdates').show();
                            }
                            else {
                                $('#divStatusUpdates').hide();
                            }
                        }
                    });
                }
            });
        }

        //subscribe to map event
        PubSub.subscribe("locationChanged", function (msg, data) {
            $('#Address,#City,#ZipCode').off('change');
            $("#Latitude").val(data.lat);
            $("#Longitude").val(data.lon);
            $("#Address").val(data.address).trigger('blur');
            $('#City').val(data.city).trigger('blur');
            $('#ZipCode').val(data.zip).trigger('blur');

            $('#Address,#City,#ZipCode').on('change', function() {
                $("#Latitude").val(null);
                $("#Longitude").val(null);
            });

        });

        // Initialize events depending on whether the browser window suits of that of a desktop or a mobile device
        if (!CSP.Main.IsMobileView()) {
            initializeDesktop();
        } else {
            initializeMobile();
        }

        $(window).on('resize', function() {
            if (!CSP.Main.IsMobileView()) {
                $('.map-frame').show();
                initializeDesktop();

                if (Modernizr.history) {
                    // A quick note:
                    // Yes, this causes form validation on resize, but it is by design:  we don't want someone to be able to
                    // scroll to the end of the wizard in mobile view and then switch to desktop view (we turn the sections into steps,
                    // instead of a long page) and bypass invalid sections.  If they do that we want them to get bumped back to the first
                    // section that is invalid.
                    var currentSection = history.state.section;
                    if (!$('#frmCreateSr').valid()) {
                        var firstInvalidSection = $('.input-validation-error').closest('main section').data('section');

                        if (firstInvalidSection < currentSection) {
                            currentSection = firstInvalidSection;
                        }
                    }
                    history.replaceState({ section: currentSection }, '', '#' + currentSection);
                    switchSection(currentSection);
                }

            } else {
                initializeMobile();
            }
            CSP.Map.Resize();
        });

        if (Modernizr.history) {
            history.replaceState({ section: 0 }, null);
        }

        if (window.File && window.FileReader) { // && window.FileList && window.Blob
            // Browser is fully supportive.
            var $fileUploadCopy = $('#FileUpload');

            $('body').on('change', '#FileUpload', function() {
                var $fileUpload = $('#FileUpload');
                if (!$fileUpload.val()) {
                    $fileUpload.replaceWith($fileUploadCopy.clone());
                }
            });

        } else {
            // Browser not supported. 
            $('div#file-upload').hide();
        }
        wireUpPageEvents();
    }

  /*
  ||   wireUpPageEvents() - Event listeners, tiggers, all that good stuff
  */
    function wireUpPageEvents() {

        $(document).ajaxStart(function() {
            $('#submitReq').prop('disabled', true);
            $('#ajax-loader').show();
        });

        $(document).ajaxComplete(function() {
            $('#submitReq').prop('disabled', false);
            $('#ajax-loader').hide();
        });

        CSP.modal.init({
            id: 'signOnModal',
            title: 'Sign In Or Register',
            message: 'Please sign in to your myVDOT account or Register in order to continue entering this claim.',
            hasActions: true,
            actionText1: 'Sign In',
            actionText2: 'Register',
            actionCallback1: function () {
                $('#loginForm').submit();
            },
            actionCallback2: function () {
                window.location.href = '/IdentityProviderAccount/';
            }
        });

         //Get the modal
        var modal = $('#signOnModal')[0];

        // Get the <span> element that closes the modal
        var span = document.getElementsByClassName("close")[0];

        // When the user clicks on the button, open the modal 
        //btn.onclick = function () {
        //    modal.style.display = "block";
        //}

        // When the user clicks on <span> (x), close the modal
        span.onclick = function () {
            modal.style.display = "none";
        }

        // When the user clicks anywhere outside of the modal, close it
        window.onclick = function (event) {
            if (event.target == modal) {
                modal.style.display = "none";
            }
        }

        $(document).on('click', '#submitReq', function(e) {

            e.preventDefault();
            var recaptchaFilled = true;
            // verify that recaptcha is visible
            if ($('#recaptchaReq').length > 0 && grecaptcha.getResponse() == "") {
                recaptchaFilled = false;
            }
            
            $('#recaptchaReq').toggle(!recaptchaFilled);

            var $form = $(this).closest('form');
            if ($form.valid() && recaptchaFilled) {
                $('#contact [disabled]').removeAttr('disabled');
                // disable the submit button upon successful validation to avoid double clicking turning into multiple submissions.
                $(this).prop('disabled', true);
                $(this).hide();
                var submittingButton = '<div id="submitting-spinner" class="tertiary button"><img src="img/ajax-loader.gif" />Submitting...</div>';
                $(this).parent().append(submittingButton);
                //$form.trigger('submit');
                // Ok normally I would do .form.submit() oir form.trigger('submit') but FIREFOX doesn't seem to like doing that for silly reasons so we have to actually
                // GIVE the browser a submit button to deal with!  I don't know.
                var $submit = $("<input type='submit' style='display:none' />");
                $(this).parent().append($submit);
                $submit.click();
            } else {
                var validationErrors = $('.input-validation-error:visible');
                if (validationErrors.length > 0) {
                    // scroll to the section the validation error is in, so that the person can see the complete thing.
                    validationErrors.first().closest('section').get(0).scrollIntoView();
                }

                // :hover state gets stuck for some reason. I have spent days trying to figure out why the iphone won't let go of :hover but here 
                // we go... delete the elemenet... append a new one.  *SIGH*
                var clone = $(this).clone();
                var parent = $(this).parent();
                $(this).remove();
                parent.append(clone);
            }
            return true;
        });

        $('input[type="file"]').on('change', function() {
            var fileSizeBox = $(this).siblings('span#fileSize');

            if ($(this).val().length > 0) {
                var fileSize = $(this).get(0).files[0].size / 1024;
                fileSize = Math.ceil(fileSize);
                fileSizeBox.text(fileSize + 'k');
            } else {
                fileSizeBox.text(' ');
            }
        });

        // Show the related followup questions when citizens choose a problem type
        $('.radio-group label').on('click', function(e) {
            e.preventDefault();
            var $this = $(this);
            var $radio = $this.find(':radio');
            var set = $radio.attr('name');
            var chosen = $radio.val();

            $radio.prop('checked', true);
            $radio.trigger('change'); // Manually trigger the change event because it won't trigger by programatic execution.

            // remove checked class from labels within this same family  
            $(':radio[name="' + set + '"]').closest('label').removeClass('checked');
            $this.addClass('checked');
            // we are checking labels, not the underlying radio input fields, so manually do it under the hood.
            $this.find(':radio').prop('checked', 'checked');

            var $moreDetails = $('.more-details');
            $moreDetails.find('div.detail-info').hide();

            //verify that in mobile view, the modal pops or we redirect when claim is selected from the radio buttons.
            if (chosen == "claim" && CSP.Main.IsMobileView()) {
                verifyUserSignedInBeforeRedirect(res);
            }

            var $chosenDetails = $moreDetails.find('div.detail-info[data-radio-value="' + chosen + '"]');
            if ($chosenDetails.length > 0) {
                $chosenDetails.show();
            }
            
        });

        $('#divProbTypes label').on('click', function (e) {
            var $this = $(this);
            var $radio = $this.find(':radio');
            var set = $radio.attr('name');
            var chosen = $radio.val();
            var $statusUpdates = $('.status-updates');
            var $weatherForm = $('#weather-form');

            if (chosen == 'weather') {
                $statusUpdates.hide();
                // this is necessary b/c mobile version shows all 'pages'
                $weatherForm.show();
            }
            else {
                $statusUpdates.show();
                $weatherForm.hide();
            }
            e.preventDefault();
        });

        // Show additional followup when citizens narrow down their problem types
        $('.more-details').on('change', 'select', function() {
            $(this).parent().find('div').hide();
            var parentCategory = $(this).closest('div').data('radio-value');
            var index = $(this).val();
            $('.maint-details div div').hide();
            $('.removal-details div div').hide();
            $('div[data-category="' + parentCategory + '"][data-subcategory="' + index + '"]').show();
        });

        $('.more-details').on('change', 'input#FixSignOrSignalType', function() {

            var shouldAskForProblem = ($(this).val() === "TrafficSignal" || $(this).val() === "PedestrianSignal");

            var trafficSignalWindow = $('#TrafficSignalProblem');
            if (shouldAskForProblem) {
                trafficSignalWindow.show();
            } else {
                trafficSignalWindow.hide();
            }
        });

        $('#CustomerEmailAddress').on('keyup', function() {
            var value = $(this).val();
            var $target = $('#email-repeat');

            if (value.length == 0) {
                $target.text('No email entered yet');
            } else {
                $target.text(value);
            }
        });


        $('#CreateAccountCheck').on('click', function() {
            var value = $(this).prop('checked');
            $('#CreateAccount').attr('value', value);

            // Re-trigger validation on the email field.  
            $('#CustomerEmailAddress').removeData("previousValue"); //clear cache when changing group
            $("#frmCreateSr").data('validator').element('#CustomerEmailAddress'); //retrigger remote call
        });

        $('input.expand-section').on('click', function() {
            // Find corresponding section
            var $target = $(this).closest('div').find('div.expand-target');
            if ($(this).is(':checked')) {
                $target.show();
            } else {
                $target.hide();
            }
        });

        $('.TogglesMapValidation').on('change', function() {
            var bypassMapValidation = $(this).data('bypass-map-validation');
            if (bypassMapValidation === "True") {
                $('#BypassMapValidation').val('True');
                $('#mapStep').addClass('no-validate-section');
            } else {
                $('#BypassMapValidation').val('');
                $('#mapStep').removeClass('no-validate-section');
            }
        });

        // Weather type stuff
      /*
        $('#frmCreateSr').on('change', '.triggerWeatherSpecificWords', function () {
            var $this = $(this);
            //in order to prevent a weird android bug where the select doesn't redraw, we need to hide the field and show it at the end to force the redraw
            $this.hide();
            var weatherTypeCategory = $('input[name="WeatherTypeCategory"]:checked').val();

            $moreDetails.find('div.detail-info').hide();
        });
      */

        // Problem type specific stuff
        $('#frmCreateSr').on('change', '.triggerProblemTypeSpecificWords', function() {
            var $this = $(this);
            //in order to prevent a weird android bug where the select doesn't redraw, we need to hide the field and show it at the end to force the redraw
            $this.hide();
            var problemTypeCategory = $('input[name="ProblemTypeCategory"]:checked').val();

            var problemTypeDropdownId = '';
            switch (problemTypeCategory) {
                case 'maintenance':
                    problemTypeDropdownId = 'MaintenanceProblemType';
                    break;    
                case 'removal':
                    problemTypeDropdownId = 'RemovalProblemType';
                    break;
                case 'signOrSafety':
                    problemTypeDropdownId = 'SignOrSafetyProblemType';
                    break;

                case 'question':
                    problemTypeDropdownId = 'NonMaintenanceQuestion';
                    break;

            }

            // Hide all the problem specific text first.
            $('div.problemTypeSpecificWords').hide();

            var problemType = null;
            if (problemTypeDropdownId != '') {
                //var isProblemTypeChange = $(this).prop('id') === problemTypeDropdownId;
                var $problemTypeDropdown = $('#' + problemTypeDropdownId);
                problemType = $problemTypeDropdown.val();
            }
            //if (problemTypeCategory === "bikepath" ||
            //    problemTypeCategory === "compliment" ||
            //    problemTypeCategory === "other" ||
            //    problemTypeCategory === "claim" ||
            //    problemTypeCategory === "newSignsOrSafety" ||
            //    ((problemTypeCategory === "maintenance") && (problemType === "Other")) ||
            //    ((problemTypeCategory === "removal") && (problemType === "Other")) ||
            //    ((problemTypeCategory === "signOrSafety") && (problemType === "Other"))) {
            //    $('#AdditionalDetailsRequired').val("True");
            //} else {
            //    $('#AdditionalDetailsRequired').val("False");
            //}
            //Make the additional details always required
            $('#AdditionalDetailsRequired').val("True");

            // Is this the category or the problem type that got changed?
            if (problemType) {

                //$problemTypeDropdown.trigger('change');
                // use both data attributes when trying to find something.
                $('.wizard-step').each(function() {
                    var $this = $(this);
                    var $problemTypeDivs = $this.find('div.problemTypeSpecificWords[data-category-value="' + problemTypeCategory + '"][data-problem-type="' + problemType + '"]');
                    if ($problemTypeDivs.length) {
                        $problemTypeDivs.show();
                        $this.find('div[data-category="' + problemTypeCategory + '"][data-subcategory="' + problemType + '"]').show();
                    } else {
                        $this.find('div.problemTypeSpecificWords[data-category-value="' + problemTypeCategory + '"]:not([data-problem-type])').show();
                    }
                });
            } else {
                $('div.problemTypeSpecificWords[data-category-value="' + problemTypeCategory + '"]:not([data-problem-type])').show();
            }

            $this.show();
        });
    }  // wireUpPageEvents

  /*
  ||  switchSection() - Transition between 'pages' of the create service request wizard.
  */
    function switchSection(sectNum) {
        if (!CSP.Main.IsMobileView()) {
            $('main section').hide().eq(sectNum).show();
            // There are 5 sections, but only 4 progress steps
            var progressStep = sectNum;
            if (progressStep > 0) progressStep -= 1; 
            $('.progress span').removeClass('current').eq(progressStep).addClass('current');
            if (sectNum == 2) {
                // I Don't want errors within the map to blow up stuff I am doing out here.
                try {
                    CSP.Map.InitMap();
                    CSP.Map.Resize();
                } catch (e) {
                    console.error(e);
                }
            }
        } else {
            $('html, body').animate({
                scrollTop: parseInt($('main section').eq(sectNum).offset().top)
            }, 1000);
        }
    }


  /*
  ||  InitializeDesktop() - If the browser resolution suits that of a desktop, wire up certain elements of the page.
  */
    var desktopInitialized = false;

    function initializeDesktop() {

        $('.section-nav').show();

        if (!desktopInitialized) {
            var sectionCt = $('main section').length;
            $('#actions').hide();

            // Paginate the sections on non-mobile views
            $('main section').each(function(index, element) {
                var html = '';
                if (index == 0) {
                    html = '<a href="#" id="continue' + index + '" data-section="' + (index + 1) + '" class="continue secondary button">Continue</a>';
                    html = '<div class="section-nav">' + html + '</div>';
                } else if (index == sectionCt - 2) {
                    html = '<a href="#" data-section="' + (index - 1) + '" class="reverse tertiary button">Go back</a> <input type="submit" id="submitReq" class="primary button" value="Submit" />';
                    html = '<div class="section-nav submit">' + html + '</div>';
                } else {
                    html = '<a href="#" data-section="' + (index - 1) + '" class="reverse tertiary button">Go back</a> <a href="#" id="continue' + index + '" data-section="' + (index + 1) + '" class="continue secondary button">Continue</a>';
                    html = '<div class="section-nav">' + html + '</div>';
                }
                $(this).append(html);
            });


            $('.continue').on('click', function (event) {
                var $frm = $('#frmCreateSr');
                if ($frm.valid()) {
                    var section = $(this).data('section');

                    var checkedLabel = $('label.checked #ProblemTypeCategory').val();
                    if (section == 1) {

                        switch (checkedLabel) {
                            case "claim":
                                verifyUserSignedInBeforeRedirect(res);
                                break;
                            default:
                                if (checkedLabel != "weather") section++; //skip past
                                break;
                        }
                    }

                    if (checkedLabel != "claim") {
                        switchSection(section);
                        pushState({ section: section }, '', "#" + section);
                        $('html, body').animate({
                            scrollTop: $frm.offset().top
                        }, 500);
                    } 
                }
                event.preventDefault();
            });

            $('.reverse').on('click', function(event) {
                // Be sure to update the history before we move.
                var section = $(this).data('section');
                if (section == 1 && $('label.checked #ProblemTypeCategory').val() != "weather") section--; // skip past

                switchSection(section);
                pushState({ section: section }, '', "#" + section);

                $('html, body').animate({
                    scrollTop: $("#frmCreateSr").offset().top
                }, 500);
                event.preventDefault();
            });

            $('#pinpoint').on('click', function(event) {
                CSP.Map.GetLocation();
                event.preventDefault();
            });

            window.onpopstate = function(event) {
                if (event.state != null && event.state.section != null) {
                    // Swith section without any validation.
                    switchSection(event.state.section);
                }
            };
            desktopInitialized = true;
        }

    }

    var mobileInitialized = false;

    /*
    ||  initializeMobile() - If the browser resolution suits that of a mobile device, wire up certain elements of the page.
    */
    /*
    function initializeMobile() {
        $('.wizard-step').show();
        $('.section-nav').hide();
        if (!mobileInitialized) {

            $('#pinpointMobile').on('click', function(event) {
                $('.map-frame').show();
                CSP.Map.Resize();
                CSP.Map.GetLocation();
                event.preventDefault();
            });
            $('#closeMap').on('click', function(event) {
                $('.map-frame').hide();
                event.preventDefault();
            });

            // I don't want Map errors blowing up my code.
            try {
                CSP.Map.InitMap();
                //CSP.Map.Resize();
            } catch (e) {
                console.error(e);
            }

            $('main section').each(function(index, element) {
                $(this).data('section', index);
            }).waypoint(function(direction) {
                if (CSP.Main.IsMobileView()) {
                    if (Modernizr.history) {
                        if (!$('.map-frame').is(':visible')) {
                            var section = $(this).data('section');
                            history.replaceState({ section: section }, '', "#" + section);
                        }
                    }
                }
            });
            mobileInitialized = true;
        }
    }
    */

    function initializeMobile() {
        $('.wizard-step').show();
        $('.section-nav').hide();

        if (!mobileInitialized) {
            $('#pinpointMobile').on('click', function (event) {
                $('.map-frame').show();
                CSP.Map.Resize();
                CSP.Map.GetLocation();
                event.preventDefault();
            });

            $('#closeMap').on('click', function (event) {
                $('.map-frame').hide();
                event.preventDefault();
            });

            try {
                CSP.Map.InitMap();
            } catch (e) {
                console.error(e);
            }

            // Setup Intersection Observer
            const observerOptions = {
                root: null, // Use viewport as root
                rootMargin: '0px',
                threshold: 0.5 // Trigger when 50% of the element is visible
            };

            observer = new IntersectionObserver((entries) => {
                entries.forEach(entry => {
                    if (entry.isIntersecting && CSP.Main.IsMobileView()) {
                        if (Modernizr.history && !$('.map-frame').is(':visible')) {
                            const section = $(entry.target).data('section');
                            history.replaceState({ section: section }, '', "#" + section);
                        }
                    }
                });
            }, observerOptions);

            // Observe all sections
            $('main section').each(function (index, element) {
                $(element).data('section', index);
                observer.observe(element);
            });

            mobileInitialized = true;
        }
    }

    // Cleanup function to disconnect observer when needed
    function cleanup() {
        if (observer) {
            observer.disconnect();
        }
    }

    var pushState = function(state, title, url) {
        if (Modernizr.history) {
            if (state.section == null || state.section != history.state.section) {
                history.pushState(state, title, url);
            }
        }
    };

    var verifyUserSignedInBeforeRedirect = function (redirectUrl) {
        if (user.signedOn) {
            window.location.href = redirectUrl;
        }
        else {
            $('#signOnModal').toggle();
        }
    }

    return {
        Initialize: initialize,
        Cleanup: cleanup
    };

}(jQuery);;
