// OpenLayers 3. See http://openlayers.org/
// License: https://raw.githubusercontent.com/openlayers/ol3/master/LICENSE.md
// Version: v3.13.1

(function (root, factory) {
  if (typeof exports === "object") {
    module.exports = factory();
  } else if (typeof define === "function" && define.amd) {
    define([], factory);
  } else {
    root.ol = factory();
  }
}(this, function () {
  var OPENLAYERS = {};
  var goog = this.goog = {};
this.CLOSURE_NO_DEPS = true;
// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Bootstrap for the Google JS Library (Closure).
 *
 * In uncompiled mode base.js will write out Closure's deps file, unless the
 * global <code>CLOSURE_NO_DEPS</code> is set to true.  This allows projects to
 * include their own deps file(s) from different locations.
 *
 * @author arv@google.com (Erik Arvidsson)
 *
 * @provideGoog
 */


/**
 * @define {boolean} Overridden to true by the compiler when
 *     --process_closure_primitives is specified.
 */
var COMPILED = false;


/**
 * Base namespace for the Closure library.  Checks to see goog is already
 * defined in the current scope before assigning to prevent clobbering if
 * base.js is loaded more than once.
 *
 * @const
 */
var goog = goog || {};


/**
 * Reference to the global context.  In most cases this will be 'window'.
 */
goog.global = this;


/**
 * A hook for overriding the define values in uncompiled mode.
 *
 * In uncompiled mode, {@code CLOSURE_UNCOMPILED_DEFINES} may be defined before
 * loading base.js.  If a key is defined in {@code CLOSURE_UNCOMPILED_DEFINES},
 * {@code goog.define} will use the value instead of the default value.  This
 * allows flags to be overwritten without compilation (this is normally
 * accomplished with the compiler's "define" flag).
 *
 * Example:
 * <pre>
 *   var CLOSURE_UNCOMPILED_DEFINES = {'goog.DEBUG': false};
 * </pre>
 *
 * @type {Object<string, (string|number|boolean)>|undefined}
 */
goog.global.CLOSURE_UNCOMPILED_DEFINES;


/**
 * A hook for overriding the define values in uncompiled or compiled mode,
 * like CLOSURE_UNCOMPILED_DEFINES but effective in compiled code.  In
 * uncompiled code CLOSURE_UNCOMPILED_DEFINES takes precedence.
 *
 * Also unlike CLOSURE_UNCOMPILED_DEFINES the values must be number, boolean or
 * string literals or the compiler will emit an error.
 *
 * While any @define value may be set, only those set with goog.define will be
 * effective for uncompiled code.
 *
 * Example:
 * <pre>
 *   var CLOSURE_DEFINES = {'goog.DEBUG': false} ;
 * </pre>
 *
 * @type {Object<string, (string|number|boolean)>|undefined}
 */
goog.global.CLOSURE_DEFINES;


/**
 * Returns true if the specified value is not undefined.
 * WARNING: Do not use this to test if an object has a property. Use the in
 * operator instead.
 *
 * @param {?} val Variable to test.
 * @return {boolean} Whether variable is defined.
 */
goog.isDef = function(val) {
  // void 0 always evaluates to undefined and hence we do not need to depend on
  // the definition of the global variable named 'undefined'.
  return val !== void 0;
};


/**
 * Builds an object structure for the provided namespace path, ensuring that
 * names that already exist are not overwritten. For example:
 * "a.b.c" -> a = {};a.b={};a.b.c={};
 * Used by goog.provide and goog.exportSymbol.
 * @param {string} name name of the object that this file defines.
 * @param {*=} opt_object the object to expose at the end of the path.
 * @param {Object=} opt_objectToExportTo The object to add the path to; default
 *     is |goog.global|.
 * @private
 */
goog.exportPath_ = function(name, opt_object, opt_objectToExportTo) {
  var parts = name.split('.');
  var cur = opt_objectToExportTo || goog.global;

  // Internet Explorer exhibits strange behavior when throwing errors from
  // methods externed in this manner.  See the testExportSymbolExceptions in
  // base_test.html for an example.
  if (!(parts[0] in cur) && cur.execScript) {
    cur.execScript('var ' + parts[0]);
  }

  // Certain browsers cannot parse code in the form for((a in b); c;);
  // This pattern is produced by the JSCompiler when it collapses the
  // statement above into the conditional loop below. To prevent this from
  // happening, use a for-loop and reserve the init logic as below.

  // Parentheses added to eliminate strict JS warning in Firefox.
  for (var part; parts.length && (part = parts.shift());) {
    if (!parts.length && goog.isDef(opt_object)) {
      // last part and we have an object; use it
      cur[part] = opt_object;
    } else if (cur[part]) {
      cur = cur[part];
    } else {
      cur = cur[part] = {};
    }
  }
};


/**
 * Defines a named value. In uncompiled mode, the value is retrieved from
 * CLOSURE_DEFINES or CLOSURE_UNCOMPILED_DEFINES if the object is defined and
 * has the property specified, and otherwise used the defined defaultValue.
 * When compiled the default can be overridden using the compiler
 * options or the value set in the CLOSURE_DEFINES object.
 *
 * @param {string} name The distinguished name to provide.
 * @param {string|number|boolean} defaultValue
 */
goog.define = function(name, defaultValue) {
  var value = defaultValue;
  if (!COMPILED) {
    if (goog.global.CLOSURE_UNCOMPILED_DEFINES &&
        Object.prototype.hasOwnProperty.call(
            goog.global.CLOSURE_UNCOMPILED_DEFINES, name)) {
      value = goog.global.CLOSURE_UNCOMPILED_DEFINES[name];
    } else if (goog.global.CLOSURE_DEFINES &&
        Object.prototype.hasOwnProperty.call(
            goog.global.CLOSURE_DEFINES, name)) {
      value = goog.global.CLOSURE_DEFINES[name];
    }
  }
  goog.exportPath_(name, value);
};


/**
 * @define {boolean} DEBUG is provided as a convenience so that debugging code
 * that should not be included in a production js_binary can be easily stripped
 * by specifying --define goog.DEBUG=false to the JSCompiler. For example, most
 * toString() methods should be declared inside an "if (goog.DEBUG)" conditional
 * because they are generally used for debugging purposes and it is difficult
 * for the JSCompiler to statically determine whether they are used.
 */
goog.define('goog.DEBUG', true);


/**
 * @define {string} LOCALE defines the locale being used for compilation. It is
 * used to select locale specific data to be compiled in js binary. BUILD rule
 * can specify this value by "--define goog.LOCALE=<locale_name>" as JSCompiler
 * option.
 *
 * Take into account that the locale code format is important. You should use
 * the canonical Unicode format with hyphen as a delimiter. Language must be
 * lowercase, Language Script - Capitalized, Region - UPPERCASE.
 * There are few examples: pt-BR, en, en-US, sr-Latin-BO, zh-Hans-CN.
 *
 * See more info about locale codes here:
 * http://www.unicode.org/reports/tr35/#Unicode_Language_and_Locale_Identifiers
 *
 * For language codes you should use values defined by ISO 693-1. See it here
 * http://www.w3.org/WAI/ER/IG/ert/iso639.htm. There is only one exception from
 * this rule: the Hebrew language. For legacy reasons the old code (iw) should
 * be used instead of the new code (he), see http://wiki/Main/IIISynonyms.
 */
goog.define('goog.LOCALE', 'en');  // default to en


/**
 * @define {boolean} Whether this code is running on trusted sites.
 *
 * On untrusted sites, several native functions can be defined or overridden by
 * external libraries like Prototype, Datejs, and JQuery and setting this flag
 * to false forces closure to use its own implementations when possible.
 *
 * If your JavaScript can be loaded by a third party site and you are wary about
 * relying on non-standard implementations, specify
 * "--define goog.TRUSTED_SITE=false" to the JSCompiler.
 */
goog.define('goog.TRUSTED_SITE', true);


/**
 * @define {boolean} Whether a project is expected to be running in strict mode.
 *
 * This define can be used to trigger alternate implementations compatible with
 * running in EcmaScript Strict mode or warn about unavailable functionality.
 * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions_and_function_scope/Strict_mode
 *
 */
goog.define('goog.STRICT_MODE_COMPATIBLE', false);


/**
 * @define {boolean} Whether code that calls {@link goog.setTestOnly} should
 *     be disallowed in the compilation unit.
 */
goog.define('goog.DISALLOW_TEST_ONLY_CODE', COMPILED && !goog.DEBUG);


/**
 * @define {boolean} Whether to use a Chrome app CSP-compliant method for
 *     loading scripts via goog.require. @see appendScriptSrcNode_.
 */
goog.define('goog.ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING', false);


/**
 * Defines a namespace in Closure.
 *
 * A namespace may only be defined once in a codebase. It may be defined using
 * goog.provide() or goog.module().
 *
 * The presence of one or more goog.provide() calls in a file indicates
 * that the file defines the given objects/namespaces.
 * Provided symbols must not be null or undefined.
 *
 * In addition, goog.provide() creates the object stubs for a namespace
 * (for example, goog.provide("goog.foo.bar") will create the object
 * goog.foo.bar if it does not already exist).
 *
 * Build tools also scan for provide/require/module statements
 * to discern dependencies, build dependency files (see deps.js), etc.
 *
 * @see goog.require
 * @see goog.module
 * @param {string} name Namespace provided by this file in the form
 *     "goog.package.part".
 */
goog.provide = function(name) {
  if (!COMPILED) {
    // Ensure that the same namespace isn't provided twice.
    // A goog.module/goog.provide maps a goog.require to a specific file
    if (goog.isProvided_(name)) {
      throw Error('Namespace "' + name + '" already declared.');
    }
  }

  goog.constructNamespace_(name);
};


/**
 * @param {string} name Namespace provided by this file in the form
 *     "goog.package.part".
 * @param {Object=} opt_obj The object to embed in the namespace.
 * @private
 */
goog.constructNamespace_ = function(name, opt_obj) {
  if (!COMPILED) {
    delete goog.implicitNamespaces_[name];

    var namespace = name;
    while ((namespace = namespace.substring(0, namespace.lastIndexOf('.')))) {
      if (goog.getObjectByName(namespace)) {
        break;
      }
      goog.implicitNamespaces_[namespace] = true;
    }
  }

  goog.exportPath_(name, opt_obj);
};


/**
 * Module identifier validation regexp.
 * Note: This is a conservative check, it is very possible to be more lenient,
 *   the primary exclusion here is "/" and "\" and a leading ".", these
 *   restrictions are intended to leave the door open for using goog.require
 *   with relative file paths rather than module identifiers.
 * @private
 */
goog.VALID_MODULE_RE_ = /^[a-zA-Z_$][a-zA-Z0-9._$]*$/;


/**
 * Defines a module in Closure.
 *
 * Marks that this file must be loaded as a module and claims the namespace.
 *
 * A namespace may only be defined once in a codebase. It may be defined using
 * goog.provide() or goog.module().
 *
 * goog.module() has three requirements:
 * - goog.module may not be used in the same file as goog.provide.
 * - goog.module must be the first statement in the file.
 * - only one goog.module is allowed per file.
 *
 * When a goog.module annotated file is loaded, it is enclosed in
 * a strict function closure. This means that:
 * - any variables declared in a goog.module file are private to the file
 * (not global), though the compiler is expected to inline the module.
 * - The code must obey all the rules of "strict" JavaScript.
 * - the file will be marked as "use strict"
 *
 * NOTE: unlike goog.provide, goog.module does not declare any symbols by
 * itself. If declared symbols are desired, use
 * goog.module.declareLegacyNamespace().
 *
 *
 * See the public goog.module proposal: http://goo.gl/Va1hin
 *
 * @param {string} name Namespace provided by this file in the form
 *     "goog.package.part", is expected but not required.
 */
goog.module = function(name) {
  if (!goog.isString(name) ||
      !name ||
      name.search(goog.VALID_MODULE_RE_) == -1) {
    throw Error('Invalid module identifier');
  }
  if (!goog.isInModuleLoader_()) {
    throw Error('Module ' + name + ' has been loaded incorrectly.');
  }
  if (goog.moduleLoaderState_.moduleName) {
    throw Error('goog.module may only be called once per module.');
  }

  // Store the module name for the loader.
  goog.moduleLoaderState_.moduleName = name;
  if (!COMPILED) {
    // Ensure that the same namespace isn't provided twice.
    // A goog.module/goog.provide maps a goog.require to a specific file
    if (goog.isProvided_(name)) {
      throw Error('Namespace "' + name + '" already declared.');
    }
    delete goog.implicitNamespaces_[name];
  }
};


/**
 * @param {string} name The module identifier.
 * @return {?} The module exports for an already loaded module or null.
 *
 * Note: This is not an alternative to goog.require, it does not
 * indicate a hard dependency, instead it is used to indicate
 * an optional dependency or to access the exports of a module
 * that has already been loaded.
 * @suppress {missingProvide}
 */
goog.module.get = function(name) {
  return goog.module.getInternal_(name);
};


/**
 * @param {string} name The module identifier.
 * @return {?} The module exports for an already loaded module or null.
 * @private
 */
goog.module.getInternal_ = function(name) {
  if (!COMPILED) {
    if (goog.isProvided_(name)) {
      // goog.require only return a value with-in goog.module files.
      return name in goog.loadedModules_ ?
          goog.loadedModules_[name] :
          goog.getObjectByName(name);
    } else {
      return null;
    }
  }
};


/**
 * @private {?{moduleName: (string|undefined)}}
 */
goog.moduleLoaderState_ = null;


/**
 * @private
 * @return {boolean} Whether a goog.module is currently being initialized.
 */
goog.isInModuleLoader_ = function() {
  return goog.moduleLoaderState_ != null;
};


/**
 * Provide the module's exports as a globally accessible object under the
 * module's declared name.  This is intended to ease migration to goog.module
 * for files that have existing usages.
 * @suppress {missingProvide}
 */
goog.module.declareLegacyNamespace = function() {
  if (!COMPILED && !goog.isInModuleLoader_()) {
    throw new Error('goog.module.declareLegacyNamespace must be called from ' +
        'within a goog.module');
  }
  if (!COMPILED && !goog.moduleLoaderState_.moduleName) {
    throw Error('goog.module must be called prior to ' +
        'goog.module.declareLegacyNamespace.');
  }
  goog.moduleLoaderState_.declareLegacyNamespace = true;
};


/**
 * Marks that the current file should only be used for testing, and never for
 * live code in production.
 *
 * In the case of unit tests, the message may optionally be an exact namespace
 * for the test (e.g. 'goog.stringTest'). The linter will then ignore the extra
 * provide (if not explicitly defined in the code).
 *
 * @param {string=} opt_message Optional message to add to the error that's
 *     raised when used in production code.
 */
goog.setTestOnly = function(opt_message) {
  if (goog.DISALLOW_TEST_ONLY_CODE) {
    opt_message = opt_message || '';
    throw Error('Importing test-only code into non-debug environment' +
                (opt_message ? ': ' + opt_message : '.'));
  }
};


/**
 * Forward declares a symbol. This is an indication to the compiler that the
 * symbol may be used in the source yet is not required and may not be provided
 * in compilation.
 *
 * The most common usage of forward declaration is code that takes a type as a
 * function parameter but does not need to require it. By forward declaring
 * instead of requiring, no hard dependency is made, and (if not required
 * elsewhere) the namespace may never be required and thus, not be pulled
 * into the JavaScript binary. If it is required elsewhere, it will be type
 * checked as normal.
 *
 *
 * @param {string} name The namespace to forward declare in the form of
 *     "goog.package.part".
 */
goog.forwardDeclare = function(name) {};


/**
 * Forward declare type information. Used to assign types to goog.global
 * referenced object that would otherwise result in unknown type references
 * and thus block property disambiguation.
 */
goog.forwardDeclare('Document');
goog.forwardDeclare('XMLHttpRequest');


if (!COMPILED) {

  /**
   * Check if the given name has been goog.provided. This will return false for
   * names that are available only as implicit namespaces.
   * @param {string} name name of the object to look for.
   * @return {boolean} Whether the name has been provided.
   * @private
   */
  goog.isProvided_ = function(name) {
    return (name in goog.loadedModules_) ||
        (!goog.implicitNamespaces_[name] &&
            goog.isDefAndNotNull(goog.getObjectByName(name)));
  };

  /**
   * Namespaces implicitly defined by goog.provide. For example,
   * goog.provide('goog.events.Event') implicitly declares that 'goog' and
   * 'goog.events' must be namespaces.
   *
   * @type {!Object<string, (boolean|undefined)>}
   * @private
   */
  goog.implicitNamespaces_ = {'goog.module': true};

  // NOTE: We add goog.module as an implicit namespace as goog.module is defined
  // here and because the existing module package has not been moved yet out of
  // the goog.module namespace. This satisifies both the debug loader and
  // ahead-of-time dependency management.
}


/**
 * Returns an object based on its fully qualified external name.  The object
 * is not found if null or undefined.  If you are using a compilation pass that
 * renames property names beware that using this function will not find renamed
 * properties.
 *
 * @param {string} name The fully qualified name.
 * @param {Object=} opt_obj The object within which to look; default is
 *     |goog.global|.
 * @return {?} The value (object or primitive) or, if not found, null.
 */
goog.getObjectByName = function(name, opt_obj) {
  var parts = name.split('.');
  var cur = opt_obj || goog.global;
  for (var part; part = parts.shift(); ) {
    if (goog.isDefAndNotNull(cur[part])) {
      cur = cur[part];
    } else {
      return null;
    }
  }
  return cur;
};


/**
 * Globalizes a whole namespace, such as goog or goog.lang.
 *
 * @param {!Object} obj The namespace to globalize.
 * @param {Object=} opt_global The object to add the properties to.
 * @deprecated Properties may be explicitly exported to the global scope, but
 *     this should no longer be done in bulk.
 */
goog.globalize = function(obj, opt_global) {
  var global = opt_global || goog.global;
  for (var x in obj) {
    global[x] = obj[x];
  }
};


/**
 * Adds a dependency from a file to the files it requires.
 * @param {string} relPath The path to the js file.
 * @param {!Array<string>} provides An array of strings with
 *     the names of the objects this file provides.
 * @param {!Array<string>} requires An array of strings with
 *     the names of the objects this file requires.
 * @param {boolean=} opt_isModule Whether this dependency must be loaded as
 *     a module as declared by goog.module.
 */
goog.addDependency = function(relPath, provides, requires, opt_isModule) {
  if (goog.DEPENDENCIES_ENABLED) {
    var provide, require;
    var path = relPath.replace(/\\/g, '/');
    var deps = goog.dependencies_;
    for (var i = 0; provide = provides[i]; i++) {
      deps.nameToPath[provide] = path;
      deps.pathIsModule[path] = !!opt_isModule;
    }
    for (var j = 0; require = requires[j]; j++) {
      if (!(path in deps.requires)) {
        deps.requires[path] = {};
      }
      deps.requires[path][require] = true;
    }
  }
};




// NOTE(nnaze): The debug DOM loader was included in base.js as an original way
// to do "debug-mode" development.  The dependency system can sometimes be
// confusing, as can the debug DOM loader's asynchronous nature.
//
// With the DOM loader, a call to goog.require() is not blocking -- the script
// will not load until some point after the current script.  If a namespace is
// needed at runtime, it needs to be defined in a previous script, or loaded via
// require() with its registered dependencies.
// User-defined namespaces may need their own deps file.  See http://go/js_deps,
// http://go/genjsdeps, or, externally, DepsWriter.
// https://developers.google.com/closure/library/docs/depswriter
//
// Because of legacy clients, the DOM loader can't be easily removed from
// base.js.  Work is being done to make it disableable or replaceable for
// different environments (DOM-less JavaScript interpreters like Rhino or V8,
// for example). See bootstrap/ for more information.


/**
 * @define {boolean} Whether to enable the debug loader.
 *
 * If enabled, a call to goog.require() will attempt to load the namespace by
 * appending a script tag to the DOM (if the namespace has been registered).
 *
 * If disabled, goog.require() will simply assert that the namespace has been
 * provided (and depend on the fact that some outside tool correctly ordered
 * the script).
 */
goog.define('goog.ENABLE_DEBUG_LOADER', true);


/**
 * @param {string} msg
 * @private
 */
goog.logToConsole_ = function(msg) {
  if (goog.global.console) {
    goog.global.console['error'](msg);
  }
};


/**
 * Implements a system for the dynamic resolution of dependencies that works in
 * parallel with the BUILD system. Note that all calls to goog.require will be
 * stripped by the JSCompiler when the --process_closure_primitives option is
 * used.
 * @see goog.provide
 * @param {string} name Namespace to include (as was given in goog.provide()) in
 *     the form "goog.package.part".
 * @return {?} If called within a goog.module file, the associated namespace or
 *     module otherwise null.
 */
goog.require = function(name) {
  // If the object already exists we do not need do do anything.
  if (!COMPILED) {
    if (goog.ENABLE_DEBUG_LOADER && goog.IS_OLD_IE_) {
      goog.maybeProcessDeferredDep_(name);
    }

    if (goog.isProvided_(name)) {
      if (goog.isInModuleLoader_()) {
        return goog.module.getInternal_(name);
      } else {
        return null;
      }
    }

    if (goog.ENABLE_DEBUG_LOADER) {
      var path = goog.getPathFromDeps_(name);
      if (path) {
        goog.writeScripts_(path);
        return null;
      }
    }

    var errorMessage = 'goog.require could not find: ' + name;
    goog.logToConsole_(errorMessage);

    throw Error(errorMessage);
  }
};


/**
 * Path for included scripts.
 * @type {string}
 */
goog.basePath = '';


/**
 * A hook for overriding the base path.
 * @type {string|undefined}
 */
goog.global.CLOSURE_BASE_PATH;


/**
 * Whether to write out Closure's deps file. By default, the deps are written.
 * @type {boolean|undefined}
 */
goog.global.CLOSURE_NO_DEPS;


/**
 * A function to import a single script. This is meant to be overridden when
 * Closure is being run in non-HTML contexts, such as web workers. It's defined
 * in the global scope so that it can be set before base.js is loaded, which
 * allows deps.js to be imported properly.
 *
 * The function is passed the script source, which is a relative URI. It should
 * return true if the script was imported, false otherwise.
 * @type {(function(string): boolean)|undefined}
 */
goog.global.CLOSURE_IMPORT_SCRIPT;


/**
 * Null function used for default values of callbacks, etc.
 * @return {void} Nothing.
 */
goog.nullFunction = function() {};


/**
 * When defining a class Foo with an abstract method bar(), you can do:
 * Foo.prototype.bar = goog.abstractMethod
 *
 * Now if a subclass of Foo fails to override bar(), an error will be thrown
 * when bar() is invoked.
 *
 * Note: This does not take the name of the function to override as an argument
 * because that would make it more difficult to obfuscate our JavaScript code.
 *
 * @type {!Function}
 * @throws {Error} when invoked to indicate the method should be overridden.
 */
goog.abstractMethod = function() {
  throw Error('unimplemented abstract method');
};


/**
 * Adds a {@code getInstance} static method that always returns the same
 * instance object.
 * @param {!Function} ctor The constructor for the class to add the static
 *     method to.
 */
goog.addSingletonGetter = function(ctor) {
  ctor.getInstance = function() {
    if (ctor.instance_) {
      return ctor.instance_;
    }
    if (goog.DEBUG) {
      // NOTE: JSCompiler can't optimize away Array#push.
      goog.instantiatedSingletons_[goog.instantiatedSingletons_.length] = ctor;
    }
    return ctor.instance_ = new ctor;
  };
};


/**
 * All singleton classes that have been instantiated, for testing. Don't read
 * it directly, use the {@code goog.testing.singleton} module. The compiler
 * removes this variable if unused.
 * @type {!Array<!Function>}
 * @private
 */
goog.instantiatedSingletons_ = [];


/**
 * @define {boolean} Whether to load goog.modules using {@code eval} when using
 * the debug loader.  This provides a better debugging experience as the
 * source is unmodified and can be edited using Chrome Workspaces or similar.
 * However in some environments the use of {@code eval} is banned
 * so we provide an alternative.
 */
goog.define('goog.LOAD_MODULE_USING_EVAL', true);


/**
 * @define {boolean} Whether the exports of goog.modules should be sealed when
 * possible.
 */
goog.define('goog.SEAL_MODULE_EXPORTS', goog.DEBUG);


/**
 * The registry of initialized modules:
 * the module identifier to module exports map.
 * @private @const {!Object<string, ?>}
 */
goog.loadedModules_ = {};


/**
 * True if goog.dependencies_ is available.
 * @const {boolean}
 */
goog.DEPENDENCIES_ENABLED = !COMPILED && goog.ENABLE_DEBUG_LOADER;


if (goog.DEPENDENCIES_ENABLED) {

  /**
   * This object is used to keep track of dependencies and other data that is
   * used for loading scripts.
   * @private
   * @type {{
   *   pathIsModule: !Object<string, boolean>,
   *   nameToPath: !Object<string, string>,
   *   requires: !Object<string, !Object<string, boolean>>,
   *   visited: !Object<string, boolean>,
   *   written: !Object<string, boolean>,
   *   deferred: !Object<string, string>
   * }}
   */
  goog.dependencies_ = {
    pathIsModule: {}, // 1 to 1

    nameToPath: {}, // 1 to 1

    requires: {}, // 1 to many

    // Used when resolving dependencies to prevent us from visiting file twice.
    visited: {},

    written: {}, // Used to keep track of script files we have written.

    deferred: {} // Used to track deferred module evaluations in old IEs
  };


  /**
   * Tries to detect whether is in the context of an HTML document.
   * @return {boolean} True if it looks like HTML document.
   * @private
   */
  goog.inHtmlDocument_ = function() {
    /** @type {Document} */
    var doc = goog.global.document;
    return typeof doc != 'undefined' &&
           'write' in doc;  // XULDocument misses write.
  };


  /**
   * Tries to detect the base path of base.js script that bootstraps Closure.
   * @private
   */
  goog.findBasePath_ = function() {
    if (goog.isDef(goog.global.CLOSURE_BASE_PATH)) {
      goog.basePath = goog.global.CLOSURE_BASE_PATH;
      return;
    } else if (!goog.inHtmlDocument_()) {
      return;
    }
    /** @type {Document} */
    var doc = goog.global.document;
    var scripts = doc.getElementsByTagName('SCRIPT');
    // Search backwards since the current script is in almost all cases the one
    // that has base.js.
    for (var i = scripts.length - 1; i >= 0; --i) {
      var script = /** @type {!HTMLScriptElement} */ (scripts[i]);
      var src = script.src;
      var qmark = src.lastIndexOf('?');
      var l = qmark == -1 ? src.length : qmark;
      if (src.substr(l - 7, 7) == 'base.js') {
        goog.basePath = src.substr(0, l - 7);
        return;
      }
    }
  };


  /**
   * Imports a script if, and only if, that script hasn't already been imported.
   * (Must be called at execution time)
   * @param {string} src Script source.
   * @param {string=} opt_sourceText The optionally source text to evaluate
   * @private
   */
  goog.importScript_ = function(src, opt_sourceText) {
    var importScript = goog.global.CLOSURE_IMPORT_SCRIPT ||
        goog.writeScriptTag_;
    if (importScript(src, opt_sourceText)) {
      goog.dependencies_.written[src] = true;
    }
  };


  /** @const @private {boolean} */
  goog.IS_OLD_IE_ = !!(!goog.global.atob && goog.global.document &&
      goog.global.document.all);


  /**
   * Given a URL initiate retrieval and execution of the module.
   * @param {string} src Script source URL.
   * @private
   */
  goog.importModule_ = function(src) {
    // In an attempt to keep browsers from timing out loading scripts using
    // synchronous XHRs, put each load in its own script block.
    var bootstrap = 'goog.retrieveAndExecModule_("' + src + '");';

    if (goog.importScript_('', bootstrap)) {
      goog.dependencies_.written[src] = true;
    }
  };


  /** @private {!Array<string>} */
  goog.queuedModules_ = [];


  /**
   * Return an appropriate module text. Suitable to insert into
   * a script tag (that is unescaped).
   * @param {string} srcUrl
   * @param {string} scriptText
   * @return {string}
   * @private
   */
  goog.wrapModule_ = function(srcUrl, scriptText) {
    if (!goog.LOAD_MODULE_USING_EVAL || !goog.isDef(goog.global.JSON)) {
      return '' +
          'goog.loadModule(function(exports) {' +
          '"use strict";' +
          scriptText +
          '\n' + // terminate any trailing single line comment.
          ';return exports' +
          '});' +
          '\n//# sourceURL=' + srcUrl + '\n';
    } else {
      return '' +
          'goog.loadModule(' +
          goog.global.JSON.stringify(
              scriptText + '\n//# sourceURL=' + srcUrl + '\n') +
          ');';
    }
  };

  // On IE9 and earlier, it is necessary to handle
  // deferred module loads. In later browsers, the
  // code to be evaluated is simply inserted as a script
  // block in the correct order. To eval deferred
  // code at the right time, we piggy back on goog.require to call
  // goog.maybeProcessDeferredDep_.
  //
  // The goog.requires are used both to bootstrap
  // the loading process (when no deps are available) and
  // declare that they should be available.
  //
  // Here we eval the sources, if all the deps are available
  // either already eval'd or goog.require'd.  This will
  // be the case when all the dependencies have already
  // been loaded, and the dependent module is loaded.
  //
  // But this alone isn't sufficient because it is also
  // necessary to handle the case where there is no root
  // that is not deferred.  For that there we register for an event
  // and trigger goog.loadQueuedModules_ handle any remaining deferred
  // evaluations.

  /**
   * Handle any remaining deferred goog.module evals.
   * @private
   */
  goog.loadQueuedModules_ = function() {
    var count = goog.queuedModules_.length;
    if (count > 0) {
      var queue = goog.queuedModules_;
      goog.queuedModules_ = [];
      for (var i = 0; i < count; i++) {
        var path = queue[i];
        goog.maybeProcessDeferredPath_(path);
      }
    }
  };


  /**
   * Eval the named module if its dependencies are
   * available.
   * @param {string} name The module to load.
   * @private
   */
  goog.maybeProcessDeferredDep_ = function(name) {
    if (goog.isDeferredModule_(name) &&
        goog.allDepsAreAvailable_(name)) {
      var path = goog.getPathFromDeps_(name);
      goog.maybeProcessDeferredPath_(goog.basePath + path);
    }
  };

  /**
   * @param {string} name The module to check.
   * @return {boolean} Whether the name represents a
   *     module whose evaluation has been deferred.
   * @private
   */
  goog.isDeferredModule_ = function(name) {
    var path = goog.getPathFromDeps_(name);
    if (path && goog.dependencies_.pathIsModule[path]) {
      var abspath = goog.basePath + path;
      return (abspath) in goog.dependencies_.deferred;
    }
    return false;
  };

  /**
   * @param {string} name The module to check.
   * @return {boolean} Whether the name represents a
   *     module whose declared dependencies have all been loaded
   *     (eval'd or a deferred module load)
   * @private
   */
  goog.allDepsAreAvailable_ = function(name) {
    var path = goog.getPathFromDeps_(name);
    if (path && (path in goog.dependencies_.requires)) {
      for (var requireName in goog.dependencies_.requires[path]) {
        if (!goog.isProvided_(requireName) &&
            !goog.isDeferredModule_(requireName)) {
          return false;
        }
      }
    }
    return true;
  };


  /**
   * @param {string} abspath
   * @private
   */
  goog.maybeProcessDeferredPath_ = function(abspath) {
    if (abspath in goog.dependencies_.deferred) {
      var src = goog.dependencies_.deferred[abspath];
      delete goog.dependencies_.deferred[abspath];
      goog.globalEval(src);
    }
  };


  /**
   * @param {function(?):?|string} moduleDef The module definition.
   */
  goog.loadModule = function(moduleDef) {
    // NOTE: we allow function definitions to be either in the from
    // of a string to eval (which keeps the original source intact) or
    // in a eval forbidden environment (CSP) we allow a function definition
    // which in its body must call {@code goog.module}, and return the exports
    // of the module.
    var previousState = goog.moduleLoaderState_;
    try {
      goog.moduleLoaderState_ = {moduleName: undefined};
      var exports;
      if (goog.isFunction(moduleDef)) {
        exports = moduleDef.call(goog.global, {});
      } else if (goog.isString(moduleDef)) {
        exports = goog.loadModuleFromSource_.call(goog.global, moduleDef);
      } else {
        throw Error('Invalid module definition');
      }

      var moduleName = goog.moduleLoaderState_.moduleName;
      if (!goog.isString(moduleName) || !moduleName) {
        throw Error('Invalid module name \"' + moduleName + '\"');
      }

      // Don't seal legacy namespaces as they may be uses as a parent of
      // another namespace
      if (goog.moduleLoaderState_.declareLegacyNamespace) {
        goog.constructNamespace_(moduleName, exports);
      } else if (goog.SEAL_MODULE_EXPORTS && Object.seal) {
        Object.seal(exports);
      }

      goog.loadedModules_[moduleName] = exports;
    } finally {
      goog.moduleLoaderState_ = previousState;
    }
  };


  /**
   * @private @const {function(string):?}
   * @suppress {newCheckTypes}
   */
  goog.loadModuleFromSource_ = function() {
    // NOTE: we avoid declaring parameters or local variables here to avoid
    // masking globals or leaking values into the module definition.
    'use strict';
    var exports = {};
    eval(arguments[0]);
    return exports;
  };


  /**
   * Writes a new script pointing to {@code src} directly into the DOM.
   *
   * NOTE: This method is not CSP-compliant. @see goog.appendScriptSrcNode_ for
   * the fallback mechanism.
   *
   * @param {string} src The script URL.
   * @private
   */
  goog.writeScriptSrcNode_ = function(src) {
    goog.global.document.write(
        '<script type="text/javascript" src="' + src + '"></' + 'script>');
  };


  /**
   * Appends a new script node to the DOM using a CSP-compliant mechanism. This
   * method exists as a fallback for document.write (which is not allowed in a
   * strict CSP context, e.g., Chrome apps).
   *
   * NOTE: This method is not analogous to using document.write to insert a
   * <script> tag; specifically, the user agent will execute a script added by
   * document.write immediately after the current script block finishes
   * executing, whereas the DOM-appended script node will not be executed until
   * the entire document is parsed and executed. That is to say, this script is
   * added to the end of the script execution queue.
   *
   * The page must not attempt to call goog.required entities until after the
   * document has loaded, e.g., in or after the window.onload callback.
   *
   * @param {string} src The script URL.
   * @private
   */
  goog.appendScriptSrcNode_ = function(src) {
    /** @type {Document} */
    var doc = goog.global.document;
    var scriptEl = doc.createElement('script');
    scriptEl.type = 'text/javascript';
    scriptEl.src = src;
    scriptEl.defer = false;
    scriptEl.async = false;
    doc.head.appendChild(scriptEl);
  };


  /**
   * The default implementation of the import function. Writes a script tag to
   * import the script.
   *
   * @param {string} src The script url.
   * @param {string=} opt_sourceText The optionally source text to evaluate
   * @return {boolean} True if the script was imported, false otherwise.
   * @private
   */
  goog.writeScriptTag_ = function(src, opt_sourceText) {
    if (goog.inHtmlDocument_()) {
      /** @type {Document} */
      var doc = goog.global.document;

      // If the user tries to require a new symbol after document load,
      // something has gone terribly wrong. Doing a document.write would
      // wipe out the page. This does not apply to the CSP-compliant method
      // of writing script tags.
      if (!goog.ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING &&
          doc.readyState == 'complete') {
        // Certain test frameworks load base.js multiple times, which tries
        // to write deps.js each time. If that happens, just fail silently.
        // These frameworks wipe the page between each load of base.js, so this
        // is OK.
        var isDeps = /\bdeps.js$/.test(src);
        if (isDeps) {
          return false;
        } else {
          throw Error('Cannot write "' + src + '" after document load');
        }
      }

      var isOldIE = goog.IS_OLD_IE_;

      if (opt_sourceText === undefined) {
        if (!isOldIE) {
          if (goog.ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING) {
            goog.appendScriptSrcNode_(src);
          } else {
            goog.writeScriptSrcNode_(src);
          }
        } else {
          var state = " onreadystatechange='goog.onScriptLoad_(this, " +
              ++goog.lastNonModuleScriptIndex_ + ")' ";
          doc.write(
              '<script type="text/javascript" src="' +
                  src + '"' + state + '></' + 'script>');
        }
      } else {
        doc.write(
            '<script type="text/javascript">' +
            opt_sourceText +
            '</' + 'script>');
      }
      return true;
    } else {
      return false;
    }
  };


  /** @private {number} */
  goog.lastNonModuleScriptIndex_ = 0;


  /**
   * A readystatechange handler for legacy IE
   * @param {!HTMLScriptElement} script
   * @param {number} scriptIndex
   * @return {boolean}
   * @private
   */
  goog.onScriptLoad_ = function(script, scriptIndex) {
    // for now load the modules when we reach the last script,
    // later allow more inter-mingling.
    if (script.readyState == 'complete' &&
        goog.lastNonModuleScriptIndex_ == scriptIndex) {
      goog.loadQueuedModules_();
    }
    return true;
  };

  /**
   * Resolves dependencies based on the dependencies added using addDependency
   * and calls importScript_ in the correct order.
   * @param {string} pathToLoad The path from which to start discovering
   *     dependencies.
   * @private
   */
  goog.writeScripts_ = function(pathToLoad) {
    /** @type {!Array<string>} The scripts we need to write this time. */
    var scripts = [];
    var seenScript = {};
    var deps = goog.dependencies_;

    /** @param {string} path */
    function visitNode(path) {
      if (path in deps.written) {
        return;
      }

      // We have already visited this one. We can get here if we have cyclic
      // dependencies.
      if (path in deps.visited) {
        return;
      }

      deps.visited[path] = true;

      if (path in deps.requires) {
        for (var requireName in deps.requires[path]) {
          // If the required name is defined, we assume that it was already
          // bootstrapped by other means.
          if (!goog.isProvided_(requireName)) {
            if (requireName in deps.nameToPath) {
              visitNode(deps.nameToPath[requireName]);
            } else {
              throw Error('Undefined nameToPath for ' + requireName);
            }
          }
        }
      }

      if (!(path in seenScript)) {
        seenScript[path] = true;
        scripts.push(path);
      }
    }

    visitNode(pathToLoad);

    // record that we are going to load all these scripts.
    for (var i = 0; i < scripts.length; i++) {
      var path = scripts[i];
      goog.dependencies_.written[path] = true;
    }

    // If a module is loaded synchronously then we need to
    // clear the current inModuleLoader value, and restore it when we are
    // done loading the current "requires".
    var moduleState = goog.moduleLoaderState_;
    goog.moduleLoaderState_ = null;

    for (var i = 0; i < scripts.length; i++) {
      var path = scripts[i];
      if (path) {
        if (!deps.pathIsModule[path]) {
          goog.importScript_(goog.basePath + path);
        } else {
          goog.importModule_(goog.basePath + path);
        }
      } else {
        goog.moduleLoaderState_ = moduleState;
        throw Error('Undefined script input');
      }
    }

    // restore the current "module loading state"
    goog.moduleLoaderState_ = moduleState;
  };


  /**
   * Looks at the dependency rules and tries to determine the script file that
   * fulfills a particular rule.
   * @param {string} rule In the form goog.namespace.Class or project.script.
   * @return {?string} Url corresponding to the rule, or null.
   * @private
   */
  goog.getPathFromDeps_ = function(rule) {
    if (rule in goog.dependencies_.nameToPath) {
      return goog.dependencies_.nameToPath[rule];
    } else {
      return null;
    }
  };

  goog.findBasePath_();

  // Allow projects to manage the deps files themselves.
  if (!goog.global.CLOSURE_NO_DEPS) {
    goog.importScript_(goog.basePath + 'deps.js');
  }
}


/**
 * Normalize a file path by removing redundant ".." and extraneous "." file
 * path components.
 * @param {string} path
 * @return {string}
 * @private
 */
goog.normalizePath_ = function(path) {
  var components = path.split('/');
  var i = 0;
  while (i < components.length) {
    if (components[i] == '.') {
      components.splice(i, 1);
    } else if (i && components[i] == '..' &&
        components[i - 1] && components[i - 1] != '..') {
      components.splice(--i, 2);
    } else {
      i++;
    }
  }
  return components.join('/');
};


/**
 * Loads file by synchronous XHR. Should not be used in production environments.
 * @param {string} src Source URL.
 * @return {string} File contents.
 * @private
 */
goog.loadFileSync_ = function(src) {
  if (goog.global.CLOSURE_LOAD_FILE_SYNC) {
    return goog.global.CLOSURE_LOAD_FILE_SYNC(src);
  } else {
    /** @type {XMLHttpRequest} */
    var xhr = new goog.global['XMLHttpRequest']();
    xhr.open('get', src, false);
    xhr.send();
    return xhr.responseText;
  }
};


/**
 * Retrieve and execute a module.
 * @param {string} src Script source URL.
 * @private
 */
goog.retrieveAndExecModule_ = function(src) {
  if (!COMPILED) {
    // The full but non-canonicalized URL for later use.
    var originalPath = src;
    // Canonicalize the path, removing any /./ or /../ since Chrome's debugging
    // console doesn't auto-canonicalize XHR loads as it does <script> srcs.
    src = goog.normalizePath_(src);

    var importScript = goog.global.CLOSURE_IMPORT_SCRIPT ||
        goog.writeScriptTag_;

    var scriptText = goog.loadFileSync_(src);

    if (scriptText != null) {
      var execModuleScript = goog.wrapModule_(src, scriptText);
      var isOldIE = goog.IS_OLD_IE_;
      if (isOldIE) {
        goog.dependencies_.deferred[originalPath] = execModuleScript;
        goog.queuedModules_.push(originalPath);
      } else {
        importScript(src, execModuleScript);
      }
    } else {
      throw new Error('load of ' + src + 'failed');
    }
  }
};


//==============================================================================
// Language Enhancements
//==============================================================================


/**
 * This is a "fixed" version of the typeof operator.  It differs from the typeof
 * operator in such a way that null returns 'null' and arrays return 'array'.
 * @param {*} value The value to get the type of.
 * @return {string} The name of the type.
 */
goog.typeOf = function(value) {
  var s = typeof value;
  if (s == 'object') {
    if (value) {
      // Check these first, so we can avoid calling Object.prototype.toString if
      // possible.
      //
      // IE improperly marshals tyepof across execution contexts, but a
      // cross-context object will still return false for "instanceof Object".
      if (value instanceof Array) {
        return 'array';
      } else if (value instanceof Object) {
        return s;
      }

      // HACK: In order to use an Object prototype method on the arbitrary
      //   value, the compiler requires the value be cast to type Object,
      //   even though the ECMA spec explicitly allows it.
      var className = Object.prototype.toString.call(
          /** @type {Object} */ (value));
      // In Firefox 3.6, attempting to access iframe window objects' length
      // property throws an NS_ERROR_FAILURE, so we need to special-case it
      // here.
      if (className == '[object Window]') {
        return 'object';
      }

      // We cannot always use constructor == Array or instanceof Array because
      // different frames have different Array objects. In IE6, if the iframe
      // where the array was created is destroyed, the array loses its
      // prototype. Then dereferencing val.splice here throws an exception, so
      // we can't use goog.isFunction. Calling typeof directly returns 'unknown'
      // so that will work. In this case, this function will return false and
      // most array functions will still work because the array is still
      // array-like (supports length and []) even though it has lost its
      // prototype.
      // Mark Miller noticed that Object.prototype.toString
      // allows access to the unforgeable [[Class]] property.
      //  15.2.4.2 Object.prototype.toString ( )
      //  When the toString method is called, the following steps are taken:
      //      1. Get the [[Class]] property of this object.
      //      2. Compute a string value by concatenating the three strings
      //         "[object ", Result(1), and "]".
      //      3. Return Result(2).
      // and this behavior survives the destruction of the execution context.
      if ((className == '[object Array]' ||
           // In IE all non value types are wrapped as objects across window
           // boundaries (not iframe though) so we have to do object detection
           // for this edge case.
           typeof value.length == 'number' &&
           typeof value.splice != 'undefined' &&
           typeof value.propertyIsEnumerable != 'undefined' &&
           !value.propertyIsEnumerable('splice')

          )) {
        return 'array';
      }
      // HACK: There is still an array case that fails.
      //     function ArrayImpostor() {}
      //     ArrayImpostor.prototype = [];
      //     var impostor = new ArrayImpostor;
      // this can be fixed by getting rid of the fast path
      // (value instanceof Array) and solely relying on
      // (value && Object.prototype.toString.vall(value) === '[object Array]')
      // but that would require many more function calls and is not warranted
      // unless closure code is receiving objects from untrusted sources.

      // IE in cross-window calls does not correctly marshal the function type
      // (it appears just as an object) so we cannot use just typeof val ==
      // 'function'. However, if the object has a call property, it is a
      // function.
      if ((className == '[object Function]' ||
          typeof value.call != 'undefined' &&
          typeof value.propertyIsEnumerable != 'undefined' &&
          !value.propertyIsEnumerable('call'))) {
        return 'function';
      }

    } else {
      return 'null';
    }

  } else if (s == 'function' && typeof value.call == 'undefined') {
    // In Safari typeof nodeList returns 'function', and on Firefox typeof
    // behaves similarly for HTML{Applet,Embed,Object}, Elements and RegExps. We
    // would like to return object for those and we can detect an invalid
    // function by making sure that the function object has a call method.
    return 'object';
  }
  return s;
};


/**
 * Returns true if the specified value is null.
 * @param {?} val Variable to test.
 * @return {boolean} Whether variable is null.
 */
goog.isNull = function(val) {
  return val === null;
};


/**
 * Returns true if the specified value is defined and not null.
 * @param {?} val Variable to test.
 * @return {boolean} Whether variable is defined and not null.
 */
goog.isDefAndNotNull = function(val) {
  // Note that undefined == null.
  return val != null;
};


/**
 * Returns true if the specified value is an array.
 * @param {?} val Variable to test.
 * @return {boolean} Whether variable is an array.
 */
goog.isArray = function(val) {
  return goog.typeOf(val) == 'array';
};


/**
 * Returns true if the object looks like an array. To qualify as array like
 * the value needs to be either a NodeList or an object with a Number length
 * property. As a special case, a function value is not array like, because its
 * length property is fixed to correspond to the number of expected arguments.
 * @param {?} val Variable to test.
 * @return {boolean} Whether variable is an array.
 */
goog.isArrayLike = function(val) {
  var type = goog.typeOf(val);
  // We do not use goog.isObject here in order to exclude function values.
  return type == 'array' || type == 'object' && typeof val.length == 'number';
};


/**
 * Returns true if the object looks like a Date. To qualify as Date-like the
 * value needs to be an object and have a getFullYear() function.
 * @param {?} val Variable to test.
 * @return {boolean} Whether variable is a like a Date.
 */
goog.isDateLike = function(val) {
  return goog.isObject(val) && typeof val.getFullYear == 'function';
};


/**
 * Returns true if the specified value is a string.
 * @param {?} val Variable to test.
 * @return {boolean} Whether variable is a string.
 */
goog.isString = function(val) {
  return typeof val == 'string';
};


/**
 * Returns true if the specified value is a boolean.
 * @param {?} val Variable to test.
 * @return {boolean} Whether variable is boolean.
 */
goog.isBoolean = function(val) {
  return typeof val == 'boolean';
};


/**
 * Returns true if the specified value is a number.
 * @param {?} val Variable to test.
 * @return {boolean} Whether variable is a number.
 */
goog.isNumber = function(val) {
  return typeof val == 'number';
};


/**
 * Returns true if the specified value is a function.
 * @param {?} val Variable to test.
 * @return {boolean} Whether variable is a function.
 */
goog.isFunction = function(val) {
  return goog.typeOf(val) == 'function';
};


/**
 * Returns true if the specified value is an object.  This includes arrays and
 * functions.
 * @param {?} val Variable to test.
 * @return {boolean} Whether variable is an object.
 */
goog.isObject = function(val) {
  var type = typeof val;
  return type == 'object' && val != null || type == 'function';
  // return Object(val) === val also works, but is slower, especially if val is
  // not an object.
};


/**
 * Gets a unique ID for an object. This mutates the object so that further calls
 * with the same object as a parameter returns the same value. The unique ID is
 * guaranteed to be unique across the current session amongst objects that are
 * passed into {@code getUid}. There is no guarantee that the ID is unique or
 * consistent across sessions. It is unsafe to generate unique ID for function
 * prototypes.
 *
 * @param {Object} obj The object to get the unique ID for.
 * @return {number} The unique ID for the object.
 */
goog.getUid = function(obj) {
  // TODO(arv): Make the type stricter, do not accept null.

  // In Opera window.hasOwnProperty exists but always returns false so we avoid
  // using it. As a consequence the unique ID generated for BaseClass.prototype
  // and SubClass.prototype will be the same.
  return obj[goog.UID_PROPERTY_] ||
      (obj[goog.UID_PROPERTY_] = ++goog.uidCounter_);
};


/**
 * Whether the given object is already assigned a unique ID.
 *
 * This does not modify the object.
 *
 * @param {!Object} obj The object to check.
 * @return {boolean} Whether there is an assigned unique id for the object.
 */
goog.hasUid = function(obj) {
  return !!obj[goog.UID_PROPERTY_];
};


/**
 * Removes the unique ID from an object. This is useful if the object was
 * previously mutated using {@code goog.getUid} in which case the mutation is
 * undone.
 * @param {Object} obj The object to remove the unique ID field from.
 */
goog.removeUid = function(obj) {
  // TODO(arv): Make the type stricter, do not accept null.

  // In IE, DOM nodes are not instances of Object and throw an exception if we
  // try to delete.  Instead we try to use removeAttribute.
  if ('removeAttribute' in obj) {
    obj.removeAttribute(goog.UID_PROPERTY_);
  }
  /** @preserveTry */
  try {
    delete obj[goog.UID_PROPERTY_];
  } catch (ex) {
  }
};


/**
 * Name for unique ID property. Initialized in a way to help avoid collisions
 * with other closure JavaScript on the same page.
 * @type {string}
 * @private
 */
goog.UID_PROPERTY_ = 'closure_uid_' + ((Math.random() * 1e9) >>> 0);


/**
 * Counter for UID.
 * @type {number}
 * @private
 */
goog.uidCounter_ = 0;


/**
 * Adds a hash code field to an object. The hash code is unique for the
 * given object.
 * @param {Object} obj The object to get the hash code for.
 * @return {number} The hash code for the object.
 * @deprecated Use goog.getUid instead.
 */
goog.getHashCode = goog.getUid;


/**
 * Removes the hash code field from an object.
 * @param {Object} obj The object to remove the field from.
 * @deprecated Use goog.removeUid instead.
 */
goog.removeHashCode = goog.removeUid;


/**
 * Clones a value. The input may be an Object, Array, or basic type. Objects and
 * arrays will be cloned recursively.
 *
 * WARNINGS:
 * <code>goog.cloneObject</code> does not detect reference loops. Objects that
 * refer to themselves will cause infinite recursion.
 *
 * <code>goog.cloneObject</code> is unaware of unique identifiers, and copies
 * UIDs created by <code>getUid</code> into cloned results.
 *
 * @param {*} obj The value to clone.
 * @return {*} A clone of the input value.
 * @deprecated goog.cloneObject is unsafe. Prefer the goog.object methods.
 */
goog.cloneObject = function(obj) {
  var type = goog.typeOf(obj);
  if (type == 'object' || type == 'array') {
    if (obj.clone) {
      return obj.clone();
    }
    var clone = type == 'array' ? [] : {};
    for (var key in obj) {
      clone[key] = goog.cloneObject(obj[key]);
    }
    return clone;
  }

  return obj;
};


/**
 * A native implementation of goog.bind.
 * @param {Function} fn A function to partially apply.
 * @param {Object|undefined} selfObj Specifies the object which this should
 *     point to when the function is run.
 * @param {...*} var_args Additional arguments that are partially applied to the
 *     function.
 * @return {!Function} A partially-applied form of the function bind() was
 *     invoked as a method of.
 * @private
 * @suppress {deprecated} The compiler thinks that Function.prototype.bind is
 *     deprecated because some people have declared a pure-JS version.
 *     Only the pure-JS version is truly deprecated.
 */
goog.bindNative_ = function(fn, selfObj, var_args) {
  return /** @type {!Function} */ (fn.call.apply(fn.bind, arguments));
};


/**
 * A pure-JS implementation of goog.bind.
 * @param {Function} fn A function to partially apply.
 * @param {Object|undefined} selfObj Specifies the object which this should
 *     point to when the function is run.
 * @param {...*} var_args Additional arguments that are partially applied to the
 *     function.
 * @return {!Function} A partially-applied form of the function bind() was
 *     invoked as a method of.
 * @private
 */
goog.bindJs_ = function(fn, selfObj, var_args) {
  if (!fn) {
    throw new Error();
  }

  if (arguments.length > 2) {
    var boundArgs = Array.prototype.slice.call(arguments, 2);
    return function() {
      // Prepend the bound arguments to the current arguments.
      var newArgs = Array.prototype.slice.call(arguments);
      Array.prototype.unshift.apply(newArgs, boundArgs);
      return fn.apply(selfObj, newArgs);
    };

  } else {
    return function() {
      return fn.apply(selfObj, arguments);
    };
  }
};


/**
 * Partially applies this function to a particular 'this object' and zero or
 * more arguments. The result is a new function with some arguments of the first
 * function pre-filled and the value of this 'pre-specified'.
 *
 * Remaining arguments specified at call-time are appended to the pre-specified
 * ones.
 *
 * Also see: {@link #partial}.
 *
 * Usage:
 * <pre>var barMethBound = goog.bind(myFunction, myObj, 'arg1', 'arg2');
 * barMethBound('arg3', 'arg4');</pre>
 *
 * @param {?function(this:T, ...)} fn A function to partially apply.
 * @param {T} selfObj Specifies the object which this should point to when the
 *     function is run.
 * @param {...*} var_args Additional arguments that are partially applied to the
 *     function.
 * @return {!Function} A partially-applied form of the function goog.bind() was
 *     invoked as a method of.
 * @template T
 * @suppress {deprecated} See above.
 */
goog.bind = function(fn, selfObj, var_args) {
  // TODO(nicksantos): narrow the type signature.
  if (Function.prototype.bind &&
      // NOTE(nicksantos): Somebody pulled base.js into the default Chrome
      // extension environment. This means that for Chrome extensions, they get
      // the implementation of Function.prototype.bind that calls goog.bind
      // instead of the native one. Even worse, we don't want to introduce a
      // circular dependency between goog.bind and Function.prototype.bind, so
      // we have to hack this to make sure it works correctly.
      Function.prototype.bind.toString().indexOf('native code') != -1) {
    goog.bind = goog.bindNative_;
  } else {
    goog.bind = goog.bindJs_;
  }
  return goog.bind.apply(null, arguments);
};


/**
 * Like goog.bind(), except that a 'this object' is not required. Useful when
 * the target function is already bound.
 *
 * Usage:
 * var g = goog.partial(f, arg1, arg2);
 * g(arg3, arg4);
 *
 * @param {Function} fn A function to partially apply.
 * @param {...*} var_args Additional arguments that are partially applied to fn.
 * @return {!Function} A partially-applied form of the function goog.partial()
 *     was invoked as a method of.
 */
goog.partial = function(fn, var_args) {
  var args = Array.prototype.slice.call(arguments, 1);
  return function() {
    // Clone the array (with slice()) and append additional arguments
    // to the existing arguments.
    var newArgs = args.slice();
    newArgs.push.apply(newArgs, arguments);
    return fn.apply(this, newArgs);
  };
};


/**
 * Copies all the members of a source object to a target object. This method
 * does not work on all browsers for all objects that contain keys such as
 * toString or hasOwnProperty. Use goog.object.extend for this purpose.
 * @param {Object} target Target.
 * @param {Object} source Source.
 */
goog.mixin = function(target, source) {
  for (var x in source) {
    target[x] = source[x];
  }

  // For IE7 or lower, the for-in-loop does not contain any properties that are
  // not enumerable on the prototype object (for example, isPrototypeOf from
  // Object.prototype) but also it will not include 'replace' on objects that
  // extend String and change 'replace' (not that it is common for anyone to
  // extend anything except Object).
};


/**
 * @return {number} An integer value representing the number of milliseconds
 *     between midnight, January 1, 1970 and the current time.
 */
goog.now = (goog.TRUSTED_SITE && Date.now) || (function() {
  // Unary plus operator converts its operand to a number which in the case of
  // a date is done by calling getTime().
  return +new Date();
});


/**
 * Evals JavaScript in the global scope.  In IE this uses execScript, other
 * browsers use goog.global.eval. If goog.global.eval does not evaluate in the
 * global scope (for example, in Safari), appends a script tag instead.
 * Throws an exception if neither execScript or eval is defined.
 * @param {string} script JavaScript string.
 */
goog.globalEval = function(script) {
  if (goog.global.execScript) {
    goog.global.execScript(script, 'JavaScript');
  } else if (goog.global.eval) {
    // Test to see if eval works
    if (goog.evalWorksForGlobals_ == null) {
      goog.global.eval('var _evalTest_ = 1;');
      if (typeof goog.global['_evalTest_'] != 'undefined') {
        try {
          delete goog.global['_evalTest_'];
        } catch (ignore) {
          // Microsoft edge fails the deletion above in strict mode.
        }
        goog.evalWorksForGlobals_ = true;
      } else {
        goog.evalWorksForGlobals_ = false;
      }
    }

    if (goog.evalWorksForGlobals_) {
      goog.global.eval(script);
    } else {
      /** @type {Document} */
      var doc = goog.global.document;
      var scriptElt = doc.createElement('SCRIPT');
      scriptElt.type = 'text/javascript';
      scriptElt.defer = false;
      // Note(user): can't use .innerHTML since "t('<test>')" will fail and
      // .text doesn't work in Safari 2.  Therefore we append a text node.
      scriptElt.appendChild(doc.createTextNode(script));
      doc.body.appendChild(scriptElt);
      doc.body.removeChild(scriptElt);
    }
  } else {
    throw Error('goog.globalEval not available');
  }
};


/**
 * Indicates whether or not we can call 'eval' directly to eval code in the
 * global scope. Set to a Boolean by the first call to goog.globalEval (which
 * empirically tests whether eval works for globals). @see goog.globalEval
 * @type {?boolean}
 * @private
 */
goog.evalWorksForGlobals_ = null;


/**
 * Optional map of CSS class names to obfuscated names used with
 * goog.getCssName().
 * @private {!Object<string, string>|undefined}
 * @see goog.setCssNameMapping
 */
goog.cssNameMapping_;


/**
 * Optional obfuscation style for CSS class names. Should be set to either
 * 'BY_WHOLE' or 'BY_PART' if defined.
 * @type {string|undefined}
 * @private
 * @see goog.setCssNameMapping
 */
goog.cssNameMappingStyle_;


/**
 * Handles strings that are intended to be used as CSS class names.
 *
 * This function works in tandem with @see goog.setCssNameMapping.
 *
 * Without any mapping set, the arguments are simple joined with a hyphen and
 * passed through unaltered.
 *
 * When there is a mapping, there are two possible styles in which these
 * mappings are used. In the BY_PART style, each part (i.e. in between hyphens)
 * of the passed in css name is rewritten according to the map. In the BY_WHOLE
 * style, the full css name is looked up in the map directly. If a rewrite is
 * not specified by the map, the compiler will output a warning.
 *
 * When the mapping is passed to the compiler, it will replace calls to
 * goog.getCssName with the strings from the mapping, e.g.
 *     var x = goog.getCssName('foo');
 *     var y = goog.getCssName(this.baseClass, 'active');
 *  becomes:
 *     var x= 'foo';
 *     var y = this.baseClass + '-active';
 *
 * If one argument is passed it will be processed, if two are passed only the
 * modifier will be processed, as it is assumed the first argument was generated
 * as a result of calling goog.getCssName.
 *
 * @param {string} className The class name.
 * @param {string=} opt_modifier A modifier to be appended to the class name.
 * @return {string} The class name or the concatenation of the class name and
 *     the modifier.
 */
goog.getCssName = function(className, opt_modifier) {
  var getMapping = function(cssName) {
    return goog.cssNameMapping_[cssName] || cssName;
  };

  var renameByParts = function(cssName) {
    // Remap all the parts individually.
    var parts = cssName.split('-');
    var mapped = [];
    for (var i = 0; i < parts.length; i++) {
      mapped.push(getMapping(parts[i]));
    }
    return mapped.join('-');
  };

  var rename;
  if (goog.cssNameMapping_) {
    rename = goog.cssNameMappingStyle_ == 'BY_WHOLE' ?
        getMapping : renameByParts;
  } else {
    rename = function(a) {
      return a;
    };
  }

  if (opt_modifier) {
    return className + '-' + rename(opt_modifier);
  } else {
    return rename(className);
  }
};


/**
 * Sets the map to check when returning a value from goog.getCssName(). Example:
 * <pre>
 * goog.setCssNameMapping({
 *   "goog": "a",
 *   "disabled": "b",
 * });
 *
 * var x = goog.getCssName('goog');
 * // The following evaluates to: "a a-b".
 * goog.getCssName('goog') + ' ' + goog.getCssName(x, 'disabled')
 * </pre>
 * When declared as a map of string literals to string literals, the JSCompiler
 * will replace all calls to goog.getCssName() using the supplied map if the
 * --process_closure_primitives flag is set.
 *
 * @param {!Object} mapping A map of strings to strings where keys are possible
 *     arguments to goog.getCssName() and values are the corresponding values
 *     that should be returned.
 * @param {string=} opt_style The style of css name mapping. There are two valid
 *     options: 'BY_PART', and 'BY_WHOLE'.
 * @see goog.getCssName for a description.
 */
goog.setCssNameMapping = function(mapping, opt_style) {
  goog.cssNameMapping_ = mapping;
  goog.cssNameMappingStyle_ = opt_style;
};


/**
 * To use CSS renaming in compiled mode, one of the input files should have a
 * call to goog.setCssNameMapping() with an object literal that the JSCompiler
 * can extract and use to replace all calls to goog.getCssName(). In uncompiled
 * mode, JavaScript code should be loaded before this base.js file that declares
 * a global variable, CLOSURE_CSS_NAME_MAPPING, which is used below. This is
 * to ensure that the mapping is loaded before any calls to goog.getCssName()
 * are made in uncompiled mode.
 *
 * A hook for overriding the CSS name mapping.
 * @type {!Object<string, string>|undefined}
 */
goog.global.CLOSURE_CSS_NAME_MAPPING;


if (!COMPILED && goog.global.CLOSURE_CSS_NAME_MAPPING) {
  // This does not call goog.setCssNameMapping() because the JSCompiler
  // requires that goog.setCssNameMapping() be called with an object literal.
  goog.cssNameMapping_ = goog.global.CLOSURE_CSS_NAME_MAPPING;
}


/**
 * Gets a localized message.
 *
 * This function is a compiler primitive. If you give the compiler a localized
 * message bundle, it will replace the string at compile-time with a localized
 * version, and expand goog.getMsg call to a concatenated string.
 *
 * Messages must be initialized in the form:
 * <code>
 * var MSG_NAME = goog.getMsg('Hello {$placeholder}', {'placeholder': 'world'});
 * </code>
 *
 * @param {string} str Translatable string, places holders in the form {$foo}.
 * @param {Object<string, string>=} opt_values Maps place holder name to value.
 * @return {string} message with placeholders filled.
 */
goog.getMsg = function(str, opt_values) {
  if (opt_values) {
    str = str.replace(/\{\$([^}]+)}/g, function(match, key) {
      return key in opt_values ? opt_values[key] : match;
    });
  }
  return str;
};


/**
 * Gets a localized message. If the message does not have a translation, gives a
 * fallback message.
 *
 * This is useful when introducing a new message that has not yet been
 * translated into all languages.
 *
 * This function is a compiler primitive. Must be used in the form:
 * <code>var x = goog.getMsgWithFallback(MSG_A, MSG_B);</code>
 * where MSG_A and MSG_B were initialized with goog.getMsg.
 *
 * @param {string} a The preferred message.
 * @param {string} b The fallback message.
 * @return {string} The best translated message.
 */
goog.getMsgWithFallback = function(a, b) {
  return a;
};


/**
 * Exposes an unobfuscated global namespace path for the given object.
 * Note that fields of the exported object *will* be obfuscated, unless they are
 * exported in turn via this function or goog.exportProperty.
 *
 * Also handy for making public items that are defined in anonymous closures.
 *
 * ex. goog.exportSymbol('public.path.Foo', Foo);
 *
 * ex. goog.exportSymbol('public.path.Foo.staticFunction', Foo.staticFunction);
 *     public.path.Foo.staticFunction();
 *
 * ex. goog.exportSymbol('public.path.Foo.prototype.myMethod',
 *                       Foo.prototype.myMethod);
 *     new public.path.Foo().myMethod();
 *
 * @param {string} publicPath Unobfuscated name to export.
 * @param {*} object Object the name should point to.
 * @param {Object=} opt_objectToExportTo The object to add the path to; default
 *     is goog.global.
 */
goog.exportSymbol = function(publicPath, object, opt_objectToExportTo) {
  goog.exportPath_(publicPath, object, opt_objectToExportTo);
};


/**
 * Exports a property unobfuscated into the object's namespace.
 * ex. goog.exportProperty(Foo, 'staticFunction', Foo.staticFunction);
 * ex. goog.exportProperty(Foo.prototype, 'myMethod', Foo.prototype.myMethod);
 * @param {Object} object Object whose static property is being exported.
 * @param {string} publicName Unobfuscated name to export.
 * @param {*} symbol Object the name should point to.
 */
goog.exportProperty = function(object, publicName, symbol) {
  object[publicName] = symbol;
};


/**
 * Inherit the prototype methods from one constructor into another.
 *
 * Usage:
 * <pre>
 * function ParentClass(a, b) { }
 * ParentClass.prototype.foo = function(a) { };
 *
 * function ChildClass(a, b, c) {
 *   ChildClass.base(this, 'constructor', a, b);
 * }
 * goog.inherits(ChildClass, ParentClass);
 *
 * var child = new ChildClass('a', 'b', 'see');
 * child.foo(); // This works.
 * </pre>
 *
 * @param {!Function} childCtor Child class.
 * @param {!Function} parentCtor Parent class.
 */
goog.inherits = function(childCtor, parentCtor) {
  /** @constructor */
  function tempCtor() {};
  tempCtor.prototype = parentCtor.prototype;
  childCtor.superClass_ = parentCtor.prototype;
  childCtor.prototype = new tempCtor();
  /** @override */
  childCtor.prototype.constructor = childCtor;

  /**
   * Calls superclass constructor/method.
   *
   * This function is only available if you use goog.inherits to
   * express inheritance relationships between classes.
   *
   * NOTE: This is a replacement for goog.base and for superClass_
   * property defined in childCtor.
   *
   * @param {!Object} me Should always be "this".
   * @param {string} methodName The method name to call. Calling
   *     superclass constructor can be done with the special string
   *     'constructor'.
   * @param {...*} var_args The arguments to pass to superclass
   *     method/constructor.
   * @return {*} The return value of the superclass method/constructor.
   */
  childCtor.base = function(me, methodName, var_args) {
    // Copying using loop to avoid deop due to passing arguments object to
    // function. This is faster in many JS engines as of late 2014.
    var args = new Array(arguments.length - 2);
    for (var i = 2; i < arguments.length; i++) {
      args[i - 2] = arguments[i];
    }
    return parentCtor.prototype[methodName].apply(me, args);
  };
};


/**
 * Call up to the superclass.
 *
 * If this is called from a constructor, then this calls the superclass
 * constructor with arguments 1-N.
 *
 * If this is called from a prototype method, then you must pass the name of the
 * method as the second argument to this function. If you do not, you will get a
 * runtime error. This calls the superclass' method with arguments 2-N.
 *
 * This function only works if you use goog.inherits to express inheritance
 * relationships between your classes.
 *
 * This function is a compiler primitive. At compile-time, the compiler will do
 * macro expansion to remove a lot of the extra overhead that this function
 * introduces. The compiler will also enforce a lot of the assumptions that this
 * function makes, and treat it as a compiler error if you break them.
 *
 * @param {!Object} me Should always be "this".
 * @param {*=} opt_methodName The method name if calling a super method.
 * @param {...*} var_args The rest of the arguments.
 * @return {*} The return value of the superclass method.
 * @suppress {es5Strict} This method can not be used in strict mode, but
 *     all Closure Library consumers must depend on this file.
 */
goog.base = function(me, opt_methodName, var_args) {
  var caller = arguments.callee.caller;

  if (goog.STRICT_MODE_COMPATIBLE || (goog.DEBUG && !caller)) {
    throw Error('arguments.caller not defined.  goog.base() cannot be used ' +
                'with strict mode code. See ' +
                'http://www.ecma-international.org/ecma-262/5.1/#sec-C');
  }

  if (caller.superClass_) {
    // Copying using loop to avoid deop due to passing arguments object to
    // function. This is faster in many JS engines as of late 2014.
    var ctorArgs = new Array(arguments.length - 1);
    for (var i = 1; i < arguments.length; i++) {
      ctorArgs[i - 1] = arguments[i];
    }
    // This is a constructor. Call the superclass constructor.
    return caller.superClass_.constructor.apply(me, ctorArgs);
  }

  // Copying using loop to avoid deop due to passing arguments object to
  // function. This is faster in many JS engines as of late 2014.
  var args = new Array(arguments.length - 2);
  for (var i = 2; i < arguments.length; i++) {
    args[i - 2] = arguments[i];
  }
  var foundCaller = false;
  for (var ctor = me.constructor;
       ctor; ctor = ctor.superClass_ && ctor.superClass_.constructor) {
    if (ctor.prototype[opt_methodName] === caller) {
      foundCaller = true;
    } else if (foundCaller) {
      return ctor.prototype[opt_methodName].apply(me, args);
    }
  }

  // If we did not find the caller in the prototype chain, then one of two
  // things happened:
  // 1) The caller is an instance method.
  // 2) This method was not called by the right caller.
  if (me[opt_methodName] === caller) {
    return me.constructor.prototype[opt_methodName].apply(me, args);
  } else {
    throw Error(
        'goog.base called from a method of one name ' +
        'to a method of a different name');
  }
};


/**
 * Allow for aliasing within scope functions.  This function exists for
 * uncompiled code - in compiled code the calls will be inlined and the aliases
 * applied.  In uncompiled code the function is simply run since the aliases as
 * written are valid JavaScript.
 *
 *
 * @param {function()} fn Function to call.  This function can contain aliases
 *     to namespaces (e.g. "var dom = goog.dom") or classes
 *     (e.g. "var Timer = goog.Timer").
 */
goog.scope = function(fn) {
  fn.call(goog.global);
};


/*
 * To support uncompiled, strict mode bundles that use eval to divide source
 * like so:
 *    eval('someSource;//# sourceUrl sourcefile.js');
 * We need to export the globally defined symbols "goog" and "COMPILED".
 * Exporting "goog" breaks the compiler optimizations, so we required that
 * be defined externally.
 * NOTE: We don't use goog.exportSymbol here because we don't want to trigger
 * extern generation when that compiler option is enabled.
 */
if (!COMPILED) {
  goog.global['COMPILED'] = COMPILED;
}



//==============================================================================
// goog.defineClass implementation
//==============================================================================


/**
 * Creates a restricted form of a Closure "class":
 *   - from the compiler's perspective, the instance returned from the
 *     constructor is sealed (no new properties may be added).  This enables
 *     better checks.
 *   - the compiler will rewrite this definition to a form that is optimal
 *     for type checking and optimization (initially this will be a more
 *     traditional form).
 *
 * @param {Function} superClass The superclass, Object or null.
 * @param {goog.defineClass.ClassDescriptor} def
 *     An object literal describing
 *     the class.  It may have the following properties:
 *     "constructor": the constructor function
 *     "statics": an object literal containing methods to add to the constructor
 *        as "static" methods or a function that will receive the constructor
 *        function as its only parameter to which static properties can
 *        be added.
 *     all other properties are added to the prototype.
 * @return {!Function} The class constructor.
 */
goog.defineClass = function(superClass, def) {
  // TODO(johnlenz): consider making the superClass an optional parameter.
  var constructor = def.constructor;
  var statics = def.statics;
  // Wrap the constructor prior to setting up the prototype and static methods.
  if (!constructor || constructor == Object.prototype.constructor) {
    constructor = function() {
      throw Error('cannot instantiate an interface (no constructor defined).');
    };
  }

  var cls = goog.defineClass.createSealingConstructor_(constructor, superClass);
  if (superClass) {
    goog.inherits(cls, superClass);
  }

  // Remove all the properties that should not be copied to the prototype.
  delete def.constructor;
  delete def.statics;

  goog.defineClass.applyProperties_(cls.prototype, def);
  if (statics != null) {
    if (statics instanceof Function) {
      statics(cls);
    } else {
      goog.defineClass.applyProperties_(cls, statics);
    }
  }

  return cls;
};


/**
 * @typedef {
 *     !Object|
 *     {constructor:!Function}|
 *     {constructor:!Function, statics:(Object|function(Function):void)}}
 * @suppress {missingProvide}
 */
goog.defineClass.ClassDescriptor;


/**
 * @define {boolean} Whether the instances returned by
 * goog.defineClass should be sealed when possible.
 */
goog.define('goog.defineClass.SEAL_CLASS_INSTANCES', goog.DEBUG);


/**
 * If goog.defineClass.SEAL_CLASS_INSTANCES is enabled and Object.seal is
 * defined, this function will wrap the constructor in a function that seals the
 * results of the provided constructor function.
 *
 * @param {!Function} ctr The constructor whose results maybe be sealed.
 * @param {Function} superClass The superclass constructor.
 * @return {!Function} The replacement constructor.
 * @private
 */
goog.defineClass.createSealingConstructor_ = function(ctr, superClass) {
  if (goog.defineClass.SEAL_CLASS_INSTANCES &&
      Object.seal instanceof Function) {
    // Don't seal subclasses of unsealable-tagged legacy classes.
    if (superClass && superClass.prototype &&
        superClass.prototype[goog.UNSEALABLE_CONSTRUCTOR_PROPERTY_]) {
      return ctr;
    }
    /**
     * @this {Object}
     * @return {?}
     */
    var wrappedCtr = function() {
      // Don't seal an instance of a subclass when it calls the constructor of
      // its super class as there is most likely still setup to do.
      var instance = ctr.apply(this, arguments) || this;
      instance[goog.UID_PROPERTY_] = instance[goog.UID_PROPERTY_];
      if (this.constructor === wrappedCtr) {
        Object.seal(instance);
      }
      return instance;
    };
    return wrappedCtr;
  }
  return ctr;
};


// TODO(johnlenz): share these values with the goog.object
/**
 * The names of the fields that are defined on Object.prototype.
 * @type {!Array<string>}
 * @private
 * @const
 */
goog.defineClass.OBJECT_PROTOTYPE_FIELDS_ = [
  'constructor',
  'hasOwnProperty',
  'isPrototypeOf',
  'propertyIsEnumerable',
  'toLocaleString',
  'toString',
  'valueOf'
];


// TODO(johnlenz): share this function with the goog.object
/**
 * @param {!Object} target The object to add properties to.
 * @param {!Object} source The object to copy properties from.
 * @private
 */
goog.defineClass.applyProperties_ = function(target, source) {
  // TODO(johnlenz): update this to support ES5 getters/setters

  var key;
  for (key in source) {
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      target[key] = source[key];
    }
  }

  // For IE the for-in-loop does not contain any properties that are not
  // enumerable on the prototype object (for example isPrototypeOf from
  // Object.prototype) and it will also not include 'replace' on objects that
  // extend String and change 'replace' (not that it is common for anyone to
  // extend anything except Object).
  for (var i = 0; i < goog.defineClass.OBJECT_PROTOTYPE_FIELDS_.length; i++) {
    key = goog.defineClass.OBJECT_PROTOTYPE_FIELDS_[i];
    if (Object.prototype.hasOwnProperty.call(source, key)) {
      target[key] = source[key];
    }
  }
};


/**
 * Sealing classes breaks the older idiom of assigning properties on the
 * prototype rather than in the constructor.  As such, goog.defineClass
 * must not seal subclasses of these old-style classes until they are fixed.
 * Until then, this marks a class as "broken", instructing defineClass
 * not to seal subclasses.
 * @param {!Function} ctr The legacy constructor to tag as unsealable.
 */
goog.tagUnsealableClass = function(ctr) {
  if (!COMPILED && goog.defineClass.SEAL_CLASS_INSTANCES) {
    ctr.prototype[goog.UNSEALABLE_CONSTRUCTOR_PROPERTY_] = true;
  }
};


/**
 * Name for unsealable tag property.
 * @const @private {string}
 */
goog.UNSEALABLE_CONSTRUCTOR_PROPERTY_ = 'goog_defineClass_legacy_unsealable';

goog.provide('ol');


/**
 * Constants defined with the define tag cannot be changed in application
 * code, but can be set at compile time.
 * Some reduce the size of the build in advanced compile mode.
 */


/**
 * @define {boolean} Assume touch.  Default is `false`.
 */
ol.ASSUME_TOUCH = false;


/**
 * TODO: rename this to something having to do with tile grids
 * see https://github.com/openlayers/ol3/issues/2076
 * @define {number} Default maximum zoom for default tile grids.
 */
ol.DEFAULT_MAX_ZOOM = 42;


/**
 * @define {number} Default min zoom level for the map view.  Default is `0`.
 */
ol.DEFAULT_MIN_ZOOM = 0;


/**
 * @define {number} Default maximum allowed threshold  (in pixels) for
 *     reprojection triangulation. Default is `0.5`.
 */
ol.DEFAULT_RASTER_REPROJECTION_ERROR_THRESHOLD = 0.5;


/**
 * @define {number} Default high water mark.
 */
ol.DEFAULT_TILE_CACHE_HIGH_WATER_MARK = 2048;


/**
 * @define {number} Default tile size.
 */
ol.DEFAULT_TILE_SIZE = 256;


/**
 * @define {string} Default WMS version.
 */
ol.DEFAULT_WMS_VERSION = '1.3.0';


/**
 * @define {number} Hysteresis pixels.
 */
ol.DRAG_BOX_HYSTERESIS_PIXELS = 8;


/**
 * @define {boolean} Enable the Canvas renderer.  Default is `true`. Setting
 *     this to false at compile time in advanced mode removes all code
 *     supporting the Canvas renderer from the build.
 */
ol.ENABLE_CANVAS = true;


/**
 * @define {boolean} Enable the DOM renderer (used as a fallback where Canvas is
 *     not available).  Default is `true`. Setting this to false at compile time
 *     in advanced mode removes all code supporting the DOM renderer from the
 *     build.
 */
ol.ENABLE_DOM = true;


/**
 * @define {boolean} Enable rendering of ol.layer.Image based layers.  Default
 *     is `true`. Setting this to false at compile time in advanced mode removes
 *     all code supporting Image layers from the build.
 */
ol.ENABLE_IMAGE = true;


/**
 * @define {boolean} Enable Closure named colors (`goog.color.names`).
 *     Enabling these colors adds about 3KB uncompressed / 1.5KB compressed to
 *     the final build size.  Default is `false`. This setting has no effect
 *     with Canvas renderer, which uses its own names, whether this is true or
 *     false.
 */
ol.ENABLE_NAMED_COLORS = false;


/**
 * @define {boolean} Enable integration with the Proj4js library.  Default is
 *     `true`.
 */
ol.ENABLE_PROJ4JS = true;


/**
 * @define {boolean} Enable automatic reprojection of raster sources. Default is
 *     `true`.
 */
ol.ENABLE_RASTER_REPROJECTION = true;


/**
 * @define {boolean} Enable rendering of ol.layer.Tile based layers.  Default is
 *     `true`. Setting this to false at compile time in advanced mode removes
 *     all code supporting Tile layers from the build.
 */
ol.ENABLE_TILE = true;


/**
 * @define {boolean} Enable rendering of ol.layer.Vector based layers.  Default
 *     is `true`. Setting this to false at compile time in advanced mode removes
 *     all code supporting Vector layers from the build.
 */
ol.ENABLE_VECTOR = true;


/**
 * @define {boolean} Enable rendering of ol.layer.VectorTile based layers.
 *     Default is `true`. Setting this to false at compile time in advanced mode
 *     removes all code supporting VectorTile layers from the build.
 */
ol.ENABLE_VECTOR_TILE = true;


/**
 * @define {boolean} Enable the WebGL renderer.  Default is `true`. Setting
 *     this to false at compile time in advanced mode removes all code
 *     supporting the WebGL renderer from the build.
 */
ol.ENABLE_WEBGL = true;


/**
 * @define {number} The size in pixels of the first atlas image. Default is
 * `256`.
 */
ol.INITIAL_ATLAS_SIZE = 256;


/**
 * @define {number} The maximum size in pixels of atlas images. Default is
 * `-1`, meaning it is not used (and `ol.WEBGL_MAX_TEXTURE_SIZE` is
 * used instead).
 */
ol.MAX_ATLAS_SIZE = -1;


/**
 * @define {number} Maximum mouse wheel delta.
 */
ol.MOUSEWHEELZOOM_MAXDELTA = 1;


/**
 * @define {number} Mouse wheel timeout duration.
 */
ol.MOUSEWHEELZOOM_TIMEOUT_DURATION = 80;


/**
 * @define {number} Maximum width and/or height extent ratio that determines
 * when the overview map should be zoomed out.
 */
ol.OVERVIEWMAP_MAX_RATIO = 0.75;


/**
 * @define {number} Minimum width and/or height extent ratio that determines
 * when the overview map should be zoomed in.
 */
ol.OVERVIEWMAP_MIN_RATIO = 0.1;


/**
 * @define {number} Maximum number of source tiles for raster reprojection of
 *     a single tile.
 *     If too many source tiles are determined to be loaded to create a single
 *     reprojected tile the browser can become unresponsive or even crash.
 *     This can happen if the developer defines projections improperly and/or
 *     with unlimited extents.
 *     If too many tiles are required, no tiles are loaded and
 *     `ol.TileState.ERROR` state is set. Default is `100`.
 */
ol.RASTER_REPROJECTION_MAX_SOURCE_TILES = 100;


/**
 * @define {number} Maximum number of subdivision steps during raster
 *     reprojection triangulation. Prevents high memory usage and large
 *     number of proj4 calls (for certain transformations and areas).
 *     At most `2*(2^this)` triangles are created for each triangulated
 *     extent (tile/image). Default is `10`.
 */
ol.RASTER_REPROJECTION_MAX_SUBDIVISION = 10;


/**
 * @define {number} Maximum allowed size of triangle relative to world width.
 *     When transforming corners of world extent between certain projections,
 *     the resulting triangulation seems to have zero error and no subdivision
 *     is performed.
 *     If the triangle width is more than this (relative to world width; 0-1),
 *     subdivison is forced (up to `ol.RASTER_REPROJECTION_MAX_SUBDIVISION`).
 *     Default is `0.25`.
 */
ol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH = 0.25;


/**
 * @define {number} Tolerance for geometry simplification in device pixels.
 */
ol.SIMPLIFY_TOLERANCE = 0.5;


/**
 * @define {number} Texture cache high water mark.
 */
ol.WEBGL_TEXTURE_CACHE_HIGH_WATER_MARK = 1024;


/**
 * The maximum supported WebGL texture size in pixels. If WebGL is not
 * supported, the value is set to `undefined`.
 * @const
 * @type {number|undefined}
 */
ol.WEBGL_MAX_TEXTURE_SIZE; // value is set in `ol.has`


/**
 * List of supported WebGL extensions.
 * @const
 * @type {Array.<string>}
 */
ol.WEBGL_EXTENSIONS; // value is set in `ol.has`


/**
 * Inherit the prototype methods from one constructor into another.
 *
 * Usage:
 *
 *     function ParentClass(a, b) { }
 *     ParentClass.prototype.foo = function(a) { }
 *
 *     function ChildClass(a, b, c) {
 *       // Call parent constructor
 *       ParentClass.call(this, a, b);
 *     }
 *     ol.inherits(ChildClass, ParentClass);
 *
 *     var child = new ChildClass('a', 'b', 'see');
 *     child.foo(); // This works.
 *
 * In addition, a superclass' implementation of a method can be invoked as
 * follows:
 *
 *     ChildClass.prototype.foo = function(a) {
 *       ChildClass.base(this, 'foo', a);
 *       // Other code here.
 *     };
 *
 * @param {!Function} childCtor Child constructor.
 * @param {!Function} parentCtor Parent constructor.
 * @function
 * @api
 */
ol.inherits =
    goog.inherits;
// note that the newline above is necessary to satisfy the linter


/**
 * A reusable function, used e.g. as a default for callbacks.
 *
 * @return {undefined} Nothing.
 */
ol.nullFunction = function() {};

// FIXME factor out common code between usedTiles and wantedTiles

goog.provide('ol.PostRenderFunction');
goog.provide('ol.PreRenderFunction');


/**
 * @typedef {function(ol.Map, ?olx.FrameState): boolean}
 */
ol.PostRenderFunction;


/**
 * Function to perform manipulations before rendering. This function is called
 * with the {@link ol.Map} as first and an optional {@link olx.FrameState} as
 * second argument. Return `true` to keep this function for the next frame,
 * `false` to remove it.
 * @typedef {function(ol.Map, ?olx.FrameState): boolean}
 * @api
 */
ol.PreRenderFunction;

// Copyright 2009 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Provides a base class for custom Error objects such that the
 * stack is correctly maintained.
 *
 * You should never need to throw goog.debug.Error(msg) directly, Error(msg) is
 * sufficient.
 *
 */

goog.provide('goog.debug.Error');



/**
 * Base class for custom error objects.
 * @param {*=} opt_msg The message associated with the error.
 * @constructor
 * @extends {Error}
 */
goog.debug.Error = function(opt_msg) {

  // Attempt to ensure there is a stack trace.
  if (Error.captureStackTrace) {
    Error.captureStackTrace(this, goog.debug.Error);
  } else {
    var stack = new Error().stack;
    if (stack) {
      this.stack = stack;
    }
  }

  if (opt_msg) {
    this.message = String(opt_msg);
  }

  /**
   * Whether to report this error to the server. Setting this to false will
   * cause the error reporter to not report the error back to the server,
   * which can be useful if the client knows that the error has already been
   * logged on the server.
   * @type {boolean}
   */
  this.reportErrorToServer = true;
};
goog.inherits(goog.debug.Error, Error);


/** @override */
goog.debug.Error.prototype.name = 'CustomError';

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Definition of goog.dom.NodeType.
 */

goog.provide('goog.dom.NodeType');


/**
 * Constants for the nodeType attribute in the Node interface.
 *
 * These constants match those specified in the Node interface. These are
 * usually present on the Node object in recent browsers, but not in older
 * browsers (specifically, early IEs) and thus are given here.
 *
 * In some browsers (early IEs), these are not defined on the Node object,
 * so they are provided here.
 *
 * See http://www.w3.org/TR/DOM-Level-2-Core/core.html#ID-1950641247
 * @enum {number}
 */
goog.dom.NodeType = {
  ELEMENT: 1,
  ATTRIBUTE: 2,
  TEXT: 3,
  CDATA_SECTION: 4,
  ENTITY_REFERENCE: 5,
  ENTITY: 6,
  PROCESSING_INSTRUCTION: 7,
  COMMENT: 8,
  DOCUMENT: 9,
  DOCUMENT_TYPE: 10,
  DOCUMENT_FRAGMENT: 11,
  NOTATION: 12
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Utilities for string manipulation.
 * @author arv@google.com (Erik Arvidsson)
 */


/**
 * Namespace for string utilities
 */
goog.provide('goog.string');
goog.provide('goog.string.Unicode');


/**
 * @define {boolean} Enables HTML escaping of lowercase letter "e" which helps
 * with detection of double-escaping as this letter is frequently used.
 */
goog.define('goog.string.DETECT_DOUBLE_ESCAPING', false);


/**
 * @define {boolean} Whether to force non-dom html unescaping.
 */
goog.define('goog.string.FORCE_NON_DOM_HTML_UNESCAPING', false);


/**
 * Common Unicode string characters.
 * @enum {string}
 */
goog.string.Unicode = {
  NBSP: '\xa0'
};


/**
 * Fast prefix-checker.
 * @param {string} str The string to check.
 * @param {string} prefix A string to look for at the start of {@code str}.
 * @return {boolean} True if {@code str} begins with {@code prefix}.
 */
goog.string.startsWith = function(str, prefix) {
  return str.lastIndexOf(prefix, 0) == 0;
};


/**
 * Fast suffix-checker.
 * @param {string} str The string to check.
 * @param {string} suffix A string to look for at the end of {@code str}.
 * @return {boolean} True if {@code str} ends with {@code suffix}.
 */
goog.string.endsWith = function(str, suffix) {
  var l = str.length - suffix.length;
  return l >= 0 && str.indexOf(suffix, l) == l;
};


/**
 * Case-insensitive prefix-checker.
 * @param {string} str The string to check.
 * @param {string} prefix  A string to look for at the end of {@code str}.
 * @return {boolean} True if {@code str} begins with {@code prefix} (ignoring
 *     case).
 */
goog.string.caseInsensitiveStartsWith = function(str, prefix) {
  return goog.string.caseInsensitiveCompare(
      prefix, str.substr(0, prefix.length)) == 0;
};


/**
 * Case-insensitive suffix-checker.
 * @param {string} str The string to check.
 * @param {string} suffix A string to look for at the end of {@code str}.
 * @return {boolean} True if {@code str} ends with {@code suffix} (ignoring
 *     case).
 */
goog.string.caseInsensitiveEndsWith = function(str, suffix) {
  return goog.string.caseInsensitiveCompare(
      suffix, str.substr(str.length - suffix.length, suffix.length)) == 0;
};


/**
 * Case-insensitive equality checker.
 * @param {string} str1 First string to check.
 * @param {string} str2 Second string to check.
 * @return {boolean} True if {@code str1} and {@code str2} are the same string,
 *     ignoring case.
 */
goog.string.caseInsensitiveEquals = function(str1, str2) {
  return str1.toLowerCase() == str2.toLowerCase();
};


/**
 * Does simple python-style string substitution.
 * subs("foo%s hot%s", "bar", "dog") becomes "foobar hotdog".
 * @param {string} str The string containing the pattern.
 * @param {...*} var_args The items to substitute into the pattern.
 * @return {string} A copy of {@code str} in which each occurrence of
 *     {@code %s} has been replaced an argument from {@code var_args}.
 */
goog.string.subs = function(str, var_args) {
  var splitParts = str.split('%s');
  var returnString = '';

  var subsArguments = Array.prototype.slice.call(arguments, 1);
  while (subsArguments.length &&
         // Replace up to the last split part. We are inserting in the
         // positions between split parts.
         splitParts.length > 1) {
    returnString += splitParts.shift() + subsArguments.shift();
  }

  return returnString + splitParts.join('%s'); // Join unused '%s'
};


/**
 * Converts multiple whitespace chars (spaces, non-breaking-spaces, new lines
 * and tabs) to a single space, and strips leading and trailing whitespace.
 * @param {string} str Input string.
 * @return {string} A copy of {@code str} with collapsed whitespace.
 */
goog.string.collapseWhitespace = function(str) {
  // Since IE doesn't include non-breaking-space (0xa0) in their \s character
  // class (as required by section 7.2 of the ECMAScript spec), we explicitly
  // include it in the regexp to enforce consistent cross-browser behavior.
  return str.replace(/[\s\xa0]+/g, ' ').replace(/^\s+|\s+$/g, '');
};


/**
 * Checks if a string is empty or contains only whitespaces.
 * @param {string} str The string to check.
 * @return {boolean} Whether {@code str} is empty or whitespace only.
 */
goog.string.isEmptyOrWhitespace = function(str) {
  // testing length == 0 first is actually slower in all browsers (about the
  // same in Opera).
  // Since IE doesn't include non-breaking-space (0xa0) in their \s character
  // class (as required by section 7.2 of the ECMAScript spec), we explicitly
  // include it in the regexp to enforce consistent cross-browser behavior.
  return /^[\s\xa0]*$/.test(str);
};


/**
 * Checks if a string is empty.
 * @param {string} str The string to check.
 * @return {boolean} Whether {@code str} is empty.
 */
goog.string.isEmptyString = function(str) {
  return str.length == 0;
};


/**
 * Checks if a string is empty or contains only whitespaces.
 *
 * TODO(user): Deprecate this when clients have been switched over to
 * goog.string.isEmptyOrWhitespace.
 *
 * @param {string} str The string to check.
 * @return {boolean} Whether {@code str} is empty or whitespace only.
 */
goog.string.isEmpty = goog.string.isEmptyOrWhitespace;


/**
 * Checks if a string is null, undefined, empty or contains only whitespaces.
 * @param {*} str The string to check.
 * @return {boolean} Whether {@code str} is null, undefined, empty, or
 *     whitespace only.
 * @deprecated Use goog.string.isEmptyOrWhitespace(goog.string.makeSafe(str))
 *     instead.
 */
goog.string.isEmptyOrWhitespaceSafe = function(str) {
  return goog.string.isEmptyOrWhitespace(goog.string.makeSafe(str));
};


/**
 * Checks if a string is null, undefined, empty or contains only whitespaces.
 *
 * TODO(user): Deprecate this when clients have been switched over to
 * goog.string.isEmptyOrWhitespaceSafe.
 *
 * @param {*} str The string to check.
 * @return {boolean} Whether {@code str} is null, undefined, empty, or
 *     whitespace only.
 */
goog.string.isEmptySafe = goog.string.isEmptyOrWhitespaceSafe;


/**
 * Checks if a string is all breaking whitespace.
 * @param {string} str The string to check.
 * @return {boolean} Whether the string is all breaking whitespace.
 */
goog.string.isBreakingWhitespace = function(str) {
  return !/[^\t\n\r ]/.test(str);
};


/**
 * Checks if a string contains all letters.
 * @param {string} str string to check.
 * @return {boolean} True if {@code str} consists entirely of letters.
 */
goog.string.isAlpha = function(str) {
  return !/[^a-zA-Z]/.test(str);
};


/**
 * Checks if a string contains only numbers.
 * @param {*} str string to check. If not a string, it will be
 *     casted to one.
 * @return {boolean} True if {@code str} is numeric.
 */
goog.string.isNumeric = function(str) {
  return !/[^0-9]/.test(str);
};


/**
 * Checks if a string contains only numbers or letters.
 * @param {string} str string to check.
 * @return {boolean} True if {@code str} is alphanumeric.
 */
goog.string.isAlphaNumeric = function(str) {
  return !/[^a-zA-Z0-9]/.test(str);
};


/**
 * Checks if a character is a space character.
 * @param {string} ch Character to check.
 * @return {boolean} True if {@code ch} is a space.
 */
goog.string.isSpace = function(ch) {
  return ch == ' ';
};


/**
 * Checks if a character is a valid unicode character.
 * @param {string} ch Character to check.
 * @return {boolean} True if {@code ch} is a valid unicode character.
 */
goog.string.isUnicodeChar = function(ch) {
  return ch.length == 1 && ch >= ' ' && ch <= '~' ||
         ch >= '\u0080' && ch <= '\uFFFD';
};


/**
 * Takes a string and replaces newlines with a space. Multiple lines are
 * replaced with a single space.
 * @param {string} str The string from which to strip newlines.
 * @return {string} A copy of {@code str} stripped of newlines.
 */
goog.string.stripNewlines = function(str) {
  return str.replace(/(\r\n|\r|\n)+/g, ' ');
};


/**
 * Replaces Windows and Mac new lines with unix style: \r or \r\n with \n.
 * @param {string} str The string to in which to canonicalize newlines.
 * @return {string} {@code str} A copy of {@code} with canonicalized newlines.
 */
goog.string.canonicalizeNewlines = function(str) {
  return str.replace(/(\r\n|\r|\n)/g, '\n');
};


/**
 * Normalizes whitespace in a string, replacing all whitespace chars with
 * a space.
 * @param {string} str The string in which to normalize whitespace.
 * @return {string} A copy of {@code str} with all whitespace normalized.
 */
goog.string.normalizeWhitespace = function(str) {
  return str.replace(/\xa0|\s/g, ' ');
};


/**
 * Normalizes spaces in a string, replacing all consecutive spaces and tabs
 * with a single space. Replaces non-breaking space with a space.
 * @param {string} str The string in which to normalize spaces.
 * @return {string} A copy of {@code str} with all consecutive spaces and tabs
 *    replaced with a single space.
 */
goog.string.normalizeSpaces = function(str) {
  return str.replace(/\xa0|[ \t]+/g, ' ');
};


/**
 * Removes the breaking spaces from the left and right of the string and
 * collapses the sequences of breaking spaces in the middle into single spaces.
 * The original and the result strings render the same way in HTML.
 * @param {string} str A string in which to collapse spaces.
 * @return {string} Copy of the string with normalized breaking spaces.
 */
goog.string.collapseBreakingSpaces = function(str) {
  return str.replace(/[\t\r\n ]+/g, ' ').replace(
      /^[\t\r\n ]+|[\t\r\n ]+$/g, '');
};


/**
 * Trims white spaces to the left and right of a string.
 * @param {string} str The string to trim.
 * @return {string} A trimmed copy of {@code str}.
 */
goog.string.trim = (goog.TRUSTED_SITE && String.prototype.trim) ?
    function(str) {
      return str.trim();
    } :
    function(str) {
      // Since IE doesn't include non-breaking-space (0xa0) in their \s
      // character class (as required by section 7.2 of the ECMAScript spec),
      // we explicitly include it in the regexp to enforce consistent
      // cross-browser behavior.
      return str.replace(/^[\s\xa0]+|[\s\xa0]+$/g, '');
    };


/**
 * Trims whitespaces at the left end of a string.
 * @param {string} str The string to left trim.
 * @return {string} A trimmed copy of {@code str}.
 */
goog.string.trimLeft = function(str) {
  // Since IE doesn't include non-breaking-space (0xa0) in their \s character
  // class (as required by section 7.2 of the ECMAScript spec), we explicitly
  // include it in the regexp to enforce consistent cross-browser behavior.
  return str.replace(/^[\s\xa0]+/, '');
};


/**
 * Trims whitespaces at the right end of a string.
 * @param {string} str The string to right trim.
 * @return {string} A trimmed copy of {@code str}.
 */
goog.string.trimRight = function(str) {
  // Since IE doesn't include non-breaking-space (0xa0) in their \s character
  // class (as required by section 7.2 of the ECMAScript spec), we explicitly
  // include it in the regexp to enforce consistent cross-browser behavior.
  return str.replace(/[\s\xa0]+$/, '');
};


/**
 * A string comparator that ignores case.
 * -1 = str1 less than str2
 *  0 = str1 equals str2
 *  1 = str1 greater than str2
 *
 * @param {string} str1 The string to compare.
 * @param {string} str2 The string to compare {@code str1} to.
 * @return {number} The comparator result, as described above.
 */
goog.string.caseInsensitiveCompare = function(str1, str2) {
  var test1 = String(str1).toLowerCase();
  var test2 = String(str2).toLowerCase();

  if (test1 < test2) {
    return -1;
  } else if (test1 == test2) {
    return 0;
  } else {
    return 1;
  }
};


/**
 * Regular expression used for splitting a string into substrings of fractional
 * numbers, integers, and non-numeric characters.
 * @type {RegExp}
 * @private
 */
goog.string.numerateCompareRegExp_ = /(\.\d+)|(\d+)|(\D+)/g;


/**
 * String comparison function that handles numbers in a way humans might expect.
 * Using this function, the string "File 2.jpg" sorts before "File 10.jpg". The
 * comparison is mostly case-insensitive, though strings that are identical
 * except for case are sorted with the upper-case strings before lower-case.
 *
 * This comparison function is significantly slower (about 500x) than either
 * the default or the case-insensitive compare. It should not be used in
 * time-critical code, but should be fast enough to sort several hundred short
 * strings (like filenames) with a reasonable delay.
 *
 * @param {string} str1 The string to compare in a numerically sensitive way.
 * @param {string} str2 The string to compare {@code str1} to.
 * @return {number} less than 0 if str1 < str2, 0 if str1 == str2, greater than
 *     0 if str1 > str2.
 */
goog.string.numerateCompare = function(str1, str2) {
  if (str1 == str2) {
    return 0;
  }
  if (!str1) {
    return -1;
  }
  if (!str2) {
    return 1;
  }

  // Using match to split the entire string ahead of time turns out to be faster
  // for most inputs than using RegExp.exec or iterating over each character.
  var tokens1 = str1.toLowerCase().match(goog.string.numerateCompareRegExp_);
  var tokens2 = str2.toLowerCase().match(goog.string.numerateCompareRegExp_);

  var count = Math.min(tokens1.length, tokens2.length);

  for (var i = 0; i < count; i++) {
    var a = tokens1[i];
    var b = tokens2[i];

    // Compare pairs of tokens, returning if one token sorts before the other.
    if (a != b) {

      // Only if both tokens are integers is a special comparison required.
      // Decimal numbers are sorted as strings (e.g., '.09' < '.1').
      var num1 = parseInt(a, 10);
      if (!isNaN(num1)) {
        var num2 = parseInt(b, 10);
        if (!isNaN(num2) && num1 - num2) {
          return num1 - num2;
        }
      }
      return a < b ? -1 : 1;
    }
  }

  // If one string is a substring of the other, the shorter string sorts first.
  if (tokens1.length != tokens2.length) {
    return tokens1.length - tokens2.length;
  }

  // The two strings must be equivalent except for case (perfect equality is
  // tested at the head of the function.) Revert to default ASCII-betical string
  // comparison to stablize the sort.
  return str1 < str2 ? -1 : 1;
};


/**
 * URL-encodes a string
 * @param {*} str The string to url-encode.
 * @return {string} An encoded copy of {@code str} that is safe for urls.
 *     Note that '#', ':', and other characters used to delimit portions
 *     of URLs *will* be encoded.
 */
goog.string.urlEncode = function(str) {
  return encodeURIComponent(String(str));
};


/**
 * URL-decodes the string. We need to specially handle '+'s because
 * the javascript library doesn't convert them to spaces.
 * @param {string} str The string to url decode.
 * @return {string} The decoded {@code str}.
 */
goog.string.urlDecode = function(str) {
  return decodeURIComponent(str.replace(/\+/g, ' '));
};


/**
 * Converts \n to <br>s or <br />s.
 * @param {string} str The string in which to convert newlines.
 * @param {boolean=} opt_xml Whether to use XML compatible tags.
 * @return {string} A copy of {@code str} with converted newlines.
 */
goog.string.newLineToBr = function(str, opt_xml) {
  return str.replace(/(\r\n|\r|\n)/g, opt_xml ? '<br />' : '<br>');
};


/**
 * Escapes double quote '"' and single quote '\'' characters in addition to
 * '&', '<', and '>' so that a string can be included in an HTML tag attribute
 * value within double or single quotes.
 *
 * It should be noted that > doesn't need to be escaped for the HTML or XML to
 * be valid, but it has been decided to escape it for consistency with other
 * implementations.
 *
 * With goog.string.DETECT_DOUBLE_ESCAPING, this function escapes also the
 * lowercase letter "e".
 *
 * NOTE(user):
 * HtmlEscape is often called during the generation of large blocks of HTML.
 * Using statics for the regular expressions and strings is an optimization
 * that can more than half the amount of time IE spends in this function for
 * large apps, since strings and regexes both contribute to GC allocations.
 *
 * Testing for the presence of a character before escaping increases the number
 * of function calls, but actually provides a speed increase for the average
 * case -- since the average case often doesn't require the escaping of all 4
 * characters and indexOf() is much cheaper than replace().
 * The worst case does suffer slightly from the additional calls, therefore the
 * opt_isLikelyToContainHtmlChars option has been included for situations
 * where all 4 HTML entities are very likely to be present and need escaping.
 *
 * Some benchmarks (times tended to fluctuate +-0.05ms):
 *                                     FireFox                     IE6
 * (no chars / average (mix of cases) / all 4 chars)
 * no checks                     0.13 / 0.22 / 0.22         0.23 / 0.53 / 0.80
 * indexOf                       0.08 / 0.17 / 0.26         0.22 / 0.54 / 0.84
 * indexOf + re test             0.07 / 0.17 / 0.28         0.19 / 0.50 / 0.85
 *
 * An additional advantage of checking if replace actually needs to be called
 * is a reduction in the number of object allocations, so as the size of the
 * application grows the difference between the various methods would increase.
 *
 * @param {string} str string to be escaped.
 * @param {boolean=} opt_isLikelyToContainHtmlChars Don't perform a check to see
 *     if the character needs replacing - use this option if you expect each of
 *     the characters to appear often. Leave false if you expect few html
 *     characters to occur in your strings, such as if you are escaping HTML.
 * @return {string} An escaped copy of {@code str}.
 */
goog.string.htmlEscape = function(str, opt_isLikelyToContainHtmlChars) {

  if (opt_isLikelyToContainHtmlChars) {
    str = str.replace(goog.string.AMP_RE_, '&amp;')
          .replace(goog.string.LT_RE_, '&lt;')
          .replace(goog.string.GT_RE_, '&gt;')
          .replace(goog.string.QUOT_RE_, '&quot;')
          .replace(goog.string.SINGLE_QUOTE_RE_, '&#39;')
          .replace(goog.string.NULL_RE_, '&#0;');
    if (goog.string.DETECT_DOUBLE_ESCAPING) {
      str = str.replace(goog.string.E_RE_, '&#101;');
    }
    return str;

  } else {
    // quick test helps in the case when there are no chars to replace, in
    // worst case this makes barely a difference to the time taken
    if (!goog.string.ALL_RE_.test(str)) return str;

    // str.indexOf is faster than regex.test in this case
    if (str.indexOf('&') != -1) {
      str = str.replace(goog.string.AMP_RE_, '&amp;');
    }
    if (str.indexOf('<') != -1) {
      str = str.replace(goog.string.LT_RE_, '&lt;');
    }
    if (str.indexOf('>') != -1) {
      str = str.replace(goog.string.GT_RE_, '&gt;');
    }
    if (str.indexOf('"') != -1) {
      str = str.replace(goog.string.QUOT_RE_, '&quot;');
    }
    if (str.indexOf('\'') != -1) {
      str = str.replace(goog.string.SINGLE_QUOTE_RE_, '&#39;');
    }
    if (str.indexOf('\x00') != -1) {
      str = str.replace(goog.string.NULL_RE_, '&#0;');
    }
    if (goog.string.DETECT_DOUBLE_ESCAPING && str.indexOf('e') != -1) {
      str = str.replace(goog.string.E_RE_, '&#101;');
    }
    return str;
  }
};


/**
 * Regular expression that matches an ampersand, for use in escaping.
 * @const {!RegExp}
 * @private
 */
goog.string.AMP_RE_ = /&/g;


/**
 * Regular expression that matches a less than sign, for use in escaping.
 * @const {!RegExp}
 * @private
 */
goog.string.LT_RE_ = /</g;


/**
 * Regular expression that matches a greater than sign, for use in escaping.
 * @const {!RegExp}
 * @private
 */
goog.string.GT_RE_ = />/g;


/**
 * Regular expression that matches a double quote, for use in escaping.
 * @const {!RegExp}
 * @private
 */
goog.string.QUOT_RE_ = /"/g;


/**
 * Regular expression that matches a single quote, for use in escaping.
 * @const {!RegExp}
 * @private
 */
goog.string.SINGLE_QUOTE_RE_ = /'/g;


/**
 * Regular expression that matches null character, for use in escaping.
 * @const {!RegExp}
 * @private
 */
goog.string.NULL_RE_ = /\x00/g;


/**
 * Regular expression that matches a lowercase letter "e", for use in escaping.
 * @const {!RegExp}
 * @private
 */
goog.string.E_RE_ = /e/g;


/**
 * Regular expression that matches any character that needs to be escaped.
 * @const {!RegExp}
 * @private
 */
goog.string.ALL_RE_ = (goog.string.DETECT_DOUBLE_ESCAPING ?
    /[\x00&<>"'e]/ :
    /[\x00&<>"']/);


/**
 * Unescapes an HTML string.
 *
 * @param {string} str The string to unescape.
 * @return {string} An unescaped copy of {@code str}.
 */
goog.string.unescapeEntities = function(str) {
  if (goog.string.contains(str, '&')) {
    // We are careful not to use a DOM if we do not have one or we explicitly
    // requested non-DOM html unescaping.
    if (!goog.string.FORCE_NON_DOM_HTML_UNESCAPING &&
        'document' in goog.global) {
      return goog.string.unescapeEntitiesUsingDom_(str);
    } else {
      // Fall back on pure XML entities
      return goog.string.unescapePureXmlEntities_(str);
    }
  }
  return str;
};


/**
 * Unescapes a HTML string using the provided document.
 *
 * @param {string} str The string to unescape.
 * @param {!Document} document A document to use in escaping the string.
 * @return {string} An unescaped copy of {@code str}.
 */
goog.string.unescapeEntitiesWithDocument = function(str, document) {
  if (goog.string.contains(str, '&')) {
    return goog.string.unescapeEntitiesUsingDom_(str, document);
  }
  return str;
};


/**
 * Unescapes an HTML string using a DOM to resolve non-XML, non-numeric
 * entities. This function is XSS-safe and whitespace-preserving.
 * @private
 * @param {string} str The string to unescape.
 * @param {Document=} opt_document An optional document to use for creating
 *     elements. If this is not specified then the default window.document
 *     will be used.
 * @return {string} The unescaped {@code str} string.
 */
goog.string.unescapeEntitiesUsingDom_ = function(str, opt_document) {
  /** @type {!Object<string, string>} */
  var seen = {'&amp;': '&', '&lt;': '<', '&gt;': '>', '&quot;': '"'};
  var div;
  if (opt_document) {
    div = opt_document.createElement('div');
  } else {
    div = goog.global.document.createElement('div');
  }
  // Match as many valid entity characters as possible. If the actual entity
  // happens to be shorter, it will still work as innerHTML will return the
  // trailing characters unchanged. Since the entity characters do not include
  // open angle bracket, there is no chance of XSS from the innerHTML use.
  // Since no whitespace is passed to innerHTML, whitespace is preserved.
  return str.replace(goog.string.HTML_ENTITY_PATTERN_, function(s, entity) {
    // Check for cached entity.
    var value = seen[s];
    if (value) {
      return value;
    }
    // Check for numeric entity.
    if (entity.charAt(0) == '#') {
      // Prefix with 0 so that hex entities (e.g. &#x10) parse as hex numbers.
      var n = Number('0' + entity.substr(1));
      if (!isNaN(n)) {
        value = String.fromCharCode(n);
      }
    }
    // Fall back to innerHTML otherwise.
    if (!value) {
      // Append a non-entity character to avoid a bug in Webkit that parses
      // an invalid entity at the end of innerHTML text as the empty string.
      div.innerHTML = s + ' ';
      // Then remove the trailing character from the result.
      value = div.firstChild.nodeValue.slice(0, -1);
    }
    // Cache and return.
    return seen[s] = value;
  });
};


/**
 * Unescapes XML entities.
 * @private
 * @param {string} str The string to unescape.
 * @return {string} An unescaped copy of {@code str}.
 */
goog.string.unescapePureXmlEntities_ = function(str) {
  return str.replace(/&([^;]+);/g, function(s, entity) {
    switch (entity) {
      case 'amp':
        return '&';
      case 'lt':
        return '<';
      case 'gt':
        return '>';
      case 'quot':
        return '"';
      default:
        if (entity.charAt(0) == '#') {
          // Prefix with 0 so that hex entities (e.g. &#x10) parse as hex.
          var n = Number('0' + entity.substr(1));
          if (!isNaN(n)) {
            return String.fromCharCode(n);
          }
        }
        // For invalid entities we just return the entity
        return s;
    }
  });
};


/**
 * Regular expression that matches an HTML entity.
 * See also HTML5: Tokenization / Tokenizing character references.
 * @private
 * @type {!RegExp}
 */
goog.string.HTML_ENTITY_PATTERN_ = /&([^;\s<&]+);?/g;


/**
 * Do escaping of whitespace to preserve spatial formatting. We use character
 * entity #160 to make it safer for xml.
 * @param {string} str The string in which to escape whitespace.
 * @param {boolean=} opt_xml Whether to use XML compatible tags.
 * @return {string} An escaped copy of {@code str}.
 */
goog.string.whitespaceEscape = function(str, opt_xml) {
  // This doesn't use goog.string.preserveSpaces for backwards compatibility.
  return goog.string.newLineToBr(str.replace(/  /g, ' &#160;'), opt_xml);
};


/**
 * Preserve spaces that would be otherwise collapsed in HTML by replacing them
 * with non-breaking space Unicode characters.
 * @param {string} str The string in which to preserve whitespace.
 * @return {string} A copy of {@code str} with preserved whitespace.
 */
goog.string.preserveSpaces = function(str) {
  return str.replace(/(^|[\n ]) /g, '$1' + goog.string.Unicode.NBSP);
};


/**
 * Strip quote characters around a string.  The second argument is a string of
 * characters to treat as quotes.  This can be a single character or a string of
 * multiple character and in that case each of those are treated as possible
 * quote characters. For example:
 *
 * <pre>
 * goog.string.stripQuotes('"abc"', '"`') --> 'abc'
 * goog.string.stripQuotes('`abc`', '"`') --> 'abc'
 * </pre>
 *
 * @param {string} str The string to strip.
 * @param {string} quoteChars The quote characters to strip.
 * @return {string} A copy of {@code str} without the quotes.
 */
goog.string.stripQuotes = function(str, quoteChars) {
  var length = quoteChars.length;
  for (var i = 0; i < length; i++) {
    var quoteChar = length == 1 ? quoteChars : quoteChars.charAt(i);
    if (str.charAt(0) == quoteChar && str.charAt(str.length - 1) == quoteChar) {
      return str.substring(1, str.length - 1);
    }
  }
  return str;
};


/**
 * Truncates a string to a certain length and adds '...' if necessary.  The
 * length also accounts for the ellipsis, so a maximum length of 10 and a string
 * 'Hello World!' produces 'Hello W...'.
 * @param {string} str The string to truncate.
 * @param {number} chars Max number of characters.
 * @param {boolean=} opt_protectEscapedCharacters Whether to protect escaped
 *     characters from being cut off in the middle.
 * @return {string} The truncated {@code str} string.
 */
goog.string.truncate = function(str, chars, opt_protectEscapedCharacters) {
  if (opt_protectEscapedCharacters) {
    str = goog.string.unescapeEntities(str);
  }

  if (str.length > chars) {
    str = str.substring(0, chars - 3) + '...';
  }

  if (opt_protectEscapedCharacters) {
    str = goog.string.htmlEscape(str);
  }

  return str;
};


/**
 * Truncate a string in the middle, adding "..." if necessary,
 * and favoring the beginning of the string.
 * @param {string} str The string to truncate the middle of.
 * @param {number} chars Max number of characters.
 * @param {boolean=} opt_protectEscapedCharacters Whether to protect escaped
 *     characters from being cutoff in the middle.
 * @param {number=} opt_trailingChars Optional number of trailing characters to
 *     leave at the end of the string, instead of truncating as close to the
 *     middle as possible.
 * @return {string} A truncated copy of {@code str}.
 */
goog.string.truncateMiddle = function(str, chars,
    opt_protectEscapedCharacters, opt_trailingChars) {
  if (opt_protectEscapedCharacters) {
    str = goog.string.unescapeEntities(str);
  }

  if (opt_trailingChars && str.length > chars) {
    if (opt_trailingChars > chars) {
      opt_trailingChars = chars;
    }
    var endPoint = str.length - opt_trailingChars;
    var startPoint = chars - opt_trailingChars;
    str = str.substring(0, startPoint) + '...' + str.substring(endPoint);
  } else if (str.length > chars) {
    // Favor the beginning of the string:
    var half = Math.floor(chars / 2);
    var endPos = str.length - half;
    half += chars % 2;
    str = str.substring(0, half) + '...' + str.substring(endPos);
  }

  if (opt_protectEscapedCharacters) {
    str = goog.string.htmlEscape(str);
  }

  return str;
};


/**
 * Special chars that need to be escaped for goog.string.quote.
 * @private {!Object<string, string>}
 */
goog.string.specialEscapeChars_ = {
  '\0': '\\0',
  '\b': '\\b',
  '\f': '\\f',
  '\n': '\\n',
  '\r': '\\r',
  '\t': '\\t',
  '\x0B': '\\x0B', // '\v' is not supported in JScript
  '"': '\\"',
  '\\': '\\\\'
};


/**
 * Character mappings used internally for goog.string.escapeChar.
 * @private {!Object<string, string>}
 */
goog.string.jsEscapeCache_ = {
  '\'': '\\\''
};


/**
 * Encloses a string in double quotes and escapes characters so that the
 * string is a valid JS string.
 * @param {string} s The string to quote.
 * @return {string} A copy of {@code s} surrounded by double quotes.
 */
goog.string.quote = function(s) {
  s = String(s);
  if (s.quote) {
    return s.quote();
  } else {
    var sb = ['"'];
    for (var i = 0; i < s.length; i++) {
      var ch = s.charAt(i);
      var cc = ch.charCodeAt(0);
      sb[i + 1] = goog.string.specialEscapeChars_[ch] ||
          ((cc > 31 && cc < 127) ? ch : goog.string.escapeChar(ch));
    }
    sb.push('"');
    return sb.join('');
  }
};


/**
 * Takes a string and returns the escaped string for that character.
 * @param {string} str The string to escape.
 * @return {string} An escaped string representing {@code str}.
 */
goog.string.escapeString = function(str) {
  var sb = [];
  for (var i = 0; i < str.length; i++) {
    sb[i] = goog.string.escapeChar(str.charAt(i));
  }
  return sb.join('');
};


/**
 * Takes a character and returns the escaped string for that character. For
 * example escapeChar(String.fromCharCode(15)) -> "\\x0E".
 * @param {string} c The character to escape.
 * @return {string} An escaped string representing {@code c}.
 */
goog.string.escapeChar = function(c) {
  if (c in goog.string.jsEscapeCache_) {
    return goog.string.jsEscapeCache_[c];
  }

  if (c in goog.string.specialEscapeChars_) {
    return goog.string.jsEscapeCache_[c] = goog.string.specialEscapeChars_[c];
  }

  var rv = c;
  var cc = c.charCodeAt(0);
  if (cc > 31 && cc < 127) {
    rv = c;
  } else {
    // tab is 9 but handled above
    if (cc < 256) {
      rv = '\\x';
      if (cc < 16 || cc > 256) {
        rv += '0';
      }
    } else {
      rv = '\\u';
      if (cc < 4096) { // \u1000
        rv += '0';
      }
    }
    rv += cc.toString(16).toUpperCase();
  }

  return goog.string.jsEscapeCache_[c] = rv;
};


/**
 * Determines whether a string contains a substring.
 * @param {string} str The string to search.
 * @param {string} subString The substring to search for.
 * @return {boolean} Whether {@code str} contains {@code subString}.
 */
goog.string.contains = function(str, subString) {
  return str.indexOf(subString) != -1;
};


/**
 * Determines whether a string contains a substring, ignoring case.
 * @param {string} str The string to search.
 * @param {string} subString The substring to search for.
 * @return {boolean} Whether {@code str} contains {@code subString}.
 */
goog.string.caseInsensitiveContains = function(str, subString) {
  return goog.string.contains(str.toLowerCase(), subString.toLowerCase());
};


/**
 * Returns the non-overlapping occurrences of ss in s.
 * If either s or ss evalutes to false, then returns zero.
 * @param {string} s The string to look in.
 * @param {string} ss The string to look for.
 * @return {number} Number of occurrences of ss in s.
 */
goog.string.countOf = function(s, ss) {
  return s && ss ? s.split(ss).length - 1 : 0;
};


/**
 * Removes a substring of a specified length at a specific
 * index in a string.
 * @param {string} s The base string from which to remove.
 * @param {number} index The index at which to remove the substring.
 * @param {number} stringLength The length of the substring to remove.
 * @return {string} A copy of {@code s} with the substring removed or the full
 *     string if nothing is removed or the input is invalid.
 */
goog.string.removeAt = function(s, index, stringLength) {
  var resultStr = s;
  // If the index is greater or equal to 0 then remove substring
  if (index >= 0 && index < s.length && stringLength > 0) {
    resultStr = s.substr(0, index) +
        s.substr(index + stringLength, s.length - index - stringLength);
  }
  return resultStr;
};


/**
 *  Removes the first occurrence of a substring from a string.
 *  @param {string} s The base string from which to remove.
 *  @param {string} ss The string to remove.
 *  @return {string} A copy of {@code s} with {@code ss} removed or the full
 *      string if nothing is removed.
 */
goog.string.remove = function(s, ss) {
  var re = new RegExp(goog.string.regExpEscape(ss), '');
  return s.replace(re, '');
};


/**
 *  Removes all occurrences of a substring from a string.
 *  @param {string} s The base string from which to remove.
 *  @param {string} ss The string to remove.
 *  @return {string} A copy of {@code s} with {@code ss} removed or the full
 *      string if nothing is removed.
 */
goog.string.removeAll = function(s, ss) {
  var re = new RegExp(goog.string.regExpEscape(ss), 'g');
  return s.replace(re, '');
};


/**
 * Escapes characters in the string that are not safe to use in a RegExp.
 * @param {*} s The string to escape. If not a string, it will be casted
 *     to one.
 * @return {string} A RegExp safe, escaped copy of {@code s}.
 */
goog.string.regExpEscape = function(s) {
  return String(s).replace(/([-()\[\]{}+?*.$\^|,:#<!\\])/g, '\\$1').
      replace(/\x08/g, '\\x08');
};


/**
 * Repeats a string n times.
 * @param {string} string The string to repeat.
 * @param {number} length The number of times to repeat.
 * @return {string} A string containing {@code length} repetitions of
 *     {@code string}.
 */
goog.string.repeat = (String.prototype.repeat) ?
    function(string, length) {
      // The native method is over 100 times faster than the alternative.
      return string.repeat(length);
    } :
    function(string, length) {
      return new Array(length + 1).join(string);
    };


/**
 * Pads number to given length and optionally rounds it to a given precision.
 * For example:
 * <pre>padNumber(1.25, 2, 3) -> '01.250'
 * padNumber(1.25, 2) -> '01.25'
 * padNumber(1.25, 2, 1) -> '01.3'
 * padNumber(1.25, 0) -> '1.25'</pre>
 *
 * @param {number} num The number to pad.
 * @param {number} length The desired length.
 * @param {number=} opt_precision The desired precision.
 * @return {string} {@code num} as a string with the given options.
 */
goog.string.padNumber = function(num, length, opt_precision) {
  var s = goog.isDef(opt_precision) ? num.toFixed(opt_precision) : String(num);
  var index = s.indexOf('.');
  if (index == -1) {
    index = s.length;
  }
  return goog.string.repeat('0', Math.max(0, length - index)) + s;
};


/**
 * Returns a string representation of the given object, with
 * null and undefined being returned as the empty string.
 *
 * @param {*} obj The object to convert.
 * @return {string} A string representation of the {@code obj}.
 */
goog.string.makeSafe = function(obj) {
  return obj == null ? '' : String(obj);
};


/**
 * Concatenates string expressions. This is useful
 * since some browsers are very inefficient when it comes to using plus to
 * concat strings. Be careful when using null and undefined here since
 * these will not be included in the result. If you need to represent these
 * be sure to cast the argument to a String first.
 * For example:
 * <pre>buildString('a', 'b', 'c', 'd') -> 'abcd'
 * buildString(null, undefined) -> ''
 * </pre>
 * @param {...*} var_args A list of strings to concatenate. If not a string,
 *     it will be casted to one.
 * @return {string} The concatenation of {@code var_args}.
 */
goog.string.buildString = function(var_args) {
  return Array.prototype.join.call(arguments, '');
};


/**
 * Returns a string with at least 64-bits of randomness.
 *
 * Doesn't trust Javascript's random function entirely. Uses a combination of
 * random and current timestamp, and then encodes the string in base-36 to
 * make it shorter.
 *
 * @return {string} A random string, e.g. sn1s7vb4gcic.
 */
goog.string.getRandomString = function() {
  var x = 2147483648;
  return Math.floor(Math.random() * x).toString(36) +
         Math.abs(Math.floor(Math.random() * x) ^ goog.now()).toString(36);
};


/**
 * Compares two version numbers.
 *
 * @param {string|number} version1 Version of first item.
 * @param {string|number} version2 Version of second item.
 *
 * @return {number}  1 if {@code version1} is higher.
 *                   0 if arguments are equal.
 *                  -1 if {@code version2} is higher.
 */
goog.string.compareVersions = function(version1, version2) {
  var order = 0;
  // Trim leading and trailing whitespace and split the versions into
  // subversions.
  var v1Subs = goog.string.trim(String(version1)).split('.');
  var v2Subs = goog.string.trim(String(version2)).split('.');
  var subCount = Math.max(v1Subs.length, v2Subs.length);

  // Iterate over the subversions, as long as they appear to be equivalent.
  for (var subIdx = 0; order == 0 && subIdx < subCount; subIdx++) {
    var v1Sub = v1Subs[subIdx] || '';
    var v2Sub = v2Subs[subIdx] || '';

    // Split the subversions into pairs of numbers and qualifiers (like 'b').
    // Two different RegExp objects are needed because they are both using
    // the 'g' flag.
    var v1CompParser = new RegExp('(\\d*)(\\D*)', 'g');
    var v2CompParser = new RegExp('(\\d*)(\\D*)', 'g');
    do {
      var v1Comp = v1CompParser.exec(v1Sub) || ['', '', ''];
      var v2Comp = v2CompParser.exec(v2Sub) || ['', '', ''];
      // Break if there are no more matches.
      if (v1Comp[0].length == 0 && v2Comp[0].length == 0) {
        break;
      }

      // Parse the numeric part of the subversion. A missing number is
      // equivalent to 0.
      var v1CompNum = v1Comp[1].length == 0 ? 0 : parseInt(v1Comp[1], 10);
      var v2CompNum = v2Comp[1].length == 0 ? 0 : parseInt(v2Comp[1], 10);

      // Compare the subversion components. The number has the highest
      // precedence. Next, if the numbers are equal, a subversion without any
      // qualifier is always higher than a subversion with any qualifier. Next,
      // the qualifiers are compared as strings.
      order = goog.string.compareElements_(v1CompNum, v2CompNum) ||
          goog.string.compareElements_(v1Comp[2].length == 0,
              v2Comp[2].length == 0) ||
          goog.string.compareElements_(v1Comp[2], v2Comp[2]);
      // Stop as soon as an inequality is discovered.
    } while (order == 0);
  }

  return order;
};


/**
 * Compares elements of a version number.
 *
 * @param {string|number|boolean} left An element from a version number.
 * @param {string|number|boolean} right An element from a version number.
 *
 * @return {number}  1 if {@code left} is higher.
 *                   0 if arguments are equal.
 *                  -1 if {@code right} is higher.
 * @private
 */
goog.string.compareElements_ = function(left, right) {
  if (left < right) {
    return -1;
  } else if (left > right) {
    return 1;
  }
  return 0;
};


/**
 * String hash function similar to java.lang.String.hashCode().
 * The hash code for a string is computed as
 * s[0] * 31 ^ (n - 1) + s[1] * 31 ^ (n - 2) + ... + s[n - 1],
 * where s[i] is the ith character of the string and n is the length of
 * the string. We mod the result to make it between 0 (inclusive) and 2^32
 * (exclusive).
 * @param {string} str A string.
 * @return {number} Hash value for {@code str}, between 0 (inclusive) and 2^32
 *  (exclusive). The empty string returns 0.
 */
goog.string.hashCode = function(str) {
  var result = 0;
  for (var i = 0; i < str.length; ++i) {
    // Normalize to 4 byte range, 0 ... 2^32.
    result = (31 * result + str.charCodeAt(i)) >>> 0;
  }
  return result;
};


/**
 * The most recent unique ID. |0 is equivalent to Math.floor in this case.
 * @type {number}
 * @private
 */
goog.string.uniqueStringCounter_ = Math.random() * 0x80000000 | 0;


/**
 * Generates and returns a string which is unique in the current document.
 * This is useful, for example, to create unique IDs for DOM elements.
 * @return {string} A unique id.
 */
goog.string.createUniqueString = function() {
  return 'goog_' + goog.string.uniqueStringCounter_++;
};


/**
 * Converts the supplied string to a number, which may be Infinity or NaN.
 * This function strips whitespace: (toNumber(' 123') === 123)
 * This function accepts scientific notation: (toNumber('1e1') === 10)
 *
 * This is better than Javascript's built-in conversions because, sadly:
 *     (Number(' ') === 0) and (parseFloat('123a') === 123)
 *
 * @param {string} str The string to convert.
 * @return {number} The number the supplied string represents, or NaN.
 */
goog.string.toNumber = function(str) {
  var num = Number(str);
  if (num == 0 && goog.string.isEmptyOrWhitespace(str)) {
    return NaN;
  }
  return num;
};


/**
 * Returns whether the given string is lower camel case (e.g. "isFooBar").
 *
 * Note that this assumes the string is entirely letters.
 * @see http://en.wikipedia.org/wiki/CamelCase#Variations_and_synonyms
 *
 * @param {string} str String to test.
 * @return {boolean} Whether the string is lower camel case.
 */
goog.string.isLowerCamelCase = function(str) {
  return /^[a-z]+([A-Z][a-z]*)*$/.test(str);
};


/**
 * Returns whether the given string is upper camel case (e.g. "FooBarBaz").
 *
 * Note that this assumes the string is entirely letters.
 * @see http://en.wikipedia.org/wiki/CamelCase#Variations_and_synonyms
 *
 * @param {string} str String to test.
 * @return {boolean} Whether the string is upper camel case.
 */
goog.string.isUpperCamelCase = function(str) {
  return /^([A-Z][a-z]*)+$/.test(str);
};


/**
 * Converts a string from selector-case to camelCase (e.g. from
 * "multi-part-string" to "multiPartString"), useful for converting
 * CSS selectors and HTML dataset keys to their equivalent JS properties.
 * @param {string} str The string in selector-case form.
 * @return {string} The string in camelCase form.
 */
goog.string.toCamelCase = function(str) {
  return String(str).replace(/\-([a-z])/g, function(all, match) {
    return match.toUpperCase();
  });
};


/**
 * Converts a string from camelCase to selector-case (e.g. from
 * "multiPartString" to "multi-part-string"), useful for converting JS
 * style and dataset properties to equivalent CSS selectors and HTML keys.
 * @param {string} str The string in camelCase form.
 * @return {string} The string in selector-case form.
 */
goog.string.toSelectorCase = function(str) {
  return String(str).replace(/([A-Z])/g, '-$1').toLowerCase();
};


/**
 * Converts a string into TitleCase. First character of the string is always
 * capitalized in addition to the first letter of every subsequent word.
 * Words are delimited by one or more whitespaces by default. Custom delimiters
 * can optionally be specified to replace the default, which doesn't preserve
 * whitespace delimiters and instead must be explicitly included if needed.
 *
 * Default delimiter => " ":
 *    goog.string.toTitleCase('oneTwoThree')    => 'OneTwoThree'
 *    goog.string.toTitleCase('one two three')  => 'One Two Three'
 *    goog.string.toTitleCase('  one   two   ') => '  One   Two   '
 *    goog.string.toTitleCase('one_two_three')  => 'One_two_three'
 *    goog.string.toTitleCase('one-two-three')  => 'One-two-three'
 *
 * Custom delimiter => "_-.":
 *    goog.string.toTitleCase('oneTwoThree', '_-.')       => 'OneTwoThree'
 *    goog.string.toTitleCase('one two three', '_-.')     => 'One two three'
 *    goog.string.toTitleCase('  one   two   ', '_-.')    => '  one   two   '
 *    goog.string.toTitleCase('one_two_three', '_-.')     => 'One_Two_Three'
 *    goog.string.toTitleCase('one-two-three', '_-.')     => 'One-Two-Three'
 *    goog.string.toTitleCase('one...two...three', '_-.') => 'One...Two...Three'
 *    goog.string.toTitleCase('one. two. three', '_-.')   => 'One. two. three'
 *    goog.string.toTitleCase('one-two.three', '_-.')     => 'One-Two.Three'
 *
 * @param {string} str String value in camelCase form.
 * @param {string=} opt_delimiters Custom delimiter character set used to
 *      distinguish words in the string value. Each character represents a
 *      single delimiter. When provided, default whitespace delimiter is
 *      overridden and must be explicitly included if needed.
 * @return {string} String value in TitleCase form.
 */
goog.string.toTitleCase = function(str, opt_delimiters) {
  var delimiters = goog.isString(opt_delimiters) ?
      goog.string.regExpEscape(opt_delimiters) : '\\s';

  // For IE8, we need to prevent using an empty character set. Otherwise,
  // incorrect matching will occur.
  delimiters = delimiters ? '|[' + delimiters + ']+' : '';

  var regexp = new RegExp('(^' + delimiters + ')([a-z])', 'g');
  return str.replace(regexp, function(all, p1, p2) {
    return p1 + p2.toUpperCase();
  });
};


/**
 * Capitalizes a string, i.e. converts the first letter to uppercase
 * and all other letters to lowercase, e.g.:
 *
 * goog.string.capitalize('one')     => 'One'
 * goog.string.capitalize('ONE')     => 'One'
 * goog.string.capitalize('one two') => 'One two'
 *
 * Note that this function does not trim initial whitespace.
 *
 * @param {string} str String value to capitalize.
 * @return {string} String value with first letter in uppercase.
 */
goog.string.capitalize = function(str) {
  return String(str.charAt(0)).toUpperCase() +
      String(str.substr(1)).toLowerCase();
};


/**
 * Parse a string in decimal or hexidecimal ('0xFFFF') form.
 *
 * To parse a particular radix, please use parseInt(string, radix) directly. See
 * https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/parseInt
 *
 * This is a wrapper for the built-in parseInt function that will only parse
 * numbers as base 10 or base 16.  Some JS implementations assume strings
 * starting with "0" are intended to be octal. ES3 allowed but discouraged
 * this behavior. ES5 forbids it.  This function emulates the ES5 behavior.
 *
 * For more information, see Mozilla JS Reference: http://goo.gl/8RiFj
 *
 * @param {string|number|null|undefined} value The value to be parsed.
 * @return {number} The number, parsed. If the string failed to parse, this
 *     will be NaN.
 */
goog.string.parseInt = function(value) {
  // Force finite numbers to strings.
  if (isFinite(value)) {
    value = String(value);
  }

  if (goog.isString(value)) {
    // If the string starts with '0x' or '-0x', parse as hex.
    return /^\s*-?0x/i.test(value) ?
        parseInt(value, 16) : parseInt(value, 10);
  }

  return NaN;
};


/**
 * Splits a string on a separator a limited number of times.
 *
 * This implementation is more similar to Python or Java, where the limit
 * parameter specifies the maximum number of splits rather than truncating
 * the number of results.
 *
 * See http://docs.python.org/2/library/stdtypes.html#str.split
 * See JavaDoc: http://goo.gl/F2AsY
 * See Mozilla reference: http://goo.gl/dZdZs
 *
 * @param {string} str String to split.
 * @param {string} separator The separator.
 * @param {number} limit The limit to the number of splits. The resulting array
 *     will have a maximum length of limit+1.  Negative numbers are the same
 *     as zero.
 * @return {!Array<string>} The string, split.
 */

goog.string.splitLimit = function(str, separator, limit) {
  var parts = str.split(separator);
  var returnVal = [];

  // Only continue doing this while we haven't hit the limit and we have
  // parts left.
  while (limit > 0 && parts.length) {
    returnVal.push(parts.shift());
    limit--;
  }

  // If there are remaining parts, append them to the end.
  if (parts.length) {
    returnVal.push(parts.join(separator));
  }

  return returnVal;
};


/**
 * Computes the Levenshtein edit distance between two strings.
 * @param {string} a
 * @param {string} b
 * @return {number} The edit distance between the two strings.
 */
goog.string.editDistance = function(a, b) {
  var v0 = [];
  var v1 = [];

  if (a == b) {
    return 0;
  }

  if (!a.length || !b.length) {
    return Math.max(a.length, b.length);
  }

  for (var i = 0; i < b.length + 1; i++) {
    v0[i] = i;
  }

  for (var i = 0; i < a.length; i++) {
    v1[0] = i + 1;

    for (var j = 0; j < b.length; j++) {
      var cost = a[i] != b[j];
      // Cost for the substring is the minimum of adding one character, removing
      // one character, or a swap.
      v1[j + 1] = Math.min(v1[j] + 1, v0[j + 1] + 1, v0[j] + cost);
    }

    for (var j = 0; j < v0.length; j++) {
      v0[j] = v1[j];
    }
  }

  return v1[b.length];
};

// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Utilities to check the preconditions, postconditions and
 * invariants runtime.
 *
 * Methods in this package should be given special treatment by the compiler
 * for type-inference. For example, <code>goog.asserts.assert(foo)</code>
 * will restrict <code>foo</code> to a truthy value.
 *
 * The compiler has an option to disable asserts. So code like:
 * <code>
 * var x = goog.asserts.assert(foo()); goog.asserts.assert(bar());
 * </code>
 * will be transformed into:
 * <code>
 * var x = foo();
 * </code>
 * The compiler will leave in foo() (because its return value is used),
 * but it will remove bar() because it assumes it does not have side-effects.
 *
 * @author agrieve@google.com (Andrew Grieve)
 */

goog.provide('goog.asserts');
goog.provide('goog.asserts.AssertionError');

goog.require('goog.debug.Error');
goog.require('goog.dom.NodeType');
goog.require('goog.string');


/**
 * @define {boolean} Whether to strip out asserts or to leave them in.
 */
goog.define('goog.asserts.ENABLE_ASSERTS', goog.DEBUG);



/**
 * Error object for failed assertions.
 * @param {string} messagePattern The pattern that was used to form message.
 * @param {!Array<*>} messageArgs The items to substitute into the pattern.
 * @constructor
 * @extends {goog.debug.Error}
 * @final
 */
goog.asserts.AssertionError = function(messagePattern, messageArgs) {
  messageArgs.unshift(messagePattern);
  goog.debug.Error.call(this, goog.string.subs.apply(null, messageArgs));
  // Remove the messagePattern afterwards to avoid permanently modifying the
  // passed in array.
  messageArgs.shift();

  /**
   * The message pattern used to format the error message. Error handlers can
   * use this to uniquely identify the assertion.
   * @type {string}
   */
  this.messagePattern = messagePattern;
};
goog.inherits(goog.asserts.AssertionError, goog.debug.Error);


/** @override */
goog.asserts.AssertionError.prototype.name = 'AssertionError';


/**
 * The default error handler.
 * @param {!goog.asserts.AssertionError} e The exception to be handled.
 */
goog.asserts.DEFAULT_ERROR_HANDLER = function(e) { throw e; };


/**
 * The handler responsible for throwing or logging assertion errors.
 * @private {function(!goog.asserts.AssertionError)}
 */
goog.asserts.errorHandler_ = goog.asserts.DEFAULT_ERROR_HANDLER;


/**
 * Throws an exception with the given message and "Assertion failed" prefixed
 * onto it.
 * @param {string} defaultMessage The message to use if givenMessage is empty.
 * @param {Array<*>} defaultArgs The substitution arguments for defaultMessage.
 * @param {string|undefined} givenMessage Message supplied by the caller.
 * @param {Array<*>} givenArgs The substitution arguments for givenMessage.
 * @throws {goog.asserts.AssertionError} When the value is not a number.
 * @private
 */
goog.asserts.doAssertFailure_ =
    function(defaultMessage, defaultArgs, givenMessage, givenArgs) {
  var message = 'Assertion failed';
  if (givenMessage) {
    message += ': ' + givenMessage;
    var args = givenArgs;
  } else if (defaultMessage) {
    message += ': ' + defaultMessage;
    args = defaultArgs;
  }
  // The '' + works around an Opera 10 bug in the unit tests. Without it,
  // a stack trace is added to var message above. With this, a stack trace is
  // not added until this line (it causes the extra garbage to be added after
  // the assertion message instead of in the middle of it).
  var e = new goog.asserts.AssertionError('' + message, args || []);
  goog.asserts.errorHandler_(e);
};


/**
 * Sets a custom error handler that can be used to customize the behavior of
 * assertion failures, for example by turning all assertion failures into log
 * messages.
 * @param {function(!goog.asserts.AssertionError)} errorHandler
 */
goog.asserts.setErrorHandler = function(errorHandler) {
  if (goog.asserts.ENABLE_ASSERTS) {
    goog.asserts.errorHandler_ = errorHandler;
  }
};


/**
 * Checks if the condition evaluates to true if goog.asserts.ENABLE_ASSERTS is
 * true.
 * @template T
 * @param {T} condition The condition to check.
 * @param {string=} opt_message Error message in case of failure.
 * @param {...*} var_args The items to substitute into the failure message.
 * @return {T} The value of the condition.
 * @throws {goog.asserts.AssertionError} When the condition evaluates to false.
 */
goog.asserts.assert = function(condition, opt_message, var_args) {
  if (goog.asserts.ENABLE_ASSERTS && !condition) {
    goog.asserts.doAssertFailure_('', null, opt_message,
        Array.prototype.slice.call(arguments, 2));
  }
  return condition;
};


/**
 * Fails if goog.asserts.ENABLE_ASSERTS is true. This function is useful in case
 * when we want to add a check in the unreachable area like switch-case
 * statement:
 *
 * <pre>
 *  switch(type) {
 *    case FOO: doSomething(); break;
 *    case BAR: doSomethingElse(); break;
 *    default: goog.assert.fail('Unrecognized type: ' + type);
 *      // We have only 2 types - "default:" section is unreachable code.
 *  }
 * </pre>
 *
 * @param {string=} opt_message Error message in case of failure.
 * @param {...*} var_args The items to substitute into the failure message.
 * @throws {goog.asserts.AssertionError} Failure.
 */
goog.asserts.fail = function(opt_message, var_args) {
  if (goog.asserts.ENABLE_ASSERTS) {
    goog.asserts.errorHandler_(new goog.asserts.AssertionError(
        'Failure' + (opt_message ? ': ' + opt_message : ''),
        Array.prototype.slice.call(arguments, 1)));
  }
};


/**
 * Checks if the value is a number if goog.asserts.ENABLE_ASSERTS is true.
 * @param {*} value The value to check.
 * @param {string=} opt_message Error message in case of failure.
 * @param {...*} var_args The items to substitute into the failure message.
 * @return {number} The value, guaranteed to be a number when asserts enabled.
 * @throws {goog.asserts.AssertionError} When the value is not a number.
 */
goog.asserts.assertNumber = function(value, opt_message, var_args) {
  if (goog.asserts.ENABLE_ASSERTS && !goog.isNumber(value)) {
    goog.asserts.doAssertFailure_('Expected number but got %s: %s.',
        [goog.typeOf(value), value], opt_message,
        Array.prototype.slice.call(arguments, 2));
  }
  return /** @type {number} */ (value);
};


/**
 * Checks if the value is a string if goog.asserts.ENABLE_ASSERTS is true.
 * @param {*} value The value to check.
 * @param {string=} opt_message Error message in case of failure.
 * @param {...*} var_args The items to substitute into the failure message.
 * @return {string} The value, guaranteed to be a string when asserts enabled.
 * @throws {goog.asserts.AssertionError} When the value is not a string.
 */
goog.asserts.assertString = function(value, opt_message, var_args) {
  if (goog.asserts.ENABLE_ASSERTS && !goog.isString(value)) {
    goog.asserts.doAssertFailure_('Expected string but got %s: %s.',
        [goog.typeOf(value), value], opt_message,
        Array.prototype.slice.call(arguments, 2));
  }
  return /** @type {string} */ (value);
};


/**
 * Checks if the value is a function if goog.asserts.ENABLE_ASSERTS is true.
 * @param {*} value The value to check.
 * @param {string=} opt_message Error message in case of failure.
 * @param {...*} var_args The items to substitute into the failure message.
 * @return {!Function} The value, guaranteed to be a function when asserts
 *     enabled.
 * @throws {goog.asserts.AssertionError} When the value is not a function.
 */
goog.asserts.assertFunction = function(value, opt_message, var_args) {
  if (goog.asserts.ENABLE_ASSERTS && !goog.isFunction(value)) {
    goog.asserts.doAssertFailure_('Expected function but got %s: %s.',
        [goog.typeOf(value), value], opt_message,
        Array.prototype.slice.call(arguments, 2));
  }
  return /** @type {!Function} */ (value);
};


/**
 * Checks if the value is an Object if goog.asserts.ENABLE_ASSERTS is true.
 * @param {*} value The value to check.
 * @param {string=} opt_message Error message in case of failure.
 * @param {...*} var_args The items to substitute into the failure message.
 * @return {!Object} The value, guaranteed to be a non-null object.
 * @throws {goog.asserts.AssertionError} When the value is not an object.
 */
goog.asserts.assertObject = function(value, opt_message, var_args) {
  if (goog.asserts.ENABLE_ASSERTS && !goog.isObject(value)) {
    goog.asserts.doAssertFailure_('Expected object but got %s: %s.',
        [goog.typeOf(value), value],
        opt_message, Array.prototype.slice.call(arguments, 2));
  }
  return /** @type {!Object} */ (value);
};


/**
 * Checks if the value is an Array if goog.asserts.ENABLE_ASSERTS is true.
 * @param {*} value The value to check.
 * @param {string=} opt_message Error message in case of failure.
 * @param {...*} var_args The items to substitute into the failure message.
 * @return {!Array<?>} The value, guaranteed to be a non-null array.
 * @throws {goog.asserts.AssertionError} When the value is not an array.
 */
goog.asserts.assertArray = function(value, opt_message, var_args) {
  if (goog.asserts.ENABLE_ASSERTS && !goog.isArray(value)) {
    goog.asserts.doAssertFailure_('Expected array but got %s: %s.',
        [goog.typeOf(value), value], opt_message,
        Array.prototype.slice.call(arguments, 2));
  }
  return /** @type {!Array<?>} */ (value);
};


/**
 * Checks if the value is a boolean if goog.asserts.ENABLE_ASSERTS is true.
 * @param {*} value The value to check.
 * @param {string=} opt_message Error message in case of failure.
 * @param {...*} var_args The items to substitute into the failure message.
 * @return {boolean} The value, guaranteed to be a boolean when asserts are
 *     enabled.
 * @throws {goog.asserts.AssertionError} When the value is not a boolean.
 */
goog.asserts.assertBoolean = function(value, opt_message, var_args) {
  if (goog.asserts.ENABLE_ASSERTS && !goog.isBoolean(value)) {
    goog.asserts.doAssertFailure_('Expected boolean but got %s: %s.',
        [goog.typeOf(value), value], opt_message,
        Array.prototype.slice.call(arguments, 2));
  }
  return /** @type {boolean} */ (value);
};


/**
 * Checks if the value is a DOM Element if goog.asserts.ENABLE_ASSERTS is true.
 * @param {*} value The value to check.
 * @param {string=} opt_message Error message in case of failure.
 * @param {...*} var_args The items to substitute into the failure message.
 * @return {!Element} The value, likely to be a DOM Element when asserts are
 *     enabled.
 * @throws {goog.asserts.AssertionError} When the value is not an Element.
 */
goog.asserts.assertElement = function(value, opt_message, var_args) {
  if (goog.asserts.ENABLE_ASSERTS && (!goog.isObject(value) ||
      value.nodeType != goog.dom.NodeType.ELEMENT)) {
    goog.asserts.doAssertFailure_('Expected Element but got %s: %s.',
        [goog.typeOf(value), value], opt_message,
        Array.prototype.slice.call(arguments, 2));
  }
  return /** @type {!Element} */ (value);
};


/**
 * Checks if the value is an instance of the user-defined type if
 * goog.asserts.ENABLE_ASSERTS is true.
 *
 * The compiler may tighten the type returned by this function.
 *
 * @param {*} value The value to check.
 * @param {function(new: T, ...)} type A user-defined constructor.
 * @param {string=} opt_message Error message in case of failure.
 * @param {...*} var_args The items to substitute into the failure message.
 * @throws {goog.asserts.AssertionError} When the value is not an instance of
 *     type.
 * @return {T}
 * @template T
 */
goog.asserts.assertInstanceof = function(value, type, opt_message, var_args) {
  if (goog.asserts.ENABLE_ASSERTS && !(value instanceof type)) {
    goog.asserts.doAssertFailure_('Expected instanceof %s but got %s.',
        [goog.asserts.getType_(type), goog.asserts.getType_(value)],
        opt_message, Array.prototype.slice.call(arguments, 3));
  }
  return value;
};


/**
 * Checks that no enumerable keys are present in Object.prototype. Such keys
 * would break most code that use {@code for (var ... in ...)} loops.
 */
goog.asserts.assertObjectPrototypeIsIntact = function() {
  for (var key in Object.prototype) {
    goog.asserts.fail(key + ' should not be enumerable in Object.prototype.');
  }
};


/**
 * Returns the type of a value. If a constructor is passed, and a suitable
 * string cannot be found, 'unknown type name' will be returned.
 * @param {*} value A constructor, object, or primitive.
 * @return {string} The best display name for the value, or 'unknown type name'.
 * @private
 */
goog.asserts.getType_ = function(value) {
  if (value instanceof Function) {
    return value.displayName || value.name || 'unknown type name';
  } else if (value instanceof Object) {
    return value.constructor.displayName || value.constructor.name ||
        Object.prototype.toString.call(value);
  } else {
    return value === null ? 'null' : typeof value;
  }
};

goog.provide('ol.math');

goog.require('goog.asserts');


/**
 * Takes a number and clamps it to within the provided bounds.
 * @param {number} value The input number.
 * @param {number} min The minimum value to return.
 * @param {number} max The maximum value to return.
 * @return {number} The input number if it is within bounds, or the nearest
 *     number within the bounds.
 */
ol.math.clamp = function(value, min, max) {
  return Math.min(Math.max(value, min), max);
};


/**
 * Return the hyperbolic cosine of a given number. The method will use the
 * native `Math.cosh` function if it is available, otherwise the hyperbolic
 * cosine will be calculated via the reference implementation of the Mozilla
 * developer network.
 *
 * @param {number} x X.
 * @return {number} Hyperbolic cosine of x.
 */
ol.math.cosh = (function() {
  // Wrapped in a iife, to save the overhead of checking for the native
  // implementation on every invocation.
  var cosh;
  if ('cosh' in Math) {
    // The environment supports the native Math.cosh function, use it…
    cosh = Math.cosh;
  } else {
    // … else, use the reference implementation of MDN:
    cosh = function(x) {
      var y = Math.exp(x);
      return (y + 1 / y) / 2;
    };
  }
  return cosh;
}());


/**
 * @param {number} x X.
 * @return {number} The smallest power of two greater than or equal to x.
 */
ol.math.roundUpToPowerOfTwo = function(x) {
  goog.asserts.assert(0 < x, 'x should be larger than 0');
  return Math.pow(2, Math.ceil(Math.log(x) / Math.LN2));
};


/**
 * Returns the square of the closest distance between the point (x, y) and the
 * line segment (x1, y1) to (x2, y2).
 * @param {number} x X.
 * @param {number} y Y.
 * @param {number} x1 X1.
 * @param {number} y1 Y1.
 * @param {number} x2 X2.
 * @param {number} y2 Y2.
 * @return {number} Squared distance.
 */
ol.math.squaredSegmentDistance = function(x, y, x1, y1, x2, y2) {
  var dx = x2 - x1;
  var dy = y2 - y1;
  if (dx !== 0 || dy !== 0) {
    var t = ((x - x1) * dx + (y - y1) * dy) / (dx * dx + dy * dy);
    if (t > 1) {
      x1 = x2;
      y1 = y2;
    } else if (t > 0) {
      x1 += dx * t;
      y1 += dy * t;
    }
  }
  return ol.math.squaredDistance(x, y, x1, y1);
};


/**
 * Returns the square of the distance between the points (x1, y1) and (x2, y2).
 * @param {number} x1 X1.
 * @param {number} y1 Y1.
 * @param {number} x2 X2.
 * @param {number} y2 Y2.
 * @return {number} Squared distance.
 */
ol.math.squaredDistance = function(x1, y1, x2, y2) {
  var dx = x2 - x1;
  var dy = y2 - y1;
  return dx * dx + dy * dy;
};


/**
 * Solves system of linear equations using Gaussian elimination method.
 *
 * @param {Array.<Array.<number>>} mat Augmented matrix (n x n + 1 column)
 *                                     in row-major order.
 * @return {Array.<number>} The resulting vector.
 */
ol.math.solveLinearSystem = function(mat) {
  var n = mat.length;

  if (goog.asserts.ENABLE_ASSERTS) {
    for (var row = 0; row < n; row++) {
      goog.asserts.assert(mat[row].length == n + 1,
                          'every row should have correct number of columns');
    }
  }

  for (var i = 0; i < n; i++) {
    // Find max in the i-th column (ignoring i - 1 first rows)
    var maxRow = i;
    var maxEl = Math.abs(mat[i][i]);
    for (var r = i + 1; r < n; r++) {
      var absValue = Math.abs(mat[r][i]);
      if (absValue > maxEl) {
        maxEl = absValue;
        maxRow = r;
      }
    }

    if (maxEl === 0) {
      return null; // matrix is singular
    }

    // Swap max row with i-th (current) row
    var tmp = mat[maxRow];
    mat[maxRow] = mat[i];
    mat[i] = tmp;

    // Subtract the i-th row to make all the remaining rows 0 in the i-th column
    for (var j = i + 1; j < n; j++) {
      var coef = -mat[j][i] / mat[i][i];
      for (var k = i; k < n + 1; k++) {
        if (i == k) {
          mat[j][k] = 0;
        } else {
          mat[j][k] += coef * mat[i][k];
        }
      }
    }
  }

  // Solve Ax=b for upper triangular matrix A (mat)
  var x = new Array(n);
  for (var l = n - 1; l >= 0; l--) {
    x[l] = mat[l][n] / mat[l][l];
    for (var m = l - 1; m >= 0; m--) {
      mat[m][n] -= mat[m][l] * x[l];
    }
  }
  return x;
};


/**
 * Converts radians to to degrees.
 *
 * @param {number} angleInRadians Angle in radians.
 * @return {number} Angle in degrees.
 */
ol.math.toDegrees = function(angleInRadians) {
  return angleInRadians * 180 / Math.PI;
};


/**
 * Converts degrees to radians.
 *
 * @param {number} angleInDegrees Angle in degrees.
 * @return {number} Angle in radians.
 */
ol.math.toRadians = function(angleInDegrees) {
  return angleInDegrees * Math.PI / 180;
};

goog.provide('ol.CenterConstraint');
goog.provide('ol.CenterConstraintType');

goog.require('ol.math');


/**
 * @typedef {function((ol.Coordinate|undefined)): (ol.Coordinate|undefined)}
 */
ol.CenterConstraintType;


/**
 * @param {ol.Extent} extent Extent.
 * @return {ol.CenterConstraintType} The constraint.
 */
ol.CenterConstraint.createExtent = function(extent) {
  return (
      /**
       * @param {ol.Coordinate|undefined} center Center.
       * @return {ol.Coordinate|undefined} Center.
       */
      function(center) {
        if (center) {
          return [
            ol.math.clamp(center[0], extent[0], extent[2]),
            ol.math.clamp(center[1], extent[1], extent[3])
          ];
        } else {
          return undefined;
        }
      });
};


/**
 * @param {ol.Coordinate|undefined} center Center.
 * @return {ol.Coordinate|undefined} Center.
 */
ol.CenterConstraint.none = function(center) {
  return center;
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Utilities for manipulating arrays.
 *
 * @author arv@google.com (Erik Arvidsson)
 */


goog.provide('goog.array');
goog.provide('goog.array.ArrayLike');

goog.require('goog.asserts');


/**
 * @define {boolean} NATIVE_ARRAY_PROTOTYPES indicates whether the code should
 * rely on Array.prototype functions, if available.
 *
 * The Array.prototype functions can be defined by external libraries like
 * Prototype and setting this flag to false forces closure to use its own
 * goog.array implementation.
 *
 * If your javascript can be loaded by a third party site and you are wary about
 * relying on the prototype functions, specify
 * "--define goog.NATIVE_ARRAY_PROTOTYPES=false" to the JSCompiler.
 *
 * Setting goog.TRUSTED_SITE to false will automatically set
 * NATIVE_ARRAY_PROTOTYPES to false.
 */
goog.define('goog.NATIVE_ARRAY_PROTOTYPES', goog.TRUSTED_SITE);


/**
 * @define {boolean} If true, JSCompiler will use the native implementation of
 * array functions where appropriate (e.g., {@code Array#filter}) and remove the
 * unused pure JS implementation.
 */
goog.define('goog.array.ASSUME_NATIVE_FUNCTIONS', false);


/**
 * @typedef {Array|NodeList|Arguments|{length: number}}
 */
goog.array.ArrayLike;


/**
 * Returns the last element in an array without removing it.
 * Same as goog.array.last.
 * @param {Array<T>|goog.array.ArrayLike} array The array.
 * @return {T} Last item in array.
 * @template T
 */
goog.array.peek = function(array) {
  return array[array.length - 1];
};


/**
 * Returns the last element in an array without removing it.
 * Same as goog.array.peek.
 * @param {Array<T>|goog.array.ArrayLike} array The array.
 * @return {T} Last item in array.
 * @template T
 */
goog.array.last = goog.array.peek;


/**
 * Reference to the original {@code Array.prototype}.
 * @private
 */
goog.array.ARRAY_PROTOTYPE_ = Array.prototype;


// NOTE(arv): Since most of the array functions are generic it allows you to
// pass an array-like object. Strings have a length and are considered array-
// like. However, the 'in' operator does not work on strings so we cannot just
// use the array path even if the browser supports indexing into strings. We
// therefore end up splitting the string.


/**
 * Returns the index of the first element of an array with a specified value, or
 * -1 if the element is not present in the array.
 *
 * See {@link http://tinyurl.com/developer-mozilla-org-array-indexof}
 *
 * @param {Array<T>|goog.array.ArrayLike} arr The array to be searched.
 * @param {T} obj The object for which we are searching.
 * @param {number=} opt_fromIndex The index at which to start the search. If
 *     omitted the search starts at index 0.
 * @return {number} The index of the first matching array element.
 * @template T
 */
goog.array.indexOf = goog.NATIVE_ARRAY_PROTOTYPES &&
                     (goog.array.ASSUME_NATIVE_FUNCTIONS ||
                      goog.array.ARRAY_PROTOTYPE_.indexOf) ?
    function(arr, obj, opt_fromIndex) {
      goog.asserts.assert(arr.length != null);

      return goog.array.ARRAY_PROTOTYPE_.indexOf.call(arr, obj, opt_fromIndex);
    } :
    function(arr, obj, opt_fromIndex) {
      var fromIndex = opt_fromIndex == null ?
          0 : (opt_fromIndex < 0 ?
               Math.max(0, arr.length + opt_fromIndex) : opt_fromIndex);

      if (goog.isString(arr)) {
        // Array.prototype.indexOf uses === so only strings should be found.
        if (!goog.isString(obj) || obj.length != 1) {
          return -1;
        }
        return arr.indexOf(obj, fromIndex);
      }

      for (var i = fromIndex; i < arr.length; i++) {
        if (i in arr && arr[i] === obj)
          return i;
      }
      return -1;
    };


/**
 * Returns the index of the last element of an array with a specified value, or
 * -1 if the element is not present in the array.
 *
 * See {@link http://tinyurl.com/developer-mozilla-org-array-lastindexof}
 *
 * @param {!Array<T>|!goog.array.ArrayLike} arr The array to be searched.
 * @param {T} obj The object for which we are searching.
 * @param {?number=} opt_fromIndex The index at which to start the search. If
 *     omitted the search starts at the end of the array.
 * @return {number} The index of the last matching array element.
 * @template T
 */
goog.array.lastIndexOf = goog.NATIVE_ARRAY_PROTOTYPES &&
                         (goog.array.ASSUME_NATIVE_FUNCTIONS ||
                          goog.array.ARRAY_PROTOTYPE_.lastIndexOf) ?
    function(arr, obj, opt_fromIndex) {
      goog.asserts.assert(arr.length != null);

      // Firefox treats undefined and null as 0 in the fromIndex argument which
      // leads it to always return -1
      var fromIndex = opt_fromIndex == null ? arr.length - 1 : opt_fromIndex;
      return goog.array.ARRAY_PROTOTYPE_.lastIndexOf.call(arr, obj, fromIndex);
    } :
    function(arr, obj, opt_fromIndex) {
      var fromIndex = opt_fromIndex == null ? arr.length - 1 : opt_fromIndex;

      if (fromIndex < 0) {
        fromIndex = Math.max(0, arr.length + fromIndex);
      }

      if (goog.isString(arr)) {
        // Array.prototype.lastIndexOf uses === so only strings should be found.
        if (!goog.isString(obj) || obj.length != 1) {
          return -1;
        }
        return arr.lastIndexOf(obj, fromIndex);
      }

      for (var i = fromIndex; i >= 0; i--) {
        if (i in arr && arr[i] === obj)
          return i;
      }
      return -1;
    };


/**
 * Calls a function for each element in an array. Skips holes in the array.
 * See {@link http://tinyurl.com/developer-mozilla-org-array-foreach}
 *
 * @param {Array<T>|goog.array.ArrayLike} arr Array or array like object over
 *     which to iterate.
 * @param {?function(this: S, T, number, ?): ?} f The function to call for every
 *     element. This function takes 3 arguments (the element, the index and the
 *     array). The return value is ignored.
 * @param {S=} opt_obj The object to be used as the value of 'this' within f.
 * @template T,S
 */
goog.array.forEach = goog.NATIVE_ARRAY_PROTOTYPES &&
                     (goog.array.ASSUME_NATIVE_FUNCTIONS ||
                      goog.array.ARRAY_PROTOTYPE_.forEach) ?
    function(arr, f, opt_obj) {
      goog.asserts.assert(arr.length != null);

      goog.array.ARRAY_PROTOTYPE_.forEach.call(arr, f, opt_obj);
    } :
    function(arr, f, opt_obj) {
      var l = arr.length;  // must be fixed during loop... see docs
      var arr2 = goog.isString(arr) ? arr.split('') : arr;
      for (var i = 0; i < l; i++) {
        if (i in arr2) {
          f.call(opt_obj, arr2[i], i, arr);
        }
      }
    };


/**
 * Calls a function for each element in an array, starting from the last
 * element rather than the first.
 *
 * @param {Array<T>|goog.array.ArrayLike} arr Array or array
 *     like object over which to iterate.
 * @param {?function(this: S, T, number, ?): ?} f The function to call for every
 *     element. This function
 *     takes 3 arguments (the element, the index and the array). The return
 *     value is ignored.
 * @param {S=} opt_obj The object to be used as the value of 'this'
 *     within f.
 * @template T,S
 */
goog.array.forEachRight = function(arr, f, opt_obj) {
  var l = arr.length;  // must be fixed during loop... see docs
  var arr2 = goog.isString(arr) ? arr.split('') : arr;
  for (var i = l - 1; i >= 0; --i) {
    if (i in arr2) {
      f.call(opt_obj, arr2[i], i, arr);
    }
  }
};


/**
 * Calls a function for each element in an array, and if the function returns
 * true adds the element to a new array.
 *
 * See {@link http://tinyurl.com/developer-mozilla-org-array-filter}
 *
 * @param {Array<T>|goog.array.ArrayLike} arr Array or array
 *     like object over which to iterate.
 * @param {?function(this:S, T, number, ?):boolean} f The function to call for
 *     every element. This function
 *     takes 3 arguments (the element, the index and the array) and must
 *     return a Boolean. If the return value is true the element is added to the
 *     result array. If it is false the element is not included.
 * @param {S=} opt_obj The object to be used as the value of 'this'
 *     within f.
 * @return {!Array<T>} a new array in which only elements that passed the test
 *     are present.
 * @template T,S
 */
goog.array.filter = goog.NATIVE_ARRAY_PROTOTYPES &&
                    (goog.array.ASSUME_NATIVE_FUNCTIONS ||
                     goog.array.ARRAY_PROTOTYPE_.filter) ?
    function(arr, f, opt_obj) {
      goog.asserts.assert(arr.length != null);

      return goog.array.ARRAY_PROTOTYPE_.filter.call(arr, f, opt_obj);
    } :
    function(arr, f, opt_obj) {
      var l = arr.length;  // must be fixed during loop... see docs
      var res = [];
      var resLength = 0;
      var arr2 = goog.isString(arr) ? arr.split('') : arr;
      for (var i = 0; i < l; i++) {
        if (i in arr2) {
          var val = arr2[i];  // in case f mutates arr2
          if (f.call(opt_obj, val, i, arr)) {
            res[resLength++] = val;
          }
        }
      }
      return res;
    };


/**
 * Calls a function for each element in an array and inserts the result into a
 * new array.
 *
 * See {@link http://tinyurl.com/developer-mozilla-org-array-map}
 *
 * @param {Array<VALUE>|goog.array.ArrayLike} arr Array or array like object
 *     over which to iterate.
 * @param {function(this:THIS, VALUE, number, ?): RESULT} f The function to call
 *     for every element. This function takes 3 arguments (the element,
 *     the index and the array) and should return something. The result will be
 *     inserted into a new array.
 * @param {THIS=} opt_obj The object to be used as the value of 'this' within f.
 * @return {!Array<RESULT>} a new array with the results from f.
 * @template THIS, VALUE, RESULT
 */
goog.array.map = goog.NATIVE_ARRAY_PROTOTYPES &&
                 (goog.array.ASSUME_NATIVE_FUNCTIONS ||
                  goog.array.ARRAY_PROTOTYPE_.map) ?
    function(arr, f, opt_obj) {
      goog.asserts.assert(arr.length != null);

      return goog.array.ARRAY_PROTOTYPE_.map.call(arr, f, opt_obj);
    } :
    function(arr, f, opt_obj) {
      var l = arr.length;  // must be fixed during loop... see docs
      var res = new Array(l);
      var arr2 = goog.isString(arr) ? arr.split('') : arr;
      for (var i = 0; i < l; i++) {
        if (i in arr2) {
          res[i] = f.call(opt_obj, arr2[i], i, arr);
        }
      }
      return res;
    };


/**
 * Passes every element of an array into a function and accumulates the result.
 *
 * See {@link http://tinyurl.com/developer-mozilla-org-array-reduce}
 *
 * For example:
 * var a = [1, 2, 3, 4];
 * goog.array.reduce(a, function(r, v, i, arr) {return r + v;}, 0);
 * returns 10
 *
 * @param {Array<T>|goog.array.ArrayLike} arr Array or array
 *     like object over which to iterate.
 * @param {function(this:S, R, T, number, ?) : R} f The function to call for
 *     every element. This function
 *     takes 4 arguments (the function's previous result or the initial value,
 *     the value of the current array element, the current array index, and the
 *     array itself)
 *     function(previousValue, currentValue, index, array).
 * @param {?} val The initial value to pass into the function on the first call.
 * @param {S=} opt_obj  The object to be used as the value of 'this'
 *     within f.
 * @return {R} Result of evaluating f repeatedly across the values of the array.
 * @template T,S,R
 */
goog.array.reduce = goog.NATIVE_ARRAY_PROTOTYPES &&
                    (goog.array.ASSUME_NATIVE_FUNCTIONS ||
                     goog.array.ARRAY_PROTOTYPE_.reduce) ?
    function(arr, f, val, opt_obj) {
      goog.asserts.assert(arr.length != null);
      if (opt_obj) {
        f = goog.bind(f, opt_obj);
      }
      return goog.array.ARRAY_PROTOTYPE_.reduce.call(arr, f, val);
    } :
    function(arr, f, val, opt_obj) {
      var rval = val;
      goog.array.forEach(arr, function(val, index) {
        rval = f.call(opt_obj, rval, val, index, arr);
      });
      return rval;
    };


/**
 * Passes every element of an array into a function and accumulates the result,
 * starting from the last element and working towards the first.
 *
 * See {@link http://tinyurl.com/developer-mozilla-org-array-reduceright}
 *
 * For example:
 * var a = ['a', 'b', 'c'];
 * goog.array.reduceRight(a, function(r, v, i, arr) {return r + v;}, '');
 * returns 'cba'
 *
 * @param {Array<T>|goog.array.ArrayLike} arr Array or array
 *     like object over which to iterate.
 * @param {?function(this:S, R, T, number, ?) : R} f The function to call for
 *     every element. This function
 *     takes 4 arguments (the function's previous result or the initial value,
 *     the value of the current array element, the current array index, and the
 *     array itself)
 *     function(previousValue, currentValue, index, array).
 * @param {?} val The initial value to pass into the function on the first call.
 * @param {S=} opt_obj The object to be used as the value of 'this'
 *     within f.
 * @return {R} Object returned as a result of evaluating f repeatedly across the
 *     values of the array.
 * @template T,S,R
 */
goog.array.reduceRight = goog.NATIVE_ARRAY_PROTOTYPES &&
                         (goog.array.ASSUME_NATIVE_FUNCTIONS ||
                          goog.array.ARRAY_PROTOTYPE_.reduceRight) ?
    function(arr, f, val, opt_obj) {
      goog.asserts.assert(arr.length != null);
      if (opt_obj) {
        f = goog.bind(f, opt_obj);
      }
      return goog.array.ARRAY_PROTOTYPE_.reduceRight.call(arr, f, val);
    } :
    function(arr, f, val, opt_obj) {
      var rval = val;
      goog.array.forEachRight(arr, function(val, index) {
        rval = f.call(opt_obj, rval, val, index, arr);
      });
      return rval;
    };


/**
 * Calls f for each element of an array. If any call returns true, some()
 * returns true (without checking the remaining elements). If all calls
 * return false, some() returns false.
 *
 * See {@link http://tinyurl.com/developer-mozilla-org-array-some}
 *
 * @param {Array<T>|goog.array.ArrayLike} arr Array or array
 *     like object over which to iterate.
 * @param {?function(this:S, T, number, ?) : boolean} f The function to call for
 *     for every element. This function takes 3 arguments (the element, the
 *     index and the array) and should return a boolean.
 * @param {S=} opt_obj  The object to be used as the value of 'this'
 *     within f.
 * @return {boolean} true if any element passes the test.
 * @template T,S
 */
goog.array.some = goog.NATIVE_ARRAY_PROTOTYPES &&
                  (goog.array.ASSUME_NATIVE_FUNCTIONS ||
                   goog.array.ARRAY_PROTOTYPE_.some) ?
    function(arr, f, opt_obj) {
      goog.asserts.assert(arr.length != null);

      return goog.array.ARRAY_PROTOTYPE_.some.call(arr, f, opt_obj);
    } :
    function(arr, f, opt_obj) {
      var l = arr.length;  // must be fixed during loop... see docs
      var arr2 = goog.isString(arr) ? arr.split('') : arr;
      for (var i = 0; i < l; i++) {
        if (i in arr2 && f.call(opt_obj, arr2[i], i, arr)) {
          return true;
        }
      }
      return false;
    };


/**
 * Call f for each element of an array. If all calls return true, every()
 * returns true. If any call returns false, every() returns false and
 * does not continue to check the remaining elements.
 *
 * See {@link http://tinyurl.com/developer-mozilla-org-array-every}
 *
 * @param {Array<T>|goog.array.ArrayLike} arr Array or array
 *     like object over which to iterate.
 * @param {?function(this:S, T, number, ?) : boolean} f The function to call for
 *     for every element. This function takes 3 arguments (the element, the
 *     index and the array) and should return a boolean.
 * @param {S=} opt_obj The object to be used as the value of 'this'
 *     within f.
 * @return {boolean} false if any element fails the test.
 * @template T,S
 */
goog.array.every = goog.NATIVE_ARRAY_PROTOTYPES &&
                   (goog.array.ASSUME_NATIVE_FUNCTIONS ||
                    goog.array.ARRAY_PROTOTYPE_.every) ?
    function(arr, f, opt_obj) {
      goog.asserts.assert(arr.length != null);

      return goog.array.ARRAY_PROTOTYPE_.every.call(arr, f, opt_obj);
    } :
    function(arr, f, opt_obj) {
      var l = arr.length;  // must be fixed during loop... see docs
      var arr2 = goog.isString(arr) ? arr.split('') : arr;
      for (var i = 0; i < l; i++) {
        if (i in arr2 && !f.call(opt_obj, arr2[i], i, arr)) {
          return false;
        }
      }
      return true;
    };


/**
 * Counts the array elements that fulfill the predicate, i.e. for which the
 * callback function returns true. Skips holes in the array.
 *
 * @param {!(Array<T>|goog.array.ArrayLike)} arr Array or array like object
 *     over which to iterate.
 * @param {function(this: S, T, number, ?): boolean} f The function to call for
 *     every element. Takes 3 arguments (the element, the index and the array).
 * @param {S=} opt_obj The object to be used as the value of 'this' within f.
 * @return {number} The number of the matching elements.
 * @template T,S
 */
goog.array.count = function(arr, f, opt_obj) {
  var count = 0;
  goog.array.forEach(arr, function(element, index, arr) {
    if (f.call(opt_obj, element, index, arr)) {
      ++count;
    }
  }, opt_obj);
  return count;
};


/**
 * Search an array for the first element that satisfies a given condition and
 * return that element.
 * @param {Array<T>|goog.array.ArrayLike} arr Array or array
 *     like object over which to iterate.
 * @param {?function(this:S, T, number, ?) : boolean} f The function to call
 *     for every element. This function takes 3 arguments (the element, the
 *     index and the array) and should return a boolean.
 * @param {S=} opt_obj An optional "this" context for the function.
 * @return {T|null} The first array element that passes the test, or null if no
 *     element is found.
 * @template T,S
 */
goog.array.find = function(arr, f, opt_obj) {
  var i = goog.array.findIndex(arr, f, opt_obj);
  return i < 0 ? null : goog.isString(arr) ? arr.charAt(i) : arr[i];
};


/**
 * Search an array for the first element that satisfies a given condition and
 * return its index.
 * @param {Array<T>|goog.array.ArrayLike} arr Array or array
 *     like object over which to iterate.
 * @param {?function(this:S, T, number, ?) : boolean} f The function to call for
 *     every element. This function
 *     takes 3 arguments (the element, the index and the array) and should
 *     return a boolean.
 * @param {S=} opt_obj An optional "this" context for the function.
 * @return {number} The index of the first array element that passes the test,
 *     or -1 if no element is found.
 * @template T,S
 */
goog.array.findIndex = function(arr, f, opt_obj) {
  var l = arr.length;  // must be fixed during loop... see docs
  var arr2 = goog.isString(arr) ? arr.split('') : arr;
  for (var i = 0; i < l; i++) {
    if (i in arr2 && f.call(opt_obj, arr2[i], i, arr)) {
      return i;
    }
  }
  return -1;
};


/**
 * Search an array (in reverse order) for the last element that satisfies a
 * given condition and return that element.
 * @param {Array<T>|goog.array.ArrayLike} arr Array or array
 *     like object over which to iterate.
 * @param {?function(this:S, T, number, ?) : boolean} f The function to call
 *     for every element. This function
 *     takes 3 arguments (the element, the index and the array) and should
 *     return a boolean.
 * @param {S=} opt_obj An optional "this" context for the function.
 * @return {T|null} The last array element that passes the test, or null if no
 *     element is found.
 * @template T,S
 */
goog.array.findRight = function(arr, f, opt_obj) {
  var i = goog.array.findIndexRight(arr, f, opt_obj);
  return i < 0 ? null : goog.isString(arr) ? arr.charAt(i) : arr[i];
};


/**
 * Search an array (in reverse order) for the last element that satisfies a
 * given condition and return its index.
 * @param {Array<T>|goog.array.ArrayLike} arr Array or array
 *     like object over which to iterate.
 * @param {?function(this:S, T, number, ?) : boolean} f The function to call
 *     for every element. This function
 *     takes 3 arguments (the element, the index and the array) and should
 *     return a boolean.
 * @param {S=} opt_obj An optional "this" context for the function.
 * @return {number} The index of the last array element that passes the test,
 *     or -1 if no element is found.
 * @template T,S
 */
goog.array.findIndexRight = function(arr, f, opt_obj) {
  var l = arr.length;  // must be fixed during loop... see docs
  var arr2 = goog.isString(arr) ? arr.split('') : arr;
  for (var i = l - 1; i >= 0; i--) {
    if (i in arr2 && f.call(opt_obj, arr2[i], i, arr)) {
      return i;
    }
  }
  return -1;
};


/**
 * Whether the array contains the given object.
 * @param {goog.array.ArrayLike} arr The array to test for the presence of the
 *     element.
 * @param {*} obj The object for which to test.
 * @return {boolean} true if obj is present.
 */
goog.array.contains = function(arr, obj) {
  return goog.array.indexOf(arr, obj) >= 0;
};


/**
 * Whether the array is empty.
 * @param {goog.array.ArrayLike} arr The array to test.
 * @return {boolean} true if empty.
 */
goog.array.isEmpty = function(arr) {
  return arr.length == 0;
};


/**
 * Clears the array.
 * @param {goog.array.ArrayLike} arr Array or array like object to clear.
 */
goog.array.clear = function(arr) {
  // For non real arrays we don't have the magic length so we delete the
  // indices.
  if (!goog.isArray(arr)) {
    for (var i = arr.length - 1; i >= 0; i--) {
      delete arr[i];
    }
  }
  arr.length = 0;
};


/**
 * Pushes an item into an array, if it's not already in the array.
 * @param {Array<T>} arr Array into which to insert the item.
 * @param {T} obj Value to add.
 * @template T
 */
goog.array.insert = function(arr, obj) {
  if (!goog.array.contains(arr, obj)) {
    arr.push(obj);
  }
};


/**
 * Inserts an object at the given index of the array.
 * @param {goog.array.ArrayLike} arr The array to modify.
 * @param {*} obj The object to insert.
 * @param {number=} opt_i The index at which to insert the object. If omitted,
 *      treated as 0. A negative index is counted from the end of the array.
 */
goog.array.insertAt = function(arr, obj, opt_i) {
  goog.array.splice(arr, opt_i, 0, obj);
};


/**
 * Inserts at the given index of the array, all elements of another array.
 * @param {goog.array.ArrayLike} arr The array to modify.
 * @param {goog.array.ArrayLike} elementsToAdd The array of elements to add.
 * @param {number=} opt_i The index at which to insert the object. If omitted,
 *      treated as 0. A negative index is counted from the end of the array.
 */
goog.array.insertArrayAt = function(arr, elementsToAdd, opt_i) {
  goog.partial(goog.array.splice, arr, opt_i, 0).apply(null, elementsToAdd);
};


/**
 * Inserts an object into an array before a specified object.
 * @param {Array<T>} arr The array to modify.
 * @param {T} obj The object to insert.
 * @param {T=} opt_obj2 The object before which obj should be inserted. If obj2
 *     is omitted or not found, obj is inserted at the end of the array.
 * @template T
 */
goog.array.insertBefore = function(arr, obj, opt_obj2) {
  var i;
  if (arguments.length == 2 || (i = goog.array.indexOf(arr, opt_obj2)) < 0) {
    arr.push(obj);
  } else {
    goog.array.insertAt(arr, obj, i);
  }
};


/**
 * Removes the first occurrence of a particular value from an array.
 * @param {Array<T>|goog.array.ArrayLike} arr Array from which to remove
 *     value.
 * @param {T} obj Object to remove.
 * @return {boolean} True if an element was removed.
 * @template T
 */
goog.array.remove = function(arr, obj) {
  var i = goog.array.indexOf(arr, obj);
  var rv;
  if ((rv = i >= 0)) {
    goog.array.removeAt(arr, i);
  }
  return rv;
};


/**
 * Removes from an array the element at index i
 * @param {goog.array.ArrayLike} arr Array or array like object from which to
 *     remove value.
 * @param {number} i The index to remove.
 * @return {boolean} True if an element was removed.
 */
goog.array.removeAt = function(arr, i) {
  goog.asserts.assert(arr.length != null);

  // use generic form of splice
  // splice returns the removed items and if successful the length of that
  // will be 1
  return goog.array.ARRAY_PROTOTYPE_.splice.call(arr, i, 1).length == 1;
};


/**
 * Removes the first value that satisfies the given condition.
 * @param {Array<T>|goog.array.ArrayLike} arr Array or array
 *     like object over which to iterate.
 * @param {?function(this:S, T, number, ?) : boolean} f The function to call
 *     for every element. This function
 *     takes 3 arguments (the element, the index and the array) and should
 *     return a boolean.
 * @param {S=} opt_obj An optional "this" context for the function.
 * @return {boolean} True if an element was removed.
 * @template T,S
 */
goog.array.removeIf = function(arr, f, opt_obj) {
  var i = goog.array.findIndex(arr, f, opt_obj);
  if (i >= 0) {
    goog.array.removeAt(arr, i);
    return true;
  }
  return false;
};


/**
 * Removes all values that satisfy the given condition.
 * @param {Array<T>|goog.array.ArrayLike} arr Array or array
 *     like object over which to iterate.
 * @param {?function(this:S, T, number, ?) : boolean} f The function to call
 *     for every element. This function
 *     takes 3 arguments (the element, the index and the array) and should
 *     return a boolean.
 * @param {S=} opt_obj An optional "this" context for the function.
 * @return {number} The number of items removed
 * @template T,S
 */
goog.array.removeAllIf = function(arr, f, opt_obj) {
  var removedCount = 0;
  goog.array.forEachRight(arr, function(val, index) {
    if (f.call(opt_obj, val, index, arr)) {
      if (goog.array.removeAt(arr, index)) {
        removedCount++;
      }
    }
  });
  return removedCount;
};


/**
 * Returns a new array that is the result of joining the arguments.  If arrays
 * are passed then their items are added, however, if non-arrays are passed they
 * will be added to the return array as is.
 *
 * Note that ArrayLike objects will be added as is, rather than having their
 * items added.
 *
 * goog.array.concat([1, 2], [3, 4]) -> [1, 2, 3, 4]
 * goog.array.concat(0, [1, 2]) -> [0, 1, 2]
 * goog.array.concat([1, 2], null) -> [1, 2, null]
 *
 * There is bug in all current versions of IE (6, 7 and 8) where arrays created
 * in an iframe become corrupted soon (not immediately) after the iframe is
 * destroyed. This is common if loading data via goog.net.IframeIo, for example.
 * This corruption only affects the concat method which will start throwing
 * Catastrophic Errors (#-2147418113).
 *
 * See http://endoflow.com/scratch/corrupted-arrays.html for a test case.
 *
 * Internally goog.array should use this, so that all methods will continue to
 * work on these broken array objects.
 *
 * @param {...*} var_args Items to concatenate.  Arrays will have each item
 *     added, while primitives and objects will be added as is.
 * @return {!Array<?>} The new resultant array.
 */
goog.array.concat = function(var_args) {
  return goog.array.ARRAY_PROTOTYPE_.concat.apply(
      goog.array.ARRAY_PROTOTYPE_, arguments);
};


/**
 * Returns a new array that contains the contents of all the arrays passed.
 * @param {...!Array<T>} var_args
 * @return {!Array<T>}
 * @template T
 */
goog.array.join = function(var_args) {
  return goog.array.ARRAY_PROTOTYPE_.concat.apply(
      goog.array.ARRAY_PROTOTYPE_, arguments);
};


/**
 * Converts an object to an array.
 * @param {Array<T>|goog.array.ArrayLike} object  The object to convert to an
 *     array.
 * @return {!Array<T>} The object converted into an array. If object has a
 *     length property, every property indexed with a non-negative number
 *     less than length will be included in the result. If object does not
 *     have a length property, an empty array will be returned.
 * @template T
 */
goog.array.toArray = function(object) {
  var length = object.length;

  // If length is not a number the following it false. This case is kept for
  // backwards compatibility since there are callers that pass objects that are
  // not array like.
  if (length > 0) {
    var rv = new Array(length);
    for (var i = 0; i < length; i++) {
      rv[i] = object[i];
    }
    return rv;
  }
  return [];
};


/**
 * Does a shallow copy of an array.
 * @param {Array<T>|goog.array.ArrayLike} arr  Array or array-like object to
 *     clone.
 * @return {!Array<T>} Clone of the input array.
 * @template T
 */
goog.array.clone = goog.array.toArray;


/**
 * Extends an array with another array, element, or "array like" object.
 * This function operates 'in-place', it does not create a new Array.
 *
 * Example:
 * var a = [];
 * goog.array.extend(a, [0, 1]);
 * a; // [0, 1]
 * goog.array.extend(a, 2);
 * a; // [0, 1, 2]
 *
 * @param {Array<VALUE>} arr1  The array to modify.
 * @param {...(Array<VALUE>|VALUE)} var_args The elements or arrays of elements
 *     to add to arr1.
 * @template VALUE
 */
goog.array.extend = function(arr1, var_args) {
  for (var i = 1; i < arguments.length; i++) {
    var arr2 = arguments[i];
    if (goog.isArrayLike(arr2)) {
      var len1 = arr1.length || 0;
      var len2 = arr2.length || 0;
      arr1.length = len1 + len2;
      for (var j = 0; j < len2; j++) {
        arr1[len1 + j] = arr2[j];
      }
    } else {
      arr1.push(arr2);
    }
  }
};


/**
 * Adds or removes elements from an array. This is a generic version of Array
 * splice. This means that it might work on other objects similar to arrays,
 * such as the arguments object.
 *
 * @param {Array<T>|goog.array.ArrayLike} arr The array to modify.
 * @param {number|undefined} index The index at which to start changing the
 *     array. If not defined, treated as 0.
 * @param {number} howMany How many elements to remove (0 means no removal. A
 *     value below 0 is treated as zero and so is any other non number. Numbers
 *     are floored).
 * @param {...T} var_args Optional, additional elements to insert into the
 *     array.
 * @return {!Array<T>} the removed elements.
 * @template T
 */
goog.array.splice = function(arr, index, howMany, var_args) {
  goog.asserts.assert(arr.length != null);

  return goog.array.ARRAY_PROTOTYPE_.splice.apply(
      arr, goog.array.slice(arguments, 1));
};


/**
 * Returns a new array from a segment of an array. This is a generic version of
 * Array slice. This means that it might work on other objects similar to
 * arrays, such as the arguments object.
 *
 * @param {Array<T>|goog.array.ArrayLike} arr The array from
 * which to copy a segment.
 * @param {number} start The index of the first element to copy.
 * @param {number=} opt_end The index after the last element to copy.
 * @return {!Array<T>} A new array containing the specified segment of the
 *     original array.
 * @template T
 */
goog.array.slice = function(arr, start, opt_end) {
  goog.asserts.assert(arr.length != null);

  // passing 1 arg to slice is not the same as passing 2 where the second is
  // null or undefined (in that case the second argument is treated as 0).
  // we could use slice on the arguments object and then use apply instead of
  // testing the length
  if (arguments.length <= 2) {
    return goog.array.ARRAY_PROTOTYPE_.slice.call(arr, start);
  } else {
    return goog.array.ARRAY_PROTOTYPE_.slice.call(arr, start, opt_end);
  }
};


/**
 * Removes all duplicates from an array (retaining only the first
 * occurrence of each array element).  This function modifies the
 * array in place and doesn't change the order of the non-duplicate items.
 *
 * For objects, duplicates are identified as having the same unique ID as
 * defined by {@link goog.getUid}.
 *
 * Alternatively you can specify a custom hash function that returns a unique
 * value for each item in the array it should consider unique.
 *
 * Runtime: N,
 * Worstcase space: 2N (no dupes)
 *
 * @param {Array<T>|goog.array.ArrayLike} arr The array from which to remove
 *     duplicates.
 * @param {Array=} opt_rv An optional array in which to return the results,
 *     instead of performing the removal inplace.  If specified, the original
 *     array will remain unchanged.
 * @param {function(T):string=} opt_hashFn An optional function to use to
 *     apply to every item in the array. This function should return a unique
 *     value for each item in the array it should consider unique.
 * @template T
 */
goog.array.removeDuplicates = function(arr, opt_rv, opt_hashFn) {
  var returnArray = opt_rv || arr;
  var defaultHashFn = function(item) {
    // Prefix each type with a single character representing the type to
    // prevent conflicting keys (e.g. true and 'true').
    return goog.isObject(item) ? 'o' + goog.getUid(item) :
        (typeof item).charAt(0) + item;
  };
  var hashFn = opt_hashFn || defaultHashFn;

  var seen = {}, cursorInsert = 0, cursorRead = 0;
  while (cursorRead < arr.length) {
    var current = arr[cursorRead++];
    var key = hashFn(current);
    if (!Object.prototype.hasOwnProperty.call(seen, key)) {
      seen[key] = true;
      returnArray[cursorInsert++] = current;
    }
  }
  returnArray.length = cursorInsert;
};


/**
 * Searches the specified array for the specified target using the binary
 * search algorithm.  If no opt_compareFn is specified, elements are compared
 * using <code>goog.array.defaultCompare</code>, which compares the elements
 * using the built in < and > operators.  This will produce the expected
 * behavior for homogeneous arrays of String(s) and Number(s). The array
 * specified <b>must</b> be sorted in ascending order (as defined by the
 * comparison function).  If the array is not sorted, results are undefined.
 * If the array contains multiple instances of the specified target value, any
 * of these instances may be found.
 *
 * Runtime: O(log n)
 *
 * @param {Array<VALUE>|goog.array.ArrayLike} arr The array to be searched.
 * @param {TARGET} target The sought value.
 * @param {function(TARGET, VALUE): number=} opt_compareFn Optional comparison
 *     function by which the array is ordered. Should take 2 arguments to
 *     compare, and return a negative number, zero, or a positive number
 *     depending on whether the first argument is less than, equal to, or
 *     greater than the second.
 * @return {number} Lowest index of the target value if found, otherwise
 *     (-(insertion point) - 1). The insertion point is where the value should
 *     be inserted into arr to preserve the sorted property.  Return value >= 0
 *     iff target is found.
 * @template TARGET, VALUE
 */
goog.array.binarySearch = function(arr, target, opt_compareFn) {
  return goog.array.binarySearch_(arr,
      opt_compareFn || goog.array.defaultCompare, false /* isEvaluator */,
      target);
};


/**
 * Selects an index in the specified array using the binary search algorithm.
 * The evaluator receives an element and determines whether the desired index
 * is before, at, or after it.  The evaluator must be consistent (formally,
 * goog.array.map(goog.array.map(arr, evaluator, opt_obj), goog.math.sign)
 * must be monotonically non-increasing).
 *
 * Runtime: O(log n)
 *
 * @param {Array<VALUE>|goog.array.ArrayLike} arr The array to be searched.
 * @param {function(this:THIS, VALUE, number, ?): number} evaluator
 *     Evaluator function that receives 3 arguments (the element, the index and
 *     the array). Should return a negative number, zero, or a positive number
 *     depending on whether the desired index is before, at, or after the
 *     element passed to it.
 * @param {THIS=} opt_obj The object to be used as the value of 'this'
 *     within evaluator.
 * @return {number} Index of the leftmost element matched by the evaluator, if
 *     such exists; otherwise (-(insertion point) - 1). The insertion point is
 *     the index of the first element for which the evaluator returns negative,
 *     or arr.length if no such element exists. The return value is non-negative
 *     iff a match is found.
 * @template THIS, VALUE
 */
goog.array.binarySelect = function(arr, evaluator, opt_obj) {
  return goog.array.binarySearch_(arr, evaluator, true /* isEvaluator */,
      undefined /* opt_target */, opt_obj);
};


/**
 * Implementation of a binary search algorithm which knows how to use both
 * comparison functions and evaluators. If an evaluator is provided, will call
 * the evaluator with the given optional data object, conforming to the
 * interface defined in binarySelect. Otherwise, if a comparison function is
 * provided, will call the comparison function against the given data object.
 *
 * This implementation purposefully does not use goog.bind or goog.partial for
 * performance reasons.
 *
 * Runtime: O(log n)
 *
 * @param {Array<VALUE>|goog.array.ArrayLike} arr The array to be searched.
 * @param {function(TARGET, VALUE): number|
 *         function(this:THIS, VALUE, number, ?): number} compareFn Either an
 *     evaluator or a comparison function, as defined by binarySearch
 *     and binarySelect above.
 * @param {boolean} isEvaluator Whether the function is an evaluator or a
 *     comparison function.
 * @param {TARGET=} opt_target If the function is a comparison function, then
 *     this is the target to binary search for.
 * @param {THIS=} opt_selfObj If the function is an evaluator, this is an
  *    optional this object for the evaluator.
 * @return {number} Lowest index of the target value if found, otherwise
 *     (-(insertion point) - 1). The insertion point is where the value should
 *     be inserted into arr to preserve the sorted property.  Return value >= 0
 *     iff target is found.
 * @template THIS, VALUE, TARGET
 * @private
 */
goog.array.binarySearch_ = function(arr, compareFn, isEvaluator, opt_target,
    opt_selfObj) {
  var left = 0;  // inclusive
  var right = arr.length;  // exclusive
  var found;
  while (left < right) {
    var middle = (left + right) >> 1;
    var compareResult;
    if (isEvaluator) {
      compareResult = compareFn.call(opt_selfObj, arr[middle], middle, arr);
    } else {
      compareResult = compareFn(opt_target, arr[middle]);
    }
    if (compareResult > 0) {
      left = middle + 1;
    } else {
      right = middle;
      // We are looking for the lowest index so we can't return immediately.
      found = !compareResult;
    }
  }
  // left is the index if found, or the insertion point otherwise.
  // ~left is a shorthand for -left - 1.
  return found ? left : ~left;
};


/**
 * Sorts the specified array into ascending order.  If no opt_compareFn is
 * specified, elements are compared using
 * <code>goog.array.defaultCompare</code>, which compares the elements using
 * the built in < and > operators.  This will produce the expected behavior
 * for homogeneous arrays of String(s) and Number(s), unlike the native sort,
 * but will give unpredictable results for heterogenous lists of strings and
 * numbers with different numbers of digits.
 *
 * This sort is not guaranteed to be stable.
 *
 * Runtime: Same as <code>Array.prototype.sort</code>
 *
 * @param {Array<T>} arr The array to be sorted.
 * @param {?function(T,T):number=} opt_compareFn Optional comparison
 *     function by which the
 *     array is to be ordered. Should take 2 arguments to compare, and return a
 *     negative number, zero, or a positive number depending on whether the
 *     first argument is less than, equal to, or greater than the second.
 * @template T
 */
goog.array.sort = function(arr, opt_compareFn) {
  // TODO(arv): Update type annotation since null is not accepted.
  arr.sort(opt_compareFn || goog.array.defaultCompare);
};


/**
 * Sorts the specified array into ascending order in a stable way.  If no
 * opt_compareFn is specified, elements are compared using
 * <code>goog.array.defaultCompare</code>, which compares the elements using
 * the built in < and > operators.  This will produce the expected behavior
 * for homogeneous arrays of String(s) and Number(s).
 *
 * Runtime: Same as <code>Array.prototype.sort</code>, plus an additional
 * O(n) overhead of copying the array twice.
 *
 * @param {Array<T>} arr The array to be sorted.
 * @param {?function(T, T): number=} opt_compareFn Optional comparison function
 *     by which the array is to be ordered. Should take 2 arguments to compare,
 *     and return a negative number, zero, or a positive number depending on
 *     whether the first argument is less than, equal to, or greater than the
 *     second.
 * @template T
 */
goog.array.stableSort = function(arr, opt_compareFn) {
  for (var i = 0; i < arr.length; i++) {
    arr[i] = {index: i, value: arr[i]};
  }
  var valueCompareFn = opt_compareFn || goog.array.defaultCompare;
  function stableCompareFn(obj1, obj2) {
    return valueCompareFn(obj1.value, obj2.value) || obj1.index - obj2.index;
  };
  goog.array.sort(arr, stableCompareFn);
  for (var i = 0; i < arr.length; i++) {
    arr[i] = arr[i].value;
  }
};


/**
 * Sort the specified array into ascending order based on item keys
 * returned by the specified key function.
 * If no opt_compareFn is specified, the keys are compared in ascending order
 * using <code>goog.array.defaultCompare</code>.
 *
 * Runtime: O(S(f(n)), where S is runtime of <code>goog.array.sort</code>
 * and f(n) is runtime of the key function.
 *
 * @param {Array<T>} arr The array to be sorted.
 * @param {function(T): K} keyFn Function taking array element and returning
 *     a key used for sorting this element.
 * @param {?function(K, K): number=} opt_compareFn Optional comparison function
 *     by which the keys are to be ordered. Should take 2 arguments to compare,
 *     and return a negative number, zero, or a positive number depending on
 *     whether the first argument is less than, equal to, or greater than the
 *     second.
 * @template T,K
 */
goog.array.sortByKey = function(arr, keyFn, opt_compareFn) {
  var keyCompareFn = opt_compareFn || goog.array.defaultCompare;
  goog.array.sort(arr, function(a, b) {
    return keyCompareFn(keyFn(a), keyFn(b));
  });
};


/**
 * Sorts an array of objects by the specified object key and compare
 * function. If no compare function is provided, the key values are
 * compared in ascending order using <code>goog.array.defaultCompare</code>.
 * This won't work for keys that get renamed by the compiler. So use
 * {'foo': 1, 'bar': 2} rather than {foo: 1, bar: 2}.
 * @param {Array<Object>} arr An array of objects to sort.
 * @param {string} key The object key to sort by.
 * @param {Function=} opt_compareFn The function to use to compare key
 *     values.
 */
goog.array.sortObjectsByKey = function(arr, key, opt_compareFn) {
  goog.array.sortByKey(arr,
      function(obj) { return obj[key]; },
      opt_compareFn);
};


/**
 * Tells if the array is sorted.
 * @param {!Array<T>} arr The array.
 * @param {?function(T,T):number=} opt_compareFn Function to compare the
 *     array elements.
 *     Should take 2 arguments to compare, and return a negative number, zero,
 *     or a positive number depending on whether the first argument is less
 *     than, equal to, or greater than the second.
 * @param {boolean=} opt_strict If true no equal elements are allowed.
 * @return {boolean} Whether the array is sorted.
 * @template T
 */
goog.array.isSorted = function(arr, opt_compareFn, opt_strict) {
  var compare = opt_compareFn || goog.array.defaultCompare;
  for (var i = 1; i < arr.length; i++) {
    var compareResult = compare(arr[i - 1], arr[i]);
    if (compareResult > 0 || compareResult == 0 && opt_strict) {
      return false;
    }
  }
  return true;
};


/**
 * Compares two arrays for equality. Two arrays are considered equal if they
 * have the same length and their corresponding elements are equal according to
 * the comparison function.
 *
 * @param {goog.array.ArrayLike} arr1 The first array to compare.
 * @param {goog.array.ArrayLike} arr2 The second array to compare.
 * @param {Function=} opt_equalsFn Optional comparison function.
 *     Should take 2 arguments to compare, and return true if the arguments
 *     are equal. Defaults to {@link goog.array.defaultCompareEquality} which
 *     compares the elements using the built-in '===' operator.
 * @return {boolean} Whether the two arrays are equal.
 */
goog.array.equals = function(arr1, arr2, opt_equalsFn) {
  if (!goog.isArrayLike(arr1) || !goog.isArrayLike(arr2) ||
      arr1.length != arr2.length) {
    return false;
  }
  var l = arr1.length;
  var equalsFn = opt_equalsFn || goog.array.defaultCompareEquality;
  for (var i = 0; i < l; i++) {
    if (!equalsFn(arr1[i], arr2[i])) {
      return false;
    }
  }
  return true;
};


/**
 * 3-way array compare function.
 * @param {!Array<VALUE>|!goog.array.ArrayLike} arr1 The first array to
 *     compare.
 * @param {!Array<VALUE>|!goog.array.ArrayLike} arr2 The second array to
 *     compare.
 * @param {function(VALUE, VALUE): number=} opt_compareFn Optional comparison
 *     function by which the array is to be ordered. Should take 2 arguments to
 *     compare, and return a negative number, zero, or a positive number
 *     depending on whether the first argument is less than, equal to, or
 *     greater than the second.
 * @return {number} Negative number, zero, or a positive number depending on
 *     whether the first argument is less than, equal to, or greater than the
 *     second.
 * @template VALUE
 */
goog.array.compare3 = function(arr1, arr2, opt_compareFn) {
  var compare = opt_compareFn || goog.array.defaultCompare;
  var l = Math.min(arr1.length, arr2.length);
  for (var i = 0; i < l; i++) {
    var result = compare(arr1[i], arr2[i]);
    if (result != 0) {
      return result;
    }
  }
  return goog.array.defaultCompare(arr1.length, arr2.length);
};


/**
 * Compares its two arguments for order, using the built in < and >
 * operators.
 * @param {VALUE} a The first object to be compared.
 * @param {VALUE} b The second object to be compared.
 * @return {number} A negative number, zero, or a positive number as the first
 *     argument is less than, equal to, or greater than the second,
 *     respectively.
 * @template VALUE
 */
goog.array.defaultCompare = function(a, b) {
  return a > b ? 1 : a < b ? -1 : 0;
};


/**
 * Compares its two arguments for inverse order, using the built in < and >
 * operators.
 * @param {VALUE} a The first object to be compared.
 * @param {VALUE} b The second object to be compared.
 * @return {number} A negative number, zero, or a positive number as the first
 *     argument is greater than, equal to, or less than the second,
 *     respectively.
 * @template VALUE
 */
goog.array.inverseDefaultCompare = function(a, b) {
  return -goog.array.defaultCompare(a, b);
};


/**
 * Compares its two arguments for equality, using the built in === operator.
 * @param {*} a The first object to compare.
 * @param {*} b The second object to compare.
 * @return {boolean} True if the two arguments are equal, false otherwise.
 */
goog.array.defaultCompareEquality = function(a, b) {
  return a === b;
};


/**
 * Inserts a value into a sorted array. The array is not modified if the
 * value is already present.
 * @param {Array<VALUE>|goog.array.ArrayLike} array The array to modify.
 * @param {VALUE} value The object to insert.
 * @param {function(VALUE, VALUE): number=} opt_compareFn Optional comparison
 *     function by which the array is ordered. Should take 2 arguments to
 *     compare, and return a negative number, zero, or a positive number
 *     depending on whether the first argument is less than, equal to, or
 *     greater than the second.
 * @return {boolean} True if an element was inserted.
 * @template VALUE
 */
goog.array.binaryInsert = function(array, value, opt_compareFn) {
  var index = goog.array.binarySearch(array, value, opt_compareFn);
  if (index < 0) {
    goog.array.insertAt(array, value, -(index + 1));
    return true;
  }
  return false;
};


/**
 * Removes a value from a sorted array.
 * @param {!Array<VALUE>|!goog.array.ArrayLike} array The array to modify.
 * @param {VALUE} value The object to remove.
 * @param {function(VALUE, VALUE): number=} opt_compareFn Optional comparison
 *     function by which the array is ordered. Should take 2 arguments to
 *     compare, and return a negative number, zero, or a positive number
 *     depending on whether the first argument is less than, equal to, or
 *     greater than the second.
 * @return {boolean} True if an element was removed.
 * @template VALUE
 */
goog.array.binaryRemove = function(array, value, opt_compareFn) {
  var index = goog.array.binarySearch(array, value, opt_compareFn);
  return (index >= 0) ? goog.array.removeAt(array, index) : false;
};


/**
 * Splits an array into disjoint buckets according to a splitting function.
 * @param {Array<T>} array The array.
 * @param {function(this:S, T,number,Array<T>):?} sorter Function to call for
 *     every element.  This takes 3 arguments (the element, the index and the
 *     array) and must return a valid object key (a string, number, etc), or
 *     undefined, if that object should not be placed in a bucket.
 * @param {S=} opt_obj The object to be used as the value of 'this' within
 *     sorter.
 * @return {!Object} An object, with keys being all of the unique return values
 *     of sorter, and values being arrays containing the items for
 *     which the splitter returned that key.
 * @template T,S
 */
goog.array.bucket = function(array, sorter, opt_obj) {
  var buckets = {};

  for (var i = 0; i < array.length; i++) {
    var value = array[i];
    var key = sorter.call(opt_obj, value, i, array);
    if (goog.isDef(key)) {
      // Push the value to the right bucket, creating it if necessary.
      var bucket = buckets[key] || (buckets[key] = []);
      bucket.push(value);
    }
  }

  return buckets;
};


/**
 * Creates a new object built from the provided array and the key-generation
 * function.
 * @param {Array<T>|goog.array.ArrayLike} arr Array or array like object over
 *     which to iterate whose elements will be the values in the new object.
 * @param {?function(this:S, T, number, ?) : string} keyFunc The function to
 *     call for every element. This function takes 3 arguments (the element, the
 *     index and the array) and should return a string that will be used as the
 *     key for the element in the new object. If the function returns the same
 *     key for more than one element, the value for that key is
 *     implementation-defined.
 * @param {S=} opt_obj The object to be used as the value of 'this'
 *     within keyFunc.
 * @return {!Object<T>} The new object.
 * @template T,S
 */
goog.array.toObject = function(arr, keyFunc, opt_obj) {
  var ret = {};
  goog.array.forEach(arr, function(element, index) {
    ret[keyFunc.call(opt_obj, element, index, arr)] = element;
  });
  return ret;
};


/**
 * Creates a range of numbers in an arithmetic progression.
 *
 * Range takes 1, 2, or 3 arguments:
 * <pre>
 * range(5) is the same as range(0, 5, 1) and produces [0, 1, 2, 3, 4]
 * range(2, 5) is the same as range(2, 5, 1) and produces [2, 3, 4]
 * range(-2, -5, -1) produces [-2, -3, -4]
 * range(-2, -5, 1) produces [], since stepping by 1 wouldn't ever reach -5.
 * </pre>
 *
 * @param {number} startOrEnd The starting value of the range if an end argument
 *     is provided. Otherwise, the start value is 0, and this is the end value.
 * @param {number=} opt_end The optional end value of the range.
 * @param {number=} opt_step The step size between range values. Defaults to 1
 *     if opt_step is undefined or 0.
 * @return {!Array<number>} An array of numbers for the requested range. May be
 *     an empty array if adding the step would not converge toward the end
 *     value.
 */
goog.array.range = function(startOrEnd, opt_end, opt_step) {
  var array = [];
  var start = 0;
  var end = startOrEnd;
  var step = opt_step || 1;
  if (opt_end !== undefined) {
    start = startOrEnd;
    end = opt_end;
  }

  if (step * (end - start) < 0) {
    // Sign mismatch: start + step will never reach the end value.
    return [];
  }

  if (step > 0) {
    for (var i = start; i < end; i += step) {
      array.push(i);
    }
  } else {
    for (var i = start; i > end; i += step) {
      array.push(i);
    }
  }
  return array;
};


/**
 * Returns an array consisting of the given value repeated N times.
 *
 * @param {VALUE} value The value to repeat.
 * @param {number} n The repeat count.
 * @return {!Array<VALUE>} An array with the repeated value.
 * @template VALUE
 */
goog.array.repeat = function(value, n) {
  var array = [];
  for (var i = 0; i < n; i++) {
    array[i] = value;
  }
  return array;
};


/**
 * Returns an array consisting of every argument with all arrays
 * expanded in-place recursively.
 *
 * @param {...*} var_args The values to flatten.
 * @return {!Array<?>} An array containing the flattened values.
 */
goog.array.flatten = function(var_args) {
  var CHUNK_SIZE = 8192;

  var result = [];
  for (var i = 0; i < arguments.length; i++) {
    var element = arguments[i];
    if (goog.isArray(element)) {
      for (var c = 0; c < element.length; c += CHUNK_SIZE) {
        var chunk = goog.array.slice(element, c, c + CHUNK_SIZE);
        var recurseResult = goog.array.flatten.apply(null, chunk);
        for (var r = 0; r < recurseResult.length; r++) {
          result.push(recurseResult[r]);
        }
      }
    } else {
      result.push(element);
    }
  }
  return result;
};


/**
 * Rotates an array in-place. After calling this method, the element at
 * index i will be the element previously at index (i - n) %
 * array.length, for all values of i between 0 and array.length - 1,
 * inclusive.
 *
 * For example, suppose list comprises [t, a, n, k, s]. After invoking
 * rotate(array, 1) (or rotate(array, -4)), array will comprise [s, t, a, n, k].
 *
 * @param {!Array<T>} array The array to rotate.
 * @param {number} n The amount to rotate.
 * @return {!Array<T>} The array.
 * @template T
 */
goog.array.rotate = function(array, n) {
  goog.asserts.assert(array.length != null);

  if (array.length) {
    n %= array.length;
    if (n > 0) {
      goog.array.ARRAY_PROTOTYPE_.unshift.apply(array, array.splice(-n, n));
    } else if (n < 0) {
      goog.array.ARRAY_PROTOTYPE_.push.apply(array, array.splice(0, -n));
    }
  }
  return array;
};


/**
 * Moves one item of an array to a new position keeping the order of the rest
 * of the items. Example use case: keeping a list of JavaScript objects
 * synchronized with the corresponding list of DOM elements after one of the
 * elements has been dragged to a new position.
 * @param {!(Array|Arguments|{length:number})} arr The array to modify.
 * @param {number} fromIndex Index of the item to move between 0 and
 *     {@code arr.length - 1}.
 * @param {number} toIndex Target index between 0 and {@code arr.length - 1}.
 */
goog.array.moveItem = function(arr, fromIndex, toIndex) {
  goog.asserts.assert(fromIndex >= 0 && fromIndex < arr.length);
  goog.asserts.assert(toIndex >= 0 && toIndex < arr.length);
  // Remove 1 item at fromIndex.
  var removedItems = goog.array.ARRAY_PROTOTYPE_.splice.call(arr, fromIndex, 1);
  // Insert the removed item at toIndex.
  goog.array.ARRAY_PROTOTYPE_.splice.call(arr, toIndex, 0, removedItems[0]);
  // We don't use goog.array.insertAt and goog.array.removeAt, because they're
  // significantly slower than splice.
};


/**
 * Creates a new array for which the element at position i is an array of the
 * ith element of the provided arrays.  The returned array will only be as long
 * as the shortest array provided; additional values are ignored.  For example,
 * the result of zipping [1, 2] and [3, 4, 5] is [[1,3], [2, 4]].
 *
 * This is similar to the zip() function in Python.  See {@link
 * http://docs.python.org/library/functions.html#zip}
 *
 * @param {...!goog.array.ArrayLike} var_args Arrays to be combined.
 * @return {!Array<!Array<?>>} A new array of arrays created from
 *     provided arrays.
 */
goog.array.zip = function(var_args) {
  if (!arguments.length) {
    return [];
  }
  var result = [];
  for (var i = 0; true; i++) {
    var value = [];
    for (var j = 0; j < arguments.length; j++) {
      var arr = arguments[j];
      // If i is larger than the array length, this is the shortest array.
      if (i >= arr.length) {
        return result;
      }
      value.push(arr[i]);
    }
    result.push(value);
  }
};


/**
 * Shuffles the values in the specified array using the Fisher-Yates in-place
 * shuffle (also known as the Knuth Shuffle). By default, calls Math.random()
 * and so resets the state of that random number generator. Similarly, may reset
 * the state of the any other specified random number generator.
 *
 * Runtime: O(n)
 *
 * @param {!Array<?>} arr The array to be shuffled.
 * @param {function():number=} opt_randFn Optional random function to use for
 *     shuffling.
 *     Takes no arguments, and returns a random number on the interval [0, 1).
 *     Defaults to Math.random() using JavaScript's built-in Math library.
 */
goog.array.shuffle = function(arr, opt_randFn) {
  var randFn = opt_randFn || Math.random;

  for (var i = arr.length - 1; i > 0; i--) {
    // Choose a random array index in [0, i] (inclusive with i).
    var j = Math.floor(randFn() * (i + 1));

    var tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
  }
};


/**
 * Returns a new array of elements from arr, based on the indexes of elements
 * provided by index_arr. For example, the result of index copying
 * ['a', 'b', 'c'] with index_arr [1,0,0,2] is ['b', 'a', 'a', 'c'].
 *
 * @param {!Array<T>} arr The array to get a indexed copy from.
 * @param {!Array<number>} index_arr An array of indexes to get from arr.
 * @return {!Array<T>} A new array of elements from arr in index_arr order.
 * @template T
 */
goog.array.copyByIndex = function(arr, index_arr) {
  var result = [];
  goog.array.forEach(index_arr, function(index) {
    result.push(arr[index]);
  });
  return result;
};

goog.provide('ol.array');

goog.require('goog.array');
goog.require('goog.asserts');


/**
 * @param {Array.<number>} arr Array.
 * @param {number} target Target.
 * @return {number} Index.
 */
ol.array.binaryFindNearest = function(arr, target) {
  var index = goog.array.binarySearch(arr, target,
      /**
       * @param {number} a A.
       * @param {number} b B.
       * @return {number} b minus a.
       */
      function(a, b) {
        return b - a;
      });
  if (index >= 0) {
    return index;
  } else if (index == -1) {
    return 0;
  } else if (index == -arr.length - 1) {
    return arr.length - 1;
  } else {
    var left = -index - 2;
    var right = -index - 1;
    if (arr[left] - target < target - arr[right]) {
      return left;
    } else {
      return right;
    }
  }
};


/**
 * Compare function for array sort that is safe for numbers.
 * @param {*} a The first object to be compared.
 * @param {*} b The second object to be compared.
 * @return {number} A negative number, zero, or a positive number as the first
 *     argument is less than, equal to, or greater than the second.
 */
ol.array.numberSafeCompareFunction = function(a, b) {
  return a > b ? 1 : a < b ? -1 : 0;
};


/**
 * Whether the array contains the given object.
 * @param {Array.<*>} arr The array to test for the presence of the element.
 * @param {*} obj The object for which to test.
 * @return {boolean} The object is in the array.
 */
ol.array.includes = function(arr, obj) {
  return arr.indexOf(obj) >= 0;
};


/**
 * @param {Array.<number>} arr Array.
 * @param {number} target Target.
 * @param {number} direction 0 means return the nearest, > 0
 *    means return the largest nearest, < 0 means return the
 *    smallest nearest.
 * @return {number} Index.
 */
ol.array.linearFindNearest = function(arr, target, direction) {
  var n = arr.length;
  if (arr[0] <= target) {
    return 0;
  } else if (target <= arr[n - 1]) {
    return n - 1;
  } else {
    var i;
    if (direction > 0) {
      for (i = 1; i < n; ++i) {
        if (arr[i] < target) {
          return i - 1;
        }
      }
    } else if (direction < 0) {
      for (i = 1; i < n; ++i) {
        if (arr[i] <= target) {
          return i;
        }
      }
    } else {
      for (i = 1; i < n; ++i) {
        if (arr[i] == target) {
          return i;
        } else if (arr[i] < target) {
          if (arr[i - 1] - target < target - arr[i]) {
            return i - 1;
          } else {
            return i;
          }
        }
      }
    }
    // We should never get here, but the compiler complains
    // if it finds a path for which no number is returned.
    goog.asserts.fail();
    return n - 1;
  }
};


/**
 * @param {Array.<*>} arr Array.
 * @param {number} begin Begin index.
 * @param {number} end End index.
 */
ol.array.reverseSubArray = function(arr, begin, end) {
  goog.asserts.assert(begin >= 0,
      'Array begin index should be equal to or greater than 0');
  goog.asserts.assert(end < arr.length,
      'Array end index should be less than the array length');
  while (begin < end) {
    var tmp = arr[begin];
    arr[begin] = arr[end];
    arr[end] = tmp;
    ++begin;
    --end;
  }
};

goog.provide('ol.ResolutionConstraint');
goog.provide('ol.ResolutionConstraintType');

goog.require('ol.array');
goog.require('ol.math');


/**
 * @typedef {function((number|undefined), number, number): (number|undefined)}
 */
ol.ResolutionConstraintType;


/**
 * @param {Array.<number>} resolutions Resolutions.
 * @return {ol.ResolutionConstraintType} Zoom function.
 */
ol.ResolutionConstraint.createSnapToResolutions = function(resolutions) {
  return (
      /**
       * @param {number|undefined} resolution Resolution.
       * @param {number} delta Delta.
       * @param {number} direction Direction.
       * @return {number|undefined} Resolution.
       */
      function(resolution, delta, direction) {
        if (resolution !== undefined) {
          var z =
              ol.array.linearFindNearest(resolutions, resolution, direction);
          z = ol.math.clamp(z + delta, 0, resolutions.length - 1);
          return resolutions[z];
        } else {
          return undefined;
        }
      });
};


/**
 * @param {number} power Power.
 * @param {number} maxResolution Maximum resolution.
 * @param {number=} opt_maxLevel Maximum level.
 * @return {ol.ResolutionConstraintType} Zoom function.
 */
ol.ResolutionConstraint.createSnapToPower = function(power, maxResolution, opt_maxLevel) {
  return (
      /**
       * @param {number|undefined} resolution Resolution.
       * @param {number} delta Delta.
       * @param {number} direction Direction.
       * @return {number|undefined} Resolution.
       */
      function(resolution, delta, direction) {
        if (resolution !== undefined) {
          var offset;
          if (direction > 0) {
            offset = 0;
          } else if (direction < 0) {
            offset = 1;
          } else {
            offset = 0.5;
          }
          var oldLevel = Math.floor(
              Math.log(maxResolution / resolution) / Math.log(power) + offset);
          var newLevel = Math.max(oldLevel + delta, 0);
          if (opt_maxLevel !== undefined) {
            newLevel = Math.min(newLevel, opt_maxLevel);
          }
          return maxResolution / Math.pow(power, newLevel);
        } else {
          return undefined;
        }
      });
};

goog.provide('ol.RotationConstraint');
goog.provide('ol.RotationConstraintType');

goog.require('ol.math');


/**
 * @typedef {function((number|undefined), number): (number|undefined)}
 */
ol.RotationConstraintType;


/**
 * @param {number|undefined} rotation Rotation.
 * @param {number} delta Delta.
 * @return {number|undefined} Rotation.
 */
ol.RotationConstraint.disable = function(rotation, delta) {
  if (rotation !== undefined) {
    return 0;
  } else {
    return undefined;
  }
};


/**
 * @param {number|undefined} rotation Rotation.
 * @param {number} delta Delta.
 * @return {number|undefined} Rotation.
 */
ol.RotationConstraint.none = function(rotation, delta) {
  if (rotation !== undefined) {
    return rotation + delta;
  } else {
    return undefined;
  }
};


/**
 * @param {number} n N.
 * @return {ol.RotationConstraintType} Rotation constraint.
 */
ol.RotationConstraint.createSnapToN = function(n) {
  var theta = 2 * Math.PI / n;
  return (
      /**
       * @param {number|undefined} rotation Rotation.
       * @param {number} delta Delta.
       * @return {number|undefined} Rotation.
       */
      function(rotation, delta) {
        if (rotation !== undefined) {
          rotation = Math.floor((rotation + delta) / theta + 0.5) * theta;
          return rotation;
        } else {
          return undefined;
        }
      });
};


/**
 * @param {number=} opt_tolerance Tolerance.
 * @return {ol.RotationConstraintType} Rotation constraint.
 */
ol.RotationConstraint.createSnapToZero = function(opt_tolerance) {
  var tolerance = opt_tolerance || ol.math.toRadians(5);
  return (
      /**
       * @param {number|undefined} rotation Rotation.
       * @param {number} delta Delta.
       * @return {number|undefined} Rotation.
       */
      function(rotation, delta) {
        if (rotation !== undefined) {
          if (Math.abs(rotation + delta) <= tolerance) {
            return 0;
          } else {
            return rotation + delta;
          }
        } else {
          return undefined;
        }
      });
};

goog.provide('ol.Constraints');

goog.require('ol.CenterConstraintType');
goog.require('ol.ResolutionConstraintType');
goog.require('ol.RotationConstraintType');


/**
 * @constructor
 * @param {ol.CenterConstraintType} centerConstraint Center constraint.
 * @param {ol.ResolutionConstraintType} resolutionConstraint
 *     Resolution constraint.
 * @param {ol.RotationConstraintType} rotationConstraint
 *     Rotation constraint.
 */
ol.Constraints = function(centerConstraint, resolutionConstraint, rotationConstraint) {

  /**
   * @type {ol.CenterConstraintType}
   */
  this.center = centerConstraint;

  /**
   * @type {ol.ResolutionConstraintType}
   */
  this.resolution = resolutionConstraint;

  /**
   * @type {ol.RotationConstraintType}
   */
  this.rotation = rotationConstraint;

};

// Copyright 2010 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview A global registry for entry points into a program,
 * so that they can be instrumented. Each module should register their
 * entry points with this registry. Designed to be compiled out
 * if no instrumentation is requested.
 *
 * Entry points may be registered before or after a call to
 * goog.debug.entryPointRegistry.monitorAll. If an entry point is registered
 * later, the existing monitor will instrument the new entry point.
 *
 * @author nicksantos@google.com (Nick Santos)
 */

goog.provide('goog.debug.EntryPointMonitor');
goog.provide('goog.debug.entryPointRegistry');

goog.require('goog.asserts');



/**
 * @interface
 */
goog.debug.EntryPointMonitor = function() {};


/**
 * Instruments a function.
 *
 * @param {!Function} fn A function to instrument.
 * @return {!Function} The instrumented function.
 */
goog.debug.EntryPointMonitor.prototype.wrap;


/**
 * Try to remove an instrumentation wrapper created by this monitor.
 * If the function passed to unwrap is not a wrapper created by this
 * monitor, then we will do nothing.
 *
 * Notice that some wrappers may not be unwrappable. For example, if other
 * monitors have applied their own wrappers, then it will be impossible to
 * unwrap them because their wrappers will have captured our wrapper.
 *
 * So it is important that entry points are unwrapped in the reverse
 * order that they were wrapped.
 *
 * @param {!Function} fn A function to unwrap.
 * @return {!Function} The unwrapped function, or {@code fn} if it was not
 *     a wrapped function created by this monitor.
 */
goog.debug.EntryPointMonitor.prototype.unwrap;


/**
 * An array of entry point callbacks.
 * @type {!Array<function(!Function)>}
 * @private
 */
goog.debug.entryPointRegistry.refList_ = [];


/**
 * Monitors that should wrap all the entry points.
 * @type {!Array<!goog.debug.EntryPointMonitor>}
 * @private
 */
goog.debug.entryPointRegistry.monitors_ = [];


/**
 * Whether goog.debug.entryPointRegistry.monitorAll has ever been called.
 * Checking this allows the compiler to optimize out the registrations.
 * @type {boolean}
 * @private
 */
goog.debug.entryPointRegistry.monitorsMayExist_ = false;


/**
 * Register an entry point with this module.
 *
 * The entry point will be instrumented when a monitor is passed to
 * goog.debug.entryPointRegistry.monitorAll. If this has already occurred, the
 * entry point is instrumented immediately.
 *
 * @param {function(!Function)} callback A callback function which is called
 *     with a transforming function to instrument the entry point. The callback
 *     is responsible for wrapping the relevant entry point with the
 *     transforming function.
 */
goog.debug.entryPointRegistry.register = function(callback) {
  // Don't use push(), so that this can be compiled out.
  goog.debug.entryPointRegistry.refList_[
      goog.debug.entryPointRegistry.refList_.length] = callback;
  // If no one calls monitorAll, this can be compiled out.
  if (goog.debug.entryPointRegistry.monitorsMayExist_) {
    var monitors = goog.debug.entryPointRegistry.monitors_;
    for (var i = 0; i < monitors.length; i++) {
      callback(goog.bind(monitors[i].wrap, monitors[i]));
    }
  }
};


/**
 * Configures a monitor to wrap all entry points.
 *
 * Entry points that have already been registered are immediately wrapped by
 * the monitor. When an entry point is registered in the future, it will also
 * be wrapped by the monitor when it is registered.
 *
 * @param {!goog.debug.EntryPointMonitor} monitor An entry point monitor.
 */
goog.debug.entryPointRegistry.monitorAll = function(monitor) {
  goog.debug.entryPointRegistry.monitorsMayExist_ = true;
  var transformer = goog.bind(monitor.wrap, monitor);
  for (var i = 0; i < goog.debug.entryPointRegistry.refList_.length; i++) {
    goog.debug.entryPointRegistry.refList_[i](transformer);
  }
  goog.debug.entryPointRegistry.monitors_.push(monitor);
};


/**
 * Try to unmonitor all the entry points that have already been registered. If
 * an entry point is registered in the future, it will not be wrapped by the
 * monitor when it is registered. Note that this may fail if the entry points
 * have additional wrapping.
 *
 * @param {!goog.debug.EntryPointMonitor} monitor The last monitor to wrap
 *     the entry points.
 * @throws {Error} If the monitor is not the most recently configured monitor.
 */
goog.debug.entryPointRegistry.unmonitorAllIfPossible = function(monitor) {
  var monitors = goog.debug.entryPointRegistry.monitors_;
  goog.asserts.assert(monitor == monitors[monitors.length - 1],
      'Only the most recent monitor can be unwrapped.');
  var transformer = goog.bind(monitor.unwrap, monitor);
  for (var i = 0; i < goog.debug.entryPointRegistry.refList_.length; i++) {
    goog.debug.entryPointRegistry.refList_[i](transformer);
  }
  monitors.length--;
};

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Utilities used by goog.labs.userAgent tools. These functions
 * should not be used outside of goog.labs.userAgent.*.
 *
 *
 * @author nnaze@google.com (Nathan Naze)
 */

goog.provide('goog.labs.userAgent.util');

goog.require('goog.string');


/**
 * Gets the native userAgent string from navigator if it exists.
 * If navigator or navigator.userAgent string is missing, returns an empty
 * string.
 * @return {string}
 * @private
 */
goog.labs.userAgent.util.getNativeUserAgentString_ = function() {
  var navigator = goog.labs.userAgent.util.getNavigator_();
  if (navigator) {
    var userAgent = navigator.userAgent;
    if (userAgent) {
      return userAgent;
    }
  }
  return '';
};


/**
 * Getter for the native navigator.
 * This is a separate function so it can be stubbed out in testing.
 * @return {Navigator}
 * @private
 */
goog.labs.userAgent.util.getNavigator_ = function() {
  return goog.global.navigator;
};


/**
 * A possible override for applications which wish to not check
 * navigator.userAgent but use a specified value for detection instead.
 * @private {string}
 */
goog.labs.userAgent.util.userAgent_ =
    goog.labs.userAgent.util.getNativeUserAgentString_();


/**
 * Applications may override browser detection on the built in
 * navigator.userAgent object by setting this string. Set to null to use the
 * browser object instead.
 * @param {?string=} opt_userAgent The User-Agent override.
 */
goog.labs.userAgent.util.setUserAgent = function(opt_userAgent) {
  goog.labs.userAgent.util.userAgent_ = opt_userAgent ||
      goog.labs.userAgent.util.getNativeUserAgentString_();
};


/**
 * @return {string} The user agent string.
 */
goog.labs.userAgent.util.getUserAgent = function() {
  return goog.labs.userAgent.util.userAgent_;
};


/**
 * @param {string} str
 * @return {boolean} Whether the user agent contains the given string, ignoring
 *     case.
 */
goog.labs.userAgent.util.matchUserAgent = function(str) {
  var userAgent = goog.labs.userAgent.util.getUserAgent();
  return goog.string.contains(userAgent, str);
};


/**
 * @param {string} str
 * @return {boolean} Whether the user agent contains the given string.
 */
goog.labs.userAgent.util.matchUserAgentIgnoreCase = function(str) {
  var userAgent = goog.labs.userAgent.util.getUserAgent();
  return goog.string.caseInsensitiveContains(userAgent, str);
};


/**
 * Parses the user agent into tuples for each section.
 * @param {string} userAgent
 * @return {!Array<!Array<string>>} Tuples of key, version, and the contents
 *     of the parenthetical.
 */
goog.labs.userAgent.util.extractVersionTuples = function(userAgent) {
  // Matches each section of a user agent string.
  // Example UA:
  // Mozilla/5.0 (iPad; U; CPU OS 3_2_1 like Mac OS X; en-us)
  // AppleWebKit/531.21.10 (KHTML, like Gecko) Mobile/7B405
  // This has three version tuples: Mozilla, AppleWebKit, and Mobile.

  var versionRegExp = new RegExp(
      // Key. Note that a key may have a space.
      // (i.e. 'Mobile Safari' in 'Mobile Safari/5.0')
      '(\\w[\\w ]+)' +

      '/' +                // slash
      '([^\\s]+)' +        // version (i.e. '5.0b')
      '\\s*' +             // whitespace
      '(?:\\((.*?)\\))?',  // parenthetical info. parentheses not matched.
      'g');

  var data = [];
  var match;

  // Iterate and collect the version tuples.  Each iteration will be the
  // next regex match.
  while (match = versionRegExp.exec(userAgent)) {
    data.push([
      match[1],  // key
      match[2],  // value
      // || undefined as this is not undefined in IE7 and IE8
      match[3] || undefined  // info
    ]);
  }

  return data;
};


// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Utilities for manipulating objects/maps/hashes.
 * @author arv@google.com (Erik Arvidsson)
 */

goog.provide('goog.object');


/**
 * Calls a function for each element in an object/map/hash.
 *
 * @param {Object<K,V>} obj The object over which to iterate.
 * @param {function(this:T,V,?,Object<K,V>):?} f The function to call
 *     for every element. This function takes 3 arguments (the value, the
 *     key and the object) and the return value is ignored.
 * @param {T=} opt_obj This is used as the 'this' object within f.
 * @template T,K,V
 */
goog.object.forEach = function(obj, f, opt_obj) {
  for (var key in obj) {
    f.call(opt_obj, obj[key], key, obj);
  }
};


/**
 * Calls a function for each element in an object/map/hash. If that call returns
 * true, adds the element to a new object.
 *
 * @param {Object<K,V>} obj The object over which to iterate.
 * @param {function(this:T,V,?,Object<K,V>):boolean} f The function to call
 *     for every element. This
 *     function takes 3 arguments (the value, the key and the object)
 *     and should return a boolean. If the return value is true the
 *     element is added to the result object. If it is false the
 *     element is not included.
 * @param {T=} opt_obj This is used as the 'this' object within f.
 * @return {!Object<K,V>} a new object in which only elements that passed the
 *     test are present.
 * @template T,K,V
 */
goog.object.filter = function(obj, f, opt_obj) {
  var res = {};
  for (var key in obj) {
    if (f.call(opt_obj, obj[key], key, obj)) {
      res[key] = obj[key];
    }
  }
  return res;
};


/**
 * For every element in an object/map/hash calls a function and inserts the
 * result into a new object.
 *
 * @param {Object<K,V>} obj The object over which to iterate.
 * @param {function(this:T,V,?,Object<K,V>):R} f The function to call
 *     for every element. This function
 *     takes 3 arguments (the value, the key and the object)
 *     and should return something. The result will be inserted
 *     into a new object.
 * @param {T=} opt_obj This is used as the 'this' object within f.
 * @return {!Object<K,R>} a new object with the results from f.
 * @template T,K,V,R
 */
goog.object.map = function(obj, f, opt_obj) {
  var res = {};
  for (var key in obj) {
    res[key] = f.call(opt_obj, obj[key], key, obj);
  }
  return res;
};


/**
 * Calls a function for each element in an object/map/hash. If any
 * call returns true, returns true (without checking the rest). If
 * all calls return false, returns false.
 *
 * @param {Object<K,V>} obj The object to check.
 * @param {function(this:T,V,?,Object<K,V>):boolean} f The function to
 *     call for every element. This function
 *     takes 3 arguments (the value, the key and the object) and should
 *     return a boolean.
 * @param {T=} opt_obj This is used as the 'this' object within f.
 * @return {boolean} true if any element passes the test.
 * @template T,K,V
 */
goog.object.some = function(obj, f, opt_obj) {
  for (var key in obj) {
    if (f.call(opt_obj, obj[key], key, obj)) {
      return true;
    }
  }
  return false;
};


/**
 * Calls a function for each element in an object/map/hash. If
 * all calls return true, returns true. If any call returns false, returns
 * false at this point and does not continue to check the remaining elements.
 *
 * @param {Object<K,V>} obj The object to check.
 * @param {?function(this:T,V,?,Object<K,V>):boolean} f The function to
 *     call for every element. This function
 *     takes 3 arguments (the value, the key and the object) and should
 *     return a boolean.
 * @param {T=} opt_obj This is used as the 'this' object within f.
 * @return {boolean} false if any element fails the test.
 * @template T,K,V
 */
goog.object.every = function(obj, f, opt_obj) {
  for (var key in obj) {
    if (!f.call(opt_obj, obj[key], key, obj)) {
      return false;
    }
  }
  return true;
};


/**
 * Returns the number of key-value pairs in the object map.
 *
 * @param {Object} obj The object for which to get the number of key-value
 *     pairs.
 * @return {number} The number of key-value pairs in the object map.
 */
goog.object.getCount = function(obj) {
  // JS1.5 has __count__ but it has been deprecated so it raises a warning...
  // in other words do not use. Also __count__ only includes the fields on the
  // actual object and not in the prototype chain.
  var rv = 0;
  for (var key in obj) {
    rv++;
  }
  return rv;
};


/**
 * Returns one key from the object map, if any exists.
 * For map literals the returned key will be the first one in most of the
 * browsers (a know exception is Konqueror).
 *
 * @param {Object} obj The object to pick a key from.
 * @return {string|undefined} The key or undefined if the object is empty.
 */
goog.object.getAnyKey = function(obj) {
  for (var key in obj) {
    return key;
  }
};


/**
 * Returns one value from the object map, if any exists.
 * For map literals the returned value will be the first one in most of the
 * browsers (a know exception is Konqueror).
 *
 * @param {Object<K,V>} obj The object to pick a value from.
 * @return {V|undefined} The value or undefined if the object is empty.
 * @template K,V
 */
goog.object.getAnyValue = function(obj) {
  for (var key in obj) {
    return obj[key];
  }
};


/**
 * Whether the object/hash/map contains the given object as a value.
 * An alias for goog.object.containsValue(obj, val).
 *
 * @param {Object<K,V>} obj The object in which to look for val.
 * @param {V} val The object for which to check.
 * @return {boolean} true if val is present.
 * @template K,V
 */
goog.object.contains = function(obj, val) {
  return goog.object.containsValue(obj, val);
};


/**
 * Returns the values of the object/map/hash.
 *
 * @param {Object<K,V>} obj The object from which to get the values.
 * @return {!Array<V>} The values in the object/map/hash.
 * @template K,V
 */
goog.object.getValues = function(obj) {
  var res = [];
  var i = 0;
  for (var key in obj) {
    res[i++] = obj[key];
  }
  return res;
};


/**
 * Returns the keys of the object/map/hash.
 *
 * @param {Object} obj The object from which to get the keys.
 * @return {!Array<string>} Array of property keys.
 */
goog.object.getKeys = function(obj) {
  var res = [];
  var i = 0;
  for (var key in obj) {
    res[i++] = key;
  }
  return res;
};


/**
 * Get a value from an object multiple levels deep.  This is useful for
 * pulling values from deeply nested objects, such as JSON responses.
 * Example usage: getValueByKeys(jsonObj, 'foo', 'entries', 3)
 *
 * @param {!Object} obj An object to get the value from.  Can be array-like.
 * @param {...(string|number|!Array<number|string>)} var_args A number of keys
 *     (as strings, or numbers, for array-like objects).  Can also be
 *     specified as a single array of keys.
 * @return {*} The resulting value.  If, at any point, the value for a key
 *     is undefined, returns undefined.
 */
goog.object.getValueByKeys = function(obj, var_args) {
  var isArrayLike = goog.isArrayLike(var_args);
  var keys = isArrayLike ? var_args : arguments;

  // Start with the 2nd parameter for the variable parameters syntax.
  for (var i = isArrayLike ? 0 : 1; i < keys.length; i++) {
    obj = obj[keys[i]];
    if (!goog.isDef(obj)) {
      break;
    }
  }

  return obj;
};


/**
 * Whether the object/map/hash contains the given key.
 *
 * @param {Object} obj The object in which to look for key.
 * @param {*} key The key for which to check.
 * @return {boolean} true If the map contains the key.
 */
goog.object.containsKey = function(obj, key) {
  return key in obj;
};


/**
 * Whether the object/map/hash contains the given value. This is O(n).
 *
 * @param {Object<K,V>} obj The object in which to look for val.
 * @param {V} val The value for which to check.
 * @return {boolean} true If the map contains the value.
 * @template K,V
 */
goog.object.containsValue = function(obj, val) {
  for (var key in obj) {
    if (obj[key] == val) {
      return true;
    }
  }
  return false;
};


/**
 * Searches an object for an element that satisfies the given condition and
 * returns its key.
 * @param {Object<K,V>} obj The object to search in.
 * @param {function(this:T,V,string,Object<K,V>):boolean} f The
 *      function to call for every element. Takes 3 arguments (the value,
 *     the key and the object) and should return a boolean.
 * @param {T=} opt_this An optional "this" context for the function.
 * @return {string|undefined} The key of an element for which the function
 *     returns true or undefined if no such element is found.
 * @template T,K,V
 */
goog.object.findKey = function(obj, f, opt_this) {
  for (var key in obj) {
    if (f.call(opt_this, obj[key], key, obj)) {
      return key;
    }
  }
  return undefined;
};


/**
 * Searches an object for an element that satisfies the given condition and
 * returns its value.
 * @param {Object<K,V>} obj The object to search in.
 * @param {function(this:T,V,string,Object<K,V>):boolean} f The function
 *     to call for every element. Takes 3 arguments (the value, the key
 *     and the object) and should return a boolean.
 * @param {T=} opt_this An optional "this" context for the function.
 * @return {V} The value of an element for which the function returns true or
 *     undefined if no such element is found.
 * @template T,K,V
 */
goog.object.findValue = function(obj, f, opt_this) {
  var key = goog.object.findKey(obj, f, opt_this);
  return key && obj[key];
};


/**
 * Whether the object/map/hash is empty.
 *
 * @param {Object} obj The object to test.
 * @return {boolean} true if obj is empty.
 */
goog.object.isEmpty = function(obj) {
  for (var key in obj) {
    return false;
  }
  return true;
};


/**
 * Removes all key value pairs from the object/map/hash.
 *
 * @param {Object} obj The object to clear.
 */
goog.object.clear = function(obj) {
  for (var i in obj) {
    delete obj[i];
  }
};


/**
 * Removes a key-value pair based on the key.
 *
 * @param {Object} obj The object from which to remove the key.
 * @param {*} key The key to remove.
 * @return {boolean} Whether an element was removed.
 */
goog.object.remove = function(obj, key) {
  var rv;
  if ((rv = key in obj)) {
    delete obj[key];
  }
  return rv;
};


/**
 * Adds a key-value pair to the object. Throws an exception if the key is
 * already in use. Use set if you want to change an existing pair.
 *
 * @param {Object<K,V>} obj The object to which to add the key-value pair.
 * @param {string} key The key to add.
 * @param {V} val The value to add.
 * @template K,V
 */
goog.object.add = function(obj, key, val) {
  if (key in obj) {
    throw Error('The object already contains the key "' + key + '"');
  }
  goog.object.set(obj, key, val);
};


/**
 * Returns the value for the given key.
 *
 * @param {Object<K,V>} obj The object from which to get the value.
 * @param {string} key The key for which to get the value.
 * @param {R=} opt_val The value to return if no item is found for the given
 *     key (default is undefined).
 * @return {V|R|undefined} The value for the given key.
 * @template K,V,R
 */
goog.object.get = function(obj, key, opt_val) {
  if (key in obj) {
    return obj[key];
  }
  return opt_val;
};


/**
 * Adds a key-value pair to the object/map/hash.
 *
 * @param {Object<K,V>} obj The object to which to add the key-value pair.
 * @param {string} key The key to add.
 * @param {V} value The value to add.
 * @template K,V
 */
goog.object.set = function(obj, key, value) {
  obj[key] = value;
};


/**
 * Adds a key-value pair to the object/map/hash if it doesn't exist yet.
 *
 * @param {Object<K,V>} obj The object to which to add the key-value pair.
 * @param {string} key The key to add.
 * @param {V} value The value to add if the key wasn't present.
 * @return {V} The value of the entry at the end of the function.
 * @template K,V
 */
goog.object.setIfUndefined = function(obj, key, value) {
  return key in obj ? obj[key] : (obj[key] = value);
};


/**
 * Sets a key and value to an object if the key is not set. The value will be
 * the return value of the given function. If the key already exists, the
 * object will not be changed and the function will not be called (the function
 * will be lazily evaluated -- only called if necessary).
 *
 * This function is particularly useful for use with a map used a as a cache.
 *
 * @param {!Object<K,V>} obj The object to which to add the key-value pair.
 * @param {string} key The key to add.
 * @param {function():V} f The value to add if the key wasn't present.
 * @return {V} The value of the entry at the end of the function.
 * @template K,V
 */
goog.object.setWithReturnValueIfNotSet = function(obj, key, f) {
  if (key in obj) {
    return obj[key];
  }

  var val = f();
  obj[key] = val;
  return val;
};


/**
 * Compares two objects for equality using === on the values.
 *
 * @param {!Object<K,V>} a
 * @param {!Object<K,V>} b
 * @return {boolean}
 * @template K,V
 */
goog.object.equals = function(a, b) {
  for (var k in a) {
    if (!(k in b) || a[k] !== b[k]) {
      return false;
    }
  }
  for (var k in b) {
    if (!(k in a)) {
      return false;
    }
  }
  return true;
};


/**
 * Does a flat clone of the object.
 *
 * @param {Object<K,V>} obj Object to clone.
 * @return {!Object<K,V>} Clone of the input object.
 * @template K,V
 */
goog.object.clone = function(obj) {
  // We cannot use the prototype trick because a lot of methods depend on where
  // the actual key is set.

  var res = {};
  for (var key in obj) {
    res[key] = obj[key];
  }
  return res;
  // We could also use goog.mixin but I wanted this to be independent from that.
};


/**
 * Clones a value. The input may be an Object, Array, or basic type. Objects and
 * arrays will be cloned recursively.
 *
 * WARNINGS:
 * <code>goog.object.unsafeClone</code> does not detect reference loops. Objects
 * that refer to themselves will cause infinite recursion.
 *
 * <code>goog.object.unsafeClone</code> is unaware of unique identifiers, and
 * copies UIDs created by <code>getUid</code> into cloned results.
 *
 * @param {*} obj The value to clone.
 * @return {*} A clone of the input value.
 */
goog.object.unsafeClone = function(obj) {
  var type = goog.typeOf(obj);
  if (type == 'object' || type == 'array') {
    if (goog.isFunction(obj.clone)) {
      return obj.clone();
    }
    var clone = type == 'array' ? [] : {};
    for (var key in obj) {
      clone[key] = goog.object.unsafeClone(obj[key]);
    }
    return clone;
  }

  return obj;
};


/**
 * Returns a new object in which all the keys and values are interchanged
 * (keys become values and values become keys). If multiple keys map to the
 * same value, the chosen transposed value is implementation-dependent.
 *
 * @param {Object} obj The object to transpose.
 * @return {!Object} The transposed object.
 */
goog.object.transpose = function(obj) {
  var transposed = {};
  for (var key in obj) {
    transposed[obj[key]] = key;
  }
  return transposed;
};


/**
 * The names of the fields that are defined on Object.prototype.
 * @type {Array<string>}
 * @private
 */
goog.object.PROTOTYPE_FIELDS_ = [
  'constructor',
  'hasOwnProperty',
  'isPrototypeOf',
  'propertyIsEnumerable',
  'toLocaleString',
  'toString',
  'valueOf'
];


/**
 * Extends an object with another object.
 * This operates 'in-place'; it does not create a new Object.
 *
 * Example:
 * var o = {};
 * goog.object.extend(o, {a: 0, b: 1});
 * o; // {a: 0, b: 1}
 * goog.object.extend(o, {b: 2, c: 3});
 * o; // {a: 0, b: 2, c: 3}
 *
 * @param {Object} target The object to modify. Existing properties will be
 *     overwritten if they are also present in one of the objects in
 *     {@code var_args}.
 * @param {...Object} var_args The objects from which values will be copied.
 */
goog.object.extend = function(target, var_args) {
  var key, source;
  for (var i = 1; i < arguments.length; i++) {
    source = arguments[i];
    for (key in source) {
      target[key] = source[key];
    }

    // For IE the for-in-loop does not contain any properties that are not
    // enumerable on the prototype object (for example isPrototypeOf from
    // Object.prototype) and it will also not include 'replace' on objects that
    // extend String and change 'replace' (not that it is common for anyone to
    // extend anything except Object).

    for (var j = 0; j < goog.object.PROTOTYPE_FIELDS_.length; j++) {
      key = goog.object.PROTOTYPE_FIELDS_[j];
      if (Object.prototype.hasOwnProperty.call(source, key)) {
        target[key] = source[key];
      }
    }
  }
};


/**
 * Creates a new object built from the key-value pairs provided as arguments.
 * @param {...*} var_args If only one argument is provided and it is an array
 *     then this is used as the arguments,  otherwise even arguments are used as
 *     the property names and odd arguments are used as the property values.
 * @return {!Object} The new object.
 * @throws {Error} If there are uneven number of arguments or there is only one
 *     non array argument.
 */
goog.object.create = function(var_args) {
  var argLength = arguments.length;
  if (argLength == 1 && goog.isArray(arguments[0])) {
    return goog.object.create.apply(null, arguments[0]);
  }

  if (argLength % 2) {
    throw Error('Uneven number of arguments');
  }

  var rv = {};
  for (var i = 0; i < argLength; i += 2) {
    rv[arguments[i]] = arguments[i + 1];
  }
  return rv;
};


/**
 * Creates a new object where the property names come from the arguments but
 * the value is always set to true
 * @param {...*} var_args If only one argument is provided and it is an array
 *     then this is used as the arguments,  otherwise the arguments are used
 *     as the property names.
 * @return {!Object} The new object.
 */
goog.object.createSet = function(var_args) {
  var argLength = arguments.length;
  if (argLength == 1 && goog.isArray(arguments[0])) {
    return goog.object.createSet.apply(null, arguments[0]);
  }

  var rv = {};
  for (var i = 0; i < argLength; i++) {
    rv[arguments[i]] = true;
  }
  return rv;
};


/**
 * Creates an immutable view of the underlying object, if the browser
 * supports immutable objects.
 *
 * In default mode, writes to this view will fail silently. In strict mode,
 * they will throw an error.
 *
 * @param {!Object<K,V>} obj An object.
 * @return {!Object<K,V>} An immutable view of that object, or the
 *     original object if this browser does not support immutables.
 * @template K,V
 */
goog.object.createImmutableView = function(obj) {
  var result = obj;
  if (Object.isFrozen && !Object.isFrozen(obj)) {
    result = Object.create(obj);
    Object.freeze(result);
  }
  return result;
};


/**
 * @param {!Object} obj An object.
 * @return {boolean} Whether this is an immutable view of the object.
 */
goog.object.isImmutableView = function(obj) {
  return !!Object.isFrozen && Object.isFrozen(obj);
};

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Closure user agent detection (Browser).
 * @see <a href="http://www.useragentstring.com/">User agent strings</a>
 * For more information on rendering engine, platform, or device see the other
 * sub-namespaces in goog.labs.userAgent, goog.labs.userAgent.platform,
 * goog.labs.userAgent.device respectively.)
 *
 * @author martone@google.com (Andy Martone)
 */

goog.provide('goog.labs.userAgent.browser');

goog.require('goog.array');
goog.require('goog.labs.userAgent.util');
goog.require('goog.object');
goog.require('goog.string');


// TODO(nnaze): Refactor to remove excessive exclusion logic in matching
// functions.


/**
 * @return {boolean} Whether the user's browser is Opera.
 * @private
 */
goog.labs.userAgent.browser.matchOpera_ = function() {
  return goog.labs.userAgent.util.matchUserAgent('Opera') ||
      goog.labs.userAgent.util.matchUserAgent('OPR');
};


/**
 * @return {boolean} Whether the user's browser is IE.
 * @private
 */
goog.labs.userAgent.browser.matchIE_ = function() {
  return goog.labs.userAgent.util.matchUserAgent('Trident') ||
      goog.labs.userAgent.util.matchUserAgent('MSIE');
};


/**
 * @return {boolean} Whether the user's browser is Edge.
 * @private
 */
goog.labs.userAgent.browser.matchEdge_ = function() {
  return goog.labs.userAgent.util.matchUserAgent('Edge');
};


/**
 * @return {boolean} Whether the user's browser is Firefox.
 * @private
 */
goog.labs.userAgent.browser.matchFirefox_ = function() {
  return goog.labs.userAgent.util.matchUserAgent('Firefox');
};


/**
 * @return {boolean} Whether the user's browser is Safari.
 * @private
 */
goog.labs.userAgent.browser.matchSafari_ = function() {
  return goog.labs.userAgent.util.matchUserAgent('Safari') &&
      !(goog.labs.userAgent.browser.matchChrome_() ||
        goog.labs.userAgent.browser.matchCoast_() ||
        goog.labs.userAgent.browser.matchOpera_() ||
        goog.labs.userAgent.browser.matchEdge_() ||
        goog.labs.userAgent.browser.isSilk() ||
        goog.labs.userAgent.util.matchUserAgent('Android'));
};


/**
 * @return {boolean} Whether the user's browser is Coast (Opera's Webkit-based
 *     iOS browser).
 * @private
 */
goog.labs.userAgent.browser.matchCoast_ = function() {
  return goog.labs.userAgent.util.matchUserAgent('Coast');
};


/**
 * @return {boolean} Whether the user's browser is iOS Webview.
 * @private
 */
goog.labs.userAgent.browser.matchIosWebview_ = function() {
  // iOS Webview does not show up as Chrome or Safari. Also check for Opera's
  // WebKit-based iOS browser, Coast.
  return (goog.labs.userAgent.util.matchUserAgent('iPad') ||
          goog.labs.userAgent.util.matchUserAgent('iPhone')) &&
      !goog.labs.userAgent.browser.matchSafari_() &&
      !goog.labs.userAgent.browser.matchChrome_() &&
      !goog.labs.userAgent.browser.matchCoast_() &&
      goog.labs.userAgent.util.matchUserAgent('AppleWebKit');
};


/**
 * @return {boolean} Whether the user's browser is Chrome.
 * @private
 */
goog.labs.userAgent.browser.matchChrome_ = function() {
  return (goog.labs.userAgent.util.matchUserAgent('Chrome') ||
      goog.labs.userAgent.util.matchUserAgent('CriOS')) &&
      !goog.labs.userAgent.browser.matchOpera_() &&
      !goog.labs.userAgent.browser.matchEdge_();
};


/**
 * @return {boolean} Whether the user's browser is the Android browser.
 * @private
 */
goog.labs.userAgent.browser.matchAndroidBrowser_ = function() {
  // Android can appear in the user agent string for Chrome on Android.
  // This is not the Android standalone browser if it does.
  return goog.labs.userAgent.util.matchUserAgent('Android') &&
      !(goog.labs.userAgent.browser.isChrome() ||
        goog.labs.userAgent.browser.isFirefox() ||
        goog.labs.userAgent.browser.isOpera() ||
        goog.labs.userAgent.browser.isSilk());
};


/**
 * @return {boolean} Whether the user's browser is Opera.
 */
goog.labs.userAgent.browser.isOpera = goog.labs.userAgent.browser.matchOpera_;


/**
 * @return {boolean} Whether the user's browser is IE.
 */
goog.labs.userAgent.browser.isIE = goog.labs.userAgent.browser.matchIE_;


/**
 * @return {boolean} Whether the user's browser is Edge.
 */
goog.labs.userAgent.browser.isEdge = goog.labs.userAgent.browser.matchEdge_;


/**
 * @return {boolean} Whether the user's browser is Firefox.
 */
goog.labs.userAgent.browser.isFirefox =
    goog.labs.userAgent.browser.matchFirefox_;


/**
 * @return {boolean} Whether the user's browser is Safari.
 */
goog.labs.userAgent.browser.isSafari =
    goog.labs.userAgent.browser.matchSafari_;


/**
 * @return {boolean} Whether the user's browser is Coast (Opera's Webkit-based
 *     iOS browser).
 */
goog.labs.userAgent.browser.isCoast =
    goog.labs.userAgent.browser.matchCoast_;


/**
 * @return {boolean} Whether the user's browser is iOS Webview.
 */
goog.labs.userAgent.browser.isIosWebview =
    goog.labs.userAgent.browser.matchIosWebview_;


/**
 * @return {boolean} Whether the user's browser is Chrome.
 */
goog.labs.userAgent.browser.isChrome =
    goog.labs.userAgent.browser.matchChrome_;


/**
 * @return {boolean} Whether the user's browser is the Android browser.
 */
goog.labs.userAgent.browser.isAndroidBrowser =
    goog.labs.userAgent.browser.matchAndroidBrowser_;


/**
 * For more information, see:
 * http://docs.aws.amazon.com/silk/latest/developerguide/user-agent.html
 * @return {boolean} Whether the user's browser is Silk.
 */
goog.labs.userAgent.browser.isSilk = function() {
  return goog.labs.userAgent.util.matchUserAgent('Silk');
};


/**
 * @return {string} The browser version or empty string if version cannot be
 *     determined. Note that for Internet Explorer, this returns the version of
 *     the browser, not the version of the rendering engine. (IE 8 in
 *     compatibility mode will return 8.0 rather than 7.0. To determine the
 *     rendering engine version, look at document.documentMode instead. See
 *     http://msdn.microsoft.com/en-us/library/cc196988(v=vs.85).aspx for more
 *     details.)
 */
goog.labs.userAgent.browser.getVersion = function() {
  var userAgentString = goog.labs.userAgent.util.getUserAgent();
  // Special case IE since IE's version is inside the parenthesis and
  // without the '/'.
  if (goog.labs.userAgent.browser.isIE()) {
    return goog.labs.userAgent.browser.getIEVersion_(userAgentString);
  }

  var versionTuples = goog.labs.userAgent.util.extractVersionTuples(
      userAgentString);

  // Construct a map for easy lookup.
  var versionMap = {};
  goog.array.forEach(versionTuples, function(tuple) {
    // Note that the tuple is of length three, but we only care about the
    // first two.
    var key = tuple[0];
    var value = tuple[1];
    versionMap[key] = value;
  });

  var versionMapHasKey = goog.partial(goog.object.containsKey, versionMap);

  // Gives the value with the first key it finds, otherwise empty string.
  function lookUpValueWithKeys(keys) {
    var key = goog.array.find(keys, versionMapHasKey);
    return versionMap[key] || '';
  }

  // Check Opera before Chrome since Opera 15+ has "Chrome" in the string.
  // See
  // http://my.opera.com/ODIN/blog/2013/07/15/opera-user-agent-strings-opera-15-and-beyond
  if (goog.labs.userAgent.browser.isOpera()) {
    // Opera 10 has Version/10.0 but Opera/9.8, so look for "Version" first.
    // Opera uses 'OPR' for more recent UAs.
    return lookUpValueWithKeys(['Version', 'Opera', 'OPR']);
  }

  // Check Edge before Chrome since it has Chrome in the string.
  if (goog.labs.userAgent.browser.isEdge()) {
    return lookUpValueWithKeys(['Edge']);
  }

  if (goog.labs.userAgent.browser.isChrome()) {
    return lookUpValueWithKeys(['Chrome', 'CriOS']);
  }

  // Usually products browser versions are in the third tuple after "Mozilla"
  // and the engine.
  var tuple = versionTuples[2];
  return tuple && tuple[1] || '';
};


/**
 * @param {string|number} version The version to check.
 * @return {boolean} Whether the browser version is higher or the same as the
 *     given version.
 */
goog.labs.userAgent.browser.isVersionOrHigher = function(version) {
  return goog.string.compareVersions(goog.labs.userAgent.browser.getVersion(),
                                     version) >= 0;
};


/**
 * Determines IE version. More information:
 * http://msdn.microsoft.com/en-us/library/ie/bg182625(v=vs.85).aspx#uaString
 * http://msdn.microsoft.com/en-us/library/hh869301(v=vs.85).aspx
 * http://blogs.msdn.com/b/ie/archive/2010/03/23/introducing-ie9-s-user-agent-string.aspx
 * http://blogs.msdn.com/b/ie/archive/2009/01/09/the-internet-explorer-8-user-agent-string-updated-edition.aspx
 *
 * @param {string} userAgent the User-Agent.
 * @return {string}
 * @private
 */
goog.labs.userAgent.browser.getIEVersion_ = function(userAgent) {
  // IE11 may identify itself as MSIE 9.0 or MSIE 10.0 due to an IE 11 upgrade
  // bug. Example UA:
  // Mozilla/5.0 (MSIE 9.0; Windows NT 6.1; WOW64; Trident/7.0; rv:11.0)
  // like Gecko.
  // See http://www.whatismybrowser.com/developers/unknown-user-agent-fragments.
  var rv = /rv: *([\d\.]*)/.exec(userAgent);
  if (rv && rv[1]) {
    return rv[1];
  }

  var version = '';
  var msie = /MSIE +([\d\.]+)/.exec(userAgent);
  if (msie && msie[1]) {
    // IE in compatibility mode usually identifies itself as MSIE 7.0; in this
    // case, use the Trident version to determine the version of IE. For more
    // details, see the links above.
    var tridentVersion = /Trident\/(\d.\d)/.exec(userAgent);
    if (msie[1] == '7.0') {
      if (tridentVersion && tridentVersion[1]) {
        switch (tridentVersion[1]) {
          case '4.0':
            version = '8.0';
            break;
          case '5.0':
            version = '9.0';
            break;
          case '6.0':
            version = '10.0';
            break;
          case '7.0':
            version = '11.0';
            break;
        }
      } else {
        version = '7.0';
      }
    } else {
      version = msie[1];
    }
  }
  return version;
};

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Closure user agent detection.
 * @see http://en.wikipedia.org/wiki/User_agent
 * For more information on browser brand, platform, or device see the other
 * sub-namespaces in goog.labs.userAgent (browser, platform, and device).
 *
 */

goog.provide('goog.labs.userAgent.engine');

goog.require('goog.array');
goog.require('goog.labs.userAgent.util');
goog.require('goog.string');


/**
 * @return {boolean} Whether the rendering engine is Presto.
 */
goog.labs.userAgent.engine.isPresto = function() {
  return goog.labs.userAgent.util.matchUserAgent('Presto');
};


/**
 * @return {boolean} Whether the rendering engine is Trident.
 */
goog.labs.userAgent.engine.isTrident = function() {
  // IE only started including the Trident token in IE8.
  return goog.labs.userAgent.util.matchUserAgent('Trident') ||
      goog.labs.userAgent.util.matchUserAgent('MSIE');
};


/**
 * @return {boolean} Whether the rendering engine is Edge.
 */
goog.labs.userAgent.engine.isEdge = function() {
  return goog.labs.userAgent.util.matchUserAgent('Edge');
};


/**
 * @return {boolean} Whether the rendering engine is WebKit.
 */
goog.labs.userAgent.engine.isWebKit = function() {
  return goog.labs.userAgent.util.matchUserAgentIgnoreCase('WebKit') &&
      !goog.labs.userAgent.engine.isEdge();
};


/**
 * @return {boolean} Whether the rendering engine is Gecko.
 */
goog.labs.userAgent.engine.isGecko = function() {
  return goog.labs.userAgent.util.matchUserAgent('Gecko') &&
      !goog.labs.userAgent.engine.isWebKit() &&
      !goog.labs.userAgent.engine.isTrident() &&
      !goog.labs.userAgent.engine.isEdge();
};


/**
 * @return {string} The rendering engine's version or empty string if version
 *     can't be determined.
 */
goog.labs.userAgent.engine.getVersion = function() {
  var userAgentString = goog.labs.userAgent.util.getUserAgent();
  if (userAgentString) {
    var tuples = goog.labs.userAgent.util.extractVersionTuples(
        userAgentString);

    var engineTuple = goog.labs.userAgent.engine.getEngineTuple_(tuples);
    if (engineTuple) {
      // In Gecko, the version string is either in the browser info or the
      // Firefox version.  See Gecko user agent string reference:
      // http://goo.gl/mULqa
      if (engineTuple[0] == 'Gecko') {
        return goog.labs.userAgent.engine.getVersionForKey_(
            tuples, 'Firefox');
      }

      return engineTuple[1];
    }

    // MSIE has only one version identifier, and the Trident version is
    // specified in the parenthetical. IE Edge is covered in the engine tuple
    // detection.
    var browserTuple = tuples[0];
    var info;
    if (browserTuple && (info = browserTuple[2])) {
      var match = /Trident\/([^\s;]+)/.exec(info);
      if (match) {
        return match[1];
      }
    }
  }
  return '';
};


/**
 * @param {!Array<!Array<string>>} tuples Extracted version tuples.
 * @return {!Array<string>|undefined} The engine tuple or undefined if not
 *     found.
 * @private
 */
goog.labs.userAgent.engine.getEngineTuple_ = function(tuples) {
  if (!goog.labs.userAgent.engine.isEdge()) {
    return tuples[1];
  }
  for (var i = 0; i < tuples.length; i++) {
    var tuple = tuples[i];
    if (tuple[0] == 'Edge') {
      return tuple;
    }
  }
};


/**
 * @param {string|number} version The version to check.
 * @return {boolean} Whether the rendering engine version is higher or the same
 *     as the given version.
 */
goog.labs.userAgent.engine.isVersionOrHigher = function(version) {
  return goog.string.compareVersions(goog.labs.userAgent.engine.getVersion(),
                                     version) >= 0;
};


/**
 * @param {!Array<!Array<string>>} tuples Version tuples.
 * @param {string} key The key to look for.
 * @return {string} The version string of the given key, if present.
 *     Otherwise, the empty string.
 * @private
 */
goog.labs.userAgent.engine.getVersionForKey_ = function(tuples, key) {
  // TODO(nnaze): Move to util if useful elsewhere.

  var pair = goog.array.find(tuples, function(pair) {
    return key == pair[0];
  });

  return pair && pair[1] || '';
};

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Closure user agent platform detection.
 * @see <a href="http://www.useragentstring.com/">User agent strings</a>
 * For more information on browser brand, rendering engine, or device see the
 * other sub-namespaces in goog.labs.userAgent (browser, engine, and device
 * respectively).
 *
 */

goog.provide('goog.labs.userAgent.platform');

goog.require('goog.labs.userAgent.util');
goog.require('goog.string');


/**
 * @return {boolean} Whether the platform is Android.
 */
goog.labs.userAgent.platform.isAndroid = function() {
  return goog.labs.userAgent.util.matchUserAgent('Android');
};


/**
 * @return {boolean} Whether the platform is iPod.
 */
goog.labs.userAgent.platform.isIpod = function() {
  return goog.labs.userAgent.util.matchUserAgent('iPod');
};


/**
 * @return {boolean} Whether the platform is iPhone.
 */
goog.labs.userAgent.platform.isIphone = function() {
  return goog.labs.userAgent.util.matchUserAgent('iPhone') &&
      !goog.labs.userAgent.util.matchUserAgent('iPod') &&
      !goog.labs.userAgent.util.matchUserAgent('iPad');
};


/**
 * @return {boolean} Whether the platform is iPad.
 */
goog.labs.userAgent.platform.isIpad = function() {
  return goog.labs.userAgent.util.matchUserAgent('iPad');
};


/**
 * @return {boolean} Whether the platform is iOS.
 */
goog.labs.userAgent.platform.isIos = function() {
  return goog.labs.userAgent.platform.isIphone() ||
      goog.labs.userAgent.platform.isIpad() ||
      goog.labs.userAgent.platform.isIpod();
};


/**
 * @return {boolean} Whether the platform is Mac.
 */
goog.labs.userAgent.platform.isMacintosh = function() {
  return goog.labs.userAgent.util.matchUserAgent('Macintosh');
};


/**
 * Note: ChromeOS is not considered to be Linux as it does not report itself
 * as Linux in the user agent string.
 * @return {boolean} Whether the platform is Linux.
 */
goog.labs.userAgent.platform.isLinux = function() {
  return goog.labs.userAgent.util.matchUserAgent('Linux');
};


/**
 * @return {boolean} Whether the platform is Windows.
 */
goog.labs.userAgent.platform.isWindows = function() {
  return goog.labs.userAgent.util.matchUserAgent('Windows');
};


/**
 * @return {boolean} Whether the platform is ChromeOS.
 */
goog.labs.userAgent.platform.isChromeOS = function() {
  return goog.labs.userAgent.util.matchUserAgent('CrOS');
};


/**
 * The version of the platform. We only determine the version for Windows,
 * Mac, and Chrome OS. It doesn't make much sense on Linux. For Windows, we only
 * look at the NT version. Non-NT-based versions (e.g. 95, 98, etc.) are given
 * version 0.0.
 *
 * @return {string} The platform version or empty string if version cannot be
 *     determined.
 */
goog.labs.userAgent.platform.getVersion = function() {
  var userAgentString = goog.labs.userAgent.util.getUserAgent();
  var version = '', re;
  if (goog.labs.userAgent.platform.isWindows()) {
    re = /Windows (?:NT|Phone) ([0-9.]+)/;
    var match = re.exec(userAgentString);
    if (match) {
      version = match[1];
    } else {
      version = '0.0';
    }
  } else if (goog.labs.userAgent.platform.isIos()) {
    re = /(?:iPhone|iPod|iPad|CPU)\s+OS\s+(\S+)/;
    var match = re.exec(userAgentString);
    // Report the version as x.y.z and not x_y_z
    version = match && match[1].replace(/_/g, '.');
  } else if (goog.labs.userAgent.platform.isMacintosh()) {
    re = /Mac OS X ([0-9_.]+)/;
    var match = re.exec(userAgentString);
    // Note: some old versions of Camino do not report an OSX version.
    // Default to 10.
    version = match ? match[1].replace(/_/g, '.') : '10';
  } else if (goog.labs.userAgent.platform.isAndroid()) {
    re = /Android\s+([^\);]+)(\)|;)/;
    var match = re.exec(userAgentString);
    version = match && match[1];
  } else if (goog.labs.userAgent.platform.isChromeOS()) {
    re = /(?:CrOS\s+(?:i686|x86_64)\s+([0-9.]+))/;
    var match = re.exec(userAgentString);
    version = match && match[1];
  }
  return version || '';
};


/**
 * @param {string|number} version The version to check.
 * @return {boolean} Whether the browser version is higher or the same as the
 *     given version.
 */
goog.labs.userAgent.platform.isVersionOrHigher = function(version) {
  return goog.string.compareVersions(goog.labs.userAgent.platform.getVersion(),
                                     version) >= 0;
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Rendering engine detection.
 * @see <a href="http://www.useragentstring.com/">User agent strings</a>
 * For information on the browser brand (such as Safari versus Chrome), see
 * goog.userAgent.product.
 * @author arv@google.com (Erik Arvidsson)
 * @see ../demos/useragent.html
 */

goog.provide('goog.userAgent');

goog.require('goog.labs.userAgent.browser');
goog.require('goog.labs.userAgent.engine');
goog.require('goog.labs.userAgent.platform');
goog.require('goog.labs.userAgent.util');
goog.require('goog.string');


/**
 * @define {boolean} Whether we know at compile-time that the browser is IE.
 */
goog.define('goog.userAgent.ASSUME_IE', false);


/**
 * @define {boolean} Whether we know at compile-time that the browser is EDGE.
 */
goog.define('goog.userAgent.ASSUME_EDGE', false);


/**
 * @define {boolean} Whether we know at compile-time that the browser is GECKO.
 */
goog.define('goog.userAgent.ASSUME_GECKO', false);


/**
 * @define {boolean} Whether we know at compile-time that the browser is WEBKIT.
 */
goog.define('goog.userAgent.ASSUME_WEBKIT', false);


/**
 * @define {boolean} Whether we know at compile-time that the browser is a
 *     mobile device running WebKit e.g. iPhone or Android.
 */
goog.define('goog.userAgent.ASSUME_MOBILE_WEBKIT', false);


/**
 * @define {boolean} Whether we know at compile-time that the browser is OPERA.
 */
goog.define('goog.userAgent.ASSUME_OPERA', false);


/**
 * @define {boolean} Whether the
 *     {@code goog.userAgent.isVersionOrHigher}
 *     function will return true for any version.
 */
goog.define('goog.userAgent.ASSUME_ANY_VERSION', false);


/**
 * Whether we know the browser engine at compile-time.
 * @type {boolean}
 * @private
 */
goog.userAgent.BROWSER_KNOWN_ =
    goog.userAgent.ASSUME_IE ||
    goog.userAgent.ASSUME_EDGE ||
    goog.userAgent.ASSUME_GECKO ||
    goog.userAgent.ASSUME_MOBILE_WEBKIT ||
    goog.userAgent.ASSUME_WEBKIT ||
    goog.userAgent.ASSUME_OPERA;


/**
 * Returns the userAgent string for the current browser.
 *
 * @return {string} The userAgent string.
 */
goog.userAgent.getUserAgentString = function() {
  return goog.labs.userAgent.util.getUserAgent();
};


/**
 * TODO(nnaze): Change type to "Navigator" and update compilation targets.
 * @return {Object} The native navigator object.
 */
goog.userAgent.getNavigator = function() {
  // Need a local navigator reference instead of using the global one,
  // to avoid the rare case where they reference different objects.
  // (in a WorkerPool, for example).
  return goog.global['navigator'] || null;
};


/**
 * Whether the user agent is Opera.
 * @type {boolean}
 */
goog.userAgent.OPERA = goog.userAgent.BROWSER_KNOWN_ ?
    goog.userAgent.ASSUME_OPERA :
    goog.labs.userAgent.browser.isOpera();


/**
 * Whether the user agent is Internet Explorer.
 * @type {boolean}
 */
goog.userAgent.IE = goog.userAgent.BROWSER_KNOWN_ ?
    goog.userAgent.ASSUME_IE :
    goog.labs.userAgent.browser.isIE();


/**
 * Whether the user agent is Microsoft Edge.
 * @type {boolean}
 */
goog.userAgent.EDGE = goog.userAgent.BROWSER_KNOWN_ ?
    goog.userAgent.ASSUME_EDGE :
    goog.labs.userAgent.engine.isEdge();


/**
 * Whether the user agent is MS Internet Explorer or MS Edge.
 * @type {boolean}
 */
goog.userAgent.EDGE_OR_IE = goog.userAgent.EDGE || goog.userAgent.IE;


/**
 * Whether the user agent is Gecko. Gecko is the rendering engine used by
 * Mozilla, Firefox, and others.
 * @type {boolean}
 */
goog.userAgent.GECKO = goog.userAgent.BROWSER_KNOWN_ ?
    goog.userAgent.ASSUME_GECKO :
    goog.labs.userAgent.engine.isGecko();


/**
 * Whether the user agent is WebKit. WebKit is the rendering engine that
 * Safari, Android and others use.
 * @type {boolean}
 */
goog.userAgent.WEBKIT = goog.userAgent.BROWSER_KNOWN_ ?
    goog.userAgent.ASSUME_WEBKIT || goog.userAgent.ASSUME_MOBILE_WEBKIT :
    goog.labs.userAgent.engine.isWebKit();


/**
 * Whether the user agent is running on a mobile device.
 *
 * This is a separate function so that the logic can be tested.
 *
 * TODO(nnaze): Investigate swapping in goog.labs.userAgent.device.isMobile().
 *
 * @return {boolean} Whether the user agent is running on a mobile device.
 * @private
 */
goog.userAgent.isMobile_ = function() {
  return goog.userAgent.WEBKIT &&
         goog.labs.userAgent.util.matchUserAgent('Mobile');
};


/**
 * Whether the user agent is running on a mobile device.
 *
 * TODO(nnaze): Consider deprecating MOBILE when labs.userAgent
 *   is promoted as the gecko/webkit logic is likely inaccurate.
 *
 * @type {boolean}
 */
goog.userAgent.MOBILE = goog.userAgent.ASSUME_MOBILE_WEBKIT ||
                        goog.userAgent.isMobile_();


/**
 * Used while transitioning code to use WEBKIT instead.
 * @type {boolean}
 * @deprecated Use {@link goog.userAgent.product.SAFARI} instead.
 * TODO(nicksantos): Delete this from goog.userAgent.
 */
goog.userAgent.SAFARI = goog.userAgent.WEBKIT;


/**
 * @return {string} the platform (operating system) the user agent is running
 *     on. Default to empty string because navigator.platform may not be defined
 *     (on Rhino, for example).
 * @private
 */
goog.userAgent.determinePlatform_ = function() {
  var navigator = goog.userAgent.getNavigator();
  return navigator && navigator.platform || '';
};


/**
 * The platform (operating system) the user agent is running on. Default to
 * empty string because navigator.platform may not be defined (on Rhino, for
 * example).
 * @type {string}
 */
goog.userAgent.PLATFORM = goog.userAgent.determinePlatform_();


/**
 * @define {boolean} Whether the user agent is running on a Macintosh operating
 *     system.
 */
goog.define('goog.userAgent.ASSUME_MAC', false);


/**
 * @define {boolean} Whether the user agent is running on a Windows operating
 *     system.
 */
goog.define('goog.userAgent.ASSUME_WINDOWS', false);


/**
 * @define {boolean} Whether the user agent is running on a Linux operating
 *     system.
 */
goog.define('goog.userAgent.ASSUME_LINUX', false);


/**
 * @define {boolean} Whether the user agent is running on a X11 windowing
 *     system.
 */
goog.define('goog.userAgent.ASSUME_X11', false);


/**
 * @define {boolean} Whether the user agent is running on Android.
 */
goog.define('goog.userAgent.ASSUME_ANDROID', false);


/**
 * @define {boolean} Whether the user agent is running on an iPhone.
 */
goog.define('goog.userAgent.ASSUME_IPHONE', false);


/**
 * @define {boolean} Whether the user agent is running on an iPad.
 */
goog.define('goog.userAgent.ASSUME_IPAD', false);


/**
 * @type {boolean}
 * @private
 */
goog.userAgent.PLATFORM_KNOWN_ =
    goog.userAgent.ASSUME_MAC ||
    goog.userAgent.ASSUME_WINDOWS ||
    goog.userAgent.ASSUME_LINUX ||
    goog.userAgent.ASSUME_X11 ||
    goog.userAgent.ASSUME_ANDROID ||
    goog.userAgent.ASSUME_IPHONE ||
    goog.userAgent.ASSUME_IPAD;


/**
 * Whether the user agent is running on a Macintosh operating system.
 * @type {boolean}
 */
goog.userAgent.MAC = goog.userAgent.PLATFORM_KNOWN_ ?
    goog.userAgent.ASSUME_MAC : goog.labs.userAgent.platform.isMacintosh();


/**
 * Whether the user agent is running on a Windows operating system.
 * @type {boolean}
 */
goog.userAgent.WINDOWS = goog.userAgent.PLATFORM_KNOWN_ ?
    goog.userAgent.ASSUME_WINDOWS :
    goog.labs.userAgent.platform.isWindows();


/**
 * Whether the user agent is Linux per the legacy behavior of
 * goog.userAgent.LINUX, which considered ChromeOS to also be
 * Linux.
 * @return {boolean}
 * @private
 */
goog.userAgent.isLegacyLinux_ = function() {
  return goog.labs.userAgent.platform.isLinux() ||
      goog.labs.userAgent.platform.isChromeOS();
};


/**
 * Whether the user agent is running on a Linux operating system.
 *
 * Note that goog.userAgent.LINUX considers ChromeOS to be Linux,
 * while goog.labs.userAgent.platform considers ChromeOS and
 * Linux to be different OSes.
 *
 * @type {boolean}
 */
goog.userAgent.LINUX = goog.userAgent.PLATFORM_KNOWN_ ?
    goog.userAgent.ASSUME_LINUX :
    goog.userAgent.isLegacyLinux_();


/**
 * @return {boolean} Whether the user agent is an X11 windowing system.
 * @private
 */
goog.userAgent.isX11_ = function() {
  var navigator = goog.userAgent.getNavigator();
  return !!navigator &&
      goog.string.contains(navigator['appVersion'] || '', 'X11');
};


/**
 * Whether the user agent is running on a X11 windowing system.
 * @type {boolean}
 */
goog.userAgent.X11 = goog.userAgent.PLATFORM_KNOWN_ ?
    goog.userAgent.ASSUME_X11 :
    goog.userAgent.isX11_();


/**
 * Whether the user agent is running on Android.
 * @type {boolean}
 */
goog.userAgent.ANDROID = goog.userAgent.PLATFORM_KNOWN_ ?
    goog.userAgent.ASSUME_ANDROID :
    goog.labs.userAgent.platform.isAndroid();


/**
 * Whether the user agent is running on an iPhone.
 * @type {boolean}
 */
goog.userAgent.IPHONE = goog.userAgent.PLATFORM_KNOWN_ ?
    goog.userAgent.ASSUME_IPHONE :
    goog.labs.userAgent.platform.isIphone();


/**
 * Whether the user agent is running on an iPad.
 * @type {boolean}
 */
goog.userAgent.IPAD = goog.userAgent.PLATFORM_KNOWN_ ?
    goog.userAgent.ASSUME_IPAD :
    goog.labs.userAgent.platform.isIpad();


/**
 * @return {string} The string that describes the version number of the user
 *     agent.
 * Assumes user agent is opera.
 * @private
 */
goog.userAgent.operaVersion_ = function() {
  var version = goog.global.opera.version;
  try {
    return version();
  } catch (e) {
    return version;
  }
};


/**
 * @return {string} The string that describes the version number of the user
 *     agent.
 * @private
 */
goog.userAgent.determineVersion_ = function() {
  // All browsers have different ways to detect the version and they all have
  // different naming schemes.

  if (goog.userAgent.OPERA && goog.global['opera']) {
    return goog.userAgent.operaVersion_();
  }

  // version is a string rather than a number because it may contain 'b', 'a',
  // and so on.
  var version = '';
  var arr = goog.userAgent.getVersionRegexResult_();
  if (arr) {
    version = arr ? arr[1] : '';
  }

  if (goog.userAgent.IE) {
    // IE9 can be in document mode 9 but be reporting an inconsistent user agent
    // version.  If it is identifying as a version lower than 9 we take the
    // documentMode as the version instead.  IE8 has similar behavior.
    // It is recommended to set the X-UA-Compatible header to ensure that IE9
    // uses documentMode 9.
    var docMode = goog.userAgent.getDocumentMode_();
    if (docMode > parseFloat(version)) {
      return String(docMode);
    }
  }

  return version;
};


/**
 * @return {Array|undefined} The version regex matches from parsing the user
 *     agent string. These regex statements must be executed inline so they can
 *     be compiled out by the closure compiler with the rest of the useragent
 *     detection logic when ASSUME_* is specified.
 * @private
 */
goog.userAgent.getVersionRegexResult_ = function() {
  var userAgent = goog.userAgent.getUserAgentString();
  if (goog.userAgent.GECKO) {
    return /rv\:([^\);]+)(\)|;)/.exec(userAgent);
  }
  if (goog.userAgent.EDGE) {
    return /Edge\/([\d\.]+)/.exec(userAgent);
  }
  if (goog.userAgent.IE) {
    return /\b(?:MSIE|rv)[: ]([^\);]+)(\)|;)/.exec(userAgent);
  }
  if (goog.userAgent.WEBKIT) {
    // WebKit/125.4
    return /WebKit\/(\S+)/.exec(userAgent);
  }
};


/**
 * @return {number|undefined} Returns the document mode (for testing).
 * @private
 */
goog.userAgent.getDocumentMode_ = function() {
  // NOTE(user): goog.userAgent may be used in context where there is no DOM.
  var doc = goog.global['document'];
  return doc ? doc['documentMode'] : undefined;
};


/**
 * The version of the user agent. This is a string because it might contain
 * 'b' (as in beta) as well as multiple dots.
 * @type {string}
 */
goog.userAgent.VERSION = goog.userAgent.determineVersion_();


/**
 * Compares two version numbers.
 *
 * @param {string} v1 Version of first item.
 * @param {string} v2 Version of second item.
 *
 * @return {number}  1 if first argument is higher
 *                   0 if arguments are equal
 *                  -1 if second argument is higher.
 * @deprecated Use goog.string.compareVersions.
 */
goog.userAgent.compare = function(v1, v2) {
  return goog.string.compareVersions(v1, v2);
};


/**
 * Cache for {@link goog.userAgent.isVersionOrHigher}.
 * Calls to compareVersions are surprisingly expensive and, as a browser's
 * version number is unlikely to change during a session, we cache the results.
 * @const
 * @private
 */
goog.userAgent.isVersionOrHigherCache_ = {};


/**
 * Whether the user agent version is higher or the same as the given version.
 * NOTE: When checking the version numbers for Firefox and Safari, be sure to
 * use the engine's version, not the browser's version number.  For example,
 * Firefox 3.0 corresponds to Gecko 1.9 and Safari 3.0 to Webkit 522.11.
 * Opera and Internet Explorer versions match the product release number.<br>
 * @see <a href="http://en.wikipedia.org/wiki/Safari_version_history">
 *     Webkit</a>
 * @see <a href="http://en.wikipedia.org/wiki/Gecko_engine">Gecko</a>
 *
 * @param {string|number} version The version to check.
 * @return {boolean} Whether the user agent version is higher or the same as
 *     the given version.
 */
goog.userAgent.isVersionOrHigher = function(version) {
  return goog.userAgent.ASSUME_ANY_VERSION ||
      goog.userAgent.isVersionOrHigherCache_[version] ||
      (goog.userAgent.isVersionOrHigherCache_[version] =
          goog.string.compareVersions(goog.userAgent.VERSION, version) >= 0);
};


/**
 * Deprecated alias to {@code goog.userAgent.isVersionOrHigher}.
 * @param {string|number} version The version to check.
 * @return {boolean} Whether the user agent version is higher or the same as
 *     the given version.
 * @deprecated Use goog.userAgent.isVersionOrHigher().
 */
goog.userAgent.isVersion = goog.userAgent.isVersionOrHigher;


/**
 * Whether the IE effective document mode is higher or the same as the given
 * document mode version.
 * NOTE: Only for IE, return false for another browser.
 *
 * @param {number} documentMode The document mode version to check.
 * @return {boolean} Whether the IE effective document mode is higher or the
 *     same as the given version.
 */
goog.userAgent.isDocumentModeOrHigher = function(documentMode) {
  return goog.userAgent.DOCUMENT_MODE >= documentMode;
};


/**
 * Deprecated alias to {@code goog.userAgent.isDocumentModeOrHigher}.
 * @param {number} version The version to check.
 * @return {boolean} Whether the IE effective document mode is higher or the
 *      same as the given version.
 * @deprecated Use goog.userAgent.isDocumentModeOrHigher().
 */
goog.userAgent.isDocumentMode = goog.userAgent.isDocumentModeOrHigher;


/**
 * For IE version < 7, documentMode is undefined, so attempt to use the
 * CSS1Compat property to see if we are in standards mode. If we are in
 * standards mode, treat the browser version as the document mode. Otherwise,
 * IE is emulating version 5.
 * @type {number|undefined}
 * @const
 */
goog.userAgent.DOCUMENT_MODE = (function() {
  var doc = goog.global['document'];
  var mode = goog.userAgent.getDocumentMode_();
  if (!doc || !goog.userAgent.IE) {
    return undefined;
  }
  return mode || (doc['compatMode'] == 'CSS1Compat' ?
      parseInt(goog.userAgent.VERSION, 10) : 5);
})();

// Copyright 2010 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Browser capability checks for the events package.
 *
 */


goog.provide('goog.events.BrowserFeature');

goog.require('goog.userAgent');


/**
 * Enum of browser capabilities.
 * @enum {boolean}
 */
goog.events.BrowserFeature = {
  /**
   * Whether the button attribute of the event is W3C compliant.  False in
   * Internet Explorer prior to version 9; document-version dependent.
   */
  HAS_W3C_BUTTON: !goog.userAgent.IE ||
      goog.userAgent.isDocumentModeOrHigher(9),

  /**
   * Whether the browser supports full W3C event model.
   */
  HAS_W3C_EVENT_SUPPORT: !goog.userAgent.IE ||
      goog.userAgent.isDocumentModeOrHigher(9),

  /**
   * To prevent default in IE7-8 for certain keydown events we need set the
   * keyCode to -1.
   */
  SET_KEY_CODE_TO_PREVENT_DEFAULT: goog.userAgent.IE &&
      !goog.userAgent.isVersionOrHigher('9'),

  /**
   * Whether the {@code navigator.onLine} property is supported.
   */
  HAS_NAVIGATOR_ONLINE_PROPERTY: !goog.userAgent.WEBKIT ||
      goog.userAgent.isVersionOrHigher('528'),

  /**
   * Whether HTML5 network online/offline events are supported.
   */
  HAS_HTML5_NETWORK_EVENT_SUPPORT:
      goog.userAgent.GECKO && goog.userAgent.isVersionOrHigher('1.9b') ||
      goog.userAgent.IE && goog.userAgent.isVersionOrHigher('8') ||
      goog.userAgent.OPERA && goog.userAgent.isVersionOrHigher('9.5') ||
      goog.userAgent.WEBKIT && goog.userAgent.isVersionOrHigher('528'),

  /**
   * Whether HTML5 network events fire on document.body, or otherwise the
   * window.
   */
  HTML5_NETWORK_EVENTS_FIRE_ON_BODY:
      goog.userAgent.GECKO && !goog.userAgent.isVersionOrHigher('8') ||
      goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9'),

  /**
   * Whether touch is enabled in the browser.
   */
  TOUCH_ENABLED:
      ('ontouchstart' in goog.global ||
          !!(goog.global['document'] &&
             document.documentElement &&
             'ontouchstart' in document.documentElement) ||
          // IE10 uses non-standard touch events, so it has a different check.
          !!(goog.global['navigator'] &&
              goog.global['navigator']['msMaxTouchPoints']))
};

// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Definition of the disposable interface.  A disposable object
 * has a dispose method to to clean up references and resources.
 * @author nnaze@google.com (Nathan Naze)
 */


goog.provide('goog.disposable.IDisposable');



/**
 * Interface for a disposable object.  If a instance requires cleanup
 * (references COM objects, DOM notes, or other disposable objects), it should
 * implement this interface (it may subclass goog.Disposable).
 * @interface
 */
goog.disposable.IDisposable = function() {};


/**
 * Disposes of the object and its resources.
 * @return {void} Nothing.
 */
goog.disposable.IDisposable.prototype.dispose = goog.abstractMethod;


/**
 * @return {boolean} Whether the object has been disposed of.
 */
goog.disposable.IDisposable.prototype.isDisposed = goog.abstractMethod;

// Copyright 2005 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Implements the disposable interface. The dispose method is used
 * to clean up references and resources.
 * @author arv@google.com (Erik Arvidsson)
 */


goog.provide('goog.Disposable');
/** @suppress {extraProvide} */
goog.provide('goog.dispose');
/** @suppress {extraProvide} */
goog.provide('goog.disposeAll');

goog.require('goog.disposable.IDisposable');



/**
 * Class that provides the basic implementation for disposable objects. If your
 * class holds one or more references to COM objects, DOM nodes, or other
 * disposable objects, it should extend this class or implement the disposable
 * interface (defined in goog.disposable.IDisposable).
 * @constructor
 * @implements {goog.disposable.IDisposable}
 */
goog.Disposable = function() {
  if (goog.Disposable.MONITORING_MODE != goog.Disposable.MonitoringMode.OFF) {
    if (goog.Disposable.INCLUDE_STACK_ON_CREATION) {
      this.creationStack = new Error().stack;
    }
    goog.Disposable.instances_[goog.getUid(this)] = this;
  }
  // Support sealing
  this.disposed_ = this.disposed_;
  this.onDisposeCallbacks_ = this.onDisposeCallbacks_;
};


/**
 * @enum {number} Different monitoring modes for Disposable.
 */
goog.Disposable.MonitoringMode = {
  /**
   * No monitoring.
   */
  OFF: 0,
  /**
   * Creating and disposing the goog.Disposable instances is monitored. All
   * disposable objects need to call the {@code goog.Disposable} base
   * constructor. The PERMANENT mode must be switched on before creating any
   * goog.Disposable instances.
   */
  PERMANENT: 1,
  /**
   * INTERACTIVE mode can be switched on and off on the fly without producing
   * errors. It also doesn't warn if the disposable objects don't call the
   * {@code goog.Disposable} base constructor.
   */
  INTERACTIVE: 2
};


/**
 * @define {number} The monitoring mode of the goog.Disposable
 *     instances. Default is OFF. Switching on the monitoring is only
 *     recommended for debugging because it has a significant impact on
 *     performance and memory usage. If switched off, the monitoring code
 *     compiles down to 0 bytes.
 */
goog.define('goog.Disposable.MONITORING_MODE', 0);


/**
 * @define {boolean} Whether to attach creation stack to each created disposable
 *     instance; This is only relevant for when MonitoringMode != OFF.
 */
goog.define('goog.Disposable.INCLUDE_STACK_ON_CREATION', true);


/**
 * Maps the unique ID of every undisposed {@code goog.Disposable} object to
 * the object itself.
 * @type {!Object<number, !goog.Disposable>}
 * @private
 */
goog.Disposable.instances_ = {};


/**
 * @return {!Array<!goog.Disposable>} All {@code goog.Disposable} objects that
 *     haven't been disposed of.
 */
goog.Disposable.getUndisposedObjects = function() {
  var ret = [];
  for (var id in goog.Disposable.instances_) {
    if (goog.Disposable.instances_.hasOwnProperty(id)) {
      ret.push(goog.Disposable.instances_[Number(id)]);
    }
  }
  return ret;
};


/**
 * Clears the registry of undisposed objects but doesn't dispose of them.
 */
goog.Disposable.clearUndisposedObjects = function() {
  goog.Disposable.instances_ = {};
};


/**
 * Whether the object has been disposed of.
 * @type {boolean}
 * @private
 */
goog.Disposable.prototype.disposed_ = false;


/**
 * Callbacks to invoke when this object is disposed.
 * @type {Array<!Function>}
 * @private
 */
goog.Disposable.prototype.onDisposeCallbacks_;


/**
 * If monitoring the goog.Disposable instances is enabled, stores the creation
 * stack trace of the Disposable instance.
 * @const {string}
 */
goog.Disposable.prototype.creationStack;


/**
 * @return {boolean} Whether the object has been disposed of.
 * @override
 */
goog.Disposable.prototype.isDisposed = function() {
  return this.disposed_;
};


/**
 * @return {boolean} Whether the object has been disposed of.
 * @deprecated Use {@link #isDisposed} instead.
 */
goog.Disposable.prototype.getDisposed = goog.Disposable.prototype.isDisposed;


/**
 * Disposes of the object. If the object hasn't already been disposed of, calls
 * {@link #disposeInternal}. Classes that extend {@code goog.Disposable} should
 * override {@link #disposeInternal} in order to delete references to COM
 * objects, DOM nodes, and other disposable objects. Reentrant.
 *
 * @return {void} Nothing.
 * @override
 */
goog.Disposable.prototype.dispose = function() {
  if (!this.disposed_) {
    // Set disposed_ to true first, in case during the chain of disposal this
    // gets disposed recursively.
    this.disposed_ = true;
    this.disposeInternal();
    if (goog.Disposable.MONITORING_MODE != goog.Disposable.MonitoringMode.OFF) {
      var uid = goog.getUid(this);
      if (goog.Disposable.MONITORING_MODE ==
          goog.Disposable.MonitoringMode.PERMANENT &&
          !goog.Disposable.instances_.hasOwnProperty(uid)) {
        throw Error(this + ' did not call the goog.Disposable base ' +
            'constructor or was disposed of after a clearUndisposedObjects ' +
            'call');
      }
      delete goog.Disposable.instances_[uid];
    }
  }
};


/**
 * Associates a disposable object with this object so that they will be disposed
 * together.
 * @param {goog.disposable.IDisposable} disposable that will be disposed when
 *     this object is disposed.
 */
goog.Disposable.prototype.registerDisposable = function(disposable) {
  this.addOnDisposeCallback(goog.partial(goog.dispose, disposable));
};


/**
 * Invokes a callback function when this object is disposed. Callbacks are
 * invoked in the order in which they were added. If a callback is added to
 * an already disposed Disposable, it will be called immediately.
 * @param {function(this:T):?} callback The callback function.
 * @param {T=} opt_scope An optional scope to call the callback in.
 * @template T
 */
goog.Disposable.prototype.addOnDisposeCallback = function(callback, opt_scope) {
  if (this.disposed_) {
    callback.call(opt_scope);
    return;
  }
  if (!this.onDisposeCallbacks_) {
    this.onDisposeCallbacks_ = [];
  }

  this.onDisposeCallbacks_.push(
      goog.isDef(opt_scope) ? goog.bind(callback, opt_scope) : callback);
};


/**
 * Deletes or nulls out any references to COM objects, DOM nodes, or other
 * disposable objects. Classes that extend {@code goog.Disposable} should
 * override this method.
 * Not reentrant. To avoid calling it twice, it must only be called from the
 * subclass' {@code disposeInternal} method. Everywhere else the public
 * {@code dispose} method must be used.
 * For example:
 * <pre>
 *   mypackage.MyClass = function() {
 *     mypackage.MyClass.base(this, 'constructor');
 *     // Constructor logic specific to MyClass.
 *     ...
 *   };
 *   goog.inherits(mypackage.MyClass, goog.Disposable);
 *
 *   mypackage.MyClass.prototype.disposeInternal = function() {
 *     // Dispose logic specific to MyClass.
 *     ...
 *     // Call superclass's disposeInternal at the end of the subclass's, like
 *     // in C++, to avoid hard-to-catch issues.
 *     mypackage.MyClass.base(this, 'disposeInternal');
 *   };
 * </pre>
 * @protected
 */
goog.Disposable.prototype.disposeInternal = function() {
  if (this.onDisposeCallbacks_) {
    while (this.onDisposeCallbacks_.length) {
      this.onDisposeCallbacks_.shift()();
    }
  }
};


/**
 * Returns True if we can verify the object is disposed.
 * Calls {@code isDisposed} on the argument if it supports it.  If obj
 * is not an object with an isDisposed() method, return false.
 * @param {*} obj The object to investigate.
 * @return {boolean} True if we can verify the object is disposed.
 */
goog.Disposable.isDisposed = function(obj) {
  if (obj && typeof obj.isDisposed == 'function') {
    return obj.isDisposed();
  }
  return false;
};


/**
 * Calls {@code dispose} on the argument if it supports it. If obj is not an
 *     object with a dispose() method, this is a no-op.
 * @param {*} obj The object to dispose of.
 */
goog.dispose = function(obj) {
  if (obj && typeof obj.dispose == 'function') {
    obj.dispose();
  }
};


/**
 * Calls {@code dispose} on each member of the list that supports it. (If the
 * member is an ArrayLike, then {@code goog.disposeAll()} will be called
 * recursively on each of its members.) If the member is not an object with a
 * {@code dispose()} method, then it is ignored.
 * @param {...*} var_args The list.
 */
goog.disposeAll = function(var_args) {
  for (var i = 0, len = arguments.length; i < len; ++i) {
    var disposable = arguments[i];
    if (goog.isArrayLike(disposable)) {
      goog.disposeAll.apply(null, disposable);
    } else {
      goog.dispose(disposable);
    }
  }
};

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

goog.provide('goog.events.EventId');



/**
 * A templated class that is used when registering for events. Typical usage:
 * <code>
 *   /** @type {goog.events.EventId<MyEventObj>}
 *   var myEventId = new goog.events.EventId(
 *       goog.events.getUniqueId(('someEvent'));
 *
 *   // No need to cast or declare here since the compiler knows the correct
 *   // type of 'evt' (MyEventObj).
 *   something.listen(myEventId, function(evt) {});
 * </code>
 *
 * @param {string} eventId
 * @template T
 * @constructor
 * @struct
 * @final
 */
goog.events.EventId = function(eventId) {
  /** @const */ this.id = eventId;
};


/**
 * @override
 */
goog.events.EventId.prototype.toString = function() {
  return this.id;
};

// Copyright 2005 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview A base class for event objects.
 *
 */


goog.provide('goog.events.Event');
goog.provide('goog.events.EventLike');

/**
 * goog.events.Event no longer depends on goog.Disposable. Keep requiring
 * goog.Disposable here to not break projects which assume this dependency.
 * @suppress {extraRequire}
 */
goog.require('goog.Disposable');
goog.require('goog.events.EventId');


/**
 * A typedef for event like objects that are dispatchable via the
 * goog.events.dispatchEvent function. strings are treated as the type for a
 * goog.events.Event. Objects are treated as an extension of a new
 * goog.events.Event with the type property of the object being used as the type
 * of the Event.
 * @typedef {string|Object|goog.events.Event|goog.events.EventId}
 */
goog.events.EventLike;



/**
 * A base class for event objects, so that they can support preventDefault and
 * stopPropagation.
 *
 * @param {string|!goog.events.EventId} type Event Type.
 * @param {Object=} opt_target Reference to the object that is the target of
 *     this event. It has to implement the {@code EventTarget} interface
 *     declared at {@link http://developer.mozilla.org/en/DOM/EventTarget}.
 * @constructor
 */
goog.events.Event = function(type, opt_target) {
  /**
   * Event type.
   * @type {string}
   */
  this.type = type instanceof goog.events.EventId ? String(type) : type;

  /**
   * TODO(tbreisacher): The type should probably be
   * EventTarget|goog.events.EventTarget.
   *
   * Target of the event.
   * @type {Object|undefined}
   */
  this.target = opt_target;

  /**
   * Object that had the listener attached.
   * @type {Object|undefined}
   */
  this.currentTarget = this.target;

  /**
   * Whether to cancel the event in internal capture/bubble processing for IE.
   * @type {boolean}
   * @public
   * @suppress {underscore|visibility} Technically public, but referencing this
   *     outside this package is strongly discouraged.
   */
  this.propagationStopped_ = false;

  /**
   * Whether the default action has been prevented.
   * This is a property to match the W3C specification at
   * {@link http://www.w3.org/TR/DOM-Level-3-Events/
   * #events-event-type-defaultPrevented}.
   * Must be treated as read-only outside the class.
   * @type {boolean}
   */
  this.defaultPrevented = false;

  /**
   * Return value for in internal capture/bubble processing for IE.
   * @type {boolean}
   * @public
   * @suppress {underscore|visibility} Technically public, but referencing this
   *     outside this package is strongly discouraged.
   */
  this.returnValue_ = true;
};


/**
 * Stops event propagation.
 */
goog.events.Event.prototype.stopPropagation = function() {
  this.propagationStopped_ = true;
};


/**
 * Prevents the default action, for example a link redirecting to a url.
 */
goog.events.Event.prototype.preventDefault = function() {
  this.defaultPrevented = true;
  this.returnValue_ = false;
};


/**
 * Stops the propagation of the event. It is equivalent to
 * {@code e.stopPropagation()}, but can be used as the callback argument of
 * {@link goog.events.listen} without declaring another function.
 * @param {!goog.events.Event} e An event.
 */
goog.events.Event.stopPropagation = function(e) {
  e.stopPropagation();
};


/**
 * Prevents the default action. It is equivalent to
 * {@code e.preventDefault()}, but can be used as the callback argument of
 * {@link goog.events.listen} without declaring another function.
 * @param {!goog.events.Event} e An event.
 */
goog.events.Event.preventDefault = function(e) {
  e.preventDefault();
};

// Copyright 2010 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Event Types.
 *
 * @author arv@google.com (Erik Arvidsson)
 */


goog.provide('goog.events.EventType');

goog.require('goog.userAgent');


/**
 * Returns a prefixed event name for the current browser.
 * @param {string} eventName The name of the event.
 * @return {string} The prefixed event name.
 * @suppress {missingRequire|missingProvide}
 * @private
 */
goog.events.getVendorPrefixedName_ = function(eventName) {
  return goog.userAgent.WEBKIT ? 'webkit' + eventName :
      (goog.userAgent.OPERA ? 'o' + eventName.toLowerCase() :
          eventName.toLowerCase());
};


/**
 * Constants for event names.
 * @enum {string}
 */
goog.events.EventType = {
  // Mouse events
  CLICK: 'click',
  RIGHTCLICK: 'rightclick',
  DBLCLICK: 'dblclick',
  MOUSEDOWN: 'mousedown',
  MOUSEUP: 'mouseup',
  MOUSEOVER: 'mouseover',
  MOUSEOUT: 'mouseout',
  MOUSEMOVE: 'mousemove',
  MOUSEENTER: 'mouseenter',
  MOUSELEAVE: 'mouseleave',
  // Select start is non-standard.
  // See http://msdn.microsoft.com/en-us/library/ie/ms536969(v=vs.85).aspx.
  SELECTSTART: 'selectstart', // IE, Safari, Chrome

  // Wheel events
  // http://www.w3.org/TR/DOM-Level-3-Events/#events-wheelevents
  WHEEL: 'wheel',

  // Key events
  KEYPRESS: 'keypress',
  KEYDOWN: 'keydown',
  KEYUP: 'keyup',

  // Focus
  BLUR: 'blur',
  FOCUS: 'focus',
  DEACTIVATE: 'deactivate', // IE only
  // NOTE: The following two events are not stable in cross-browser usage.
  //     WebKit and Opera implement DOMFocusIn/Out.
  //     IE implements focusin/out.
  //     Gecko implements neither see bug at
  //     https://bugzilla.mozilla.org/show_bug.cgi?id=396927.
  // The DOM Events Level 3 Draft deprecates DOMFocusIn in favor of focusin:
  //     http://dev.w3.org/2006/webapi/DOM-Level-3-Events/html/DOM3-Events.html
  // You can use FOCUS in Capture phase until implementations converge.
  FOCUSIN: goog.userAgent.IE ? 'focusin' : 'DOMFocusIn',
  FOCUSOUT: goog.userAgent.IE ? 'focusout' : 'DOMFocusOut',

  // Forms
  CHANGE: 'change',
  RESET: 'reset',
  SELECT: 'select',
  SUBMIT: 'submit',
  INPUT: 'input',
  PROPERTYCHANGE: 'propertychange', // IE only

  // Drag and drop
  DRAGSTART: 'dragstart',
  DRAG: 'drag',
  DRAGENTER: 'dragenter',
  DRAGOVER: 'dragover',
  DRAGLEAVE: 'dragleave',
  DROP: 'drop',
  DRAGEND: 'dragend',

  // Touch events
  // Note that other touch events exist, but we should follow the W3C list here.
  // http://www.w3.org/TR/touch-events/#list-of-touchevent-types
  TOUCHSTART: 'touchstart',
  TOUCHMOVE: 'touchmove',
  TOUCHEND: 'touchend',
  TOUCHCANCEL: 'touchcancel',

  // Misc
  BEFOREUNLOAD: 'beforeunload',
  CONSOLEMESSAGE: 'consolemessage',
  CONTEXTMENU: 'contextmenu',
  DOMCONTENTLOADED: 'DOMContentLoaded',
  ERROR: 'error',
  HELP: 'help',
  LOAD: 'load',
  LOSECAPTURE: 'losecapture',
  ORIENTATIONCHANGE: 'orientationchange',
  READYSTATECHANGE: 'readystatechange',
  RESIZE: 'resize',
  SCROLL: 'scroll',
  UNLOAD: 'unload',

  // HTML 5 History events
  // See http://www.w3.org/TR/html5/browsers.html#event-definitions-0
  HASHCHANGE: 'hashchange',
  PAGEHIDE: 'pagehide',
  PAGESHOW: 'pageshow',
  POPSTATE: 'popstate',

  // Copy and Paste
  // Support is limited. Make sure it works on your favorite browser
  // before using.
  // http://www.quirksmode.org/dom/events/cutcopypaste.html
  COPY: 'copy',
  PASTE: 'paste',
  CUT: 'cut',
  BEFORECOPY: 'beforecopy',
  BEFORECUT: 'beforecut',
  BEFOREPASTE: 'beforepaste',

  // HTML5 online/offline events.
  // http://www.w3.org/TR/offline-webapps/#related
  ONLINE: 'online',
  OFFLINE: 'offline',

  // HTML 5 worker events
  MESSAGE: 'message',
  CONNECT: 'connect',

  // CSS animation events.
  /** @suppress {missingRequire} */
  ANIMATIONSTART: goog.events.getVendorPrefixedName_('AnimationStart'),
  /** @suppress {missingRequire} */
  ANIMATIONEND: goog.events.getVendorPrefixedName_('AnimationEnd'),
  /** @suppress {missingRequire} */
  ANIMATIONITERATION: goog.events.getVendorPrefixedName_('AnimationIteration'),

  // CSS transition events. Based on the browser support described at:
  // https://developer.mozilla.org/en/css/css_transitions#Browser_compatibility
  /** @suppress {missingRequire} */
  TRANSITIONEND: goog.events.getVendorPrefixedName_('TransitionEnd'),

  // W3C Pointer Events
  // http://www.w3.org/TR/pointerevents/
  POINTERDOWN: 'pointerdown',
  POINTERUP: 'pointerup',
  POINTERCANCEL: 'pointercancel',
  POINTERMOVE: 'pointermove',
  POINTEROVER: 'pointerover',
  POINTEROUT: 'pointerout',
  POINTERENTER: 'pointerenter',
  POINTERLEAVE: 'pointerleave',
  GOTPOINTERCAPTURE: 'gotpointercapture',
  LOSTPOINTERCAPTURE: 'lostpointercapture',

  // IE specific events.
  // See http://msdn.microsoft.com/en-us/library/ie/hh772103(v=vs.85).aspx
  // Note: these events will be supplanted in IE11.
  MSGESTURECHANGE: 'MSGestureChange',
  MSGESTUREEND: 'MSGestureEnd',
  MSGESTUREHOLD: 'MSGestureHold',
  MSGESTURESTART: 'MSGestureStart',
  MSGESTURETAP: 'MSGestureTap',
  MSGOTPOINTERCAPTURE: 'MSGotPointerCapture',
  MSINERTIASTART: 'MSInertiaStart',
  MSLOSTPOINTERCAPTURE: 'MSLostPointerCapture',
  MSPOINTERCANCEL: 'MSPointerCancel',
  MSPOINTERDOWN: 'MSPointerDown',
  MSPOINTERENTER: 'MSPointerEnter',
  MSPOINTERHOVER: 'MSPointerHover',
  MSPOINTERLEAVE: 'MSPointerLeave',
  MSPOINTERMOVE: 'MSPointerMove',
  MSPOINTEROUT: 'MSPointerOut',
  MSPOINTEROVER: 'MSPointerOver',
  MSPOINTERUP: 'MSPointerUp',

  // Native IMEs/input tools events.
  TEXT: 'text',
  TEXTINPUT: 'textInput',
  COMPOSITIONSTART: 'compositionstart',
  COMPOSITIONUPDATE: 'compositionupdate',
  COMPOSITIONEND: 'compositionend',

  // Webview tag events
  // See http://developer.chrome.com/dev/apps/webview_tag.html
  EXIT: 'exit',
  LOADABORT: 'loadabort',
  LOADCOMMIT: 'loadcommit',
  LOADREDIRECT: 'loadredirect',
  LOADSTART: 'loadstart',
  LOADSTOP: 'loadstop',
  RESPONSIVE: 'responsive',
  SIZECHANGED: 'sizechanged',
  UNRESPONSIVE: 'unresponsive',

  // HTML5 Page Visibility API.  See details at
  // {@code goog.labs.dom.PageVisibilityMonitor}.
  VISIBILITYCHANGE: 'visibilitychange',

  // LocalStorage event.
  STORAGE: 'storage',

  // DOM Level 2 mutation events (deprecated).
  DOMSUBTREEMODIFIED: 'DOMSubtreeModified',
  DOMNODEINSERTED: 'DOMNodeInserted',
  DOMNODEREMOVED: 'DOMNodeRemoved',
  DOMNODEREMOVEDFROMDOCUMENT: 'DOMNodeRemovedFromDocument',
  DOMNODEINSERTEDINTODOCUMENT: 'DOMNodeInsertedIntoDocument',
  DOMATTRMODIFIED: 'DOMAttrModified',
  DOMCHARACTERDATAMODIFIED: 'DOMCharacterDataModified',

  // Print events.
  BEFOREPRINT: 'beforeprint',
  AFTERPRINT: 'afterprint'
};

// Copyright 2009 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Useful compiler idioms.
 *
 * @author johnlenz@google.com (John Lenz)
 */

goog.provide('goog.reflect');


/**
 * Syntax for object literal casts.
 * @see http://go/jscompiler-renaming
 * @see https://github.com/google/closure-compiler/wiki/Type-Based-Property-Renaming
 *
 * Use this if you have an object literal whose keys need to have the same names
 * as the properties of some class even after they are renamed by the compiler.
 *
 * @param {!Function} type Type to cast to.
 * @param {Object} object Object literal to cast.
 * @return {Object} The object literal.
 */
goog.reflect.object = function(type, object) {
  return object;
};


/**
 * To assert to the compiler that an operation is needed when it would
 * otherwise be stripped. For example:
 * <code>
 *     // Force a layout
 *     goog.reflect.sinkValue(dialog.offsetHeight);
 * </code>
 * @type {!Function}
 */
goog.reflect.sinkValue = function(x) {
  goog.reflect.sinkValue[' '](x);
  return x;
};


/**
 * The compiler should optimize this function away iff no one ever uses
 * goog.reflect.sinkValue.
 */
goog.reflect.sinkValue[' '] = goog.nullFunction;


/**
 * Check if a property can be accessed without throwing an exception.
 * @param {Object} obj The owner of the property.
 * @param {string} prop The property name.
 * @return {boolean} Whether the property is accessible. Will also return true
 *     if obj is null.
 */
goog.reflect.canAccessProperty = function(obj, prop) {
  /** @preserveTry */
  try {
    goog.reflect.sinkValue(obj[prop]);
    return true;
  } catch (e) {}
  return false;
};

// Copyright 2005 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview A patched, standardized event object for browser events.
 *
 * <pre>
 * The patched event object contains the following members:
 * - type           {string}    Event type, e.g. 'click'
 * - target         {Object}    The element that actually triggered the event
 * - currentTarget  {Object}    The element the listener is attached to
 * - relatedTarget  {Object}    For mouseover and mouseout, the previous object
 * - offsetX        {number}    X-coordinate relative to target
 * - offsetY        {number}    Y-coordinate relative to target
 * - clientX        {number}    X-coordinate relative to viewport
 * - clientY        {number}    Y-coordinate relative to viewport
 * - screenX        {number}    X-coordinate relative to the edge of the screen
 * - screenY        {number}    Y-coordinate relative to the edge of the screen
 * - button         {number}    Mouse button. Use isButton() to test.
 * - keyCode        {number}    Key-code
 * - ctrlKey        {boolean}   Was ctrl key depressed
 * - altKey         {boolean}   Was alt key depressed
 * - shiftKey       {boolean}   Was shift key depressed
 * - metaKey        {boolean}   Was meta key depressed
 * - defaultPrevented {boolean} Whether the default action has been prevented
 * - state          {Object}    History state object
 *
 * NOTE: The keyCode member contains the raw browser keyCode. For normalized
 * key and character code use {@link goog.events.KeyHandler}.
 * </pre>
 *
 * @author arv@google.com (Erik Arvidsson)
 */

goog.provide('goog.events.BrowserEvent');
goog.provide('goog.events.BrowserEvent.MouseButton');

goog.require('goog.events.BrowserFeature');
goog.require('goog.events.Event');
goog.require('goog.events.EventType');
goog.require('goog.reflect');
goog.require('goog.userAgent');



/**
 * Accepts a browser event object and creates a patched, cross browser event
 * object.
 * The content of this object will not be initialized if no event object is
 * provided. If this is the case, init() needs to be invoked separately.
 * @param {Event=} opt_e Browser event object.
 * @param {EventTarget=} opt_currentTarget Current target for event.
 * @constructor
 * @extends {goog.events.Event}
 */
goog.events.BrowserEvent = function(opt_e, opt_currentTarget) {
  goog.events.BrowserEvent.base(this, 'constructor', opt_e ? opt_e.type : '');

  /**
   * Target that fired the event.
   * @override
   * @type {Node}
   */
  this.target = null;

  /**
   * Node that had the listener attached.
   * @override
   * @type {Node|undefined}
   */
  this.currentTarget = null;

  /**
   * For mouseover and mouseout events, the related object for the event.
   * @type {Node}
   */
  this.relatedTarget = null;

  /**
   * X-coordinate relative to target.
   * @type {number}
   */
  this.offsetX = 0;

  /**
   * Y-coordinate relative to target.
   * @type {number}
   */
  this.offsetY = 0;

  /**
   * X-coordinate relative to the window.
   * @type {number}
   */
  this.clientX = 0;

  /**
   * Y-coordinate relative to the window.
   * @type {number}
   */
  this.clientY = 0;

  /**
   * X-coordinate relative to the monitor.
   * @type {number}
   */
  this.screenX = 0;

  /**
   * Y-coordinate relative to the monitor.
   * @type {number}
   */
  this.screenY = 0;

  /**
   * Which mouse button was pressed.
   * @type {number}
   */
  this.button = 0;

  /**
   * Keycode of key press.
   * @type {number}
   */
  this.keyCode = 0;

  /**
   * Keycode of key press.
   * @type {number}
   */
  this.charCode = 0;

  /**
   * Whether control was pressed at time of event.
   * @type {boolean}
   */
  this.ctrlKey = false;

  /**
   * Whether alt was pressed at time of event.
   * @type {boolean}
   */
  this.altKey = false;

  /**
   * Whether shift was pressed at time of event.
   * @type {boolean}
   */
  this.shiftKey = false;

  /**
   * Whether the meta key was pressed at time of event.
   * @type {boolean}
   */
  this.metaKey = false;

  /**
   * History state object, only set for PopState events where it's a copy of the
   * state object provided to pushState or replaceState.
   * @type {Object}
   */
  this.state = null;

  /**
   * Whether the default platform modifier key was pressed at time of event.
   * (This is control for all platforms except Mac, where it's Meta.)
   * @type {boolean}
   */
  this.platformModifierKey = false;

  /**
   * The browser event object.
   * @private {Event}
   */
  this.event_ = null;

  if (opt_e) {
    this.init(opt_e, opt_currentTarget);
  }
};
goog.inherits(goog.events.BrowserEvent, goog.events.Event);


/**
 * Normalized button constants for the mouse.
 * @enum {number}
 */
goog.events.BrowserEvent.MouseButton = {
  LEFT: 0,
  MIDDLE: 1,
  RIGHT: 2
};


/**
 * Static data for mapping mouse buttons.
 * @type {!Array<number>}
 */
goog.events.BrowserEvent.IEButtonMap = [
  1, // LEFT
  4, // MIDDLE
  2  // RIGHT
];


/**
 * Accepts a browser event object and creates a patched, cross browser event
 * object.
 * @param {Event} e Browser event object.
 * @param {EventTarget=} opt_currentTarget Current target for event.
 */
goog.events.BrowserEvent.prototype.init = function(e, opt_currentTarget) {
  var type = this.type = e.type;

  /**
   * On touch devices use the first "changed touch" as the relevant touch.
   * @type {Touch}
   */
  var relevantTouch = e.changedTouches ? e.changedTouches[0] : null;

  // TODO(nicksantos): Change this.target to type EventTarget.
  this.target = /** @type {Node} */ (e.target) || e.srcElement;

  // TODO(nicksantos): Change this.currentTarget to type EventTarget.
  this.currentTarget = /** @type {Node} */ (opt_currentTarget);

  var relatedTarget = /** @type {Node} */ (e.relatedTarget);
  if (relatedTarget) {
    // There's a bug in FireFox where sometimes, relatedTarget will be a
    // chrome element, and accessing any property of it will get a permission
    // denied exception. See:
    // https://bugzilla.mozilla.org/show_bug.cgi?id=497780
    if (goog.userAgent.GECKO) {
      if (!goog.reflect.canAccessProperty(relatedTarget, 'nodeName')) {
        relatedTarget = null;
      }
    }
    // TODO(arv): Use goog.events.EventType when it has been refactored into its
    // own file.
  } else if (type == goog.events.EventType.MOUSEOVER) {
    relatedTarget = e.fromElement;
  } else if (type == goog.events.EventType.MOUSEOUT) {
    relatedTarget = e.toElement;
  }

  this.relatedTarget = relatedTarget;

  if (!goog.isNull(relevantTouch)) {
    this.clientX = relevantTouch.clientX !== undefined ?
        relevantTouch.clientX : relevantTouch.pageX;
    this.clientY = relevantTouch.clientY !== undefined ?
        relevantTouch.clientY : relevantTouch.pageY;
    this.screenX = relevantTouch.screenX || 0;
    this.screenY = relevantTouch.screenY || 0;
  } else {
    // Webkit emits a lame warning whenever layerX/layerY is accessed.
    // http://code.google.com/p/chromium/issues/detail?id=101733
    this.offsetX = (goog.userAgent.WEBKIT || e.offsetX !== undefined) ?
        e.offsetX : e.layerX;
    this.offsetY = (goog.userAgent.WEBKIT || e.offsetY !== undefined) ?
        e.offsetY : e.layerY;
    this.clientX = e.clientX !== undefined ? e.clientX : e.pageX;
    this.clientY = e.clientY !== undefined ? e.clientY : e.pageY;
    this.screenX = e.screenX || 0;
    this.screenY = e.screenY || 0;
  }

  this.button = e.button;

  this.keyCode = e.keyCode || 0;
  this.charCode = e.charCode || (type == 'keypress' ? e.keyCode : 0);
  this.ctrlKey = e.ctrlKey;
  this.altKey = e.altKey;
  this.shiftKey = e.shiftKey;
  this.metaKey = e.metaKey;
  this.platformModifierKey = goog.userAgent.MAC ? e.metaKey : e.ctrlKey;
  this.state = e.state;
  this.event_ = e;
  if (e.defaultPrevented) {
    this.preventDefault();
  }
};


/**
 * Tests to see which button was pressed during the event. This is really only
 * useful in IE and Gecko browsers. And in IE, it's only useful for
 * mousedown/mouseup events, because click only fires for the left mouse button.
 *
 * Safari 2 only reports the left button being clicked, and uses the value '1'
 * instead of 0. Opera only reports a mousedown event for the middle button, and
 * no mouse events for the right button. Opera has default behavior for left and
 * middle click that can only be overridden via a configuration setting.
 *
 * There's a nice table of this mess at http://www.unixpapa.com/js/mouse.html.
 *
 * @param {goog.events.BrowserEvent.MouseButton} button The button
 *     to test for.
 * @return {boolean} True if button was pressed.
 */
goog.events.BrowserEvent.prototype.isButton = function(button) {
  if (!goog.events.BrowserFeature.HAS_W3C_BUTTON) {
    if (this.type == 'click') {
      return button == goog.events.BrowserEvent.MouseButton.LEFT;
    } else {
      return !!(this.event_.button &
          goog.events.BrowserEvent.IEButtonMap[button]);
    }
  } else {
    return this.event_.button == button;
  }
};


/**
 * Whether this has an "action"-producing mouse button.
 *
 * By definition, this includes left-click on windows/linux, and left-click
 * without the ctrl key on Macs.
 *
 * @return {boolean} The result.
 */
goog.events.BrowserEvent.prototype.isMouseActionButton = function() {
  // Webkit does not ctrl+click to be a right-click, so we
  // normalize it to behave like Gecko and Opera.
  return this.isButton(goog.events.BrowserEvent.MouseButton.LEFT) &&
      !(goog.userAgent.WEBKIT && goog.userAgent.MAC && this.ctrlKey);
};


/**
 * @override
 */
goog.events.BrowserEvent.prototype.stopPropagation = function() {
  goog.events.BrowserEvent.superClass_.stopPropagation.call(this);
  if (this.event_.stopPropagation) {
    this.event_.stopPropagation();
  } else {
    this.event_.cancelBubble = true;
  }
};


/**
 * @override
 */
goog.events.BrowserEvent.prototype.preventDefault = function() {
  goog.events.BrowserEvent.superClass_.preventDefault.call(this);
  var be = this.event_;
  if (!be.preventDefault) {
    be.returnValue = false;
    if (goog.events.BrowserFeature.SET_KEY_CODE_TO_PREVENT_DEFAULT) {
      /** @preserveTry */
      try {
        // Most keys can be prevented using returnValue. Some special keys
        // require setting the keyCode to -1 as well:
        //
        // In IE7:
        // F3, F5, F10, F11, Ctrl+P, Crtl+O, Ctrl+F (these are taken from IE6)
        //
        // In IE8:
        // Ctrl+P, Crtl+O, Ctrl+F (F1-F12 cannot be stopped through the event)
        //
        // We therefore do this for all function keys as well as when Ctrl key
        // is pressed.
        var VK_F1 = 112;
        var VK_F12 = 123;
        if (be.ctrlKey || be.keyCode >= VK_F1 && be.keyCode <= VK_F12) {
          be.keyCode = -1;
        }
      } catch (ex) {
        // IE throws an 'access denied' exception when trying to change
        // keyCode in some situations (e.g. srcElement is input[type=file],
        // or srcElement is an anchor tag rewritten by parent's innerHTML).
        // Do nothing in this case.
      }
    }
  } else {
    be.preventDefault();
  }
};


/**
 * @return {Event} The underlying browser event object.
 */
goog.events.BrowserEvent.prototype.getBrowserEvent = function() {
  return this.event_;
};

// Copyright 2012 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview An interface for a listenable JavaScript object.
 * @author chrishenry@google.com (Chris Henry)
 */

goog.provide('goog.events.Listenable');
goog.provide('goog.events.ListenableKey');

/** @suppress {extraRequire} */
goog.require('goog.events.EventId');



/**
 * A listenable interface. A listenable is an object with the ability
 * to dispatch/broadcast events to "event listeners" registered via
 * listen/listenOnce.
 *
 * The interface allows for an event propagation mechanism similar
 * to one offered by native browser event targets, such as
 * capture/bubble mechanism, stopping propagation, and preventing
 * default actions. Capture/bubble mechanism depends on the ancestor
 * tree constructed via {@code #getParentEventTarget}; this tree
 * must be directed acyclic graph. The meaning of default action(s)
 * in preventDefault is specific to a particular use case.
 *
 * Implementations that do not support capture/bubble or can not have
 * a parent listenable can simply not implement any ability to set the
 * parent listenable (and have {@code #getParentEventTarget} return
 * null).
 *
 * Implementation of this class can be used with or independently from
 * goog.events.
 *
 * Implementation must call {@code #addImplementation(implClass)}.
 *
 * @interface
 * @see goog.events
 * @see http://www.w3.org/TR/DOM-Level-2-Events/events.html
 */
goog.events.Listenable = function() {};


/**
 * An expando property to indicate that an object implements
 * goog.events.Listenable.
 *
 * See addImplementation/isImplementedBy.
 *
 * @type {string}
 * @const
 */
goog.events.Listenable.IMPLEMENTED_BY_PROP =
    'closure_listenable_' + ((Math.random() * 1e6) | 0);


/**
 * Marks a given class (constructor) as an implementation of
 * Listenable, do that we can query that fact at runtime. The class
 * must have already implemented the interface.
 * @param {!Function} cls The class constructor. The corresponding
 *     class must have already implemented the interface.
 */
goog.events.Listenable.addImplementation = function(cls) {
  cls.prototype[goog.events.Listenable.IMPLEMENTED_BY_PROP] = true;
};


/**
 * @param {Object} obj The object to check.
 * @return {boolean} Whether a given instance implements Listenable. The
 *     class/superclass of the instance must call addImplementation.
 */
goog.events.Listenable.isImplementedBy = function(obj) {
  return !!(obj && obj[goog.events.Listenable.IMPLEMENTED_BY_PROP]);
};


/**
 * Adds an event listener. A listener can only be added once to an
 * object and if it is added again the key for the listener is
 * returned. Note that if the existing listener is a one-off listener
 * (registered via listenOnce), it will no longer be a one-off
 * listener after a call to listen().
 *
 * @param {string|!goog.events.EventId<EVENTOBJ>} type The event type id.
 * @param {function(this:SCOPE, EVENTOBJ):(boolean|undefined)} listener Callback
 *     method.
 * @param {boolean=} opt_useCapture Whether to fire in capture phase
 *     (defaults to false).
 * @param {SCOPE=} opt_listenerScope Object in whose scope to call the
 *     listener.
 * @return {goog.events.ListenableKey} Unique key for the listener.
 * @template SCOPE,EVENTOBJ
 */
goog.events.Listenable.prototype.listen;


/**
 * Adds an event listener that is removed automatically after the
 * listener fired once.
 *
 * If an existing listener already exists, listenOnce will do
 * nothing. In particular, if the listener was previously registered
 * via listen(), listenOnce() will not turn the listener into a
 * one-off listener. Similarly, if there is already an existing
 * one-off listener, listenOnce does not modify the listeners (it is
 * still a once listener).
 *
 * @param {string|!goog.events.EventId<EVENTOBJ>} type The event type id.
 * @param {function(this:SCOPE, EVENTOBJ):(boolean|undefined)} listener Callback
 *     method.
 * @param {boolean=} opt_useCapture Whether to fire in capture phase
 *     (defaults to false).
 * @param {SCOPE=} opt_listenerScope Object in whose scope to call the
 *     listener.
 * @return {goog.events.ListenableKey} Unique key for the listener.
 * @template SCOPE,EVENTOBJ
 */
goog.events.Listenable.prototype.listenOnce;


/**
 * Removes an event listener which was added with listen() or listenOnce().
 *
 * @param {string|!goog.events.EventId<EVENTOBJ>} type The event type id.
 * @param {function(this:SCOPE, EVENTOBJ):(boolean|undefined)} listener Callback
 *     method.
 * @param {boolean=} opt_useCapture Whether to fire in capture phase
 *     (defaults to false).
 * @param {SCOPE=} opt_listenerScope Object in whose scope to call
 *     the listener.
 * @return {boolean} Whether any listener was removed.
 * @template SCOPE,EVENTOBJ
 */
goog.events.Listenable.prototype.unlisten;


/**
 * Removes an event listener which was added with listen() by the key
 * returned by listen().
 *
 * @param {goog.events.ListenableKey} key The key returned by
 *     listen() or listenOnce().
 * @return {boolean} Whether any listener was removed.
 */
goog.events.Listenable.prototype.unlistenByKey;


/**
 * Dispatches an event (or event like object) and calls all listeners
 * listening for events of this type. The type of the event is decided by the
 * type property on the event object.
 *
 * If any of the listeners returns false OR calls preventDefault then this
 * function will return false.  If one of the capture listeners calls
 * stopPropagation, then the bubble listeners won't fire.
 *
 * @param {goog.events.EventLike} e Event object.
 * @return {boolean} If anyone called preventDefault on the event object (or
 *     if any of the listeners returns false) this will also return false.
 */
goog.events.Listenable.prototype.dispatchEvent;


/**
 * Removes all listeners from this listenable. If type is specified,
 * it will only remove listeners of the particular type. otherwise all
 * registered listeners will be removed.
 *
 * @param {string=} opt_type Type of event to remove, default is to
 *     remove all types.
 * @return {number} Number of listeners removed.
 */
goog.events.Listenable.prototype.removeAllListeners;


/**
 * Returns the parent of this event target to use for capture/bubble
 * mechanism.
 *
 * NOTE(chrishenry): The name reflects the original implementation of
 * custom event target ({@code goog.events.EventTarget}). We decided
 * that changing the name is not worth it.
 *
 * @return {goog.events.Listenable} The parent EventTarget or null if
 *     there is no parent.
 */
goog.events.Listenable.prototype.getParentEventTarget;


/**
 * Fires all registered listeners in this listenable for the given
 * type and capture mode, passing them the given eventObject. This
 * does not perform actual capture/bubble. Only implementors of the
 * interface should be using this.
 *
 * @param {string|!goog.events.EventId<EVENTOBJ>} type The type of the
 *     listeners to fire.
 * @param {boolean} capture The capture mode of the listeners to fire.
 * @param {EVENTOBJ} eventObject The event object to fire.
 * @return {boolean} Whether all listeners succeeded without
 *     attempting to prevent default behavior. If any listener returns
 *     false or called goog.events.Event#preventDefault, this returns
 *     false.
 * @template EVENTOBJ
 */
goog.events.Listenable.prototype.fireListeners;


/**
 * Gets all listeners in this listenable for the given type and
 * capture mode.
 *
 * @param {string|!goog.events.EventId} type The type of the listeners to fire.
 * @param {boolean} capture The capture mode of the listeners to fire.
 * @return {!Array<goog.events.ListenableKey>} An array of registered
 *     listeners.
 * @template EVENTOBJ
 */
goog.events.Listenable.prototype.getListeners;


/**
 * Gets the goog.events.ListenableKey for the event or null if no such
 * listener is in use.
 *
 * @param {string|!goog.events.EventId<EVENTOBJ>} type The name of the event
 *     without the 'on' prefix.
 * @param {function(this:SCOPE, EVENTOBJ):(boolean|undefined)} listener The
 *     listener function to get.
 * @param {boolean} capture Whether the listener is a capturing listener.
 * @param {SCOPE=} opt_listenerScope Object in whose scope to call the
 *     listener.
 * @return {goog.events.ListenableKey} the found listener or null if not found.
 * @template SCOPE,EVENTOBJ
 */
goog.events.Listenable.prototype.getListener;


/**
 * Whether there is any active listeners matching the specified
 * signature. If either the type or capture parameters are
 * unspecified, the function will match on the remaining criteria.
 *
 * @param {string|!goog.events.EventId<EVENTOBJ>=} opt_type Event type.
 * @param {boolean=} opt_capture Whether to check for capture or bubble
 *     listeners.
 * @return {boolean} Whether there is any active listeners matching
 *     the requested type and/or capture phase.
 * @template EVENTOBJ
 */
goog.events.Listenable.prototype.hasListener;



/**
 * An interface that describes a single registered listener.
 * @interface
 */
goog.events.ListenableKey = function() {};


/**
 * Counter used to create a unique key
 * @type {number}
 * @private
 */
goog.events.ListenableKey.counter_ = 0;


/**
 * Reserves a key to be used for ListenableKey#key field.
 * @return {number} A number to be used to fill ListenableKey#key
 *     field.
 */
goog.events.ListenableKey.reserveKey = function() {
  return ++goog.events.ListenableKey.counter_;
};


/**
 * The source event target.
 * @type {!(Object|goog.events.Listenable|goog.events.EventTarget)}
 */
goog.events.ListenableKey.prototype.src;


/**
 * The event type the listener is listening to.
 * @type {string}
 */
goog.events.ListenableKey.prototype.type;


/**
 * The listener function.
 * @type {function(?):?|{handleEvent:function(?):?}|null}
 */
goog.events.ListenableKey.prototype.listener;


/**
 * Whether the listener works on capture phase.
 * @type {boolean}
 */
goog.events.ListenableKey.prototype.capture;


/**
 * The 'this' object for the listener function's scope.
 * @type {Object}
 */
goog.events.ListenableKey.prototype.handler;


/**
 * A globally unique number to identify the key.
 * @type {number}
 */
goog.events.ListenableKey.prototype.key;

// Copyright 2005 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Listener object.
 * @see ../demos/events.html
 */

goog.provide('goog.events.Listener');

goog.require('goog.events.ListenableKey');



/**
 * Simple class that stores information about a listener
 * @param {!Function} listener Callback function.
 * @param {Function} proxy Wrapper for the listener that patches the event.
 * @param {EventTarget|goog.events.Listenable} src Source object for
 *     the event.
 * @param {string} type Event type.
 * @param {boolean} capture Whether in capture or bubble phase.
 * @param {Object=} opt_handler Object in whose context to execute the callback.
 * @implements {goog.events.ListenableKey}
 * @constructor
 */
goog.events.Listener = function(
    listener, proxy, src, type, capture, opt_handler) {
  if (goog.events.Listener.ENABLE_MONITORING) {
    this.creationStack = new Error().stack;
  }

  /**
   * Callback function.
   * @type {Function}
   */
  this.listener = listener;

  /**
   * A wrapper over the original listener. This is used solely to
   * handle native browser events (it is used to simulate the capture
   * phase and to patch the event object).
   * @type {Function}
   */
  this.proxy = proxy;

  /**
   * Object or node that callback is listening to
   * @type {EventTarget|goog.events.Listenable}
   */
  this.src = src;

  /**
   * The event type.
   * @const {string}
   */
  this.type = type;

  /**
   * Whether the listener is being called in the capture or bubble phase
   * @const {boolean}
   */
  this.capture = !!capture;

  /**
   * Optional object whose context to execute the listener in
   * @type {Object|undefined}
   */
  this.handler = opt_handler;

  /**
   * The key of the listener.
   * @const {number}
   * @override
   */
  this.key = goog.events.ListenableKey.reserveKey();

  /**
   * Whether to remove the listener after it has been called.
   * @type {boolean}
   */
  this.callOnce = false;

  /**
   * Whether the listener has been removed.
   * @type {boolean}
   */
  this.removed = false;
};


/**
 * @define {boolean} Whether to enable the monitoring of the
 *     goog.events.Listener instances. Switching on the monitoring is only
 *     recommended for debugging because it has a significant impact on
 *     performance and memory usage. If switched off, the monitoring code
 *     compiles down to 0 bytes.
 */
goog.define('goog.events.Listener.ENABLE_MONITORING', false);


/**
 * If monitoring the goog.events.Listener instances is enabled, stores the
 * creation stack trace of the Disposable instance.
 * @type {string}
 */
goog.events.Listener.prototype.creationStack;


/**
 * Marks this listener as removed. This also remove references held by
 * this listener object (such as listener and event source).
 */
goog.events.Listener.prototype.markAsRemoved = function() {
  this.removed = true;
  this.listener = null;
  this.proxy = null;
  this.src = null;
  this.handler = null;
};

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview A map of listeners that provides utility functions to
 * deal with listeners on an event target. Used by
 * {@code goog.events.EventTarget}.
 *
 * WARNING: Do not use this class from outside goog.events package.
 *
 * @visibility {//closure/goog/bin/sizetests:__pkg__}
 * @visibility {//closure/goog/events:__pkg__}
 * @visibility {//closure/goog/labs/events:__pkg__}
 */

goog.provide('goog.events.ListenerMap');

goog.require('goog.array');
goog.require('goog.events.Listener');
goog.require('goog.object');



/**
 * Creates a new listener map.
 * @param {EventTarget|goog.events.Listenable} src The src object.
 * @constructor
 * @final
 */
goog.events.ListenerMap = function(src) {
  /** @type {EventTarget|goog.events.Listenable} */
  this.src = src;

  /**
   * Maps of event type to an array of listeners.
   * @type {Object<string, !Array<!goog.events.Listener>>}
   */
  this.listeners = {};

  /**
   * The count of types in this map that have registered listeners.
   * @private {number}
   */
  this.typeCount_ = 0;
};


/**
 * @return {number} The count of event types in this map that actually
 *     have registered listeners.
 */
goog.events.ListenerMap.prototype.getTypeCount = function() {
  return this.typeCount_;
};


/**
 * @return {number} Total number of registered listeners.
 */
goog.events.ListenerMap.prototype.getListenerCount = function() {
  var count = 0;
  for (var type in this.listeners) {
    count += this.listeners[type].length;
  }
  return count;
};


/**
 * Adds an event listener. A listener can only be added once to an
 * object and if it is added again the key for the listener is
 * returned.
 *
 * Note that a one-off listener will not change an existing listener,
 * if any. On the other hand a normal listener will change existing
 * one-off listener to become a normal listener.
 *
 * @param {string|!goog.events.EventId} type The listener event type.
 * @param {!Function} listener This listener callback method.
 * @param {boolean} callOnce Whether the listener is a one-off
 *     listener.
 * @param {boolean=} opt_useCapture The capture mode of the listener.
 * @param {Object=} opt_listenerScope Object in whose scope to call the
 *     listener.
 * @return {goog.events.ListenableKey} Unique key for the listener.
 */
goog.events.ListenerMap.prototype.add = function(
    type, listener, callOnce, opt_useCapture, opt_listenerScope) {
  var typeStr = type.toString();
  var listenerArray = this.listeners[typeStr];
  if (!listenerArray) {
    listenerArray = this.listeners[typeStr] = [];
    this.typeCount_++;
  }

  var listenerObj;
  var index = goog.events.ListenerMap.findListenerIndex_(
      listenerArray, listener, opt_useCapture, opt_listenerScope);
  if (index > -1) {
    listenerObj = listenerArray[index];
    if (!callOnce) {
      // Ensure that, if there is an existing callOnce listener, it is no
      // longer a callOnce listener.
      listenerObj.callOnce = false;
    }
  } else {
    listenerObj = new goog.events.Listener(
        listener, null, this.src, typeStr, !!opt_useCapture, opt_listenerScope);
    listenerObj.callOnce = callOnce;
    listenerArray.push(listenerObj);
  }
  return listenerObj;
};


/**
 * Removes a matching listener.
 * @param {string|!goog.events.EventId} type The listener event type.
 * @param {!Function} listener This listener callback method.
 * @param {boolean=} opt_useCapture The capture mode of the listener.
 * @param {Object=} opt_listenerScope Object in whose scope to call the
 *     listener.
 * @return {boolean} Whether any listener was removed.
 */
goog.events.ListenerMap.prototype.remove = function(
    type, listener, opt_useCapture, opt_listenerScope) {
  var typeStr = type.toString();
  if (!(typeStr in this.listeners)) {
    return false;
  }

  var listenerArray = this.listeners[typeStr];
  var index = goog.events.ListenerMap.findListenerIndex_(
      listenerArray, listener, opt_useCapture, opt_listenerScope);
  if (index > -1) {
    var listenerObj = listenerArray[index];
    listenerObj.markAsRemoved();
    goog.array.removeAt(listenerArray, index);
    if (listenerArray.length == 0) {
      delete this.listeners[typeStr];
      this.typeCount_--;
    }
    return true;
  }
  return false;
};


/**
 * Removes the given listener object.
 * @param {goog.events.ListenableKey} listener The listener to remove.
 * @return {boolean} Whether the listener is removed.
 */
goog.events.ListenerMap.prototype.removeByKey = function(listener) {
  var type = listener.type;
  if (!(type in this.listeners)) {
    return false;
  }

  var removed = goog.array.remove(this.listeners[type], listener);
  if (removed) {
    listener.markAsRemoved();
    if (this.listeners[type].length == 0) {
      delete this.listeners[type];
      this.typeCount_--;
    }
  }
  return removed;
};


/**
 * Removes all listeners from this map. If opt_type is provided, only
 * listeners that match the given type are removed.
 * @param {string|!goog.events.EventId=} opt_type Type of event to remove.
 * @return {number} Number of listeners removed.
 */
goog.events.ListenerMap.prototype.removeAll = function(opt_type) {
  var typeStr = opt_type && opt_type.toString();
  var count = 0;
  for (var type in this.listeners) {
    if (!typeStr || type == typeStr) {
      var listenerArray = this.listeners[type];
      for (var i = 0; i < listenerArray.length; i++) {
        ++count;
        listenerArray[i].markAsRemoved();
      }
      delete this.listeners[type];
      this.typeCount_--;
    }
  }
  return count;
};


/**
 * Gets all listeners that match the given type and capture mode. The
 * returned array is a copy (but the listener objects are not).
 * @param {string|!goog.events.EventId} type The type of the listeners
 *     to retrieve.
 * @param {boolean} capture The capture mode of the listeners to retrieve.
 * @return {!Array<goog.events.ListenableKey>} An array of matching
 *     listeners.
 */
goog.events.ListenerMap.prototype.getListeners = function(type, capture) {
  var listenerArray = this.listeners[type.toString()];
  var rv = [];
  if (listenerArray) {
    for (var i = 0; i < listenerArray.length; ++i) {
      var listenerObj = listenerArray[i];
      if (listenerObj.capture == capture) {
        rv.push(listenerObj);
      }
    }
  }
  return rv;
};


/**
 * Gets the goog.events.ListenableKey for the event or null if no such
 * listener is in use.
 *
 * @param {string|!goog.events.EventId} type The type of the listener
 *     to retrieve.
 * @param {!Function} listener The listener function to get.
 * @param {boolean} capture Whether the listener is a capturing listener.
 * @param {Object=} opt_listenerScope Object in whose scope to call the
 *     listener.
 * @return {goog.events.ListenableKey} the found listener or null if not found.
 */
goog.events.ListenerMap.prototype.getListener = function(
    type, listener, capture, opt_listenerScope) {
  var listenerArray = this.listeners[type.toString()];
  var i = -1;
  if (listenerArray) {
    i = goog.events.ListenerMap.findListenerIndex_(
        listenerArray, listener, capture, opt_listenerScope);
  }
  return i > -1 ? listenerArray[i] : null;
};


/**
 * Whether there is a matching listener. If either the type or capture
 * parameters are unspecified, the function will match on the
 * remaining criteria.
 *
 * @param {string|!goog.events.EventId=} opt_type The type of the listener.
 * @param {boolean=} opt_capture The capture mode of the listener.
 * @return {boolean} Whether there is an active listener matching
 *     the requested type and/or capture phase.
 */
goog.events.ListenerMap.prototype.hasListener = function(
    opt_type, opt_capture) {
  var hasType = goog.isDef(opt_type);
  var typeStr = hasType ? opt_type.toString() : '';
  var hasCapture = goog.isDef(opt_capture);

  return goog.object.some(
      this.listeners, function(listenerArray, type) {
        for (var i = 0; i < listenerArray.length; ++i) {
          if ((!hasType || listenerArray[i].type == typeStr) &&
              (!hasCapture || listenerArray[i].capture == opt_capture)) {
            return true;
          }
        }

        return false;
      });
};


/**
 * Finds the index of a matching goog.events.Listener in the given
 * listenerArray.
 * @param {!Array<!goog.events.Listener>} listenerArray Array of listener.
 * @param {!Function} listener The listener function.
 * @param {boolean=} opt_useCapture The capture flag for the listener.
 * @param {Object=} opt_listenerScope The listener scope.
 * @return {number} The index of the matching listener within the
 *     listenerArray.
 * @private
 */
goog.events.ListenerMap.findListenerIndex_ = function(
    listenerArray, listener, opt_useCapture, opt_listenerScope) {
  for (var i = 0; i < listenerArray.length; ++i) {
    var listenerObj = listenerArray[i];
    if (!listenerObj.removed &&
        listenerObj.listener == listener &&
        listenerObj.capture == !!opt_useCapture &&
        listenerObj.handler == opt_listenerScope) {
      return i;
    }
  }
  return -1;
};

// Copyright 2005 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview An event manager for both native browser event
 * targets and custom JavaScript event targets
 * ({@code goog.events.Listenable}). This provides an abstraction
 * over browsers' event systems.
 *
 * It also provides a simulation of W3C event model's capture phase in
 * Internet Explorer (IE 8 and below). Caveat: the simulation does not
 * interact well with listeners registered directly on the elements
 * (bypassing goog.events) or even with listeners registered via
 * goog.events in a separate JS binary. In these cases, we provide
 * no ordering guarantees.
 *
 * The listeners will receive a "patched" event object. Such event object
 * contains normalized values for certain event properties that differs in
 * different browsers.
 *
 * Example usage:
 * <pre>
 * goog.events.listen(myNode, 'click', function(e) { alert('woo') });
 * goog.events.listen(myNode, 'mouseover', mouseHandler, true);
 * goog.events.unlisten(myNode, 'mouseover', mouseHandler, true);
 * goog.events.removeAll(myNode);
 * </pre>
 *
 *                                            in IE and event object patching]
 * @author arv@google.com (Erik Arvidsson)
 *
 * @see ../demos/events.html
 * @see ../demos/event-propagation.html
 * @see ../demos/stopevent.html
 */

// IMPLEMENTATION NOTES:
// goog.events stores an auxiliary data structure on each EventTarget
// source being listened on. This allows us to take advantage of GC,
// having the data structure GC'd when the EventTarget is GC'd. This
// GC behavior is equivalent to using W3C DOM Events directly.

goog.provide('goog.events');
goog.provide('goog.events.CaptureSimulationMode');
goog.provide('goog.events.Key');
goog.provide('goog.events.ListenableType');

goog.require('goog.asserts');
goog.require('goog.debug.entryPointRegistry');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.BrowserFeature');
goog.require('goog.events.Listenable');
goog.require('goog.events.ListenerMap');

goog.forwardDeclare('goog.debug.ErrorHandler');
goog.forwardDeclare('goog.events.EventWrapper');


/**
 * @typedef {number|goog.events.ListenableKey}
 */
goog.events.Key;


/**
 * @typedef {EventTarget|goog.events.Listenable}
 */
goog.events.ListenableType;


/**
 * Property name on a native event target for the listener map
 * associated with the event target.
 * @private @const {string}
 */
goog.events.LISTENER_MAP_PROP_ = 'closure_lm_' + ((Math.random() * 1e6) | 0);


/**
 * String used to prepend to IE event types.
 * @const
 * @private
 */
goog.events.onString_ = 'on';


/**
 * Map of computed "on<eventname>" strings for IE event types. Caching
 * this removes an extra object allocation in goog.events.listen which
 * improves IE6 performance.
 * @const
 * @dict
 * @private
 */
goog.events.onStringMap_ = {};


/**
 * @enum {number} Different capture simulation mode for IE8-.
 */
goog.events.CaptureSimulationMode = {
  /**
   * Does not perform capture simulation. Will asserts in IE8- when you
   * add capture listeners.
   */
  OFF_AND_FAIL: 0,

  /**
   * Does not perform capture simulation, silently ignore capture
   * listeners.
   */
  OFF_AND_SILENT: 1,

  /**
   * Performs capture simulation.
   */
  ON: 2
};


/**
 * @define {number} The capture simulation mode for IE8-. By default,
 *     this is ON.
 */
goog.define('goog.events.CAPTURE_SIMULATION_MODE', 2);


/**
 * Estimated count of total native listeners.
 * @private {number}
 */
goog.events.listenerCountEstimate_ = 0;


/**
 * Adds an event listener for a specific event on a native event
 * target (such as a DOM element) or an object that has implemented
 * {@link goog.events.Listenable}. A listener can only be added once
 * to an object and if it is added again the key for the listener is
 * returned. Note that if the existing listener is a one-off listener
 * (registered via listenOnce), it will no longer be a one-off
 * listener after a call to listen().
 *
 * @param {EventTarget|goog.events.Listenable} src The node to listen
 *     to events on.
 * @param {string|Array<string>|
 *     !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
 *     type Event type or array of event types.
 * @param {function(this:T, EVENTOBJ):?|{handleEvent:function(?):?}|null}
 *     listener Callback method, or an object with a handleEvent function.
 *     WARNING: passing an Object is now softly deprecated.
 * @param {boolean=} opt_capt Whether to fire in capture phase (defaults to
 *     false).
 * @param {T=} opt_handler Element in whose scope to call the listener.
 * @return {goog.events.Key} Unique key for the listener.
 * @template T,EVENTOBJ
 */
goog.events.listen = function(src, type, listener, opt_capt, opt_handler) {
  if (goog.isArray(type)) {
    for (var i = 0; i < type.length; i++) {
      goog.events.listen(src, type[i], listener, opt_capt, opt_handler);
    }
    return null;
  }

  listener = goog.events.wrapListener(listener);
  if (goog.events.Listenable.isImplementedBy(src)) {
    return src.listen(
        /** @type {string|!goog.events.EventId} */ (type),
        listener, opt_capt, opt_handler);
  } else {
    return goog.events.listen_(
        /** @type {!EventTarget} */ (src),
        /** @type {string|!goog.events.EventId} */ (type),
        listener, /* callOnce */ false, opt_capt, opt_handler);
  }
};


/**
 * Adds an event listener for a specific event on a native event
 * target. A listener can only be added once to an object and if it
 * is added again the key for the listener is returned.
 *
 * Note that a one-off listener will not change an existing listener,
 * if any. On the other hand a normal listener will change existing
 * one-off listener to become a normal listener.
 *
 * @param {EventTarget} src The node to listen to events on.
 * @param {string|!goog.events.EventId} type Event type.
 * @param {!Function} listener Callback function.
 * @param {boolean} callOnce Whether the listener is a one-off
 *     listener or otherwise.
 * @param {boolean=} opt_capt Whether to fire in capture phase (defaults to
 *     false).
 * @param {Object=} opt_handler Element in whose scope to call the listener.
 * @return {goog.events.ListenableKey} Unique key for the listener.
 * @private
 */
goog.events.listen_ = function(
    src, type, listener, callOnce, opt_capt, opt_handler) {
  if (!type) {
    throw Error('Invalid event type');
  }

  var capture = !!opt_capt;
  if (capture && !goog.events.BrowserFeature.HAS_W3C_EVENT_SUPPORT) {
    if (goog.events.CAPTURE_SIMULATION_MODE ==
        goog.events.CaptureSimulationMode.OFF_AND_FAIL) {
      goog.asserts.fail('Can not register capture listener in IE8-.');
      return null;
    } else if (goog.events.CAPTURE_SIMULATION_MODE ==
        goog.events.CaptureSimulationMode.OFF_AND_SILENT) {
      return null;
    }
  }

  var listenerMap = goog.events.getListenerMap_(src);
  if (!listenerMap) {
    src[goog.events.LISTENER_MAP_PROP_] = listenerMap =
        new goog.events.ListenerMap(src);
  }

  var listenerObj = listenerMap.add(
      type, listener, callOnce, opt_capt, opt_handler);

  // If the listenerObj already has a proxy, it has been set up
  // previously. We simply return.
  if (listenerObj.proxy) {
    return listenerObj;
  }

  var proxy = goog.events.getProxy();
  listenerObj.proxy = proxy;

  proxy.src = src;
  proxy.listener = listenerObj;

  // Attach the proxy through the browser's API
  if (src.addEventListener) {
    src.addEventListener(type.toString(), proxy, capture);
  } else if (src.attachEvent) {
    // The else if above used to be an unconditional else. It would call
    // exception on IE11, spoiling the day of some callers. The previous
    // incarnation of this code, from 2007, indicates that it replaced an
    // earlier still version that caused excess allocations on IE6.
    src.attachEvent(goog.events.getOnString_(type.toString()), proxy);
  } else {
    throw Error('addEventListener and attachEvent are unavailable.');
  }

  goog.events.listenerCountEstimate_++;
  return listenerObj;
};


/**
 * Helper function for returning a proxy function.
 * @return {!Function} A new or reused function object.
 */
goog.events.getProxy = function() {
  var proxyCallbackFunction = goog.events.handleBrowserEvent_;
  // Use a local var f to prevent one allocation.
  var f = goog.events.BrowserFeature.HAS_W3C_EVENT_SUPPORT ?
      function(eventObject) {
        return proxyCallbackFunction.call(f.src, f.listener, eventObject);
      } :
      function(eventObject) {
        var v = proxyCallbackFunction.call(f.src, f.listener, eventObject);
        // NOTE(chrishenry): In IE, we hack in a capture phase. However, if
        // there is inline event handler which tries to prevent default (for
        // example <a href="..." onclick="return false">...</a>) in a
        // descendant element, the prevent default will be overridden
        // by this listener if this listener were to return true. Hence, we
        // return undefined.
        if (!v) return v;
      };
  return f;
};


/**
 * Adds an event listener for a specific event on a native event
 * target (such as a DOM element) or an object that has implemented
 * {@link goog.events.Listenable}. After the event has fired the event
 * listener is removed from the target.
 *
 * If an existing listener already exists, listenOnce will do
 * nothing. In particular, if the listener was previously registered
 * via listen(), listenOnce() will not turn the listener into a
 * one-off listener. Similarly, if there is already an existing
 * one-off listener, listenOnce does not modify the listeners (it is
 * still a once listener).
 *
 * @param {EventTarget|goog.events.Listenable} src The node to listen
 *     to events on.
 * @param {string|Array<string>|
 *     !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
 *     type Event type or array of event types.
 * @param {function(this:T, EVENTOBJ):?|{handleEvent:function(?):?}|null}
 *     listener Callback method.
 * @param {boolean=} opt_capt Fire in capture phase?.
 * @param {T=} opt_handler Element in whose scope to call the listener.
 * @return {goog.events.Key} Unique key for the listener.
 * @template T,EVENTOBJ
 */
goog.events.listenOnce = function(src, type, listener, opt_capt, opt_handler) {
  if (goog.isArray(type)) {
    for (var i = 0; i < type.length; i++) {
      goog.events.listenOnce(src, type[i], listener, opt_capt, opt_handler);
    }
    return null;
  }

  listener = goog.events.wrapListener(listener);
  if (goog.events.Listenable.isImplementedBy(src)) {
    return src.listenOnce(
        /** @type {string|!goog.events.EventId} */ (type),
        listener, opt_capt, opt_handler);
  } else {
    return goog.events.listen_(
        /** @type {!EventTarget} */ (src),
        /** @type {string|!goog.events.EventId} */ (type),
        listener, /* callOnce */ true, opt_capt, opt_handler);
  }
};


/**
 * Adds an event listener with a specific event wrapper on a DOM Node or an
 * object that has implemented {@link goog.events.Listenable}. A listener can
 * only be added once to an object.
 *
 * @param {EventTarget|goog.events.Listenable} src The target to
 *     listen to events on.
 * @param {goog.events.EventWrapper} wrapper Event wrapper to use.
 * @param {function(this:T, ?):?|{handleEvent:function(?):?}|null} listener
 *     Callback method, or an object with a handleEvent function.
 * @param {boolean=} opt_capt Whether to fire in capture phase (defaults to
 *     false).
 * @param {T=} opt_handler Element in whose scope to call the listener.
 * @template T
 */
goog.events.listenWithWrapper = function(src, wrapper, listener, opt_capt,
    opt_handler) {
  wrapper.listen(src, listener, opt_capt, opt_handler);
};


/**
 * Removes an event listener which was added with listen().
 *
 * @param {EventTarget|goog.events.Listenable} src The target to stop
 *     listening to events on.
 * @param {string|Array<string>|
 *     !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
 *     type Event type or array of event types to unlisten to.
 * @param {function(?):?|{handleEvent:function(?):?}|null} listener The
 *     listener function to remove.
 * @param {boolean=} opt_capt In DOM-compliant browsers, this determines
 *     whether the listener is fired during the capture or bubble phase of the
 *     event.
 * @param {Object=} opt_handler Element in whose scope to call the listener.
 * @return {?boolean} indicating whether the listener was there to remove.
 * @template EVENTOBJ
 */
goog.events.unlisten = function(src, type, listener, opt_capt, opt_handler) {
  if (goog.isArray(type)) {
    for (var i = 0; i < type.length; i++) {
      goog.events.unlisten(src, type[i], listener, opt_capt, opt_handler);
    }
    return null;
  }

  listener = goog.events.wrapListener(listener);
  if (goog.events.Listenable.isImplementedBy(src)) {
    return src.unlisten(
        /** @type {string|!goog.events.EventId} */ (type),
        listener, opt_capt, opt_handler);
  }

  if (!src) {
    // TODO(chrishenry): We should tighten the API to only accept
    // non-null objects, or add an assertion here.
    return false;
  }

  var capture = !!opt_capt;
  var listenerMap = goog.events.getListenerMap_(
      /** @type {!EventTarget} */ (src));
  if (listenerMap) {
    var listenerObj = listenerMap.getListener(
        /** @type {string|!goog.events.EventId} */ (type),
        listener, capture, opt_handler);
    if (listenerObj) {
      return goog.events.unlistenByKey(listenerObj);
    }
  }

  return false;
};


/**
 * Removes an event listener which was added with listen() by the key
 * returned by listen().
 *
 * @param {goog.events.Key} key The key returned by listen() for this
 *     event listener.
 * @return {boolean} indicating whether the listener was there to remove.
 */
goog.events.unlistenByKey = function(key) {
  // TODO(chrishenry): Remove this check when tests that rely on this
  // are fixed.
  if (goog.isNumber(key)) {
    return false;
  }

  var listener = key;
  if (!listener || listener.removed) {
    return false;
  }

  var src = listener.src;
  if (goog.events.Listenable.isImplementedBy(src)) {
    return src.unlistenByKey(listener);
  }

  var type = listener.type;
  var proxy = listener.proxy;
  if (src.removeEventListener) {
    src.removeEventListener(type, proxy, listener.capture);
  } else if (src.detachEvent) {
    src.detachEvent(goog.events.getOnString_(type), proxy);
  }
  goog.events.listenerCountEstimate_--;

  var listenerMap = goog.events.getListenerMap_(
      /** @type {!EventTarget} */ (src));
  // TODO(chrishenry): Try to remove this conditional and execute the
  // first branch always. This should be safe.
  if (listenerMap) {
    listenerMap.removeByKey(listener);
    if (listenerMap.getTypeCount() == 0) {
      // Null the src, just because this is simple to do (and useful
      // for IE <= 7).
      listenerMap.src = null;
      // We don't use delete here because IE does not allow delete
      // on a window object.
      src[goog.events.LISTENER_MAP_PROP_] = null;
    }
  } else {
    listener.markAsRemoved();
  }

  return true;
};


/**
 * Removes an event listener which was added with listenWithWrapper().
 *
 * @param {EventTarget|goog.events.Listenable} src The target to stop
 *     listening to events on.
 * @param {goog.events.EventWrapper} wrapper Event wrapper to use.
 * @param {function(?):?|{handleEvent:function(?):?}|null} listener The
 *     listener function to remove.
 * @param {boolean=} opt_capt In DOM-compliant browsers, this determines
 *     whether the listener is fired during the capture or bubble phase of the
 *     event.
 * @param {Object=} opt_handler Element in whose scope to call the listener.
 */
goog.events.unlistenWithWrapper = function(src, wrapper, listener, opt_capt,
    opt_handler) {
  wrapper.unlisten(src, listener, opt_capt, opt_handler);
};


/**
 * Removes all listeners from an object. You can also optionally
 * remove listeners of a particular type.
 *
 * @param {Object|undefined} obj Object to remove listeners from. Must be an
 *     EventTarget or a goog.events.Listenable.
 * @param {string|!goog.events.EventId=} opt_type Type of event to remove.
 *     Default is all types.
 * @return {number} Number of listeners removed.
 */
goog.events.removeAll = function(obj, opt_type) {
  // TODO(chrishenry): Change the type of obj to
  // (!EventTarget|!goog.events.Listenable).

  if (!obj) {
    return 0;
  }

  if (goog.events.Listenable.isImplementedBy(obj)) {
    return obj.removeAllListeners(opt_type);
  }

  var listenerMap = goog.events.getListenerMap_(
      /** @type {!EventTarget} */ (obj));
  if (!listenerMap) {
    return 0;
  }

  var count = 0;
  var typeStr = opt_type && opt_type.toString();
  for (var type in listenerMap.listeners) {
    if (!typeStr || type == typeStr) {
      // Clone so that we don't need to worry about unlistenByKey
      // changing the content of the ListenerMap.
      var listeners = listenerMap.listeners[type].concat();
      for (var i = 0; i < listeners.length; ++i) {
        if (goog.events.unlistenByKey(listeners[i])) {
          ++count;
        }
      }
    }
  }
  return count;
};


/**
 * Gets the listeners for a given object, type and capture phase.
 *
 * @param {Object} obj Object to get listeners for.
 * @param {string|!goog.events.EventId} type Event type.
 * @param {boolean} capture Capture phase?.
 * @return {Array<goog.events.Listener>} Array of listener objects.
 */
goog.events.getListeners = function(obj, type, capture) {
  if (goog.events.Listenable.isImplementedBy(obj)) {
    return obj.getListeners(type, capture);
  } else {
    if (!obj) {
      // TODO(chrishenry): We should tighten the API to accept
      // !EventTarget|goog.events.Listenable, and add an assertion here.
      return [];
    }

    var listenerMap = goog.events.getListenerMap_(
        /** @type {!EventTarget} */ (obj));
    return listenerMap ? listenerMap.getListeners(type, capture) : [];
  }
};


/**
 * Gets the goog.events.Listener for the event or null if no such listener is
 * in use.
 *
 * @param {EventTarget|goog.events.Listenable} src The target from
 *     which to get listeners.
 * @param {?string|!goog.events.EventId<EVENTOBJ>} type The type of the event.
 * @param {function(EVENTOBJ):?|{handleEvent:function(?):?}|null} listener The
 *     listener function to get.
 * @param {boolean=} opt_capt In DOM-compliant browsers, this determines
 *                            whether the listener is fired during the
 *                            capture or bubble phase of the event.
 * @param {Object=} opt_handler Element in whose scope to call the listener.
 * @return {goog.events.ListenableKey} the found listener or null if not found.
 * @template EVENTOBJ
 */
goog.events.getListener = function(src, type, listener, opt_capt, opt_handler) {
  // TODO(chrishenry): Change type from ?string to string, or add assertion.
  type = /** @type {string} */ (type);
  listener = goog.events.wrapListener(listener);
  var capture = !!opt_capt;
  if (goog.events.Listenable.isImplementedBy(src)) {
    return src.getListener(type, listener, capture, opt_handler);
  }

  if (!src) {
    // TODO(chrishenry): We should tighten the API to only accept
    // non-null objects, or add an assertion here.
    return null;
  }

  var listenerMap = goog.events.getListenerMap_(
      /** @type {!EventTarget} */ (src));
  if (listenerMap) {
    return listenerMap.getListener(type, listener, capture, opt_handler);
  }
  return null;
};


/**
 * Returns whether an event target has any active listeners matching the
 * specified signature. If either the type or capture parameters are
 * unspecified, the function will match on the remaining criteria.
 *
 * @param {EventTarget|goog.events.Listenable} obj Target to get
 *     listeners for.
 * @param {string|!goog.events.EventId=} opt_type Event type.
 * @param {boolean=} opt_capture Whether to check for capture or bubble-phase
 *     listeners.
 * @return {boolean} Whether an event target has one or more listeners matching
 *     the requested type and/or capture phase.
 */
goog.events.hasListener = function(obj, opt_type, opt_capture) {
  if (goog.events.Listenable.isImplementedBy(obj)) {
    return obj.hasListener(opt_type, opt_capture);
  }

  var listenerMap = goog.events.getListenerMap_(
      /** @type {!EventTarget} */ (obj));
  return !!listenerMap && listenerMap.hasListener(opt_type, opt_capture);
};


/**
 * Provides a nice string showing the normalized event objects public members
 * @param {Object} e Event Object.
 * @return {string} String of the public members of the normalized event object.
 */
goog.events.expose = function(e) {
  var str = [];
  for (var key in e) {
    if (e[key] && e[key].id) {
      str.push(key + ' = ' + e[key] + ' (' + e[key].id + ')');
    } else {
      str.push(key + ' = ' + e[key]);
    }
  }
  return str.join('\n');
};


/**
 * Returns a string with on prepended to the specified type. This is used for IE
 * which expects "on" to be prepended. This function caches the string in order
 * to avoid extra allocations in steady state.
 * @param {string} type Event type.
 * @return {string} The type string with 'on' prepended.
 * @private
 */
goog.events.getOnString_ = function(type) {
  if (type in goog.events.onStringMap_) {
    return goog.events.onStringMap_[type];
  }
  return goog.events.onStringMap_[type] = goog.events.onString_ + type;
};


/**
 * Fires an object's listeners of a particular type and phase
 *
 * @param {Object} obj Object whose listeners to call.
 * @param {string|!goog.events.EventId} type Event type.
 * @param {boolean} capture Which event phase.
 * @param {Object} eventObject Event object to be passed to listener.
 * @return {boolean} True if all listeners returned true else false.
 */
goog.events.fireListeners = function(obj, type, capture, eventObject) {
  if (goog.events.Listenable.isImplementedBy(obj)) {
    return obj.fireListeners(type, capture, eventObject);
  }

  return goog.events.fireListeners_(obj, type, capture, eventObject);
};


/**
 * Fires an object's listeners of a particular type and phase.
 * @param {Object} obj Object whose listeners to call.
 * @param {string|!goog.events.EventId} type Event type.
 * @param {boolean} capture Which event phase.
 * @param {Object} eventObject Event object to be passed to listener.
 * @return {boolean} True if all listeners returned true else false.
 * @private
 */
goog.events.fireListeners_ = function(obj, type, capture, eventObject) {
  /** @type {boolean} */
  var retval = true;

  var listenerMap = goog.events.getListenerMap_(
      /** @type {EventTarget} */ (obj));
  if (listenerMap) {
    // TODO(chrishenry): Original code avoids array creation when there
    // is no listener, so we do the same. If this optimization turns
    // out to be not required, we can replace this with
    // listenerMap.getListeners(type, capture) instead, which is simpler.
    var listenerArray = listenerMap.listeners[type.toString()];
    if (listenerArray) {
      listenerArray = listenerArray.concat();
      for (var i = 0; i < listenerArray.length; i++) {
        var listener = listenerArray[i];
        // We might not have a listener if the listener was removed.
        if (listener && listener.capture == capture && !listener.removed) {
          var result = goog.events.fireListener(listener, eventObject);
          retval = retval && (result !== false);
        }
      }
    }
  }
  return retval;
};


/**
 * Fires a listener with a set of arguments
 *
 * @param {goog.events.Listener} listener The listener object to call.
 * @param {Object} eventObject The event object to pass to the listener.
 * @return {boolean} Result of listener.
 */
goog.events.fireListener = function(listener, eventObject) {
  var listenerFn = listener.listener;
  var listenerHandler = listener.handler || listener.src;

  if (listener.callOnce) {
    goog.events.unlistenByKey(listener);
  }
  return listenerFn.call(listenerHandler, eventObject);
};


/**
 * Gets the total number of listeners currently in the system.
 * @return {number} Number of listeners.
 * @deprecated This returns estimated count, now that Closure no longer
 * stores a central listener registry. We still return an estimation
 * to keep existing listener-related tests passing. In the near future,
 * this function will be removed.
 */
goog.events.getTotalListenerCount = function() {
  return goog.events.listenerCountEstimate_;
};


/**
 * Dispatches an event (or event like object) and calls all listeners
 * listening for events of this type. The type of the event is decided by the
 * type property on the event object.
 *
 * If any of the listeners returns false OR calls preventDefault then this
 * function will return false.  If one of the capture listeners calls
 * stopPropagation, then the bubble listeners won't fire.
 *
 * @param {goog.events.Listenable} src The event target.
 * @param {goog.events.EventLike} e Event object.
 * @return {boolean} If anyone called preventDefault on the event object (or
 *     if any of the handlers returns false) this will also return false.
 *     If there are no handlers, or if all handlers return true, this returns
 *     true.
 */
goog.events.dispatchEvent = function(src, e) {
  goog.asserts.assert(
      goog.events.Listenable.isImplementedBy(src),
      'Can not use goog.events.dispatchEvent with ' +
      'non-goog.events.Listenable instance.');
  return src.dispatchEvent(e);
};


/**
 * Installs exception protection for the browser event entry point using the
 * given error handler.
 *
 * @param {goog.debug.ErrorHandler} errorHandler Error handler with which to
 *     protect the entry point.
 */
goog.events.protectBrowserEventEntryPoint = function(errorHandler) {
  goog.events.handleBrowserEvent_ = errorHandler.protectEntryPoint(
      goog.events.handleBrowserEvent_);
};


/**
 * Handles an event and dispatches it to the correct listeners. This
 * function is a proxy for the real listener the user specified.
 *
 * @param {goog.events.Listener} listener The listener object.
 * @param {Event=} opt_evt Optional event object that gets passed in via the
 *     native event handlers.
 * @return {boolean} Result of the event handler.
 * @this {EventTarget} The object or Element that fired the event.
 * @private
 */
goog.events.handleBrowserEvent_ = function(listener, opt_evt) {
  if (listener.removed) {
    return true;
  }

  // Synthesize event propagation if the browser does not support W3C
  // event model.
  if (!goog.events.BrowserFeature.HAS_W3C_EVENT_SUPPORT) {
    var ieEvent = opt_evt ||
        /** @type {Event} */ (goog.getObjectByName('window.event'));
    var evt = new goog.events.BrowserEvent(ieEvent, this);
    /** @type {boolean} */
    var retval = true;

    if (goog.events.CAPTURE_SIMULATION_MODE ==
            goog.events.CaptureSimulationMode.ON) {
      // If we have not marked this event yet, we should perform capture
      // simulation.
      if (!goog.events.isMarkedIeEvent_(ieEvent)) {
        goog.events.markIeEvent_(ieEvent);

        var ancestors = [];
        for (var parent = evt.currentTarget; parent;
             parent = parent.parentNode) {
          ancestors.push(parent);
        }

        // Fire capture listeners.
        var type = listener.type;
        for (var i = ancestors.length - 1; !evt.propagationStopped_ && i >= 0;
             i--) {
          evt.currentTarget = ancestors[i];
          var result = goog.events.fireListeners_(ancestors[i], type, true, evt);
          retval = retval && result;
        }

        // Fire bubble listeners.
        //
        // We can technically rely on IE to perform bubble event
        // propagation. However, it turns out that IE fires events in
        // opposite order of attachEvent registration, which broke
        // some code and tests that rely on the order. (While W3C DOM
        // Level 2 Events TR leaves the event ordering unspecified,
        // modern browsers and W3C DOM Level 3 Events Working Draft
        // actually specify the order as the registration order.)
        for (var i = 0; !evt.propagationStopped_ && i < ancestors.length; i++) {
          evt.currentTarget = ancestors[i];
          var result = goog.events.fireListeners_(ancestors[i], type, false, evt);
          retval = retval && result;
        }
      }
    } else {
      retval = goog.events.fireListener(listener, evt);
    }
    return retval;
  }

  // Otherwise, simply fire the listener.
  return goog.events.fireListener(
      listener, new goog.events.BrowserEvent(opt_evt, this));
};


/**
 * This is used to mark the IE event object so we do not do the Closure pass
 * twice for a bubbling event.
 * @param {Event} e The IE browser event.
 * @private
 */
goog.events.markIeEvent_ = function(e) {
  // Only the keyCode and the returnValue can be changed. We use keyCode for
  // non keyboard events.
  // event.returnValue is a bit more tricky. It is undefined by default. A
  // boolean false prevents the default action. In a window.onbeforeunload and
  // the returnValue is non undefined it will be alerted. However, we will only
  // modify the returnValue for keyboard events. We can get a problem if non
  // closure events sets the keyCode or the returnValue

  var useReturnValue = false;

  if (e.keyCode == 0) {
    // We cannot change the keyCode in case that srcElement is input[type=file].
    // We could test that that is the case but that would allocate 3 objects.
    // If we use try/catch we will only allocate extra objects in the case of a
    // failure.
    /** @preserveTry */
    try {
      e.keyCode = -1;
      return;
    } catch (ex) {
      useReturnValue = true;
    }
  }

  if (useReturnValue ||
      /** @type {boolean|undefined} */ (e.returnValue) == undefined) {
    e.returnValue = true;
  }
};


/**
 * This is used to check if an IE event has already been handled by the Closure
 * system so we do not do the Closure pass twice for a bubbling event.
 * @param {Event} e  The IE browser event.
 * @return {boolean} True if the event object has been marked.
 * @private
 */
goog.events.isMarkedIeEvent_ = function(e) {
  return e.keyCode < 0 || e.returnValue != undefined;
};


/**
 * Counter to create unique event ids.
 * @private {number}
 */
goog.events.uniqueIdCounter_ = 0;


/**
 * Creates a unique event id.
 *
 * @param {string} identifier The identifier.
 * @return {string} A unique identifier.
 * @idGenerator
 */
goog.events.getUniqueId = function(identifier) {
  return identifier + '_' + goog.events.uniqueIdCounter_++;
};


/**
 * @param {EventTarget} src The source object.
 * @return {goog.events.ListenerMap} A listener map for the given
 *     source object, or null if none exists.
 * @private
 */
goog.events.getListenerMap_ = function(src) {
  var listenerMap = src[goog.events.LISTENER_MAP_PROP_];
  // IE serializes the property as well (e.g. when serializing outer
  // HTML). So we must check that the value is of the correct type.
  return listenerMap instanceof goog.events.ListenerMap ? listenerMap : null;
};


/**
 * Expando property for listener function wrapper for Object with
 * handleEvent.
 * @private @const {string}
 */
goog.events.LISTENER_WRAPPER_PROP_ = '__closure_events_fn_' +
    ((Math.random() * 1e9) >>> 0);


/**
 * @param {Object|Function} listener The listener function or an
 *     object that contains handleEvent method.
 * @return {!Function} Either the original function or a function that
 *     calls obj.handleEvent. If the same listener is passed to this
 *     function more than once, the same function is guaranteed to be
 *     returned.
 */
goog.events.wrapListener = function(listener) {
  goog.asserts.assert(listener, 'Listener can not be null.');

  if (goog.isFunction(listener)) {
    return listener;
  }

  goog.asserts.assert(
      listener.handleEvent, 'An object listener must have handleEvent method.');
  if (!listener[goog.events.LISTENER_WRAPPER_PROP_]) {
    listener[goog.events.LISTENER_WRAPPER_PROP_] =
        function(e) { return listener.handleEvent(e); };
  }
  return listener[goog.events.LISTENER_WRAPPER_PROP_];
};


// Register the browser event handler as an entry point, so that
// it can be monitored for exception handling, etc.
goog.debug.entryPointRegistry.register(
    /**
     * @param {function(!Function): !Function} transformer The transforming
     *     function.
     */
    function(transformer) {
      goog.events.handleBrowserEvent_ = transformer(
          goog.events.handleBrowserEvent_);
    });

// Copyright 2005 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview A disposable implementation of a custom
 * listenable/event target. See also: documentation for
 * {@code goog.events.Listenable}.
 *
 * @author arv@google.com (Erik Arvidsson) [Original implementation]
 * @see ../demos/eventtarget.html
 * @see goog.events.Listenable
 */

goog.provide('goog.events.EventTarget');

goog.require('goog.Disposable');
goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.Listenable');
goog.require('goog.events.ListenerMap');
goog.require('goog.object');



/**
 * An implementation of {@code goog.events.Listenable} with full W3C
 * EventTarget-like support (capture/bubble mechanism, stopping event
 * propagation, preventing default actions).
 *
 * You may subclass this class to turn your class into a Listenable.
 *
 * Unless propagation is stopped, an event dispatched by an
 * EventTarget will bubble to the parent returned by
 * {@code getParentEventTarget}. To set the parent, call
 * {@code setParentEventTarget}. Subclasses that don't support
 * changing the parent can override the setter to throw an error.
 *
 * Example usage:
 * <pre>
 *   var source = new goog.events.EventTarget();
 *   function handleEvent(e) {
 *     alert('Type: ' + e.type + '; Target: ' + e.target);
 *   }
 *   source.listen('foo', handleEvent);
 *   // Or: goog.events.listen(source, 'foo', handleEvent);
 *   ...
 *   source.dispatchEvent('foo');  // will call handleEvent
 *   ...
 *   source.unlisten('foo', handleEvent);
 *   // Or: goog.events.unlisten(source, 'foo', handleEvent);
 * </pre>
 *
 * @constructor
 * @extends {goog.Disposable}
 * @implements {goog.events.Listenable}
 */
goog.events.EventTarget = function() {
  goog.Disposable.call(this);

  /**
   * Maps of event type to an array of listeners.
   * @private {!goog.events.ListenerMap}
   */
  this.eventTargetListeners_ = new goog.events.ListenerMap(this);

  /**
   * The object to use for event.target. Useful when mixing in an
   * EventTarget to another object.
   * @private {!Object}
   */
  this.actualEventTarget_ = this;

  /**
   * Parent event target, used during event bubbling.
   *
   * TODO(chrishenry): Change this to goog.events.Listenable. This
   * currently breaks people who expect getParentEventTarget to return
   * goog.events.EventTarget.
   *
   * @private {goog.events.EventTarget}
   */
  this.parentEventTarget_ = null;
};
goog.inherits(goog.events.EventTarget, goog.Disposable);
goog.events.Listenable.addImplementation(goog.events.EventTarget);


/**
 * An artificial cap on the number of ancestors you can have. This is mainly
 * for loop detection.
 * @const {number}
 * @private
 */
goog.events.EventTarget.MAX_ANCESTORS_ = 1000;


/**
 * Returns the parent of this event target to use for bubbling.
 *
 * @return {goog.events.EventTarget} The parent EventTarget or null if
 *     there is no parent.
 * @override
 */
goog.events.EventTarget.prototype.getParentEventTarget = function() {
  return this.parentEventTarget_;
};


/**
 * Sets the parent of this event target to use for capture/bubble
 * mechanism.
 * @param {goog.events.EventTarget} parent Parent listenable (null if none).
 */
goog.events.EventTarget.prototype.setParentEventTarget = function(parent) {
  this.parentEventTarget_ = parent;
};


/**
 * Adds an event listener to the event target. The same handler can only be
 * added once per the type. Even if you add the same handler multiple times
 * using the same type then it will only be called once when the event is
 * dispatched.
 *
 * @param {string} type The type of the event to listen for.
 * @param {function(?):?|{handleEvent:function(?):?}|null} handler The function
 *     to handle the event. The handler can also be an object that implements
 *     the handleEvent method which takes the event object as argument.
 * @param {boolean=} opt_capture In DOM-compliant browsers, this determines
 *     whether the listener is fired during the capture or bubble phase
 *     of the event.
 * @param {Object=} opt_handlerScope Object in whose scope to call
 *     the listener.
 * @deprecated Use {@code #listen} instead, when possible. Otherwise, use
 *     {@code goog.events.listen} if you are passing Object
 *     (instead of Function) as handler.
 */
goog.events.EventTarget.prototype.addEventListener = function(
    type, handler, opt_capture, opt_handlerScope) {
  goog.events.listen(this, type, handler, opt_capture, opt_handlerScope);
};


/**
 * Removes an event listener from the event target. The handler must be the
 * same object as the one added. If the handler has not been added then
 * nothing is done.
 *
 * @param {string} type The type of the event to listen for.
 * @param {function(?):?|{handleEvent:function(?):?}|null} handler The function
 *     to handle the event. The handler can also be an object that implements
 *     the handleEvent method which takes the event object as argument.
 * @param {boolean=} opt_capture In DOM-compliant browsers, this determines
 *     whether the listener is fired during the capture or bubble phase
 *     of the event.
 * @param {Object=} opt_handlerScope Object in whose scope to call
 *     the listener.
 * @deprecated Use {@code #unlisten} instead, when possible. Otherwise, use
 *     {@code goog.events.unlisten} if you are passing Object
 *     (instead of Function) as handler.
 */
goog.events.EventTarget.prototype.removeEventListener = function(
    type, handler, opt_capture, opt_handlerScope) {
  goog.events.unlisten(this, type, handler, opt_capture, opt_handlerScope);
};


/** @override */
goog.events.EventTarget.prototype.dispatchEvent = function(e) {
  this.assertInitialized_();

  var ancestorsTree, ancestor = this.getParentEventTarget();
  if (ancestor) {
    ancestorsTree = [];
    var ancestorCount = 1;
    for (; ancestor; ancestor = ancestor.getParentEventTarget()) {
      ancestorsTree.push(ancestor);
      goog.asserts.assert(
          (++ancestorCount < goog.events.EventTarget.MAX_ANCESTORS_),
          'infinite loop');
    }
  }

  return goog.events.EventTarget.dispatchEventInternal_(
      this.actualEventTarget_, e, ancestorsTree);
};


/**
 * Removes listeners from this object.  Classes that extend EventTarget may
 * need to override this method in order to remove references to DOM Elements
 * and additional listeners.
 * @override
 */
goog.events.EventTarget.prototype.disposeInternal = function() {
  goog.events.EventTarget.superClass_.disposeInternal.call(this);

  this.removeAllListeners();
  this.parentEventTarget_ = null;
};


/** @override */
goog.events.EventTarget.prototype.listen = function(
    type, listener, opt_useCapture, opt_listenerScope) {
  this.assertInitialized_();
  return this.eventTargetListeners_.add(
      String(type), listener, false /* callOnce */, opt_useCapture,
      opt_listenerScope);
};


/** @override */
goog.events.EventTarget.prototype.listenOnce = function(
    type, listener, opt_useCapture, opt_listenerScope) {
  return this.eventTargetListeners_.add(
      String(type), listener, true /* callOnce */, opt_useCapture,
      opt_listenerScope);
};


/** @override */
goog.events.EventTarget.prototype.unlisten = function(
    type, listener, opt_useCapture, opt_listenerScope) {
  return this.eventTargetListeners_.remove(
      String(type), listener, opt_useCapture, opt_listenerScope);
};


/** @override */
goog.events.EventTarget.prototype.unlistenByKey = function(key) {
  return this.eventTargetListeners_.removeByKey(key);
};


/** @override */
goog.events.EventTarget.prototype.removeAllListeners = function(opt_type) {
  // TODO(chrishenry): Previously, removeAllListeners can be called on
  // uninitialized EventTarget, so we preserve that behavior. We
  // should remove this when usages that rely on that fact are purged.
  if (!this.eventTargetListeners_) {
    return 0;
  }
  return this.eventTargetListeners_.removeAll(opt_type);
};


/** @override */
goog.events.EventTarget.prototype.fireListeners = function(
    type, capture, eventObject) {
  // TODO(chrishenry): Original code avoids array creation when there
  // is no listener, so we do the same. If this optimization turns
  // out to be not required, we can replace this with
  // getListeners(type, capture) instead, which is simpler.
  var listenerArray = this.eventTargetListeners_.listeners[String(type)];
  if (!listenerArray) {
    return true;
  }
  listenerArray = listenerArray.concat();

  var rv = true;
  for (var i = 0; i < listenerArray.length; ++i) {
    var listener = listenerArray[i];
    // We might not have a listener if the listener was removed.
    if (listener && !listener.removed && listener.capture == capture) {
      var listenerFn = listener.listener;
      var listenerHandler = listener.handler || listener.src;

      if (listener.callOnce) {
        this.unlistenByKey(listener);
      }
      rv = listenerFn.call(listenerHandler, eventObject) !== false && rv;
    }
  }

  return rv && eventObject.returnValue_ != false;
};


/** @override */
goog.events.EventTarget.prototype.getListeners = function(type, capture) {
  return this.eventTargetListeners_.getListeners(String(type), capture);
};


/** @override */
goog.events.EventTarget.prototype.getListener = function(
    type, listener, capture, opt_listenerScope) {
  return this.eventTargetListeners_.getListener(
      String(type), listener, capture, opt_listenerScope);
};


/** @override */
goog.events.EventTarget.prototype.hasListener = function(
    opt_type, opt_capture) {
  var id = goog.isDef(opt_type) ? String(opt_type) : undefined;
  return this.eventTargetListeners_.hasListener(id, opt_capture);
};


/**
 * Sets the target to be used for {@code event.target} when firing
 * event. Mainly used for testing. For example, see
 * {@code goog.testing.events.mixinListenable}.
 * @param {!Object} target The target.
 */
goog.events.EventTarget.prototype.setTargetForTesting = function(target) {
  this.actualEventTarget_ = target;
};


/**
 * Asserts that the event target instance is initialized properly.
 * @private
 */
goog.events.EventTarget.prototype.assertInitialized_ = function() {
  goog.asserts.assert(
      this.eventTargetListeners_,
      'Event target is not initialized. Did you call the superclass ' +
      '(goog.events.EventTarget) constructor?');
};


/**
 * Dispatches the given event on the ancestorsTree.
 *
 * @param {!Object} target The target to dispatch on.
 * @param {goog.events.Event|Object|string} e The event object.
 * @param {Array<goog.events.Listenable>=} opt_ancestorsTree The ancestors
 *     tree of the target, in reverse order from the closest ancestor
 *     to the root event target. May be null if the target has no ancestor.
 * @return {boolean} If anyone called preventDefault on the event object (or
 *     if any of the listeners returns false) this will also return false.
 * @private
 */
goog.events.EventTarget.dispatchEventInternal_ = function(
    target, e, opt_ancestorsTree) {
  var type = e.type || /** @type {string} */ (e);

  // If accepting a string or object, create a custom event object so that
  // preventDefault and stopPropagation work with the event.
  if (goog.isString(e)) {
    e = new goog.events.Event(e, target);
  } else if (!(e instanceof goog.events.Event)) {
    var oldEvent = e;
    e = new goog.events.Event(type, target);
    goog.object.extend(e, oldEvent);
  } else {
    e.target = e.target || target;
  }

  var rv = true, currentTarget;

  // Executes all capture listeners on the ancestors, if any.
  if (opt_ancestorsTree) {
    for (var i = opt_ancestorsTree.length - 1; !e.propagationStopped_ && i >= 0;
         i--) {
      currentTarget = e.currentTarget = opt_ancestorsTree[i];
      rv = currentTarget.fireListeners(type, true, e) && rv;
    }
  }

  // Executes capture and bubble listeners on the target.
  if (!e.propagationStopped_) {
    currentTarget = e.currentTarget = target;
    rv = currentTarget.fireListeners(type, true, e) && rv;
    if (!e.propagationStopped_) {
      rv = currentTarget.fireListeners(type, false, e) && rv;
    }
  }

  // Executes all bubble listeners on the ancestors, if any.
  if (opt_ancestorsTree) {
    for (i = 0; !e.propagationStopped_ && i < opt_ancestorsTree.length; i++) {
      currentTarget = e.currentTarget = opt_ancestorsTree[i];
      rv = currentTarget.fireListeners(type, false, e) && rv;
    }
  }

  return rv;
};

goog.provide('ol.Observable');

goog.require('goog.events');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');


/**
 * @classdesc
 * Abstract base class; normally only used for creating subclasses and not
 * instantiated in apps.
 * An event target providing convenient methods for listener registration
 * and unregistration. A generic `change` event is always available through
 * {@link ol.Observable#changed}.
 *
 * @constructor
 * @extends {goog.events.EventTarget}
 * @fires change
 * @struct
 * @api stable
 */
ol.Observable = function() {

  goog.base(this);

  /**
   * @private
   * @type {number}
   */
  this.revision_ = 0;

};
goog.inherits(ol.Observable, goog.events.EventTarget);


/**
 * Removes an event listener using the key returned by `on()` or `once()`.
 * @param {goog.events.Key} key The key returned by `on()` or `once()`.
 * @api stable
 */
ol.Observable.unByKey = function(key) {
  goog.events.unlistenByKey(key);
};


/**
 * Increases the revision counter and dispatches a 'change' event.
 * @api
 */
ol.Observable.prototype.changed = function() {
  ++this.revision_;
  this.dispatchEvent(goog.events.EventType.CHANGE);
};


/**
 * Triggered when the revision counter is increased.
 * @event change
 * @api
 */


/**
 * Dispatches an event and calls all listeners listening for events
 * of this type. The event parameter can either be a string or an
 * Object with a `type` property.
 *
 * @param {goog.events.EventLike} event Event object.
 * @function
 * @api
 */
ol.Observable.prototype.dispatchEvent;


/**
 * Get the version number for this object.  Each time the object is modified,
 * its version number will be incremented.
 * @return {number} Revision.
 * @api
 */
ol.Observable.prototype.getRevision = function() {
  return this.revision_;
};


/**
 * Listen for a certain type of event.
 * @param {string|Array.<string>} type The event type or array of event types.
 * @param {function(?): ?} listener The listener function.
 * @param {Object=} opt_this The object to use as `this` in `listener`.
 * @return {goog.events.Key} Unique key for the listener.
 * @api stable
 */
ol.Observable.prototype.on = function(type, listener, opt_this) {
  return goog.events.listen(this, type, listener, false, opt_this);
};


/**
 * Listen once for a certain type of event.
 * @param {string|Array.<string>} type The event type or array of event types.
 * @param {function(?): ?} listener The listener function.
 * @param {Object=} opt_this The object to use as `this` in `listener`.
 * @return {goog.events.Key} Unique key for the listener.
 * @api stable
 */
ol.Observable.prototype.once = function(type, listener, opt_this) {
  return goog.events.listenOnce(this, type, listener, false, opt_this);
};


/**
 * Unlisten for a certain type of event.
 * @param {string|Array.<string>} type The event type or array of event types.
 * @param {function(?): ?} listener The listener function.
 * @param {Object=} opt_this The object which was used as `this` by the
 * `listener`.
 * @api stable
 */
ol.Observable.prototype.un = function(type, listener, opt_this) {
  goog.events.unlisten(this, type, listener, false, opt_this);
};


/**
 * Removes an event listener using the key returned by `on()` or `once()`.
 * Note that using the {@link ol.Observable.unByKey} static function is to
 * be preferred.
 * @param {goog.events.Key} key The key returned by `on()` or `once()`.
 * @function
 * @api stable
 */
ol.Observable.prototype.unByKey = ol.Observable.unByKey;

goog.provide('ol.Object');
goog.provide('ol.ObjectEvent');
goog.provide('ol.ObjectEventType');

goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('ol.Observable');


/**
 * @enum {string}
 */
ol.ObjectEventType = {
  /**
   * Triggered when a property is changed.
   * @event ol.ObjectEvent#propertychange
   * @api stable
   */
  PROPERTYCHANGE: 'propertychange'
};


/**
 * @classdesc
 * Events emitted by {@link ol.Object} instances are instances of this type.
 *
 * @param {string} type The event type.
 * @param {string} key The property name.
 * @param {*} oldValue The old value for `key`.
 * @extends {goog.events.Event}
 * @implements {oli.ObjectEvent}
 * @constructor
 */
ol.ObjectEvent = function(type, key, oldValue) {
  goog.base(this, type);

  /**
   * The name of the property whose value is changing.
   * @type {string}
   * @api stable
   */
  this.key = key;

  /**
   * The old value. To get the new value use `e.target.get(e.key)` where
   * `e` is the event object.
   * @type {*}
   * @api stable
   */
  this.oldValue = oldValue;

};
goog.inherits(ol.ObjectEvent, goog.events.Event);


/**
 * @classdesc
 * Abstract base class; normally only used for creating subclasses and not
 * instantiated in apps.
 * Most non-trivial classes inherit from this.
 *
 * This extends {@link ol.Observable} with observable properties, where each
 * property is observable as well as the object as a whole.
 *
 * Classes that inherit from this have pre-defined properties, to which you can
 * add your owns. The pre-defined properties are listed in this documentation as
 * 'Observable Properties', and have their own accessors; for example,
 * {@link ol.Map} has a `target` property, accessed with `getTarget()`  and
 * changed with `setTarget()`. Not all properties are however settable. There
 * are also general-purpose accessors `get()` and `set()`. For example,
 * `get('target')` is equivalent to `getTarget()`.
 *
 * The `set` accessors trigger a change event, and you can monitor this by
 * registering a listener. For example, {@link ol.View} has a `center`
 * property, so `view.on('change:center', function(evt) {...});` would call the
 * function whenever the value of the center property changes. Within the
 * function, `evt.target` would be the view, so `evt.target.getCenter()` would
 * return the new center.
 *
 * You can add your own observable properties with
 * `object.set('prop', 'value')`, and retrieve that with `object.get('prop')`.
 * You can listen for changes on that property value with
 * `object.on('change:prop', listener)`. You can get a list of all
 * properties with {@link ol.Object#getProperties object.getProperties()}.
 *
 * Note that the observable properties are separate from standard JS properties.
 * You can, for example, give your map object a title with
 * `map.title='New title'` and with `map.set('title', 'Another title')`. The
 * first will be a `hasOwnProperty`; the second will appear in
 * `getProperties()`. Only the second is observable.
 *
 * Properties can be deleted by using the unset method. E.g.
 * object.unset('foo').
 *
 * @constructor
 * @extends {ol.Observable}
 * @param {Object.<string, *>=} opt_values An object with key-value pairs.
 * @fires ol.ObjectEvent
 * @api
 */
ol.Object = function(opt_values) {
  goog.base(this);

  // Call goog.getUid to ensure that the order of objects' ids is the same as
  // the order in which they were created.  This also helps to ensure that
  // object properties are always added in the same order, which helps many
  // JavaScript engines generate faster code.
  goog.getUid(this);

  /**
   * @private
   * @type {!Object.<string, *>}
   */
  this.values_ = {};

  if (opt_values !== undefined) {
    this.setProperties(opt_values);
  }
};
goog.inherits(ol.Object, ol.Observable);


/**
 * @private
 * @type {Object.<string, string>}
 */
ol.Object.changeEventTypeCache_ = {};


/**
 * @param {string} key Key name.
 * @return {string} Change name.
 */
ol.Object.getChangeEventType = function(key) {
  return ol.Object.changeEventTypeCache_.hasOwnProperty(key) ?
      ol.Object.changeEventTypeCache_[key] :
      (ol.Object.changeEventTypeCache_[key] = 'change:' + key);
};


/**
 * Gets a value.
 * @param {string} key Key name.
 * @return {*} Value.
 * @api stable
 */
ol.Object.prototype.get = function(key) {
  var value;
  if (this.values_.hasOwnProperty(key)) {
    value = this.values_[key];
  }
  return value;
};


/**
 * Get a list of object property names.
 * @return {Array.<string>} List of property names.
 * @api stable
 */
ol.Object.prototype.getKeys = function() {
  return Object.keys(this.values_);
};


/**
 * Get an object of all property names and values.
 * @return {Object.<string, *>} Object.
 * @api stable
 */
ol.Object.prototype.getProperties = function() {
  var properties = {};
  var key;
  for (key in this.values_) {
    properties[key] = this.values_[key];
  }
  return properties;
};


/**
 * @param {string} key Key name.
 * @param {*} oldValue Old value.
 */
ol.Object.prototype.notify = function(key, oldValue) {
  var eventType;
  eventType = ol.Object.getChangeEventType(key);
  this.dispatchEvent(new ol.ObjectEvent(eventType, key, oldValue));
  eventType = ol.ObjectEventType.PROPERTYCHANGE;
  this.dispatchEvent(new ol.ObjectEvent(eventType, key, oldValue));
};


/**
 * Sets a value.
 * @param {string} key Key name.
 * @param {*} value Value.
 * @param {boolean=} opt_silent Update without triggering an event.
 * @api stable
 */
ol.Object.prototype.set = function(key, value, opt_silent) {
  if (opt_silent) {
    this.values_[key] = value;
  } else {
    var oldValue = this.values_[key];
    this.values_[key] = value;
    if (oldValue !== value) {
      this.notify(key, oldValue);
    }
  }
};


/**
 * Sets a collection of key-value pairs.  Note that this changes any existing
 * properties and adds new ones (it does not remove any existing properties).
 * @param {Object.<string, *>} values Values.
 * @param {boolean=} opt_silent Update without triggering an event.
 * @api stable
 */
ol.Object.prototype.setProperties = function(values, opt_silent) {
  var key;
  for (key in values) {
    this.set(key, values[key], opt_silent);
  }
};


/**
 * Unsets a property.
 * @param {string} key Key name.
 * @param {boolean=} opt_silent Unset without triggering an event.
 * @api stable
 */
ol.Object.prototype.unset = function(key, opt_silent) {
  if (key in this.values_) {
    var oldValue = this.values_[key];
    delete this.values_[key];
    if (!opt_silent) {
      this.notify(key, oldValue);
    }
  }
};

goog.provide('ol.Size');
goog.provide('ol.size');


goog.require('goog.asserts');


/**
 * An array of numbers representing a size: `[width, height]`.
 * @typedef {Array.<number>}
 * @api stable
 */
ol.Size;


/**
 * Returns a buffered size.
 * @param {ol.Size} size Size.
 * @param {number} buffer Buffer.
 * @param {ol.Size=} opt_size Optional reusable size array.
 * @return {ol.Size} The buffered size.
 */
ol.size.buffer = function(size, buffer, opt_size) {
  if (opt_size === undefined) {
    opt_size = [0, 0];
  }
  opt_size[0] = size[0] + 2 * buffer;
  opt_size[1] = size[1] + 2 * buffer;
  return opt_size;
};


/**
 * Compares sizes for equality.
 * @param {ol.Size} a Size.
 * @param {ol.Size} b Size.
 * @return {boolean} Equals.
 */
ol.size.equals = function(a, b) {
  return a[0] == b[0] && a[1] == b[1];
};


/**
 * Determines if a size has a positive area.
 * @param {ol.Size} size The size to test.
 * @return {boolean} The size has a positive area.
 */
ol.size.hasArea = function(size) {
  return size[0] > 0 && size[1] > 0;
};


/**
 * Returns a size scaled by a ratio. The result will be an array of integers.
 * @param {ol.Size} size Size.
 * @param {number} ratio Ratio.
 * @param {ol.Size=} opt_size Optional reusable size array.
 * @return {ol.Size} The scaled size.
 */
ol.size.scale = function(size, ratio, opt_size) {
  if (opt_size === undefined) {
    opt_size = [0, 0];
  }
  opt_size[0] = (size[0] * ratio + 0.5) | 0;
  opt_size[1] = (size[1] * ratio + 0.5) | 0;
  return opt_size;
};


/**
 * Returns an `ol.Size` array for the passed in number (meaning: square) or
 * `ol.Size` array.
 * (meaning: non-square),
 * @param {number|ol.Size} size Width and height.
 * @param {ol.Size=} opt_size Optional reusable size array.
 * @return {ol.Size} Size.
 * @api stable
 */
ol.size.toSize = function(size, opt_size) {
  if (goog.isArray(size)) {
    return size;
  } else {
    goog.asserts.assert(goog.isNumber(size));
    if (opt_size === undefined) {
      opt_size = [size, size];
    } else {
      opt_size[0] = size;
      opt_size[1] = size;
    }
    return opt_size;
  }
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Additional mathematical functions.
 */

goog.provide('goog.math');

goog.require('goog.array');
goog.require('goog.asserts');


/**
 * Returns a random integer greater than or equal to 0 and less than {@code a}.
 * @param {number} a  The upper bound for the random integer (exclusive).
 * @return {number} A random integer N such that 0 <= N < a.
 */
goog.math.randomInt = function(a) {
  return Math.floor(Math.random() * a);
};


/**
 * Returns a random number greater than or equal to {@code a} and less than
 * {@code b}.
 * @param {number} a  The lower bound for the random number (inclusive).
 * @param {number} b  The upper bound for the random number (exclusive).
 * @return {number} A random number N such that a <= N < b.
 */
goog.math.uniformRandom = function(a, b) {
  return a + Math.random() * (b - a);
};


/**
 * Takes a number and clamps it to within the provided bounds.
 * @param {number} value The input number.
 * @param {number} min The minimum value to return.
 * @param {number} max The maximum value to return.
 * @return {number} The input number if it is within bounds, or the nearest
 *     number within the bounds.
 */
goog.math.clamp = function(value, min, max) {
  return Math.min(Math.max(value, min), max);
};


/**
 * The % operator in JavaScript returns the remainder of a / b, but differs from
 * some other languages in that the result will have the same sign as the
 * dividend. For example, -1 % 8 == -1, whereas in some other languages
 * (such as Python) the result would be 7. This function emulates the more
 * correct modulo behavior, which is useful for certain applications such as
 * calculating an offset index in a circular list.
 *
 * @param {number} a The dividend.
 * @param {number} b The divisor.
 * @return {number} a % b where the result is between 0 and b (either 0 <= x < b
 *     or b < x <= 0, depending on the sign of b).
 */
goog.math.modulo = function(a, b) {
  var r = a % b;
  // If r and b differ in sign, add b to wrap the result to the correct sign.
  return (r * b < 0) ? r + b : r;
};


/**
 * Performs linear interpolation between values a and b. Returns the value
 * between a and b proportional to x (when x is between 0 and 1. When x is
 * outside this range, the return value is a linear extrapolation).
 * @param {number} a A number.
 * @param {number} b A number.
 * @param {number} x The proportion between a and b.
 * @return {number} The interpolated value between a and b.
 */
goog.math.lerp = function(a, b, x) {
  return a + x * (b - a);
};


/**
 * Tests whether the two values are equal to each other, within a certain
 * tolerance to adjust for floating point errors.
 * @param {number} a A number.
 * @param {number} b A number.
 * @param {number=} opt_tolerance Optional tolerance range. Defaults
 *     to 0.000001. If specified, should be greater than 0.
 * @return {boolean} Whether {@code a} and {@code b} are nearly equal.
 */
goog.math.nearlyEquals = function(a, b, opt_tolerance) {
  return Math.abs(a - b) <= (opt_tolerance || 0.000001);
};


// TODO(user): Rename to normalizeAngle, retaining old name as deprecated
// alias.
/**
 * Normalizes an angle to be in range [0-360). Angles outside this range will
 * be normalized to be the equivalent angle with that range.
 * @param {number} angle Angle in degrees.
 * @return {number} Standardized angle.
 */
goog.math.standardAngle = function(angle) {
  return goog.math.modulo(angle, 360);
};


/**
 * Normalizes an angle to be in range [0-2*PI). Angles outside this range will
 * be normalized to be the equivalent angle with that range.
 * @param {number} angle Angle in radians.
 * @return {number} Standardized angle.
 */
goog.math.standardAngleInRadians = function(angle) {
  return goog.math.modulo(angle, 2 * Math.PI);
};


/**
 * Converts degrees to radians.
 * @param {number} angleDegrees Angle in degrees.
 * @return {number} Angle in radians.
 */
goog.math.toRadians = function(angleDegrees) {
  return angleDegrees * Math.PI / 180;
};


/**
 * Converts radians to degrees.
 * @param {number} angleRadians Angle in radians.
 * @return {number} Angle in degrees.
 */
goog.math.toDegrees = function(angleRadians) {
  return angleRadians * 180 / Math.PI;
};


/**
 * For a given angle and radius, finds the X portion of the offset.
 * @param {number} degrees Angle in degrees (zero points in +X direction).
 * @param {number} radius Radius.
 * @return {number} The x-distance for the angle and radius.
 */
goog.math.angleDx = function(degrees, radius) {
  return radius * Math.cos(goog.math.toRadians(degrees));
};


/**
 * For a given angle and radius, finds the Y portion of the offset.
 * @param {number} degrees Angle in degrees (zero points in +X direction).
 * @param {number} radius Radius.
 * @return {number} The y-distance for the angle and radius.
 */
goog.math.angleDy = function(degrees, radius) {
  return radius * Math.sin(goog.math.toRadians(degrees));
};


/**
 * Computes the angle between two points (x1,y1) and (x2,y2).
 * Angle zero points in the +X direction, 90 degrees points in the +Y
 * direction (down) and from there we grow clockwise towards 360 degrees.
 * @param {number} x1 x of first point.
 * @param {number} y1 y of first point.
 * @param {number} x2 x of second point.
 * @param {number} y2 y of second point.
 * @return {number} Standardized angle in degrees of the vector from
 *     x1,y1 to x2,y2.
 */
goog.math.angle = function(x1, y1, x2, y2) {
  return goog.math.standardAngle(goog.math.toDegrees(Math.atan2(y2 - y1,
                                                                x2 - x1)));
};


/**
 * Computes the difference between startAngle and endAngle (angles in degrees).
 * @param {number} startAngle  Start angle in degrees.
 * @param {number} endAngle  End angle in degrees.
 * @return {number} The number of degrees that when added to
 *     startAngle will result in endAngle. Positive numbers mean that the
 *     direction is clockwise. Negative numbers indicate a counter-clockwise
 *     direction.
 *     The shortest route (clockwise vs counter-clockwise) between the angles
 *     is used.
 *     When the difference is 180 degrees, the function returns 180 (not -180)
 *     angleDifference(30, 40) is 10, and angleDifference(40, 30) is -10.
 *     angleDifference(350, 10) is 20, and angleDifference(10, 350) is -20.
 */
goog.math.angleDifference = function(startAngle, endAngle) {
  var d = goog.math.standardAngle(endAngle) -
          goog.math.standardAngle(startAngle);
  if (d > 180) {
    d = d - 360;
  } else if (d <= -180) {
    d = 360 + d;
  }
  return d;
};


/**
 * Returns the sign of a number as per the "sign" or "signum" function.
 * @param {number} x The number to take the sign of.
 * @return {number} -1 when negative, 1 when positive, 0 when 0. Preserves
 *     signed zeros and NaN.
 */
goog.math.sign = Math.sign || function(x) {
  if (x > 0) {
    return 1;
  }
  if (x < 0) {
    return -1;
  }
  return x;  // Preserves signed zeros and NaN.
};


/**
 * JavaScript implementation of Longest Common Subsequence problem.
 * http://en.wikipedia.org/wiki/Longest_common_subsequence
 *
 * Returns the longest possible array that is subarray of both of given arrays.
 *
 * @param {Array<Object>} array1 First array of objects.
 * @param {Array<Object>} array2 Second array of objects.
 * @param {Function=} opt_compareFn Function that acts as a custom comparator
 *     for the array ojects. Function should return true if objects are equal,
 *     otherwise false.
 * @param {Function=} opt_collectorFn Function used to decide what to return
 *     as a result subsequence. It accepts 2 arguments: index of common element
 *     in the first array and index in the second. The default function returns
 *     element from the first array.
 * @return {!Array<Object>} A list of objects that are common to both arrays
 *     such that there is no common subsequence with size greater than the
 *     length of the list.
 */
goog.math.longestCommonSubsequence = function(
    array1, array2, opt_compareFn, opt_collectorFn) {

  var compare = opt_compareFn || function(a, b) {
    return a == b;
  };

  var collect = opt_collectorFn || function(i1, i2) {
    return array1[i1];
  };

  var length1 = array1.length;
  var length2 = array2.length;

  var arr = [];
  for (var i = 0; i < length1 + 1; i++) {
    arr[i] = [];
    arr[i][0] = 0;
  }

  for (var j = 0; j < length2 + 1; j++) {
    arr[0][j] = 0;
  }

  for (i = 1; i <= length1; i++) {
    for (j = 1; j <= length2; j++) {
      if (compare(array1[i - 1], array2[j - 1])) {
        arr[i][j] = arr[i - 1][j - 1] + 1;
      } else {
        arr[i][j] = Math.max(arr[i - 1][j], arr[i][j - 1]);
      }
    }
  }

  // Backtracking
  var result = [];
  var i = length1, j = length2;
  while (i > 0 && j > 0) {
    if (compare(array1[i - 1], array2[j - 1])) {
      result.unshift(collect(i - 1, j - 1));
      i--;
      j--;
    } else {
      if (arr[i - 1][j] > arr[i][j - 1]) {
        i--;
      } else {
        j--;
      }
    }
  }

  return result;
};


/**
 * Returns the sum of the arguments.
 * @param {...number} var_args Numbers to add.
 * @return {number} The sum of the arguments (0 if no arguments were provided,
 *     {@code NaN} if any of the arguments is not a valid number).
 */
goog.math.sum = function(var_args) {
  return /** @type {number} */ (goog.array.reduce(arguments,
      function(sum, value) {
        return sum + value;
      }, 0));
};


/**
 * Returns the arithmetic mean of the arguments.
 * @param {...number} var_args Numbers to average.
 * @return {number} The average of the arguments ({@code NaN} if no arguments
 *     were provided or any of the arguments is not a valid number).
 */
goog.math.average = function(var_args) {
  return goog.math.sum.apply(null, arguments) / arguments.length;
};


/**
 * Returns the unbiased sample variance of the arguments. For a definition,
 * see e.g. http://en.wikipedia.org/wiki/Variance
 * @param {...number} var_args Number samples to analyze.
 * @return {number} The unbiased sample variance of the arguments (0 if fewer
 *     than two samples were provided, or {@code NaN} if any of the samples is
 *     not a valid number).
 */
goog.math.sampleVariance = function(var_args) {
  var sampleSize = arguments.length;
  if (sampleSize < 2) {
    return 0;
  }

  var mean = goog.math.average.apply(null, arguments);
  var variance = goog.math.sum.apply(null, goog.array.map(arguments,
      function(val) {
        return Math.pow(val - mean, 2);
      })) / (sampleSize - 1);

  return variance;
};


/**
 * Returns the sample standard deviation of the arguments.  For a definition of
 * sample standard deviation, see e.g.
 * http://en.wikipedia.org/wiki/Standard_deviation
 * @param {...number} var_args Number samples to analyze.
 * @return {number} The sample standard deviation of the arguments (0 if fewer
 *     than two samples were provided, or {@code NaN} if any of the samples is
 *     not a valid number).
 */
goog.math.standardDeviation = function(var_args) {
  return Math.sqrt(goog.math.sampleVariance.apply(null, arguments));
};


/**
 * Returns whether the supplied number represents an integer, i.e. that is has
 * no fractional component.  No range-checking is performed on the number.
 * @param {number} num The number to test.
 * @return {boolean} Whether {@code num} is an integer.
 */
goog.math.isInt = function(num) {
  return isFinite(num) && num % 1 == 0;
};


/**
 * Returns whether the supplied number is finite and not NaN.
 * @param {number} num The number to test.
 * @return {boolean} Whether {@code num} is a finite number.
 */
goog.math.isFiniteNumber = function(num) {
  return isFinite(num) && !isNaN(num);
};


/**
 * @param {number} num The number to test.
 * @return {boolean} Whether it is negative zero.
 */
goog.math.isNegativeZero = function(num) {
  return num == 0 && 1 / num < 0;
};


/**
 * Returns the precise value of floor(log10(num)).
 * Simpler implementations didn't work because of floating point rounding
 * errors. For example
 * <ul>
 * <li>Math.floor(Math.log(num) / Math.LN10) is off by one for num == 1e+3.
 * <li>Math.floor(Math.log(num) * Math.LOG10E) is off by one for num == 1e+15.
 * <li>Math.floor(Math.log10(num)) is off by one for num == 1e+15 - 1.
 * </ul>
 * @param {number} num A floating point number.
 * @return {number} Its logarithm to base 10 rounded down to the nearest
 *     integer if num > 0. -Infinity if num == 0. NaN if num < 0.
 */
goog.math.log10Floor = function(num) {
  if (num > 0) {
    var x = Math.round(Math.log(num) * Math.LOG10E);
    return x - (parseFloat('1e' + x) > num);
  }
  return num == 0 ? -Infinity : NaN;
};


/**
 * A tweaked variant of {@code Math.floor} which tolerates if the passed number
 * is infinitesimally smaller than the closest integer. It often happens with
 * the results of floating point calculations because of the finite precision
 * of the intermediate results. For example {@code Math.floor(Math.log(1000) /
 * Math.LN10) == 2}, not 3 as one would expect.
 * @param {number} num A number.
 * @param {number=} opt_epsilon An infinitesimally small positive number, the
 *     rounding error to tolerate.
 * @return {number} The largest integer less than or equal to {@code num}.
 */
goog.math.safeFloor = function(num, opt_epsilon) {
  goog.asserts.assert(!goog.isDef(opt_epsilon) || opt_epsilon > 0);
  return Math.floor(num + (opt_epsilon || 2e-15));
};


/**
 * A tweaked variant of {@code Math.ceil}. See {@code goog.math.safeFloor} for
 * details.
 * @param {number} num A number.
 * @param {number=} opt_epsilon An infinitesimally small positive number, the
 *     rounding error to tolerate.
 * @return {number} The smallest integer greater than or equal to {@code num}.
 */
goog.math.safeCeil = function(num, opt_epsilon) {
  goog.asserts.assert(!goog.isDef(opt_epsilon) || opt_epsilon > 0);
  return Math.ceil(num - (opt_epsilon || 2e-15));
};

goog.provide('ol.Coordinate');
goog.provide('ol.CoordinateFormatType');
goog.provide('ol.coordinate');

goog.require('goog.math');
goog.require('goog.string');


/**
 * A function that takes a {@link ol.Coordinate} and transforms it into a
 * `{string}`.
 *
 * @typedef {function((ol.Coordinate|undefined)): string}
 * @api stable
 */
ol.CoordinateFormatType;


/**
 * An array of numbers representing an xy coordinate. Example: `[16, 48]`.
 * @typedef {Array.<number>} ol.Coordinate
 * @api stable
 */
ol.Coordinate;


/**
 * Add `delta` to `coordinate`. `coordinate` is modified in place and returned
 * by the function.
 *
 * Example:
 *
 *     var coord = [7.85, 47.983333];
 *     ol.coordinate.add(coord, [-2, 4]);
 *     // coord is now [5.85, 51.983333]
 *
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {ol.Coordinate} delta Delta.
 * @return {ol.Coordinate} The input coordinate adjusted by the given delta.
 * @api stable
 */
ol.coordinate.add = function(coordinate, delta) {
  coordinate[0] += delta[0];
  coordinate[1] += delta[1];
  return coordinate;
};


/**
 * Calculates the point closest to the passed coordinate on the passed segment.
 * This is the foot of the perpendicular of the coordinate to the segment when
 * the foot is on the segment, or the closest segment coordinate when the foot
 * is outside the segment.
 *
 * @param {ol.Coordinate} coordinate The coordinate.
 * @param {Array.<ol.Coordinate>} segment The two coordinates of the segment.
 * @return {ol.Coordinate} The foot of the perpendicular of the coordinate to
 *     the segment.
 */
ol.coordinate.closestOnSegment = function(coordinate, segment) {
  var x0 = coordinate[0];
  var y0 = coordinate[1];
  var start = segment[0];
  var end = segment[1];
  var x1 = start[0];
  var y1 = start[1];
  var x2 = end[0];
  var y2 = end[1];
  var dx = x2 - x1;
  var dy = y2 - y1;
  var along = (dx === 0 && dy === 0) ? 0 :
      ((dx * (x0 - x1)) + (dy * (y0 - y1))) / ((dx * dx + dy * dy) || 0);
  var x, y;
  if (along <= 0) {
    x = x1;
    y = y1;
  } else if (along >= 1) {
    x = x2;
    y = y2;
  } else {
    x = x1 + along * dx;
    y = y1 + along * dy;
  }
  return [x, y];
};


/**
 * Returns a {@link ol.CoordinateFormatType} function that can be used to format
 * a {ol.Coordinate} to a string.
 *
 * Example without specifying the fractional digits:
 *
 *     var coord = [7.85, 47.983333];
 *     var stringifyFunc = ol.coordinate.createStringXY();
 *     var out = stringifyFunc(coord);
 *     // out is now '8, 48'
 *
 * Example with explicitly specifying 2 fractional digits:
 *
 *     var coord = [7.85, 47.983333];
 *     var stringifyFunc = ol.coordinate.createStringXY(2);
 *     var out = stringifyFunc(coord);
 *     // out is now '7.85, 47.98'
 *
 * @param {number=} opt_fractionDigits The number of digits to include
 *    after the decimal point. Default is `0`.
 * @return {ol.CoordinateFormatType} Coordinate format.
 * @api stable
 */
ol.coordinate.createStringXY = function(opt_fractionDigits) {
  return (
      /**
       * @param {ol.Coordinate|undefined} coordinate Coordinate.
       * @return {string} String XY.
       */
      function(coordinate) {
        return ol.coordinate.toStringXY(coordinate, opt_fractionDigits);
      });
};


/**
 * @private
 * @param {number} degrees Degrees.
 * @param {string} hemispheres Hemispheres.
 * @return {string} String.
 */
ol.coordinate.degreesToStringHDMS_ = function(degrees, hemispheres) {
  var normalizedDegrees = goog.math.modulo(degrees + 180, 360) - 180;
  var x = Math.abs(Math.round(3600 * normalizedDegrees));
  return Math.floor(x / 3600) + '\u00b0 ' +
      goog.string.padNumber(Math.floor((x / 60) % 60), 2) + '\u2032 ' +
      goog.string.padNumber(Math.floor(x % 60), 2) + '\u2033 ' +
      hemispheres.charAt(normalizedDegrees < 0 ? 1 : 0);
};


/**
 * Transforms the given {@link ol.Coordinate} to a string using the given string
 * template. The strings `{x}` and `{y}` in the template will be replaced with
 * the first and second coordinate values respectively.
 *
 * Example without specifying the fractional digits:
 *
 *     var coord = [7.85, 47.983333];
 *     var template = 'Coordinate is ({x}|{y}).';
 *     var out = ol.coordinate.format(coord, template);
 *     // out is now 'Coordinate is (8|48).'
 *
 * Example explicitly specifying the fractional digits:
 *
 *     var coord = [7.85, 47.983333];
 *     var template = 'Coordinate is ({x}|{y}).';
 *     var out = ol.coordinate.format(coord, template, 2);
 *     // out is now 'Coordinate is (7.85|47.98).'
 *
 * @param {ol.Coordinate|undefined} coordinate Coordinate.
 * @param {string} template A template string with `{x}` and `{y}` placeholders
 *     that will be replaced by first and second coordinate values.
 * @param {number=} opt_fractionDigits The number of digits to include
 *    after the decimal point. Default is `0`.
 * @return {string} Formatted coordinate.
 * @api stable
 */
ol.coordinate.format = function(coordinate, template, opt_fractionDigits) {
  if (coordinate) {
    return template
      .replace('{x}', coordinate[0].toFixed(opt_fractionDigits))
      .replace('{y}', coordinate[1].toFixed(opt_fractionDigits));
  } else {
    return '';
  }
};


/**
 * @param {ol.Coordinate} coordinate1 First coordinate.
 * @param {ol.Coordinate} coordinate2 Second coordinate.
 * @return {boolean} Whether the passed coordinates are equal.
 */
ol.coordinate.equals = function(coordinate1, coordinate2) {
  var equals = true;
  for (var i = coordinate1.length - 1; i >= 0; --i) {
    if (coordinate1[i] != coordinate2[i]) {
      equals = false;
      break;
    }
  }
  return equals;
};


/**
 * Rotate `coordinate` by `angle`. `coordinate` is modified in place and
 * returned by the function.
 *
 * Example:
 *
 *     var coord = [7.85, 47.983333];
 *     var rotateRadians = Math.PI / 2; // 90 degrees
 *     ol.coordinate.rotate(coord, rotateRadians);
 *     // coord is now [-47.983333, 7.85]
 *
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {number} angle Angle in radian.
 * @return {ol.Coordinate} Coordinate.
 * @api stable
 */
ol.coordinate.rotate = function(coordinate, angle) {
  var cosAngle = Math.cos(angle);
  var sinAngle = Math.sin(angle);
  var x = coordinate[0] * cosAngle - coordinate[1] * sinAngle;
  var y = coordinate[1] * cosAngle + coordinate[0] * sinAngle;
  coordinate[0] = x;
  coordinate[1] = y;
  return coordinate;
};


/**
 * Scale `coordinate` by `scale`. `coordinate` is modified in place and returned
 * by the function.
 *
 * Example:
 *
 *     var coord = [7.85, 47.983333];
 *     var scale = 1.2;
 *     ol.coordinate.scale(coord, scale);
 *     // coord is now [9.42, 57.5799996]
 *
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {number} scale Scale factor.
 * @return {ol.Coordinate} Coordinate.
 */
ol.coordinate.scale = function(coordinate, scale) {
  coordinate[0] *= scale;
  coordinate[1] *= scale;
  return coordinate;
};


/**
 * Subtract `delta` to `coordinate`. `coordinate` is modified in place and
 * returned by the function.
 *
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {ol.Coordinate} delta Delta.
 * @return {ol.Coordinate} Coordinate.
 */
ol.coordinate.sub = function(coordinate, delta) {
  coordinate[0] -= delta[0];
  coordinate[1] -= delta[1];
  return coordinate;
};


/**
 * @param {ol.Coordinate} coord1 First coordinate.
 * @param {ol.Coordinate} coord2 Second coordinate.
 * @return {number} Squared distance between coord1 and coord2.
 */
ol.coordinate.squaredDistance = function(coord1, coord2) {
  var dx = coord1[0] - coord2[0];
  var dy = coord1[1] - coord2[1];
  return dx * dx + dy * dy;
};


/**
 * Calculate the squared distance from a coordinate to a line segment.
 *
 * @param {ol.Coordinate} coordinate Coordinate of the point.
 * @param {Array.<ol.Coordinate>} segment Line segment (2 coordinates).
 * @return {number} Squared distance from the point to the line segment.
 */
ol.coordinate.squaredDistanceToSegment = function(coordinate, segment) {
  return ol.coordinate.squaredDistance(coordinate,
      ol.coordinate.closestOnSegment(coordinate, segment));
};


/**
 * Format a geographic coordinate with the hemisphere, degrees, minutes, and
 * seconds.
 *
 * Example:
 *
 *     var coord = [7.85, 47.983333];
 *     var out = ol.coordinate.toStringHDMS(coord);
 *     // out is now '47° 59′ 0″ N 7° 51′ 0″ E'
 *
 * @param {ol.Coordinate|undefined} coordinate Coordinate.
 * @return {string} Hemisphere, degrees, minutes and seconds.
 * @api stable
 */
ol.coordinate.toStringHDMS = function(coordinate) {
  if (coordinate) {
    return ol.coordinate.degreesToStringHDMS_(coordinate[1], 'NS') + ' ' +
        ol.coordinate.degreesToStringHDMS_(coordinate[0], 'EW');
  } else {
    return '';
  }
};


/**
 * Format a coordinate as a comma delimited string.
 *
 * Example without specifying fractional digits:
 *
 *     var coord = [7.85, 47.983333];
 *     var out = ol.coordinate.toStringXY(coord);
 *     // out is now '8, 48'
 *
 * Example explicitly specifying 1 fractional digit:
 *
 *     var coord = [7.85, 47.983333];
 *     var out = ol.coordinate.toStringXY(coord, 1);
 *     // out is now '7.8, 48.0'
 *
 * @param {ol.Coordinate|undefined} coordinate Coordinate.
 * @param {number=} opt_fractionDigits The number of digits to include
 *    after the decimal point. Default is `0`.
 * @return {string} XY.
 * @api stable
 */
ol.coordinate.toStringXY = function(coordinate, opt_fractionDigits) {
  return ol.coordinate.format(coordinate, '{x}, {y}', opt_fractionDigits);
};


/**
 * Create an ol.Coordinate from an Array and take into account axis order.
 *
 * Examples:
 *
 *     var northCoord = ol.coordinate.fromProjectedArray([1, 2], 'n');
 *     // northCoord is now [2, 1]
 *
 *     var eastCoord = ol.coordinate.fromProjectedArray([1, 2], 'e');
 *     // eastCoord is now [1, 2]
 *
 * @param {Array} array The array with coordinates.
 * @param {string} axis the axis info.
 * @return {ol.Coordinate} The coordinate created.
 */
ol.coordinate.fromProjectedArray = function(array, axis) {
  var firstAxis = axis.charAt(0);
  if (firstAxis === 'n' || firstAxis === 's') {
    return [array[1], array[0]];
  } else {
    return array;
  }
};

// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// 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.


/**
 * @fileoverview Supplies a Float32Array implementation that implements
 *     most of the Float32Array spec and that can be used when a built-in
 *     implementation is not available.
 *
 *     Note that if no existing Float32Array implementation is found then
 *     this class and all its public properties are exported as Float32Array.
 *
 *     Adding support for the other TypedArray classes here does not make sense
 *     since this vector math library only needs Float32Array.
 *
 */
goog.provide('goog.vec.Float32Array');



/**
 * Constructs a new Float32Array. The new array is initialized to all zeros.
 *
 * @param {goog.vec.Float32Array|Array|ArrayBuffer|number} p0
 *     The length of the array, or an array to initialize the contents of the
 *     new Float32Array.
 * @constructor
 * @final
 */
goog.vec.Float32Array = function(p0) {
  this.length = /** @type {number} */ (p0.length || p0);
  for (var i = 0; i < this.length; i++) {
    this[i] = p0[i] || 0;
  }
};


/**
 * The number of bytes in an element (as defined by the Typed Array
 * specification).
 *
 * @type {number}
 */
goog.vec.Float32Array.BYTES_PER_ELEMENT = 4;


/**
 * The number of bytes in an element (as defined by the Typed Array
 * specification).
 *
 * @type {number}
 */
goog.vec.Float32Array.prototype.BYTES_PER_ELEMENT = 4;


/**
 * Sets elements of the array.
 * @param {Array<number>|Float32Array} values The array of values.
 * @param {number=} opt_offset The offset in this array to start.
 */
goog.vec.Float32Array.prototype.set = function(values, opt_offset) {
  opt_offset = opt_offset || 0;
  for (var i = 0; i < values.length && opt_offset + i < this.length; i++) {
    this[opt_offset + i] = values[i];
  }
};


/**
 * Creates a string representation of this array.
 * @return {string} The string version of this array.
 * @override
 */
goog.vec.Float32Array.prototype.toString = Array.prototype.join;


/**
 * Note that we cannot implement the subarray() or (deprecated) slice()
 * methods properly since doing so would require being able to overload
 * the [] operator which is not possible in javascript.  So we leave
 * them unimplemented.  Any attempt to call these methods will just result
 * in a javascript error since we leave them undefined.
 */


/**
 * If no existing Float32Array implementation is found then we export
 * goog.vec.Float32Array as Float32Array.
 */
if (typeof Float32Array == 'undefined') {
  goog.exportProperty(goog.vec.Float32Array, 'BYTES_PER_ELEMENT',
                      goog.vec.Float32Array.BYTES_PER_ELEMENT);
  goog.exportProperty(goog.vec.Float32Array.prototype, 'BYTES_PER_ELEMENT',
                      goog.vec.Float32Array.prototype.BYTES_PER_ELEMENT);
  goog.exportProperty(goog.vec.Float32Array.prototype, 'set',
                      goog.vec.Float32Array.prototype.set);
  goog.exportProperty(goog.vec.Float32Array.prototype, 'toString',
                      goog.vec.Float32Array.prototype.toString);
  goog.exportSymbol('Float32Array', goog.vec.Float32Array);
}

// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// 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.


/**
 * @fileoverview Supplies a Float64Array implementation that implements
 * most of the Float64Array spec and that can be used when a built-in
 * implementation is not available.
 *
 * Note that if no existing Float64Array implementation is found then this
 * class and all its public properties are exported as Float64Array.
 *
 * Adding support for the other TypedArray classes here does not make sense
 * since this vector math library only needs Float32Array and Float64Array.
 *
 */
goog.provide('goog.vec.Float64Array');



/**
 * Constructs a new Float64Array. The new array is initialized to all zeros.
 *
 * @param {goog.vec.Float64Array|Array|ArrayBuffer|number} p0
 *     The length of the array, or an array to initialize the contents of the
 *     new Float64Array.
 * @constructor
 * @final
 */
goog.vec.Float64Array = function(p0) {
  this.length = /** @type {number} */ (p0.length || p0);
  for (var i = 0; i < this.length; i++) {
    this[i] = p0[i] || 0;
  }
};


/**
 * The number of bytes in an element (as defined by the Typed Array
 * specification).
 *
 * @type {number}
 */
goog.vec.Float64Array.BYTES_PER_ELEMENT = 8;


/**
 * The number of bytes in an element (as defined by the Typed Array
 * specification).
 *
 * @type {number}
 */
goog.vec.Float64Array.prototype.BYTES_PER_ELEMENT = 8;


/**
 * Sets elements of the array.
 * @param {Array<number>|Float64Array} values The array of values.
 * @param {number=} opt_offset The offset in this array to start.
 */
goog.vec.Float64Array.prototype.set = function(values, opt_offset) {
  opt_offset = opt_offset || 0;
  for (var i = 0; i < values.length && opt_offset + i < this.length; i++) {
    this[opt_offset + i] = values[i];
  }
};


/**
 * Creates a string representation of this array.
 * @return {string} The string version of this array.
 * @override
 */
goog.vec.Float64Array.prototype.toString = Array.prototype.join;


/**
 * Note that we cannot implement the subarray() or (deprecated) slice()
 * methods properly since doing so would require being able to overload
 * the [] operator which is not possible in javascript.  So we leave
 * them unimplemented.  Any attempt to call these methods will just result
 * in a javascript error since we leave them undefined.
 */


/**
 * If no existing Float64Array implementation is found then we export
 * goog.vec.Float64Array as Float64Array.
 */
if (typeof Float64Array == 'undefined') {
  try {
    goog.exportProperty(goog.vec.Float64Array, 'BYTES_PER_ELEMENT',
                        goog.vec.Float64Array.BYTES_PER_ELEMENT);
  } catch (float64ArrayError) {
    // Do nothing.  This code is in place to fix b/7225850, in which an error
    // is incorrectly thrown for Google TV on an old Chrome.
    // TODO(user): remove after that version is retired.
  }

  goog.exportProperty(goog.vec.Float64Array.prototype, 'BYTES_PER_ELEMENT',
                      goog.vec.Float64Array.prototype.BYTES_PER_ELEMENT);
  goog.exportProperty(goog.vec.Float64Array.prototype, 'set',
                      goog.vec.Float64Array.prototype.set);
  goog.exportProperty(goog.vec.Float64Array.prototype, 'toString',
                      goog.vec.Float64Array.prototype.toString);
  goog.exportSymbol('Float64Array', goog.vec.Float64Array);
}

// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// 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.


/**
 * @fileoverview Supplies global data types and constants for the vector math
 *     library.
 */
goog.provide('goog.vec');
goog.provide('goog.vec.AnyType');
goog.provide('goog.vec.ArrayType');
goog.provide('goog.vec.Float32');
goog.provide('goog.vec.Float64');
goog.provide('goog.vec.Number');


/**
 * On platforms that don't have native Float32Array or Float64Array support we
 * use a javascript implementation so that this math library can be used on all
 * platforms.
 * @suppress {extraRequire}
 */
goog.require('goog.vec.Float32Array');
/** @suppress {extraRequire} */
goog.require('goog.vec.Float64Array');

// All vector and matrix operations are based upon arrays of numbers using
// either Float32Array, Float64Array, or a standard Javascript Array of
// Numbers.


/** @typedef {!Float32Array} */
goog.vec.Float32;


/** @typedef {!Float64Array} */
goog.vec.Float64;


/** @typedef {!Array<number>} */
goog.vec.Number;


/** @typedef {!goog.vec.Float32|!goog.vec.Float64|!goog.vec.Number} */
goog.vec.AnyType;


/**
 * @deprecated Use AnyType.
 * @typedef {!Float32Array|!Array<number>}
 */
goog.vec.ArrayType;


/**
 * For graphics work, 6 decimal places of accuracy are typically all that is
 * required.
 *
 * @type {number}
 * @const
 */
goog.vec.EPSILON = 1e-6;

// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// 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.


/**
 * @fileoverview Supplies 3 element vectors that are compatible with WebGL.
 * Each element is a float32 since that is typically the desired size of a
 * 3-vector in the GPU.  The API is structured to avoid unnecessary memory
 * allocations.  The last parameter will typically be the output vector and
 * an object can be both an input and output parameter to all methods except
 * where noted.
 *
 */
goog.provide('goog.vec.Vec3');

/** @suppress {extraRequire} */
goog.require('goog.vec');

/** @typedef {goog.vec.Float32} */ goog.vec.Vec3.Float32;
/** @typedef {goog.vec.Float64} */ goog.vec.Vec3.Float64;
/** @typedef {goog.vec.Number} */ goog.vec.Vec3.Number;
/** @typedef {goog.vec.AnyType} */ goog.vec.Vec3.AnyType;

// The following two types are deprecated - use the above types instead.
/** @typedef {Float32Array} */ goog.vec.Vec3.Type;
/** @typedef {goog.vec.ArrayType} */ goog.vec.Vec3.Vec3Like;


/**
 * Creates a 3 element vector of Float32. The array is initialized to zero.
 *
 * @return {!goog.vec.Vec3.Float32} The new 3 element array.
 */
goog.vec.Vec3.createFloat32 = function() {
  return new Float32Array(3);
};


/**
 * Creates a 3 element vector of Float64. The array is initialized to zero.
 *
 * @return {!goog.vec.Vec3.Float64} The new 3 element array.
 */
goog.vec.Vec3.createFloat64 = function() {
  return new Float64Array(3);
};


/**
 * Creates a 3 element vector of Number. The array is initialized to zero.
 *
 * @return {!goog.vec.Vec3.Number} The new 3 element array.
 */
goog.vec.Vec3.createNumber = function() {
  var a = new Array(3);
  goog.vec.Vec3.setFromValues(a, 0, 0, 0);
  return a;
};


/**
 * Creates a 3 element vector of Float32Array. The array is initialized to zero.
 *
 * @deprecated Use createFloat32.
 * @return {!goog.vec.Vec3.Type} The new 3 element array.
 */
goog.vec.Vec3.create = function() {
  return new Float32Array(3);
};


/**
 * Creates a new 3 element FLoat32 vector initialized with the value from the
 * given array.
 *
 * @param {goog.vec.Vec3.AnyType} vec The source 3 element array.
 * @return {!goog.vec.Vec3.Float32} The new 3 element array.
 */
goog.vec.Vec3.createFloat32FromArray = function(vec) {
  var newVec = goog.vec.Vec3.createFloat32();
  goog.vec.Vec3.setFromArray(newVec, vec);
  return newVec;
};


/**
 * Creates a new 3 element Float32 vector initialized with the supplied values.
 *
 * @param {number} v0 The value for element at index 0.
 * @param {number} v1 The value for element at index 1.
 * @param {number} v2 The value for element at index 2.
 * @return {!goog.vec.Vec3.Float32} The new vector.
 */
goog.vec.Vec3.createFloat32FromValues = function(v0, v1, v2) {
  var a = goog.vec.Vec3.createFloat32();
  goog.vec.Vec3.setFromValues(a, v0, v1, v2);
  return a;
};


/**
 * Creates a clone of the given 3 element Float32 vector.
 *
 * @param {goog.vec.Vec3.Float32} vec The source 3 element vector.
 * @return {!goog.vec.Vec3.Float32} The new cloned vector.
 */
goog.vec.Vec3.cloneFloat32 = goog.vec.Vec3.createFloat32FromArray;


/**
 * Creates a new 3 element Float64 vector initialized with the value from the
 * given array.
 *
 * @param {goog.vec.Vec3.AnyType} vec The source 3 element array.
 * @return {!goog.vec.Vec3.Float64} The new 3 element array.
 */
goog.vec.Vec3.createFloat64FromArray = function(vec) {
  var newVec = goog.vec.Vec3.createFloat64();
  goog.vec.Vec3.setFromArray(newVec, vec);
  return newVec;
};


/**
* Creates a new 3 element Float64 vector initialized with the supplied values.
*
* @param {number} v0 The value for element at index 0.
* @param {number} v1 The value for element at index 1.
* @param {number} v2 The value for element at index 2.
* @return {!goog.vec.Vec3.Float64} The new vector.
*/
goog.vec.Vec3.createFloat64FromValues = function(v0, v1, v2) {
  var vec = goog.vec.Vec3.createFloat64();
  goog.vec.Vec3.setFromValues(vec, v0, v1, v2);
  return vec;
};


/**
 * Creates a clone of the given 3 element vector.
 *
 * @param {goog.vec.Vec3.Float64} vec The source 3 element vector.
 * @return {!goog.vec.Vec3.Float64} The new cloned vector.
 */
goog.vec.Vec3.cloneFloat64 = goog.vec.Vec3.createFloat64FromArray;


/**
 * Creates a new 3 element vector initialized with the value from the given
 * array.
 *
 * @deprecated Use createFloat32FromArray.
 * @param {goog.vec.Vec3.Vec3Like} vec The source 3 element array.
 * @return {!goog.vec.Vec3.Type} The new 3 element array.
 */
goog.vec.Vec3.createFromArray = function(vec) {
  var newVec = goog.vec.Vec3.create();
  goog.vec.Vec3.setFromArray(newVec, vec);
  return newVec;
};


/**
 * Creates a new 3 element vector initialized with the supplied values.
 *
 * @deprecated Use createFloat32FromValues.
 * @param {number} v0 The value for element at index 0.
 * @param {number} v1 The value for element at index 1.
 * @param {number} v2 The value for element at index 2.
 * @return {!goog.vec.Vec3.Type} The new vector.
 */
goog.vec.Vec3.createFromValues = function(v0, v1, v2) {
  var vec = goog.vec.Vec3.create();
  goog.vec.Vec3.setFromValues(vec, v0, v1, v2);
  return vec;
};


/**
 * Creates a clone of the given 3 element vector.
 *
 * @deprecated Use cloneFloat32.
 * @param {goog.vec.Vec3.Vec3Like} vec The source 3 element vector.
 * @return {!goog.vec.Vec3.Type} The new cloned vector.
 */
goog.vec.Vec3.clone = function(vec) {
  var newVec = goog.vec.Vec3.create();
  goog.vec.Vec3.setFromArray(newVec, vec);
  return newVec;
};


/**
 * Initializes the vector with the given values.
 *
 * @param {goog.vec.Vec3.AnyType} vec The vector to receive the values.
 * @param {number} v0 The value for element at index 0.
 * @param {number} v1 The value for element at index 1.
 * @param {number} v2 The value for element at index 2.
 * @return {!goog.vec.Vec3.AnyType} Return vec so that operations can be
 *     chained together.
 */
goog.vec.Vec3.setFromValues = function(vec, v0, v1, v2) {
  vec[0] = v0;
  vec[1] = v1;
  vec[2] = v2;
  return vec;
};


/**
 * Initializes the vector with the given array of values.
 *
 * @param {goog.vec.Vec3.AnyType} vec The vector to receive the
 *     values.
 * @param {goog.vec.Vec3.AnyType} values The array of values.
 * @return {!goog.vec.Vec3.AnyType} Return vec so that operations can be
 *     chained together.
 */
goog.vec.Vec3.setFromArray = function(vec, values) {
  vec[0] = values[0];
  vec[1] = values[1];
  vec[2] = values[2];
  return vec;
};


/**
 * Performs a component-wise addition of vec0 and vec1 together storing the
 * result into resultVec.
 *
 * @param {goog.vec.Vec3.AnyType} vec0 The first addend.
 * @param {goog.vec.Vec3.AnyType} vec1 The second addend.
 * @param {goog.vec.Vec3.AnyType} resultVec The vector to
 *     receive the result. May be vec0 or vec1.
 * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Vec3.add = function(vec0, vec1, resultVec) {
  resultVec[0] = vec0[0] + vec1[0];
  resultVec[1] = vec0[1] + vec1[1];
  resultVec[2] = vec0[2] + vec1[2];
  return resultVec;
};


/**
 * Performs a component-wise subtraction of vec1 from vec0 storing the
 * result into resultVec.
 *
 * @param {goog.vec.Vec3.AnyType} vec0 The minuend.
 * @param {goog.vec.Vec3.AnyType} vec1 The subtrahend.
 * @param {goog.vec.Vec3.AnyType} resultVec The vector to
 *     receive the result. May be vec0 or vec1.
 * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Vec3.subtract = function(vec0, vec1, resultVec) {
  resultVec[0] = vec0[0] - vec1[0];
  resultVec[1] = vec0[1] - vec1[1];
  resultVec[2] = vec0[2] - vec1[2];
  return resultVec;
};


/**
 * Negates vec0, storing the result into resultVec.
 *
 * @param {goog.vec.Vec3.AnyType} vec0 The vector to negate.
 * @param {goog.vec.Vec3.AnyType} resultVec The vector to
 *     receive the result. May be vec0.
 * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Vec3.negate = function(vec0, resultVec) {
  resultVec[0] = -vec0[0];
  resultVec[1] = -vec0[1];
  resultVec[2] = -vec0[2];
  return resultVec;
};


/**
 * Takes the absolute value of each component of vec0 storing the result in
 * resultVec.
 *
 * @param {goog.vec.Vec3.AnyType} vec0 The source vector.
 * @param {goog.vec.Vec3.AnyType} resultVec The vector to receive the result.
 *     May be vec0.
 * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Vec3.abs = function(vec0, resultVec) {
  resultVec[0] = Math.abs(vec0[0]);
  resultVec[1] = Math.abs(vec0[1]);
  resultVec[2] = Math.abs(vec0[2]);
  return resultVec;
};


/**
 * Multiplies each component of vec0 with scalar storing the product into
 * resultVec.
 *
 * @param {goog.vec.Vec3.AnyType} vec0 The source vector.
 * @param {number} scalar The value to multiply with each component of vec0.
 * @param {goog.vec.Vec3.AnyType} resultVec The vector to
 *     receive the result. May be vec0.
 * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Vec3.scale = function(vec0, scalar, resultVec) {
  resultVec[0] = vec0[0] * scalar;
  resultVec[1] = vec0[1] * scalar;
  resultVec[2] = vec0[2] * scalar;
  return resultVec;
};


/**
 * Returns the magnitudeSquared of the given vector.
 *
 * @param {goog.vec.Vec3.AnyType} vec0 The vector.
 * @return {number} The magnitude of the vector.
 */
goog.vec.Vec3.magnitudeSquared = function(vec0) {
  var x = vec0[0], y = vec0[1], z = vec0[2];
  return x * x + y * y + z * z;
};


/**
 * Returns the magnitude of the given vector.
 *
 * @param {goog.vec.Vec3.AnyType} vec0 The vector.
 * @return {number} The magnitude of the vector.
 */
goog.vec.Vec3.magnitude = function(vec0) {
  var x = vec0[0], y = vec0[1], z = vec0[2];
  return Math.sqrt(x * x + y * y + z * z);
};


/**
 * Normalizes the given vector storing the result into resultVec.
 *
 * @param {goog.vec.Vec3.AnyType} vec0 The vector to normalize.
 * @param {goog.vec.Vec3.AnyType} resultVec The vector to
 *     receive the result. May be vec0.
 * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Vec3.normalize = function(vec0, resultVec) {
  var ilen = 1 / goog.vec.Vec3.magnitude(vec0);
  resultVec[0] = vec0[0] * ilen;
  resultVec[1] = vec0[1] * ilen;
  resultVec[2] = vec0[2] * ilen;
  return resultVec;
};


/**
 * Returns the scalar product of vectors v0 and v1.
 *
 * @param {goog.vec.Vec3.AnyType} v0 The first vector.
 * @param {goog.vec.Vec3.AnyType} v1 The second vector.
 * @return {number} The scalar product.
 */
goog.vec.Vec3.dot = function(v0, v1) {
  return v0[0] * v1[0] + v0[1] * v1[1] + v0[2] * v1[2];
};


/**
 * Computes the vector (cross) product of v0 and v1 storing the result into
 * resultVec.
 *
 * @param {goog.vec.Vec3.AnyType} v0 The first vector.
 * @param {goog.vec.Vec3.AnyType} v1 The second vector.
 * @param {goog.vec.Vec3.AnyType} resultVec The vector to receive the
 *     results. May be either v0 or v1.
 * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Vec3.cross = function(v0, v1, resultVec) {
  var x0 = v0[0], y0 = v0[1], z0 = v0[2];
  var x1 = v1[0], y1 = v1[1], z1 = v1[2];
  resultVec[0] = y0 * z1 - z0 * y1;
  resultVec[1] = z0 * x1 - x0 * z1;
  resultVec[2] = x0 * y1 - y0 * x1;
  return resultVec;
};


/**
 * Returns the squared distance between two points.
 *
 * @param {goog.vec.Vec3.AnyType} vec0 First point.
 * @param {goog.vec.Vec3.AnyType} vec1 Second point.
 * @return {number} The squared distance between the points.
 */
goog.vec.Vec3.distanceSquared = function(vec0, vec1) {
  var x = vec0[0] - vec1[0];
  var y = vec0[1] - vec1[1];
  var z = vec0[2] - vec1[2];
  return x * x + y * y + z * z;
};


/**
 * Returns the distance between two points.
 *
 * @param {goog.vec.Vec3.AnyType} vec0 First point.
 * @param {goog.vec.Vec3.AnyType} vec1 Second point.
 * @return {number} The distance between the points.
 */
goog.vec.Vec3.distance = function(vec0, vec1) {
  return Math.sqrt(goog.vec.Vec3.distanceSquared(vec0, vec1));
};


/**
 * Returns a unit vector pointing from one point to another.
 * If the input points are equal then the result will be all zeros.
 *
 * @param {goog.vec.Vec3.AnyType} vec0 Origin point.
 * @param {goog.vec.Vec3.AnyType} vec1 Target point.
 * @param {goog.vec.Vec3.AnyType} resultVec The vector to receive the
 *     results (may be vec0 or vec1).
 * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Vec3.direction = function(vec0, vec1, resultVec) {
  var x = vec1[0] - vec0[0];
  var y = vec1[1] - vec0[1];
  var z = vec1[2] - vec0[2];
  var d = Math.sqrt(x * x + y * y + z * z);
  if (d) {
    d = 1 / d;
    resultVec[0] = x * d;
    resultVec[1] = y * d;
    resultVec[2] = z * d;
  } else {
    resultVec[0] = resultVec[1] = resultVec[2] = 0;
  }
  return resultVec;
};


/**
 * Linearly interpolate from vec0 to v1 according to f. The value of f should be
 * in the range [0..1] otherwise the results are undefined.
 *
 * @param {goog.vec.Vec3.AnyType} v0 The first vector.
 * @param {goog.vec.Vec3.AnyType} v1 The second vector.
 * @param {number} f The interpolation factor.
 * @param {goog.vec.Vec3.AnyType} resultVec The vector to receive the
 *     results (may be v0 or v1).
 * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Vec3.lerp = function(v0, v1, f, resultVec) {
  var x = v0[0], y = v0[1], z = v0[2];
  resultVec[0] = (v1[0] - x) * f + x;
  resultVec[1] = (v1[1] - y) * f + y;
  resultVec[2] = (v1[2] - z) * f + z;
  return resultVec;
};


/**
 * Compares the components of vec0 with the components of another vector or
 * scalar, storing the larger values in resultVec.
 *
 * @param {goog.vec.Vec3.AnyType} vec0 The source vector.
 * @param {goog.vec.Vec3.AnyType|number} limit The limit vector or scalar.
 * @param {goog.vec.Vec3.AnyType} resultVec The vector to receive the
 *     results (may be vec0 or limit).
 * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Vec3.max = function(vec0, limit, resultVec) {
  if (goog.isNumber(limit)) {
    resultVec[0] = Math.max(vec0[0], limit);
    resultVec[1] = Math.max(vec0[1], limit);
    resultVec[2] = Math.max(vec0[2], limit);
  } else {
    resultVec[0] = Math.max(vec0[0], limit[0]);
    resultVec[1] = Math.max(vec0[1], limit[1]);
    resultVec[2] = Math.max(vec0[2], limit[2]);
  }
  return resultVec;
};


/**
 * Compares the components of vec0 with the components of another vector or
 * scalar, storing the smaller values in resultVec.
 *
 * @param {goog.vec.Vec3.AnyType} vec0 The source vector.
 * @param {goog.vec.Vec3.AnyType|number} limit The limit vector or scalar.
 * @param {goog.vec.Vec3.AnyType} resultVec The vector to receive the
 *     results (may be vec0 or limit).
 * @return {!goog.vec.Vec3.AnyType} Return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Vec3.min = function(vec0, limit, resultVec) {
  if (goog.isNumber(limit)) {
    resultVec[0] = Math.min(vec0[0], limit);
    resultVec[1] = Math.min(vec0[1], limit);
    resultVec[2] = Math.min(vec0[2], limit);
  } else {
    resultVec[0] = Math.min(vec0[0], limit[0]);
    resultVec[1] = Math.min(vec0[1], limit[1]);
    resultVec[2] = Math.min(vec0[2], limit[2]);
  }
  return resultVec;
};


/**
 * Returns true if the components of v0 are equal to the components of v1.
 *
 * @param {goog.vec.Vec3.AnyType} v0 The first vector.
 * @param {goog.vec.Vec3.AnyType} v1 The second vector.
 * @return {boolean} True if the vectors are equal, false otherwise.
 */
goog.vec.Vec3.equals = function(v0, v1) {
  return v0.length == v1.length &&
      v0[0] == v1[0] && v0[1] == v1[1] && v0[2] == v1[2];
};

// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// 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.


/**
 * @fileoverview Supplies 4 element vectors that are compatible with WebGL.
 * Each element is a float32 since that is typically the desired size of a
 * 4-vector in the GPU.  The API is structured to avoid unnecessary memory
 * allocations.  The last parameter will typically be the output vector and
 * an object can be both an input and output parameter to all methods except
 * where noted.
 *
 */
goog.provide('goog.vec.Vec4');

/** @suppress {extraRequire} */
goog.require('goog.vec');

/** @typedef {goog.vec.Float32} */ goog.vec.Vec4.Float32;
/** @typedef {goog.vec.Float64} */ goog.vec.Vec4.Float64;
/** @typedef {goog.vec.Number} */ goog.vec.Vec4.Number;
/** @typedef {goog.vec.AnyType} */ goog.vec.Vec4.AnyType;

// The following two types are deprecated - use the above types instead.
/** @typedef {Float32Array} */ goog.vec.Vec4.Type;
/** @typedef {goog.vec.ArrayType} */ goog.vec.Vec4.Vec4Like;


/**
 * Creates a 4 element vector of Float32. The array is initialized to zero.
 *
 * @return {!goog.vec.Vec4.Float32} The new 3 element array.
 */
goog.vec.Vec4.createFloat32 = function() {
  return new Float32Array(4);
};


/**
 * Creates a 4 element vector of Float64. The array is initialized to zero.
 *
 * @return {!goog.vec.Vec4.Float64} The new 4 element array.
 */
goog.vec.Vec4.createFloat64 = function() {
  return new Float64Array(4);
};


/**
 * Creates a 4 element vector of Number. The array is initialized to zero.
 *
 * @return {!goog.vec.Vec4.Number} The new 4 element array.
 */
goog.vec.Vec4.createNumber = function() {
  var v = new Array(4);
  goog.vec.Vec4.setFromValues(v, 0, 0, 0, 0);
  return v;
};


/**
 * Creates a 4 element vector of Float32Array. The array is initialized to zero.
 *
 * @deprecated Use createFloat32.
 * @return {!goog.vec.Vec4.Type} The new 4 element array.
 */
goog.vec.Vec4.create = function() {
  return new Float32Array(4);
};


/**
 * Creates a new 4 element vector initialized with the value from the given
 * array.
 *
 * @deprecated Use createFloat32FromArray.
 * @param {goog.vec.Vec4.Vec4Like} vec The source 4 element array.
 * @return {!goog.vec.Vec4.Type} The new 4 element array.
 */
goog.vec.Vec4.createFromArray = function(vec) {
  var newVec = goog.vec.Vec4.create();
  goog.vec.Vec4.setFromArray(newVec, vec);
  return newVec;
};


/**
 * Creates a new 4 element FLoat32 vector initialized with the value from the
 * given array.
 *
 * @param {goog.vec.Vec4.AnyType} vec The source 3 element array.
 * @return {!goog.vec.Vec4.Float32} The new 3 element array.
 */
goog.vec.Vec4.createFloat32FromArray = function(vec) {
  var newVec = goog.vec.Vec4.createFloat32();
  goog.vec.Vec4.setFromArray(newVec, vec);
  return newVec;
};


/**
 * Creates a new 4 element Float32 vector initialized with the supplied values.
 *
 * @param {number} v0 The value for element at index 0.
 * @param {number} v1 The value for element at index 1.
 * @param {number} v2 The value for element at index 2.
 * @param {number} v3 The value for element at index 3.
 * @return {!goog.vec.Vec4.Float32} The new vector.
 */
goog.vec.Vec4.createFloat32FromValues = function(v0, v1, v2, v3) {
  var vec = goog.vec.Vec4.createFloat32();
  goog.vec.Vec4.setFromValues(vec, v0, v1, v2, v3);
  return vec;
};


/**
 * Creates a clone of the given 4 element Float32 vector.
 *
 * @param {goog.vec.Vec4.Float32} vec The source 3 element vector.
 * @return {!goog.vec.Vec4.Float32} The new cloned vector.
 */
goog.vec.Vec4.cloneFloat32 = goog.vec.Vec4.createFloat32FromArray;


/**
 * Creates a new 4 element Float64 vector initialized with the value from the
 * given array.
 *
 * @param {goog.vec.Vec4.AnyType} vec The source 4 element array.
 * @return {!goog.vec.Vec4.Float64} The new 4 element array.
 */
goog.vec.Vec4.createFloat64FromArray = function(vec) {
  var newVec = goog.vec.Vec4.createFloat64();
  goog.vec.Vec4.setFromArray(newVec, vec);
  return newVec;
};


/**
* Creates a new 4 element Float64 vector initialized with the supplied values.
*
* @param {number} v0 The value for element at index 0.
* @param {number} v1 The value for element at index 1.
* @param {number} v2 The value for element at index 2.
* @param {number} v3 The value for element at index 3.
* @return {!goog.vec.Vec4.Float64} The new vector.
*/
goog.vec.Vec4.createFloat64FromValues = function(v0, v1, v2, v3) {
  var vec = goog.vec.Vec4.createFloat64();
  goog.vec.Vec4.setFromValues(vec, v0, v1, v2, v3);
  return vec;
};


/**
 * Creates a clone of the given 4 element vector.
 *
 * @param {goog.vec.Vec4.Float64} vec The source 4 element vector.
 * @return {!goog.vec.Vec4.Float64} The new cloned vector.
 */
goog.vec.Vec4.cloneFloat64 = goog.vec.Vec4.createFloat64FromArray;


/**
 * Creates a new 4 element vector initialized with the supplied values.
 *
 * @deprecated Use createFloat32FromValues.
 * @param {number} v0 The value for element at index 0.
 * @param {number} v1 The value for element at index 1.
 * @param {number} v2 The value for element at index 2.
 * @param {number} v3 The value for element at index 3.
 * @return {!goog.vec.Vec4.Type} The new vector.
 */
goog.vec.Vec4.createFromValues = function(v0, v1, v2, v3) {
  var vec = goog.vec.Vec4.create();
  goog.vec.Vec4.setFromValues(vec, v0, v1, v2, v3);
  return vec;
};


/**
 * Creates a clone of the given 4 element vector.
 *
 * @deprecated Use cloneFloat32.
 * @param {goog.vec.Vec4.Vec4Like} vec The source 4 element vector.
 * @return {!goog.vec.Vec4.Type} The new cloned vector.
 */
goog.vec.Vec4.clone = goog.vec.Vec4.createFromArray;


/**
 * Initializes the vector with the given values.
 *
 * @param {goog.vec.Vec4.AnyType} vec The vector to receive the values.
 * @param {number} v0 The value for element at index 0.
 * @param {number} v1 The value for element at index 1.
 * @param {number} v2 The value for element at index 2.
 * @param {number} v3 The value for element at index 3.
 * @return {!goog.vec.Vec4.AnyType} Return vec so that operations can be
 *     chained together.
 */
goog.vec.Vec4.setFromValues = function(vec, v0, v1, v2, v3) {
  vec[0] = v0;
  vec[1] = v1;
  vec[2] = v2;
  vec[3] = v3;
  return vec;
};


/**
 * Initializes the vector with the given array of values.
 *
 * @param {goog.vec.Vec4.AnyType} vec The vector to receive the
 *     values.
 * @param {goog.vec.Vec4.AnyType} values The array of values.
 * @return {!goog.vec.Vec4.AnyType} Return vec so that operations can be
 *     chained together.
 */
goog.vec.Vec4.setFromArray = function(vec, values) {
  vec[0] = values[0];
  vec[1] = values[1];
  vec[2] = values[2];
  vec[3] = values[3];
  return vec;
};


/**
 * Performs a component-wise addition of vec0 and vec1 together storing the
 * result into resultVec.
 *
 * @param {goog.vec.Vec4.AnyType} vec0 The first addend.
 * @param {goog.vec.Vec4.AnyType} vec1 The second addend.
 * @param {goog.vec.Vec4.AnyType} resultVec The vector to
 *     receive the result. May be vec0 or vec1.
 * @return {!goog.vec.Vec4.AnyType} Return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Vec4.add = function(vec0, vec1, resultVec) {
  resultVec[0] = vec0[0] + vec1[0];
  resultVec[1] = vec0[1] + vec1[1];
  resultVec[2] = vec0[2] + vec1[2];
  resultVec[3] = vec0[3] + vec1[3];
  return resultVec;
};


/**
 * Performs a component-wise subtraction of vec1 from vec0 storing the
 * result into resultVec.
 *
 * @param {goog.vec.Vec4.AnyType} vec0 The minuend.
 * @param {goog.vec.Vec4.AnyType} vec1 The subtrahend.
 * @param {goog.vec.Vec4.AnyType} resultVec The vector to
 *     receive the result. May be vec0 or vec1.
 * @return {!goog.vec.Vec4.AnyType} Return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Vec4.subtract = function(vec0, vec1, resultVec) {
  resultVec[0] = vec0[0] - vec1[0];
  resultVec[1] = vec0[1] - vec1[1];
  resultVec[2] = vec0[2] - vec1[2];
  resultVec[3] = vec0[3] - vec1[3];
  return resultVec;
};


/**
 * Negates vec0, storing the result into resultVec.
 *
 * @param {goog.vec.Vec4.AnyType} vec0 The vector to negate.
 * @param {goog.vec.Vec4.AnyType} resultVec The vector to
 *     receive the result. May be vec0.
 * @return {!goog.vec.Vec4.AnyType} Return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Vec4.negate = function(vec0, resultVec) {
  resultVec[0] = -vec0[0];
  resultVec[1] = -vec0[1];
  resultVec[2] = -vec0[2];
  resultVec[3] = -vec0[3];
  return resultVec;
};


/**
 * Takes the absolute value of each component of vec0 storing the result in
 * resultVec.
 *
 * @param {goog.vec.Vec4.AnyType} vec0 The source vector.
 * @param {goog.vec.Vec4.AnyType} resultVec The vector to receive the result.
 *     May be vec0.
 * @return {!goog.vec.Vec4.AnyType} Return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Vec4.abs = function(vec0, resultVec) {
  resultVec[0] = Math.abs(vec0[0]);
  resultVec[1] = Math.abs(vec0[1]);
  resultVec[2] = Math.abs(vec0[2]);
  resultVec[3] = Math.abs(vec0[3]);
  return resultVec;
};


/**
 * Multiplies each component of vec0 with scalar storing the product into
 * resultVec.
 *
 * @param {goog.vec.Vec4.AnyType} vec0 The source vector.
 * @param {number} scalar The value to multiply with each component of vec0.
 * @param {goog.vec.Vec4.AnyType} resultVec The vector to
 *     receive the result. May be vec0.
 * @return {!goog.vec.Vec4.AnyType} Return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Vec4.scale = function(vec0, scalar, resultVec) {
  resultVec[0] = vec0[0] * scalar;
  resultVec[1] = vec0[1] * scalar;
  resultVec[2] = vec0[2] * scalar;
  resultVec[3] = vec0[3] * scalar;
  return resultVec;
};


/**
 * Returns the magnitudeSquared of the given vector.
 *
 * @param {goog.vec.Vec4.AnyType} vec0 The vector.
 * @return {number} The magnitude of the vector.
 */
goog.vec.Vec4.magnitudeSquared = function(vec0) {
  var x = vec0[0], y = vec0[1], z = vec0[2], w = vec0[3];
  return x * x + y * y + z * z + w * w;
};


/**
 * Returns the magnitude of the given vector.
 *
 * @param {goog.vec.Vec4.AnyType} vec0 The vector.
 * @return {number} The magnitude of the vector.
 */
goog.vec.Vec4.magnitude = function(vec0) {
  var x = vec0[0], y = vec0[1], z = vec0[2], w = vec0[3];
  return Math.sqrt(x * x + y * y + z * z + w * w);
};


/**
 * Normalizes the given vector storing the result into resultVec.
 *
 * @param {goog.vec.Vec4.AnyType} vec0 The vector to normalize.
 * @param {goog.vec.Vec4.AnyType} resultVec The vector to
 *     receive the result. May be vec0.
 * @return {!goog.vec.Vec4.AnyType} Return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Vec4.normalize = function(vec0, resultVec) {
  var ilen = 1 / goog.vec.Vec4.magnitude(vec0);
  resultVec[0] = vec0[0] * ilen;
  resultVec[1] = vec0[1] * ilen;
  resultVec[2] = vec0[2] * ilen;
  resultVec[3] = vec0[3] * ilen;
  return resultVec;
};


/**
 * Returns the scalar product of vectors v0 and v1.
 *
 * @param {goog.vec.Vec4.AnyType} v0 The first vector.
 * @param {goog.vec.Vec4.AnyType} v1 The second vector.
 * @return {number} The scalar product.
 */
goog.vec.Vec4.dot = function(v0, v1) {
  return v0[0] * v1[0] + v0[1] * v1[1] + v0[2] * v1[2] + v0[3] * v1[3];
};


/**
 * Linearly interpolate from v0 to v1 according to f. The value of f should be
 * in the range [0..1] otherwise the results are undefined.
 *
 * @param {goog.vec.Vec4.AnyType} v0 The first vector.
 * @param {goog.vec.Vec4.AnyType} v1 The second vector.
 * @param {number} f The interpolation factor.
 * @param {goog.vec.Vec4.AnyType} resultVec The vector to receive the
 *     results (may be v0 or v1).
 * @return {!goog.vec.Vec4.AnyType} Return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Vec4.lerp = function(v0, v1, f, resultVec) {
  var x = v0[0], y = v0[1], z = v0[2], w = v0[3];
  resultVec[0] = (v1[0] - x) * f + x;
  resultVec[1] = (v1[1] - y) * f + y;
  resultVec[2] = (v1[2] - z) * f + z;
  resultVec[3] = (v1[3] - w) * f + w;
  return resultVec;
};


/**
 * Compares the components of vec0 with the components of another vector or
 * scalar, storing the larger values in resultVec.
 *
 * @param {goog.vec.Vec4.AnyType} vec0 The source vector.
 * @param {goog.vec.Vec4.AnyType|number} limit The limit vector or scalar.
 * @param {goog.vec.Vec4.AnyType} resultVec The vector to receive the
 *     results (may be vec0 or limit).
 * @return {!goog.vec.Vec4.AnyType} Return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Vec4.max = function(vec0, limit, resultVec) {
  if (goog.isNumber(limit)) {
    resultVec[0] = Math.max(vec0[0], limit);
    resultVec[1] = Math.max(vec0[1], limit);
    resultVec[2] = Math.max(vec0[2], limit);
    resultVec[3] = Math.max(vec0[3], limit);
  } else {
    resultVec[0] = Math.max(vec0[0], limit[0]);
    resultVec[1] = Math.max(vec0[1], limit[1]);
    resultVec[2] = Math.max(vec0[2], limit[2]);
    resultVec[3] = Math.max(vec0[3], limit[3]);
  }
  return resultVec;
};


/**
 * Compares the components of vec0 with the components of another vector or
 * scalar, storing the smaller values in resultVec.
 *
 * @param {goog.vec.Vec4.AnyType} vec0 The source vector.
 * @param {goog.vec.Vec4.AnyType|number} limit The limit vector or scalar.
 * @param {goog.vec.Vec4.AnyType} resultVec The vector to receive the
 *     results (may be vec0 or limit).
 * @return {!goog.vec.Vec4.AnyType} Return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Vec4.min = function(vec0, limit, resultVec) {
  if (goog.isNumber(limit)) {
    resultVec[0] = Math.min(vec0[0], limit);
    resultVec[1] = Math.min(vec0[1], limit);
    resultVec[2] = Math.min(vec0[2], limit);
    resultVec[3] = Math.min(vec0[3], limit);
  } else {
    resultVec[0] = Math.min(vec0[0], limit[0]);
    resultVec[1] = Math.min(vec0[1], limit[1]);
    resultVec[2] = Math.min(vec0[2], limit[2]);
    resultVec[3] = Math.min(vec0[3], limit[3]);
  }
  return resultVec;
};


/**
 * Returns true if the components of v0 are equal to the components of v1.
 *
 * @param {goog.vec.Vec4.AnyType} v0 The first vector.
 * @param {goog.vec.Vec4.AnyType} v1 The second vector.
 * @return {boolean} True if the vectors are equal, false otherwise.
 */
goog.vec.Vec4.equals = function(v0, v1) {
  return v0.length == v1.length &&
      v0[0] == v1[0] && v0[1] == v1[1] && v0[2] == v1[2] && v0[3] == v1[3];
};

// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Implements 4x4 matrices and their related functions which are
 * compatible with WebGL. The API is structured to avoid unnecessary memory
 * allocations.  The last parameter will typically be the output vector and
 * an object can be both an input and output parameter to all methods except
 * where noted. Matrix operations follow the mathematical form when multiplying
 * vectors as follows: resultVec = matrix * vec.
 *
 * The matrices are stored in column-major order.
 *
 */
goog.provide('goog.vec.Mat4');

goog.require('goog.vec');
goog.require('goog.vec.Vec3');
goog.require('goog.vec.Vec4');


/** @typedef {goog.vec.Float32} */ goog.vec.Mat4.Float32;
/** @typedef {goog.vec.Float64} */ goog.vec.Mat4.Float64;
/** @typedef {goog.vec.Number} */ goog.vec.Mat4.Number;
/** @typedef {goog.vec.AnyType} */ goog.vec.Mat4.AnyType;

// The following two types are deprecated - use the above types instead.
/** @typedef {!Float32Array} */ goog.vec.Mat4.Type;
/** @typedef {goog.vec.ArrayType} */ goog.vec.Mat4.Mat4Like;


/**
 * Creates the array representation of a 4x4 matrix of Float32.
 * The use of the array directly instead of a class reduces overhead.
 * The returned matrix is cleared to all zeros.
 *
 * @return {!goog.vec.Mat4.Float32} The new matrix.
 */
goog.vec.Mat4.createFloat32 = function() {
  return new Float32Array(16);
};


/**
 * Creates the array representation of a 4x4 matrix of Float64.
 * The returned matrix is cleared to all zeros.
 *
 * @return {!goog.vec.Mat4.Float64} The new matrix.
 */
goog.vec.Mat4.createFloat64 = function() {
  return new Float64Array(16);
};


/**
 * Creates the array representation of a 4x4 matrix of Number.
 * The returned matrix is cleared to all zeros.
 *
 * @return {!goog.vec.Mat4.Number} The new matrix.
 */
goog.vec.Mat4.createNumber = function() {
  var a = new Array(16);
  goog.vec.Mat4.setFromValues(a,
                              0, 0, 0, 0,
                              0, 0, 0, 0,
                              0, 0, 0, 0,
                              0, 0, 0, 0);
  return a;
};


/**
 * Creates the array representation of a 4x4 matrix of Float32.
 * The returned matrix is cleared to all zeros.
 *
 * @deprecated Use createFloat32.
 * @return {!goog.vec.Mat4.Type} The new matrix.
 */
goog.vec.Mat4.create = function() {
  return goog.vec.Mat4.createFloat32();
};


/**
 * Creates a 4x4 identity matrix of Float32.
 *
 * @return {!goog.vec.Mat4.Float32} The new 16 element array.
 */
goog.vec.Mat4.createFloat32Identity = function() {
  var mat = goog.vec.Mat4.createFloat32();
  mat[0] = mat[5] = mat[10] = mat[15] = 1;
  return mat;
};


/**
 * Creates a 4x4 identity matrix of Float64.
 *
 * @return {!goog.vec.Mat4.Float64} The new 16 element array.
 */
goog.vec.Mat4.createFloat64Identity = function() {
  var mat = goog.vec.Mat4.createFloat64();
  mat[0] = mat[5] = mat[10] = mat[15] = 1;
  return mat;
};


/**
 * Creates a 4x4 identity matrix of Number.
 * The returned matrix is cleared to all zeros.
 *
 * @return {!goog.vec.Mat4.Number} The new 16 element array.
 */
goog.vec.Mat4.createNumberIdentity = function() {
  var a = new Array(16);
  goog.vec.Mat4.setFromValues(a,
                              1, 0, 0, 0,
                              0, 1, 0, 0,
                              0, 0, 1, 0,
                              0, 0, 0, 1);
  return a;
};


/**
 * Creates the array representation of a 4x4 matrix of Float32.
 * The returned matrix is cleared to all zeros.
 *
 * @deprecated Use createFloat32Identity.
 * @return {!goog.vec.Mat4.Type} The new 16 element array.
 */
goog.vec.Mat4.createIdentity = function() {
  return goog.vec.Mat4.createFloat32Identity();
};


/**
 * Creates a 4x4 matrix of Float32 initialized from the given array.
 *
 * @param {goog.vec.Mat4.AnyType} matrix The array containing the
 *     matrix values in column major order.
 * @return {!goog.vec.Mat4.Float32} The new, 16 element array.
 */
goog.vec.Mat4.createFloat32FromArray = function(matrix) {
  var newMatrix = goog.vec.Mat4.createFloat32();
  goog.vec.Mat4.setFromArray(newMatrix, matrix);
  return newMatrix;
};


/**
 * Creates a 4x4 matrix of Float32 initialized from the given values.
 *
 * @param {number} v00 The values at (0, 0).
 * @param {number} v10 The values at (1, 0).
 * @param {number} v20 The values at (2, 0).
 * @param {number} v30 The values at (3, 0).
 * @param {number} v01 The values at (0, 1).
 * @param {number} v11 The values at (1, 1).
 * @param {number} v21 The values at (2, 1).
 * @param {number} v31 The values at (3, 1).
 * @param {number} v02 The values at (0, 2).
 * @param {number} v12 The values at (1, 2).
 * @param {number} v22 The values at (2, 2).
 * @param {number} v32 The values at (3, 2).
 * @param {number} v03 The values at (0, 3).
 * @param {number} v13 The values at (1, 3).
 * @param {number} v23 The values at (2, 3).
 * @param {number} v33 The values at (3, 3).
 * @return {!goog.vec.Mat4.Float32} The new, 16 element array.
 */
goog.vec.Mat4.createFloat32FromValues = function(
    v00, v10, v20, v30,
    v01, v11, v21, v31,
    v02, v12, v22, v32,
    v03, v13, v23, v33) {
  var newMatrix = goog.vec.Mat4.createFloat32();
  goog.vec.Mat4.setFromValues(
      newMatrix, v00, v10, v20, v30, v01, v11, v21, v31, v02, v12, v22, v32,
      v03, v13, v23, v33);
  return newMatrix;
};


/**
 * Creates a clone of a 4x4 matrix of Float32.
 *
 * @param {goog.vec.Mat4.Float32} matrix The source 4x4 matrix.
 * @return {!goog.vec.Mat4.Float32} The new 4x4 element matrix.
 */
goog.vec.Mat4.cloneFloat32 = goog.vec.Mat4.createFloat32FromArray;


/**
 * Creates a 4x4 matrix of Float64 initialized from the given array.
 *
 * @param {goog.vec.Mat4.AnyType} matrix The array containing the
 *     matrix values in column major order.
 * @return {!goog.vec.Mat4.Float64} The new, nine element array.
 */
goog.vec.Mat4.createFloat64FromArray = function(matrix) {
  var newMatrix = goog.vec.Mat4.createFloat64();
  goog.vec.Mat4.setFromArray(newMatrix, matrix);
  return newMatrix;
};


/**
 * Creates a 4x4 matrix of Float64 initialized from the given values.
 *
 * @param {number} v00 The values at (0, 0).
 * @param {number} v10 The values at (1, 0).
 * @param {number} v20 The values at (2, 0).
 * @param {number} v30 The values at (3, 0).
 * @param {number} v01 The values at (0, 1).
 * @param {number} v11 The values at (1, 1).
 * @param {number} v21 The values at (2, 1).
 * @param {number} v31 The values at (3, 1).
 * @param {number} v02 The values at (0, 2).
 * @param {number} v12 The values at (1, 2).
 * @param {number} v22 The values at (2, 2).
 * @param {number} v32 The values at (3, 2).
 * @param {number} v03 The values at (0, 3).
 * @param {number} v13 The values at (1, 3).
 * @param {number} v23 The values at (2, 3).
 * @param {number} v33 The values at (3, 3).
 * @return {!goog.vec.Mat4.Float64} The new, 16 element array.
 */
goog.vec.Mat4.createFloat64FromValues = function(
    v00, v10, v20, v30,
    v01, v11, v21, v31,
    v02, v12, v22, v32,
    v03, v13, v23, v33) {
  var newMatrix = goog.vec.Mat4.createFloat64();
  goog.vec.Mat4.setFromValues(
      newMatrix, v00, v10, v20, v30, v01, v11, v21, v31, v02, v12, v22, v32,
      v03, v13, v23, v33);
  return newMatrix;
};


/**
 * Creates a clone of a 4x4 matrix of Float64.
 *
 * @param {goog.vec.Mat4.Float64} matrix The source 4x4 matrix.
 * @return {!goog.vec.Mat4.Float64} The new 4x4 element matrix.
 */
goog.vec.Mat4.cloneFloat64 = goog.vec.Mat4.createFloat64FromArray;


/**
 * Creates a 4x4 matrix of Float32 initialized from the given array.
 *
 * @deprecated Use createFloat32FromArray.
 * @param {goog.vec.Mat4.Mat4Like} matrix The array containing the
 *     matrix values in column major order.
 * @return {!goog.vec.Mat4.Type} The new, nine element array.
 */
goog.vec.Mat4.createFromArray = function(matrix) {
  var newMatrix = goog.vec.Mat4.createFloat32();
  goog.vec.Mat4.setFromArray(newMatrix, matrix);
  return newMatrix;
};


/**
 * Creates a 4x4 matrix of Float32 initialized from the given values.
 *
 * @deprecated Use createFloat32FromValues.
 * @param {number} v00 The values at (0, 0).
 * @param {number} v10 The values at (1, 0).
 * @param {number} v20 The values at (2, 0).
 * @param {number} v30 The values at (3, 0).
 * @param {number} v01 The values at (0, 1).
 * @param {number} v11 The values at (1, 1).
 * @param {number} v21 The values at (2, 1).
 * @param {number} v31 The values at (3, 1).
 * @param {number} v02 The values at (0, 2).
 * @param {number} v12 The values at (1, 2).
 * @param {number} v22 The values at (2, 2).
 * @param {number} v32 The values at (3, 2).
 * @param {number} v03 The values at (0, 3).
 * @param {number} v13 The values at (1, 3).
 * @param {number} v23 The values at (2, 3).
 * @param {number} v33 The values at (3, 3).
 * @return {!goog.vec.Mat4.Type} The new, 16 element array.
 */
goog.vec.Mat4.createFromValues = function(
    v00, v10, v20, v30,
    v01, v11, v21, v31,
    v02, v12, v22, v32,
    v03, v13, v23, v33) {
  return goog.vec.Mat4.createFloat32FromValues(
      v00, v10, v20, v30, v01, v11, v21, v31, v02, v12, v22, v32,
      v03, v13, v23, v33);
};


/**
 * Creates a clone of a 4x4 matrix of Float32.
 *
 * @deprecated Use cloneFloat32.
 * @param {goog.vec.Mat4.Mat4Like} matrix The source 4x4 matrix.
 * @return {!goog.vec.Mat4.Type} The new 4x4 element matrix.
 */
goog.vec.Mat4.clone = goog.vec.Mat4.createFromArray;


/**
 * Retrieves the element at the requested row and column.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix containing the
 *     value to retrieve.
 * @param {number} row The row index.
 * @param {number} column The column index.
 * @return {number} The element value at the requested row, column indices.
 */
goog.vec.Mat4.getElement = function(mat, row, column) {
  return mat[row + column * 4];
};


/**
 * Sets the element at the requested row and column.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix to set the value on.
 * @param {number} row The row index.
 * @param {number} column The column index.
 * @param {number} value The value to set at the requested row, column.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained together.
 */
goog.vec.Mat4.setElement = function(mat, row, column, value) {
  mat[row + column * 4] = value;
  return mat;
};


/**
 * Initializes the matrix from the set of values. Note the values supplied are
 * in column major order.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix to receive the
 *     values.
 * @param {number} v00 The values at (0, 0).
 * @param {number} v10 The values at (1, 0).
 * @param {number} v20 The values at (2, 0).
 * @param {number} v30 The values at (3, 0).
 * @param {number} v01 The values at (0, 1).
 * @param {number} v11 The values at (1, 1).
 * @param {number} v21 The values at (2, 1).
 * @param {number} v31 The values at (3, 1).
 * @param {number} v02 The values at (0, 2).
 * @param {number} v12 The values at (1, 2).
 * @param {number} v22 The values at (2, 2).
 * @param {number} v32 The values at (3, 2).
 * @param {number} v03 The values at (0, 3).
 * @param {number} v13 The values at (1, 3).
 * @param {number} v23 The values at (2, 3).
 * @param {number} v33 The values at (3, 3).
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained together.
 */
goog.vec.Mat4.setFromValues = function(
    mat, v00, v10, v20, v30, v01, v11, v21, v31, v02, v12, v22, v32,
    v03, v13, v23, v33) {
  mat[0] = v00;
  mat[1] = v10;
  mat[2] = v20;
  mat[3] = v30;
  mat[4] = v01;
  mat[5] = v11;
  mat[6] = v21;
  mat[7] = v31;
  mat[8] = v02;
  mat[9] = v12;
  mat[10] = v22;
  mat[11] = v32;
  mat[12] = v03;
  mat[13] = v13;
  mat[14] = v23;
  mat[15] = v33;
  return mat;
};


/**
 * Sets the matrix from the array of values stored in column major order.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix to receive the values.
 * @param {goog.vec.Mat4.AnyType} values The column major ordered
 *     array of values to store in the matrix.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained together.
 */
goog.vec.Mat4.setFromArray = function(mat, values) {
  mat[0] = values[0];
  mat[1] = values[1];
  mat[2] = values[2];
  mat[3] = values[3];
  mat[4] = values[4];
  mat[5] = values[5];
  mat[6] = values[6];
  mat[7] = values[7];
  mat[8] = values[8];
  mat[9] = values[9];
  mat[10] = values[10];
  mat[11] = values[11];
  mat[12] = values[12];
  mat[13] = values[13];
  mat[14] = values[14];
  mat[15] = values[15];
  return mat;
};


/**
 * Sets the matrix from the array of values stored in row major order.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix to receive the values.
 * @param {goog.vec.Mat4.AnyType} values The row major ordered array of
 *     values to store in the matrix.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained together.
 */
goog.vec.Mat4.setFromRowMajorArray = function(mat, values) {
  mat[0] = values[0];
  mat[1] = values[4];
  mat[2] = values[8];
  mat[3] = values[12];

  mat[4] = values[1];
  mat[5] = values[5];
  mat[6] = values[9];
  mat[7] = values[13];

  mat[8] = values[2];
  mat[9] = values[6];
  mat[10] = values[10];
  mat[11] = values[14];

  mat[12] = values[3];
  mat[13] = values[7];
  mat[14] = values[11];
  mat[15] = values[15];

  return mat;
};


/**
 * Sets the diagonal values of the matrix from the given values.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix to receive the values.
 * @param {number} v00 The values for (0, 0).
 * @param {number} v11 The values for (1, 1).
 * @param {number} v22 The values for (2, 2).
 * @param {number} v33 The values for (3, 3).
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained together.
 */
goog.vec.Mat4.setDiagonalValues = function(mat, v00, v11, v22, v33) {
  mat[0] = v00;
  mat[5] = v11;
  mat[10] = v22;
  mat[15] = v33;
  return mat;
};


/**
 * Sets the diagonal values of the matrix from the given vector.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix to receive the values.
 * @param {goog.vec.Vec4.AnyType} vec The vector containing the values.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained together.
 */
goog.vec.Mat4.setDiagonal = function(mat, vec) {
  mat[0] = vec[0];
  mat[5] = vec[1];
  mat[10] = vec[2];
  mat[15] = vec[3];
  return mat;
};


/**
 * Gets the diagonal values of the matrix into the given vector.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix containing the values.
 * @param {goog.vec.Vec4.AnyType} vec The vector to receive the values.
 * @param {number=} opt_diagonal Which diagonal to get. A value of 0 selects the
 *     main diagonal, a positive number selects a super diagonal and a negative
 *     number selects a sub diagonal.
 * @return {goog.vec.Vec4.AnyType} return vec so that operations can be
 *     chained together.
 */
goog.vec.Mat4.getDiagonal = function(mat, vec, opt_diagonal) {
  if (!opt_diagonal) {
    // This is the most common case, so we avoid the for loop.
    vec[0] = mat[0];
    vec[1] = mat[5];
    vec[2] = mat[10];
    vec[3] = mat[15];
  } else {
    var offset = opt_diagonal > 0 ? 4 * opt_diagonal : -opt_diagonal;
    for (var i = 0; i < 4 - Math.abs(opt_diagonal); i++) {
      vec[i] = mat[offset + 5 * i];
    }
  }
  return vec;
};


/**
 * Sets the specified column with the supplied values.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix to recieve the values.
 * @param {number} column The column index to set the values on.
 * @param {number} v0 The value for row 0.
 * @param {number} v1 The value for row 1.
 * @param {number} v2 The value for row 2.
 * @param {number} v3 The value for row 3.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained together.
 */
goog.vec.Mat4.setColumnValues = function(mat, column, v0, v1, v2, v3) {
  var i = column * 4;
  mat[i] = v0;
  mat[i + 1] = v1;
  mat[i + 2] = v2;
  mat[i + 3] = v3;
  return mat;
};


/**
 * Sets the specified column with the value from the supplied vector.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix to receive the values.
 * @param {number} column The column index to set the values on.
 * @param {goog.vec.Vec4.AnyType} vec The vector of elements for the column.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained together.
 */
goog.vec.Mat4.setColumn = function(mat, column, vec) {
  var i = column * 4;
  mat[i] = vec[0];
  mat[i + 1] = vec[1];
  mat[i + 2] = vec[2];
  mat[i + 3] = vec[3];
  return mat;
};


/**
 * Retrieves the specified column from the matrix into the given vector.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix supplying the values.
 * @param {number} column The column to get the values from.
 * @param {goog.vec.Vec4.AnyType} vec The vector of elements to
 *     receive the column.
 * @return {goog.vec.Vec4.AnyType} return vec so that operations can be
 *     chained together.
 */
goog.vec.Mat4.getColumn = function(mat, column, vec) {
  var i = column * 4;
  vec[0] = mat[i];
  vec[1] = mat[i + 1];
  vec[2] = mat[i + 2];
  vec[3] = mat[i + 3];
  return vec;
};


/**
 * Sets the columns of the matrix from the given vectors.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix to receive the values.
 * @param {goog.vec.Vec4.AnyType} vec0 The values for column 0.
 * @param {goog.vec.Vec4.AnyType} vec1 The values for column 1.
 * @param {goog.vec.Vec4.AnyType} vec2 The values for column 2.
 * @param {goog.vec.Vec4.AnyType} vec3 The values for column 3.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained together.
 */
goog.vec.Mat4.setColumns = function(mat, vec0, vec1, vec2, vec3) {
  goog.vec.Mat4.setColumn(mat, 0, vec0);
  goog.vec.Mat4.setColumn(mat, 1, vec1);
  goog.vec.Mat4.setColumn(mat, 2, vec2);
  goog.vec.Mat4.setColumn(mat, 3, vec3);
  return mat;
};


/**
 * Retrieves the column values from the given matrix into the given vectors.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix supplying the columns.
 * @param {goog.vec.Vec4.AnyType} vec0 The vector to receive column 0.
 * @param {goog.vec.Vec4.AnyType} vec1 The vector to receive column 1.
 * @param {goog.vec.Vec4.AnyType} vec2 The vector to receive column 2.
 * @param {goog.vec.Vec4.AnyType} vec3 The vector to receive column 3.
 */
goog.vec.Mat4.getColumns = function(mat, vec0, vec1, vec2, vec3) {
  goog.vec.Mat4.getColumn(mat, 0, vec0);
  goog.vec.Mat4.getColumn(mat, 1, vec1);
  goog.vec.Mat4.getColumn(mat, 2, vec2);
  goog.vec.Mat4.getColumn(mat, 3, vec3);
};


/**
 * Sets the row values from the supplied values.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix to receive the values.
 * @param {number} row The index of the row to receive the values.
 * @param {number} v0 The value for column 0.
 * @param {number} v1 The value for column 1.
 * @param {number} v2 The value for column 2.
 * @param {number} v3 The value for column 3.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained together.
 */
goog.vec.Mat4.setRowValues = function(mat, row, v0, v1, v2, v3) {
  mat[row] = v0;
  mat[row + 4] = v1;
  mat[row + 8] = v2;
  mat[row + 12] = v3;
  return mat;
};


/**
 * Sets the row values from the supplied vector.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix to receive the row values.
 * @param {number} row The index of the row.
 * @param {goog.vec.Vec4.AnyType} vec The vector containing the values.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained together.
 */
goog.vec.Mat4.setRow = function(mat, row, vec) {
  mat[row] = vec[0];
  mat[row + 4] = vec[1];
  mat[row + 8] = vec[2];
  mat[row + 12] = vec[3];
  return mat;
};


/**
 * Retrieves the row values into the given vector.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix supplying the values.
 * @param {number} row The index of the row supplying the values.
 * @param {goog.vec.Vec4.AnyType} vec The vector to receive the row.
 * @return {goog.vec.Vec4.AnyType} return vec so that operations can be
 *     chained together.
 */
goog.vec.Mat4.getRow = function(mat, row, vec) {
  vec[0] = mat[row];
  vec[1] = mat[row + 4];
  vec[2] = mat[row + 8];
  vec[3] = mat[row + 12];
  return vec;
};


/**
 * Sets the rows of the matrix from the supplied vectors.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix to receive the values.
 * @param {goog.vec.Vec4.AnyType} vec0 The values for row 0.
 * @param {goog.vec.Vec4.AnyType} vec1 The values for row 1.
 * @param {goog.vec.Vec4.AnyType} vec2 The values for row 2.
 * @param {goog.vec.Vec4.AnyType} vec3 The values for row 3.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained together.
 */
goog.vec.Mat4.setRows = function(mat, vec0, vec1, vec2, vec3) {
  goog.vec.Mat4.setRow(mat, 0, vec0);
  goog.vec.Mat4.setRow(mat, 1, vec1);
  goog.vec.Mat4.setRow(mat, 2, vec2);
  goog.vec.Mat4.setRow(mat, 3, vec3);
  return mat;
};


/**
 * Retrieves the rows of the matrix into the supplied vectors.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix to supply the values.
 * @param {goog.vec.Vec4.AnyType} vec0 The vector to receive row 0.
 * @param {goog.vec.Vec4.AnyType} vec1 The vector to receive row 1.
 * @param {goog.vec.Vec4.AnyType} vec2 The vector to receive row 2.
 * @param {goog.vec.Vec4.AnyType} vec3 The vector to receive row 3.
 */
goog.vec.Mat4.getRows = function(mat, vec0, vec1, vec2, vec3) {
  goog.vec.Mat4.getRow(mat, 0, vec0);
  goog.vec.Mat4.getRow(mat, 1, vec1);
  goog.vec.Mat4.getRow(mat, 2, vec2);
  goog.vec.Mat4.getRow(mat, 3, vec3);
};


/**
 * Makes the given 4x4 matrix the zero matrix.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @return {!goog.vec.Mat4.AnyType} return mat so operations can be chained.
 */
goog.vec.Mat4.makeZero = function(mat) {
  mat[0] = 0;
  mat[1] = 0;
  mat[2] = 0;
  mat[3] = 0;
  mat[4] = 0;
  mat[5] = 0;
  mat[6] = 0;
  mat[7] = 0;
  mat[8] = 0;
  mat[9] = 0;
  mat[10] = 0;
  mat[11] = 0;
  mat[12] = 0;
  mat[13] = 0;
  mat[14] = 0;
  mat[15] = 0;
  return mat;
};


/**
 * Makes the given 4x4 matrix the identity matrix.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @return {goog.vec.Mat4.AnyType} return mat so operations can be chained.
 */
goog.vec.Mat4.makeIdentity = function(mat) {
  mat[0] = 1;
  mat[1] = 0;
  mat[2] = 0;
  mat[3] = 0;
  mat[4] = 0;
  mat[5] = 1;
  mat[6] = 0;
  mat[7] = 0;
  mat[8] = 0;
  mat[9] = 0;
  mat[10] = 1;
  mat[11] = 0;
  mat[12] = 0;
  mat[13] = 0;
  mat[14] = 0;
  mat[15] = 1;
  return mat;
};


/**
 * Performs a per-component addition of the matrix mat0 and mat1, storing
 * the result into resultMat.
 *
 * @param {goog.vec.Mat4.AnyType} mat0 The first addend.
 * @param {goog.vec.Mat4.AnyType} mat1 The second addend.
 * @param {goog.vec.Mat4.AnyType} resultMat The matrix to
 *     receive the results (may be either mat0 or mat1).
 * @return {goog.vec.Mat4.AnyType} return resultMat so that operations can be
 *     chained together.
 */
goog.vec.Mat4.addMat = function(mat0, mat1, resultMat) {
  resultMat[0] = mat0[0] + mat1[0];
  resultMat[1] = mat0[1] + mat1[1];
  resultMat[2] = mat0[2] + mat1[2];
  resultMat[3] = mat0[3] + mat1[3];
  resultMat[4] = mat0[4] + mat1[4];
  resultMat[5] = mat0[5] + mat1[5];
  resultMat[6] = mat0[6] + mat1[6];
  resultMat[7] = mat0[7] + mat1[7];
  resultMat[8] = mat0[8] + mat1[8];
  resultMat[9] = mat0[9] + mat1[9];
  resultMat[10] = mat0[10] + mat1[10];
  resultMat[11] = mat0[11] + mat1[11];
  resultMat[12] = mat0[12] + mat1[12];
  resultMat[13] = mat0[13] + mat1[13];
  resultMat[14] = mat0[14] + mat1[14];
  resultMat[15] = mat0[15] + mat1[15];
  return resultMat;
};


/**
 * Performs a per-component subtraction of the matrix mat0 and mat1,
 * storing the result into resultMat.
 *
 * @param {goog.vec.Mat4.AnyType} mat0 The minuend.
 * @param {goog.vec.Mat4.AnyType} mat1 The subtrahend.
 * @param {goog.vec.Mat4.AnyType} resultMat The matrix to receive
 *     the results (may be either mat0 or mat1).
 * @return {goog.vec.Mat4.AnyType} return resultMat so that operations can be
 *     chained together.
 */
goog.vec.Mat4.subMat = function(mat0, mat1, resultMat) {
  resultMat[0] = mat0[0] - mat1[0];
  resultMat[1] = mat0[1] - mat1[1];
  resultMat[2] = mat0[2] - mat1[2];
  resultMat[3] = mat0[3] - mat1[3];
  resultMat[4] = mat0[4] - mat1[4];
  resultMat[5] = mat0[5] - mat1[5];
  resultMat[6] = mat0[6] - mat1[6];
  resultMat[7] = mat0[7] - mat1[7];
  resultMat[8] = mat0[8] - mat1[8];
  resultMat[9] = mat0[9] - mat1[9];
  resultMat[10] = mat0[10] - mat1[10];
  resultMat[11] = mat0[11] - mat1[11];
  resultMat[12] = mat0[12] - mat1[12];
  resultMat[13] = mat0[13] - mat1[13];
  resultMat[14] = mat0[14] - mat1[14];
  resultMat[15] = mat0[15] - mat1[15];
  return resultMat;
};


/**
 * Multiplies matrix mat with the given scalar, storing the result
 * into resultMat.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @param {number} scalar The scalar value to multiply to each element of mat.
 * @param {goog.vec.Mat4.AnyType} resultMat The matrix to receive
 *     the results (may be mat).
 * @return {goog.vec.Mat4.AnyType} return resultMat so that operations can be
 *     chained together.
 */
goog.vec.Mat4.multScalar = function(mat, scalar, resultMat) {
  resultMat[0] = mat[0] * scalar;
  resultMat[1] = mat[1] * scalar;
  resultMat[2] = mat[2] * scalar;
  resultMat[3] = mat[3] * scalar;
  resultMat[4] = mat[4] * scalar;
  resultMat[5] = mat[5] * scalar;
  resultMat[6] = mat[6] * scalar;
  resultMat[7] = mat[7] * scalar;
  resultMat[8] = mat[8] * scalar;
  resultMat[9] = mat[9] * scalar;
  resultMat[10] = mat[10] * scalar;
  resultMat[11] = mat[11] * scalar;
  resultMat[12] = mat[12] * scalar;
  resultMat[13] = mat[13] * scalar;
  resultMat[14] = mat[14] * scalar;
  resultMat[15] = mat[15] * scalar;
  return resultMat;
};


/**
 * Multiplies the two matrices mat0 and mat1 using matrix multiplication,
 * storing the result into resultMat.
 *
 * @param {goog.vec.Mat4.AnyType} mat0 The first (left hand) matrix.
 * @param {goog.vec.Mat4.AnyType} mat1 The second (right hand) matrix.
 * @param {goog.vec.Mat4.AnyType} resultMat The matrix to receive
 *     the results (may be either mat0 or mat1).
 * @return {goog.vec.Mat4.AnyType} return resultMat so that operations can be
 *     chained together.
 */
goog.vec.Mat4.multMat = function(mat0, mat1, resultMat) {
  var a00 = mat0[0], a10 = mat0[1], a20 = mat0[2], a30 = mat0[3];
  var a01 = mat0[4], a11 = mat0[5], a21 = mat0[6], a31 = mat0[7];
  var a02 = mat0[8], a12 = mat0[9], a22 = mat0[10], a32 = mat0[11];
  var a03 = mat0[12], a13 = mat0[13], a23 = mat0[14], a33 = mat0[15];

  var b00 = mat1[0], b10 = mat1[1], b20 = mat1[2], b30 = mat1[3];
  var b01 = mat1[4], b11 = mat1[5], b21 = mat1[6], b31 = mat1[7];
  var b02 = mat1[8], b12 = mat1[9], b22 = mat1[10], b32 = mat1[11];
  var b03 = mat1[12], b13 = mat1[13], b23 = mat1[14], b33 = mat1[15];

  resultMat[0] = a00 * b00 + a01 * b10 + a02 * b20 + a03 * b30;
  resultMat[1] = a10 * b00 + a11 * b10 + a12 * b20 + a13 * b30;
  resultMat[2] = a20 * b00 + a21 * b10 + a22 * b20 + a23 * b30;
  resultMat[3] = a30 * b00 + a31 * b10 + a32 * b20 + a33 * b30;

  resultMat[4] = a00 * b01 + a01 * b11 + a02 * b21 + a03 * b31;
  resultMat[5] = a10 * b01 + a11 * b11 + a12 * b21 + a13 * b31;
  resultMat[6] = a20 * b01 + a21 * b11 + a22 * b21 + a23 * b31;
  resultMat[7] = a30 * b01 + a31 * b11 + a32 * b21 + a33 * b31;

  resultMat[8] = a00 * b02 + a01 * b12 + a02 * b22 + a03 * b32;
  resultMat[9] = a10 * b02 + a11 * b12 + a12 * b22 + a13 * b32;
  resultMat[10] = a20 * b02 + a21 * b12 + a22 * b22 + a23 * b32;
  resultMat[11] = a30 * b02 + a31 * b12 + a32 * b22 + a33 * b32;

  resultMat[12] = a00 * b03 + a01 * b13 + a02 * b23 + a03 * b33;
  resultMat[13] = a10 * b03 + a11 * b13 + a12 * b23 + a13 * b33;
  resultMat[14] = a20 * b03 + a21 * b13 + a22 * b23 + a23 * b33;
  resultMat[15] = a30 * b03 + a31 * b13 + a32 * b23 + a33 * b33;
  return resultMat;
};


/**
 * Transposes the given matrix mat storing the result into resultMat.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix to transpose.
 * @param {goog.vec.Mat4.AnyType} resultMat The matrix to receive
 *     the results (may be mat).
 * @return {goog.vec.Mat4.AnyType} return resultMat so that operations can be
 *     chained together.
 */
goog.vec.Mat4.transpose = function(mat, resultMat) {
  if (resultMat == mat) {
    var a10 = mat[1], a20 = mat[2], a30 = mat[3];
    var a21 = mat[6], a31 = mat[7];
    var a32 = mat[11];
    resultMat[1] = mat[4];
    resultMat[2] = mat[8];
    resultMat[3] = mat[12];
    resultMat[4] = a10;
    resultMat[6] = mat[9];
    resultMat[7] = mat[13];
    resultMat[8] = a20;
    resultMat[9] = a21;
    resultMat[11] = mat[14];
    resultMat[12] = a30;
    resultMat[13] = a31;
    resultMat[14] = a32;
  } else {
    resultMat[0] = mat[0];
    resultMat[1] = mat[4];
    resultMat[2] = mat[8];
    resultMat[3] = mat[12];

    resultMat[4] = mat[1];
    resultMat[5] = mat[5];
    resultMat[6] = mat[9];
    resultMat[7] = mat[13];

    resultMat[8] = mat[2];
    resultMat[9] = mat[6];
    resultMat[10] = mat[10];
    resultMat[11] = mat[14];

    resultMat[12] = mat[3];
    resultMat[13] = mat[7];
    resultMat[14] = mat[11];
    resultMat[15] = mat[15];
  }
  return resultMat;
};


/**
 * Computes the determinant of the matrix.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix to compute the matrix for.
 * @return {number} The determinant of the matrix.
 */
goog.vec.Mat4.determinant = function(mat) {
  var m00 = mat[0], m10 = mat[1], m20 = mat[2], m30 = mat[3];
  var m01 = mat[4], m11 = mat[5], m21 = mat[6], m31 = mat[7];
  var m02 = mat[8], m12 = mat[9], m22 = mat[10], m32 = mat[11];
  var m03 = mat[12], m13 = mat[13], m23 = mat[14], m33 = mat[15];

  var a0 = m00 * m11 - m10 * m01;
  var a1 = m00 * m21 - m20 * m01;
  var a2 = m00 * m31 - m30 * m01;
  var a3 = m10 * m21 - m20 * m11;
  var a4 = m10 * m31 - m30 * m11;
  var a5 = m20 * m31 - m30 * m21;
  var b0 = m02 * m13 - m12 * m03;
  var b1 = m02 * m23 - m22 * m03;
  var b2 = m02 * m33 - m32 * m03;
  var b3 = m12 * m23 - m22 * m13;
  var b4 = m12 * m33 - m32 * m13;
  var b5 = m22 * m33 - m32 * m23;

  return a0 * b5 - a1 * b4 + a2 * b3 + a3 * b2 - a4 * b1 + a5 * b0;
};


/**
 * Computes the inverse of mat storing the result into resultMat. If the
 * inverse is defined, this function returns true, false otherwise.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix to invert.
 * @param {goog.vec.Mat4.AnyType} resultMat The matrix to receive
 *     the result (may be mat).
 * @return {boolean} True if the inverse is defined. If false is returned,
 *     resultMat is not modified.
 */
goog.vec.Mat4.invert = function(mat, resultMat) {
  var m00 = mat[0], m10 = mat[1], m20 = mat[2], m30 = mat[3];
  var m01 = mat[4], m11 = mat[5], m21 = mat[6], m31 = mat[7];
  var m02 = mat[8], m12 = mat[9], m22 = mat[10], m32 = mat[11];
  var m03 = mat[12], m13 = mat[13], m23 = mat[14], m33 = mat[15];

  var a0 = m00 * m11 - m10 * m01;
  var a1 = m00 * m21 - m20 * m01;
  var a2 = m00 * m31 - m30 * m01;
  var a3 = m10 * m21 - m20 * m11;
  var a4 = m10 * m31 - m30 * m11;
  var a5 = m20 * m31 - m30 * m21;
  var b0 = m02 * m13 - m12 * m03;
  var b1 = m02 * m23 - m22 * m03;
  var b2 = m02 * m33 - m32 * m03;
  var b3 = m12 * m23 - m22 * m13;
  var b4 = m12 * m33 - m32 * m13;
  var b5 = m22 * m33 - m32 * m23;

  var det = a0 * b5 - a1 * b4 + a2 * b3 + a3 * b2 - a4 * b1 + a5 * b0;
  if (det == 0) {
    return false;
  }

  var idet = 1.0 / det;
  resultMat[0] = (m11 * b5 - m21 * b4 + m31 * b3) * idet;
  resultMat[1] = (-m10 * b5 + m20 * b4 - m30 * b3) * idet;
  resultMat[2] = (m13 * a5 - m23 * a4 + m33 * a3) * idet;
  resultMat[3] = (-m12 * a5 + m22 * a4 - m32 * a3) * idet;
  resultMat[4] = (-m01 * b5 + m21 * b2 - m31 * b1) * idet;
  resultMat[5] = (m00 * b5 - m20 * b2 + m30 * b1) * idet;
  resultMat[6] = (-m03 * a5 + m23 * a2 - m33 * a1) * idet;
  resultMat[7] = (m02 * a5 - m22 * a2 + m32 * a1) * idet;
  resultMat[8] = (m01 * b4 - m11 * b2 + m31 * b0) * idet;
  resultMat[9] = (-m00 * b4 + m10 * b2 - m30 * b0) * idet;
  resultMat[10] = (m03 * a4 - m13 * a2 + m33 * a0) * idet;
  resultMat[11] = (-m02 * a4 + m12 * a2 - m32 * a0) * idet;
  resultMat[12] = (-m01 * b3 + m11 * b1 - m21 * b0) * idet;
  resultMat[13] = (m00 * b3 - m10 * b1 + m20 * b0) * idet;
  resultMat[14] = (-m03 * a3 + m13 * a1 - m23 * a0) * idet;
  resultMat[15] = (m02 * a3 - m12 * a1 + m22 * a0) * idet;
  return true;
};


/**
 * Returns true if the components of mat0 are equal to the components of mat1.
 *
 * @param {goog.vec.Mat4.AnyType} mat0 The first matrix.
 * @param {goog.vec.Mat4.AnyType} mat1 The second matrix.
 * @return {boolean} True if the the two matrices are equivalent.
 */
goog.vec.Mat4.equals = function(mat0, mat1) {
  return mat0.length == mat1.length &&
      mat0[0] == mat1[0] &&
      mat0[1] == mat1[1] &&
      mat0[2] == mat1[2] &&
      mat0[3] == mat1[3] &&
      mat0[4] == mat1[4] &&
      mat0[5] == mat1[5] &&
      mat0[6] == mat1[6] &&
      mat0[7] == mat1[7] &&
      mat0[8] == mat1[8] &&
      mat0[9] == mat1[9] &&
      mat0[10] == mat1[10] &&
      mat0[11] == mat1[11] &&
      mat0[12] == mat1[12] &&
      mat0[13] == mat1[13] &&
      mat0[14] == mat1[14] &&
      mat0[15] == mat1[15];
};


/**
 * Transforms the given vector with the given matrix storing the resulting,
 * transformed vector into resultVec. The input vector is multiplied against the
 * upper 3x4 matrix omitting the projective component.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix supplying the transformation.
 * @param {goog.vec.Vec3.AnyType} vec The 3 element vector to transform.
 * @param {goog.vec.Vec3.AnyType} resultVec The 3 element vector to
 *     receive the results (may be vec).
 * @return {goog.vec.Vec3.AnyType} return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Mat4.multVec3 = function(mat, vec, resultVec) {
  var x = vec[0], y = vec[1], z = vec[2];
  resultVec[0] = x * mat[0] + y * mat[4] + z * mat[8] + mat[12];
  resultVec[1] = x * mat[1] + y * mat[5] + z * mat[9] + mat[13];
  resultVec[2] = x * mat[2] + y * mat[6] + z * mat[10] + mat[14];
  return resultVec;
};


/**
 * Transforms the given vector with the given matrix storing the resulting,
 * transformed vector into resultVec. The input vector is multiplied against the
 * upper 3x3 matrix omitting the projective component and translation
 * components.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix supplying the transformation.
 * @param {goog.vec.Vec3.AnyType} vec The 3 element vector to transform.
 * @param {goog.vec.Vec3.AnyType} resultVec The 3 element vector to
 *     receive the results (may be vec).
 * @return {goog.vec.Vec3.AnyType} return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Mat4.multVec3NoTranslate = function(mat, vec, resultVec) {
  var x = vec[0], y = vec[1], z = vec[2];
  resultVec[0] = x * mat[0] + y * mat[4] + z * mat[8];
  resultVec[1] = x * mat[1] + y * mat[5] + z * mat[9];
  resultVec[2] = x * mat[2] + y * mat[6] + z * mat[10];
  return resultVec;
};


/**
 * Transforms the given vector with the given matrix storing the resulting,
 * transformed vector into resultVec. The input vector is multiplied against the
 * full 4x4 matrix with the homogeneous divide applied to reduce the 4 element
 * vector to a 3 element vector.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix supplying the transformation.
 * @param {goog.vec.Vec3.AnyType} vec The 3 element vector to transform.
 * @param {goog.vec.Vec3.AnyType} resultVec The 3 element vector
 *     to receive the results (may be vec).
 * @return {goog.vec.Vec3.AnyType} return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Mat4.multVec3Projective = function(mat, vec, resultVec) {
  var x = vec[0], y = vec[1], z = vec[2];
  var invw = 1 / (x * mat[3] + y * mat[7] + z * mat[11] + mat[15]);
  resultVec[0] = (x * mat[0] + y * mat[4] + z * mat[8] + mat[12]) * invw;
  resultVec[1] = (x * mat[1] + y * mat[5] + z * mat[9] + mat[13]) * invw;
  resultVec[2] = (x * mat[2] + y * mat[6] + z * mat[10] + mat[14]) * invw;
  return resultVec;
};


/**
 * Transforms the given vector with the given matrix storing the resulting,
 * transformed vector into resultVec.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix supplying the transformation.
 * @param {goog.vec.Vec4.AnyType} vec The vector to transform.
 * @param {goog.vec.Vec4.AnyType} resultVec The vector to
 *     receive the results (may be vec).
 * @return {goog.vec.Vec4.AnyType} return resultVec so that operations can be
 *     chained together.
 */
goog.vec.Mat4.multVec4 = function(mat, vec, resultVec) {
  var x = vec[0], y = vec[1], z = vec[2], w = vec[3];
  resultVec[0] = x * mat[0] + y * mat[4] + z * mat[8] + w * mat[12];
  resultVec[1] = x * mat[1] + y * mat[5] + z * mat[9] + w * mat[13];
  resultVec[2] = x * mat[2] + y * mat[6] + z * mat[10] + w * mat[14];
  resultVec[3] = x * mat[3] + y * mat[7] + z * mat[11] + w * mat[15];
  return resultVec;
};


/**
 * Makes the given 4x4 matrix a translation matrix with x, y and z
 * translation factors.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @param {number} x The translation along the x axis.
 * @param {number} y The translation along the y axis.
 * @param {number} z The translation along the z axis.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained.
 */
goog.vec.Mat4.makeTranslate = function(mat, x, y, z) {
  goog.vec.Mat4.makeIdentity(mat);
  return goog.vec.Mat4.setColumnValues(mat, 3, x, y, z, 1);
};


/**
 * Makes the given 4x4 matrix as a scale matrix with x, y and z scale factors.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @param {number} x The scale along the x axis.
 * @param {number} y The scale along the y axis.
 * @param {number} z The scale along the z axis.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained.
 */
goog.vec.Mat4.makeScale = function(mat, x, y, z) {
  goog.vec.Mat4.makeIdentity(mat);
  return goog.vec.Mat4.setDiagonalValues(mat, x, y, z, 1);
};


/**
 * Makes the given 4x4 matrix a rotation matrix with the given rotation
 * angle about the axis defined by the vector (ax, ay, az).
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @param {number} angle The rotation angle in radians.
 * @param {number} ax The x component of the rotation axis.
 * @param {number} ay The y component of the rotation axis.
 * @param {number} az The z component of the rotation axis.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained.
 */
goog.vec.Mat4.makeRotate = function(mat, angle, ax, ay, az) {
  var c = Math.cos(angle);
  var d = 1 - c;
  var s = Math.sin(angle);

  return goog.vec.Mat4.setFromValues(mat,
      ax * ax * d + c,
      ax * ay * d + az * s,
      ax * az * d - ay * s,
      0,

      ax * ay * d - az * s,
      ay * ay * d + c,
      ay * az * d + ax * s,
      0,

      ax * az * d + ay * s,
      ay * az * d - ax * s,
      az * az * d + c,
      0,

      0, 0, 0, 1);
};


/**
 * Makes the given 4x4 matrix a rotation matrix with the given rotation
 * angle about the X axis.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @param {number} angle The rotation angle in radians.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained.
 */
goog.vec.Mat4.makeRotateX = function(mat, angle) {
  var c = Math.cos(angle);
  var s = Math.sin(angle);
  return goog.vec.Mat4.setFromValues(
      mat, 1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1);
};


/**
 * Makes the given 4x4 matrix a rotation matrix with the given rotation
 * angle about the Y axis.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @param {number} angle The rotation angle in radians.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained.
 */
goog.vec.Mat4.makeRotateY = function(mat, angle) {
  var c = Math.cos(angle);
  var s = Math.sin(angle);
  return goog.vec.Mat4.setFromValues(
      mat, c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1);
};


/**
 * Makes the given 4x4 matrix a rotation matrix with the given rotation
 * angle about the Z axis.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @param {number} angle The rotation angle in radians.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained.
 */
goog.vec.Mat4.makeRotateZ = function(mat, angle) {
  var c = Math.cos(angle);
  var s = Math.sin(angle);
  return goog.vec.Mat4.setFromValues(
      mat, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1);
};


/**
 * Makes the given 4x4 matrix a perspective projection matrix.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @param {number} left The coordinate of the left clipping plane.
 * @param {number} right The coordinate of the right clipping plane.
 * @param {number} bottom The coordinate of the bottom clipping plane.
 * @param {number} top The coordinate of the top clipping plane.
 * @param {number} near The distance to the near clipping plane.
 * @param {number} far The distance to the far clipping plane.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained.
 */
goog.vec.Mat4.makeFrustum = function(mat, left, right, bottom, top, near, far) {
  var x = (2 * near) / (right - left);
  var y = (2 * near) / (top - bottom);
  var a = (right + left) / (right - left);
  var b = (top + bottom) / (top - bottom);
  var c = -(far + near) / (far - near);
  var d = -(2 * far * near) / (far - near);

  return goog.vec.Mat4.setFromValues(mat,
      x, 0, 0, 0,
      0, y, 0, 0,
      a, b, c, -1,
      0, 0, d, 0
  );
};


/**
 * Makse the given 4x4 matrix  perspective projection matrix given a
 * field of view and aspect ratio.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @param {number} fovy The field of view along the y (vertical) axis in
 *     radians.
 * @param {number} aspect The x (width) to y (height) aspect ratio.
 * @param {number} near The distance to the near clipping plane.
 * @param {number} far The distance to the far clipping plane.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained.
 */
goog.vec.Mat4.makePerspective = function(mat, fovy, aspect, near, far) {
  var angle = fovy / 2;
  var dz = far - near;
  var sinAngle = Math.sin(angle);
  if (dz == 0 || sinAngle == 0 || aspect == 0) {
    return mat;
  }

  var cot = Math.cos(angle) / sinAngle;
  return goog.vec.Mat4.setFromValues(mat,
      cot / aspect, 0, 0, 0,
      0, cot, 0, 0,
      0, 0, -(far + near) / dz, -1,
      0, 0, -(2 * near * far) / dz, 0
  );
};


/**
 * Makes the given 4x4 matrix an orthographic projection matrix.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @param {number} left The coordinate of the left clipping plane.
 * @param {number} right The coordinate of the right clipping plane.
 * @param {number} bottom The coordinate of the bottom clipping plane.
 * @param {number} top The coordinate of the top clipping plane.
 * @param {number} near The distance to the near clipping plane.
 * @param {number} far The distance to the far clipping plane.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained.
 */
goog.vec.Mat4.makeOrtho = function(mat, left, right, bottom, top, near, far) {
  var x = 2 / (right - left);
  var y = 2 / (top - bottom);
  var z = -2 / (far - near);
  var a = -(right + left) / (right - left);
  var b = -(top + bottom) / (top - bottom);
  var c = -(far + near) / (far - near);

  return goog.vec.Mat4.setFromValues(mat,
      x, 0, 0, 0,
      0, y, 0, 0,
      0, 0, z, 0,
      a, b, c, 1
  );
};


/**
 * Makes the given 4x4 matrix a modelview matrix of a camera so that
 * the camera is 'looking at' the given center point.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @param {goog.vec.Vec3.AnyType} eyePt The position of the eye point
 *     (camera origin).
 * @param {goog.vec.Vec3.AnyType} centerPt The point to aim the camera at.
 * @param {goog.vec.Vec3.AnyType} worldUpVec The vector that identifies
 *     the up direction for the camera.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained.
 */
goog.vec.Mat4.makeLookAt = function(mat, eyePt, centerPt, worldUpVec) {
  // Compute the direction vector from the eye point to the center point and
  // normalize.
  var fwdVec = goog.vec.Mat4.tmpVec4_[0];
  goog.vec.Vec3.subtract(centerPt, eyePt, fwdVec);
  goog.vec.Vec3.normalize(fwdVec, fwdVec);
  fwdVec[3] = 0;

  // Compute the side vector from the forward vector and the input up vector.
  var sideVec = goog.vec.Mat4.tmpVec4_[1];
  goog.vec.Vec3.cross(fwdVec, worldUpVec, sideVec);
  goog.vec.Vec3.normalize(sideVec, sideVec);
  sideVec[3] = 0;

  // Now the up vector to form the orthonormal basis.
  var upVec = goog.vec.Mat4.tmpVec4_[2];
  goog.vec.Vec3.cross(sideVec, fwdVec, upVec);
  goog.vec.Vec3.normalize(upVec, upVec);
  upVec[3] = 0;

  // Update the view matrix with the new orthonormal basis and position the
  // camera at the given eye point.
  goog.vec.Vec3.negate(fwdVec, fwdVec);
  goog.vec.Mat4.setRow(mat, 0, sideVec);
  goog.vec.Mat4.setRow(mat, 1, upVec);
  goog.vec.Mat4.setRow(mat, 2, fwdVec);
  goog.vec.Mat4.setRowValues(mat, 3, 0, 0, 0, 1);
  goog.vec.Mat4.translate(
      mat, -eyePt[0], -eyePt[1], -eyePt[2]);

  return mat;
};


/**
 * Decomposes a matrix into the lookAt vectors eyePt, fwdVec and worldUpVec.
 * The matrix represents the modelview matrix of a camera. It is the inverse
 * of lookAt except for the output of the fwdVec instead of centerPt.
 * The centerPt itself cannot be recovered from a modelview matrix.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @param {goog.vec.Vec3.AnyType} eyePt The position of the eye point
 *     (camera origin).
 * @param {goog.vec.Vec3.AnyType} fwdVec The vector describing where
 *     the camera points to.
 * @param {goog.vec.Vec3.AnyType} worldUpVec The vector that
 *     identifies the up direction for the camera.
 * @return {boolean} True if the method succeeds, false otherwise.
 *     The method can only fail if the inverse of viewMatrix is not defined.
 */
goog.vec.Mat4.toLookAt = function(mat, eyePt, fwdVec, worldUpVec) {
  // Get eye of the camera.
  var matInverse = goog.vec.Mat4.tmpMat4_[0];
  if (!goog.vec.Mat4.invert(mat, matInverse)) {
    // The input matrix does not have a valid inverse.
    return false;
  }

  if (eyePt) {
    eyePt[0] = matInverse[12];
    eyePt[1] = matInverse[13];
    eyePt[2] = matInverse[14];
  }

  // Get forward vector from the definition of lookAt.
  if (fwdVec || worldUpVec) {
    if (!fwdVec) {
      fwdVec = goog.vec.Mat4.tmpVec3_[0];
    }
    fwdVec[0] = -mat[2];
    fwdVec[1] = -mat[6];
    fwdVec[2] = -mat[10];
    // Normalize forward vector.
    goog.vec.Vec3.normalize(fwdVec, fwdVec);
  }

  if (worldUpVec) {
    // Get side vector from the definition of gluLookAt.
    var side = goog.vec.Mat4.tmpVec3_[1];
    side[0] = mat[0];
    side[1] = mat[4];
    side[2] = mat[8];
    // Compute up vector as a up = side x forward.
    goog.vec.Vec3.cross(side, fwdVec, worldUpVec);
    // Normalize up vector.
    goog.vec.Vec3.normalize(worldUpVec, worldUpVec);
  }
  return true;
};


/**
 * Makes the given 4x4 matrix a rotation matrix given Euler angles using
 * the ZXZ convention.
 * Given the euler angles [theta1, theta2, theta3], the rotation is defined as
 * rotation = rotation_z(theta1) * rotation_x(theta2) * rotation_z(theta3),
 * with theta1 in [0, 2 * pi], theta2 in [0, pi] and theta3 in [0, 2 * pi].
 * rotation_x(theta) means rotation around the X axis of theta radians,
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @param {number} theta1 The angle of rotation around the Z axis in radians.
 * @param {number} theta2 The angle of rotation around the X axis in radians.
 * @param {number} theta3 The angle of rotation around the Z axis in radians.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained.
 */
goog.vec.Mat4.makeEulerZXZ = function(mat, theta1, theta2, theta3) {
  var c1 = Math.cos(theta1);
  var s1 = Math.sin(theta1);

  var c2 = Math.cos(theta2);
  var s2 = Math.sin(theta2);

  var c3 = Math.cos(theta3);
  var s3 = Math.sin(theta3);

  mat[0] = c1 * c3 - c2 * s1 * s3;
  mat[1] = c2 * c1 * s3 + c3 * s1;
  mat[2] = s3 * s2;
  mat[3] = 0;

  mat[4] = -c1 * s3 - c3 * c2 * s1;
  mat[5] = c1 * c2 * c3 - s1 * s3;
  mat[6] = c3 * s2;
  mat[7] = 0;

  mat[8] = s2 * s1;
  mat[9] = -c1 * s2;
  mat[10] = c2;
  mat[11] = 0;

  mat[12] = 0;
  mat[13] = 0;
  mat[14] = 0;
  mat[15] = 1;

  return mat;
};


/**
 * Decomposes a rotation matrix into Euler angles using the ZXZ convention so
 * that rotation = rotation_z(theta1) * rotation_x(theta2) * rotation_z(theta3),
 * with theta1 in [0, 2 * pi], theta2 in [0, pi] and theta3 in [0, 2 * pi].
 * rotation_x(theta) means rotation around the X axis of theta radians.
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @param {goog.vec.Vec3.AnyType} euler The ZXZ Euler angles in
 *     radians as [theta1, theta2, theta3].
 * @param {boolean=} opt_theta2IsNegative Whether theta2 is in [-pi, 0] instead
 *     of the default [0, pi].
 * @return {goog.vec.Vec4.AnyType} return euler so that operations can be
 *     chained together.
 */
goog.vec.Mat4.toEulerZXZ = function(mat, euler, opt_theta2IsNegative) {
  // There is an ambiguity in the sign of sinTheta2 because of the sqrt.
  var sinTheta2 = Math.sqrt(mat[2] * mat[2] + mat[6] * mat[6]);

  // By default we explicitely constrain theta2 to be in [0, pi],
  // so sinTheta2 is always positive. We can change the behavior and specify
  // theta2 to be negative in [-pi, 0] with opt_Theta2IsNegative.
  var signTheta2 = opt_theta2IsNegative ? -1 : 1;

  if (sinTheta2 > goog.vec.EPSILON) {
    euler[2] = Math.atan2(mat[2] * signTheta2, mat[6] * signTheta2);
    euler[1] = Math.atan2(sinTheta2 * signTheta2, mat[10]);
    euler[0] = Math.atan2(mat[8] * signTheta2, -mat[9] * signTheta2);
  } else {
    // There is also an arbitrary choice for theta1 = 0 or theta2 = 0 here.
    // We assume theta1 = 0 as some applications do not allow the camera to roll
    // (i.e. have theta1 != 0).
    euler[0] = 0;
    euler[1] = Math.atan2(sinTheta2 * signTheta2, mat[10]);
    euler[2] = Math.atan2(mat[1], mat[0]);
  }

  // Atan2 outputs angles in [-pi, pi] so we bring them back to [0, 2 * pi].
  euler[0] = (euler[0] + Math.PI * 2) % (Math.PI * 2);
  euler[2] = (euler[2] + Math.PI * 2) % (Math.PI * 2);
  // For theta2 we want the angle to be in [0, pi] or [-pi, 0] depending on
  // signTheta2.
  euler[1] = ((euler[1] * signTheta2 + Math.PI * 2) % (Math.PI * 2)) *
      signTheta2;

  return euler;
};


/**
 * Translates the given matrix by x,y,z.  Equvialent to:
 * goog.vec.Mat4.multMat(
 *     mat,
 *     goog.vec.Mat4.makeTranslate(goog.vec.Mat4.create(), x, y, z),
 *     mat);
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @param {number} x The translation along the x axis.
 * @param {number} y The translation along the y axis.
 * @param {number} z The translation along the z axis.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained.
 */
goog.vec.Mat4.translate = function(mat, x, y, z) {
  return goog.vec.Mat4.setColumnValues(
      mat, 3,
      mat[0] * x + mat[4] * y + mat[8] * z + mat[12],
      mat[1] * x + mat[5] * y + mat[9] * z + mat[13],
      mat[2] * x + mat[6] * y + mat[10] * z + mat[14],
      mat[3] * x + mat[7] * y + mat[11] * z + mat[15]);
};


/**
 * Scales the given matrix by x,y,z.  Equivalent to:
 * goog.vec.Mat4.multMat(
 *     mat,
 *     goog.vec.Mat4.makeScale(goog.vec.Mat4.create(), x, y, z),
 *     mat);
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @param {number} x The x scale factor.
 * @param {number} y The y scale factor.
 * @param {number} z The z scale factor.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained.
 */
goog.vec.Mat4.scale = function(mat, x, y, z) {
  return goog.vec.Mat4.setFromValues(
      mat,
      mat[0] * x, mat[1] * x, mat[2] * x, mat[3] * x,
      mat[4] * y, mat[5] * y, mat[6] * y, mat[7] * y,
      mat[8] * z, mat[9] * z, mat[10] * z, mat[11] * z,
      mat[12], mat[13], mat[14], mat[15]);
};


/**
 * Rotate the given matrix by angle about the x,y,z axis.  Equivalent to:
 * goog.vec.Mat4.multMat(
 *     mat,
 *     goog.vec.Mat4.makeRotate(goog.vec.Mat4.create(), angle, x, y, z),
 *     mat);
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @param {number} angle The angle in radians.
 * @param {number} x The x component of the rotation axis.
 * @param {number} y The y component of the rotation axis.
 * @param {number} z The z component of the rotation axis.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained.
 */
goog.vec.Mat4.rotate = function(mat, angle, x, y, z) {
  var m00 = mat[0], m10 = mat[1], m20 = mat[2], m30 = mat[3];
  var m01 = mat[4], m11 = mat[5], m21 = mat[6], m31 = mat[7];
  var m02 = mat[8], m12 = mat[9], m22 = mat[10], m32 = mat[11];
  var m03 = mat[12], m13 = mat[13], m23 = mat[14], m33 = mat[15];

  var cosAngle = Math.cos(angle);
  var sinAngle = Math.sin(angle);
  var diffCosAngle = 1 - cosAngle;
  var r00 = x * x * diffCosAngle + cosAngle;
  var r10 = x * y * diffCosAngle + z * sinAngle;
  var r20 = x * z * diffCosAngle - y * sinAngle;

  var r01 = x * y * diffCosAngle - z * sinAngle;
  var r11 = y * y * diffCosAngle + cosAngle;
  var r21 = y * z * diffCosAngle + x * sinAngle;

  var r02 = x * z * diffCosAngle + y * sinAngle;
  var r12 = y * z * diffCosAngle - x * sinAngle;
  var r22 = z * z * diffCosAngle + cosAngle;

  return goog.vec.Mat4.setFromValues(
      mat,
      m00 * r00 + m01 * r10 + m02 * r20,
      m10 * r00 + m11 * r10 + m12 * r20,
      m20 * r00 + m21 * r10 + m22 * r20,
      m30 * r00 + m31 * r10 + m32 * r20,

      m00 * r01 + m01 * r11 + m02 * r21,
      m10 * r01 + m11 * r11 + m12 * r21,
      m20 * r01 + m21 * r11 + m22 * r21,
      m30 * r01 + m31 * r11 + m32 * r21,

      m00 * r02 + m01 * r12 + m02 * r22,
      m10 * r02 + m11 * r12 + m12 * r22,
      m20 * r02 + m21 * r12 + m22 * r22,
      m30 * r02 + m31 * r12 + m32 * r22,

      m03, m13, m23, m33);
};


/**
 * Rotate the given matrix by angle about the x axis.  Equivalent to:
 * goog.vec.Mat4.multMat(
 *     mat,
 *     goog.vec.Mat4.makeRotateX(goog.vec.Mat4.create(), angle),
 *     mat);
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @param {number} angle The angle in radians.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained.
 */
goog.vec.Mat4.rotateX = function(mat, angle) {
  var m01 = mat[4], m11 = mat[5], m21 = mat[6], m31 = mat[7];
  var m02 = mat[8], m12 = mat[9], m22 = mat[10], m32 = mat[11];

  var c = Math.cos(angle);
  var s = Math.sin(angle);

  mat[4] = m01 * c + m02 * s;
  mat[5] = m11 * c + m12 * s;
  mat[6] = m21 * c + m22 * s;
  mat[7] = m31 * c + m32 * s;
  mat[8] = m01 * -s + m02 * c;
  mat[9] = m11 * -s + m12 * c;
  mat[10] = m21 * -s + m22 * c;
  mat[11] = m31 * -s + m32 * c;

  return mat;
};


/**
 * Rotate the given matrix by angle about the y axis.  Equivalent to:
 * goog.vec.Mat4.multMat(
 *     mat,
 *     goog.vec.Mat4.makeRotateY(goog.vec.Mat4.create(), angle),
 *     mat);
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @param {number} angle The angle in radians.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained.
 */
goog.vec.Mat4.rotateY = function(mat, angle) {
  var m00 = mat[0], m10 = mat[1], m20 = mat[2], m30 = mat[3];
  var m02 = mat[8], m12 = mat[9], m22 = mat[10], m32 = mat[11];

  var c = Math.cos(angle);
  var s = Math.sin(angle);

  mat[0] = m00 * c + m02 * -s;
  mat[1] = m10 * c + m12 * -s;
  mat[2] = m20 * c + m22 * -s;
  mat[3] = m30 * c + m32 * -s;
  mat[8] = m00 * s + m02 * c;
  mat[9] = m10 * s + m12 * c;
  mat[10] = m20 * s + m22 * c;
  mat[11] = m30 * s + m32 * c;

  return mat;
};


/**
 * Rotate the given matrix by angle about the z axis.  Equivalent to:
 * goog.vec.Mat4.multMat(
 *     mat,
 *     goog.vec.Mat4.makeRotateZ(goog.vec.Mat4.create(), angle),
 *     mat);
 *
 * @param {goog.vec.Mat4.AnyType} mat The matrix.
 * @param {number} angle The angle in radians.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained.
 */
goog.vec.Mat4.rotateZ = function(mat, angle) {
  var m00 = mat[0], m10 = mat[1], m20 = mat[2], m30 = mat[3];
  var m01 = mat[4], m11 = mat[5], m21 = mat[6], m31 = mat[7];

  var c = Math.cos(angle);
  var s = Math.sin(angle);

  mat[0] = m00 * c + m01 * s;
  mat[1] = m10 * c + m11 * s;
  mat[2] = m20 * c + m21 * s;
  mat[3] = m30 * c + m31 * s;
  mat[4] = m00 * -s + m01 * c;
  mat[5] = m10 * -s + m11 * c;
  mat[6] = m20 * -s + m21 * c;
  mat[7] = m30 * -s + m31 * c;

  return mat;
};


/**
 * Retrieves the translation component of the transformation matrix.
 *
 * @param {goog.vec.Mat4.AnyType} mat The transformation matrix.
 * @param {goog.vec.Vec3.AnyType} translation The vector for storing the
 *     result.
 * @return {goog.vec.Mat4.AnyType} return mat so that operations can be
 *     chained.
 */
goog.vec.Mat4.getTranslation = function(mat, translation) {
  translation[0] = mat[12];
  translation[1] = mat[13];
  translation[2] = mat[14];
  return translation;
};


/**
 * @type {!Array<!goog.vec.Vec3.Type>}
 * @private
 */
goog.vec.Mat4.tmpVec3_ = [
  goog.vec.Vec3.createFloat64(),
  goog.vec.Vec3.createFloat64()
];


/**
 * @type {!Array<!goog.vec.Vec4.Type>}
 * @private
 */
goog.vec.Mat4.tmpVec4_ = [
  goog.vec.Vec4.createFloat64(),
  goog.vec.Vec4.createFloat64(),
  goog.vec.Vec4.createFloat64()
];


/**
 * @type {!Array<!goog.vec.Mat4.Type>}
 * @private
 */
goog.vec.Mat4.tmpMat4_ = [
  goog.vec.Mat4.createFloat64()
];

goog.provide('ol.TransformFunction');


/**
 * A transform function accepts an array of input coordinate values, an optional
 * output array, and an optional dimension (default should be 2).  The function
 * transforms the input coordinate values, populates the output array, and
 * returns the output array.
 *
 * @typedef {function(Array.<number>, Array.<number>=, number=): Array.<number>}
 * @api stable
 */
ol.TransformFunction;

goog.provide('ol.Extent');
goog.provide('ol.extent');
goog.provide('ol.extent.Corner');
goog.provide('ol.extent.Relationship');

goog.require('goog.asserts');
goog.require('goog.vec.Mat4');
goog.require('ol.Coordinate');
goog.require('ol.Size');
goog.require('ol.TransformFunction');


/**
 * An array of numbers representing an extent: `[minx, miny, maxx, maxy]`.
 * @typedef {Array.<number>}
 * @api stable
 */
ol.Extent;


/**
 * Extent corner.
 * @enum {string}
 */
ol.extent.Corner = {
  BOTTOM_LEFT: 'bottom-left',
  BOTTOM_RIGHT: 'bottom-right',
  TOP_LEFT: 'top-left',
  TOP_RIGHT: 'top-right'
};


/**
 * Relationship to an extent.
 * @enum {number}
 */
ol.extent.Relationship = {
  UNKNOWN: 0,
  INTERSECTING: 1,
  ABOVE: 2,
  RIGHT: 4,
  BELOW: 8,
  LEFT: 16
};


/**
 * Build an extent that includes all given coordinates.
 *
 * @param {Array.<ol.Coordinate>} coordinates Coordinates.
 * @return {ol.Extent} Bounding extent.
 * @api stable
 */
ol.extent.boundingExtent = function(coordinates) {
  var extent = ol.extent.createEmpty();
  for (var i = 0, ii = coordinates.length; i < ii; ++i) {
    ol.extent.extendCoordinate(extent, coordinates[i]);
  }
  return extent;
};


/**
 * @param {Array.<number>} xs Xs.
 * @param {Array.<number>} ys Ys.
 * @param {ol.Extent=} opt_extent Destination extent.
 * @private
 * @return {ol.Extent} Extent.
 */
ol.extent.boundingExtentXYs_ = function(xs, ys, opt_extent) {
  goog.asserts.assert(xs.length > 0, 'xs length should be larger than 0');
  goog.asserts.assert(ys.length > 0, 'ys length should be larger than 0');
  var minX = Math.min.apply(null, xs);
  var minY = Math.min.apply(null, ys);
  var maxX = Math.max.apply(null, xs);
  var maxY = Math.max.apply(null, ys);
  return ol.extent.createOrUpdate(minX, minY, maxX, maxY, opt_extent);
};


/**
 * Return extent increased by the provided value.
 * @param {ol.Extent} extent Extent.
 * @param {number} value The amount by which the extent should be buffered.
 * @param {ol.Extent=} opt_extent Extent.
 * @return {ol.Extent} Extent.
 * @api stable
 */
ol.extent.buffer = function(extent, value, opt_extent) {
  if (opt_extent) {
    opt_extent[0] = extent[0] - value;
    opt_extent[1] = extent[1] - value;
    opt_extent[2] = extent[2] + value;
    opt_extent[3] = extent[3] + value;
    return opt_extent;
  } else {
    return [
      extent[0] - value,
      extent[1] - value,
      extent[2] + value,
      extent[3] + value
    ];
  }
};


/**
 * Creates a clone of an extent.
 *
 * @param {ol.Extent} extent Extent to clone.
 * @param {ol.Extent=} opt_extent Extent.
 * @return {ol.Extent} The clone.
 */
ol.extent.clone = function(extent, opt_extent) {
  if (opt_extent) {
    opt_extent[0] = extent[0];
    opt_extent[1] = extent[1];
    opt_extent[2] = extent[2];
    opt_extent[3] = extent[3];
    return opt_extent;
  } else {
    return extent.slice();
  }
};


/**
 * @param {ol.Extent} extent Extent.
 * @param {number} x X.
 * @param {number} y Y.
 * @return {number} Closest squared distance.
 */
ol.extent.closestSquaredDistanceXY = function(extent, x, y) {
  var dx, dy;
  if (x < extent[0]) {
    dx = extent[0] - x;
  } else if (extent[2] < x) {
    dx = x - extent[2];
  } else {
    dx = 0;
  }
  if (y < extent[1]) {
    dy = extent[1] - y;
  } else if (extent[3] < y) {
    dy = y - extent[3];
  } else {
    dy = 0;
  }
  return dx * dx + dy * dy;
};


/**
 * Check if the passed coordinate is contained or on the edge of the extent.
 *
 * @param {ol.Extent} extent Extent.
 * @param {ol.Coordinate} coordinate Coordinate.
 * @return {boolean} The coordinate is contained in the extent.
 * @api stable
 */
ol.extent.containsCoordinate = function(extent, coordinate) {
  return ol.extent.containsXY(extent, coordinate[0], coordinate[1]);
};


/**
 * Check if one extent contains another.
 *
 * An extent is deemed contained if it lies completely within the other extent,
 * including if they share one or more edges.
 *
 * @param {ol.Extent} extent1 Extent 1.
 * @param {ol.Extent} extent2 Extent 2.
 * @return {boolean} The second extent is contained by or on the edge of the
 *     first.
 * @api stable
 */
ol.extent.containsExtent = function(extent1, extent2) {
  return extent1[0] <= extent2[0] && extent2[2] <= extent1[2] &&
      extent1[1] <= extent2[1] && extent2[3] <= extent1[3];
};


/**
 * Check if the passed coordinate is contained or on the edge of the extent.
 *
 * @param {ol.Extent} extent Extent.
 * @param {number} x X coordinate.
 * @param {number} y Y coordinate.
 * @return {boolean} The x, y values are contained in the extent.
 * @api stable
 */
ol.extent.containsXY = function(extent, x, y) {
  return extent[0] <= x && x <= extent[2] && extent[1] <= y && y <= extent[3];
};


/**
 * Get the relationship between a coordinate and extent.
 * @param {ol.Extent} extent The extent.
 * @param {ol.Coordinate} coordinate The coordinate.
 * @return {number} The relationship (bitwise compare with
 *     ol.extent.Relationship).
 */
ol.extent.coordinateRelationship = function(extent, coordinate) {
  var minX = extent[0];
  var minY = extent[1];
  var maxX = extent[2];
  var maxY = extent[3];
  var x = coordinate[0];
  var y = coordinate[1];
  var relationship = ol.extent.Relationship.UNKNOWN;
  if (x < minX) {
    relationship = relationship | ol.extent.Relationship.LEFT;
  } else if (x > maxX) {
    relationship = relationship | ol.extent.Relationship.RIGHT;
  }
  if (y < minY) {
    relationship = relationship | ol.extent.Relationship.BELOW;
  } else if (y > maxY) {
    relationship = relationship | ol.extent.Relationship.ABOVE;
  }
  if (relationship === ol.extent.Relationship.UNKNOWN) {
    relationship = ol.extent.Relationship.INTERSECTING;
  }
  return relationship;
};


/**
 * Create an empty extent.
 * @return {ol.Extent} Empty extent.
 * @api stable
 */
ol.extent.createEmpty = function() {
  return [Infinity, Infinity, -Infinity, -Infinity];
};


/**
 * Create a new extent or update the provided extent.
 * @param {number} minX Minimum X.
 * @param {number} minY Minimum Y.
 * @param {number} maxX Maximum X.
 * @param {number} maxY Maximum Y.
 * @param {ol.Extent=} opt_extent Destination extent.
 * @return {ol.Extent} Extent.
 */
ol.extent.createOrUpdate = function(minX, minY, maxX, maxY, opt_extent) {
  if (opt_extent) {
    opt_extent[0] = minX;
    opt_extent[1] = minY;
    opt_extent[2] = maxX;
    opt_extent[3] = maxY;
    return opt_extent;
  } else {
    return [minX, minY, maxX, maxY];
  }
};


/**
 * Create a new empty extent or make the provided one empty.
 * @param {ol.Extent=} opt_extent Extent.
 * @return {ol.Extent} Extent.
 */
ol.extent.createOrUpdateEmpty = function(opt_extent) {
  return ol.extent.createOrUpdate(
      Infinity, Infinity, -Infinity, -Infinity, opt_extent);
};


/**
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {ol.Extent=} opt_extent Extent.
 * @return {ol.Extent} Extent.
 */
ol.extent.createOrUpdateFromCoordinate = function(coordinate, opt_extent) {
  var x = coordinate[0];
  var y = coordinate[1];
  return ol.extent.createOrUpdate(x, y, x, y, opt_extent);
};


/**
 * @param {Array.<ol.Coordinate>} coordinates Coordinates.
 * @param {ol.Extent=} opt_extent Extent.
 * @return {ol.Extent} Extent.
 */
ol.extent.createOrUpdateFromCoordinates = function(coordinates, opt_extent) {
  var extent = ol.extent.createOrUpdateEmpty(opt_extent);
  return ol.extent.extendCoordinates(extent, coordinates);
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {ol.Extent=} opt_extent Extent.
 * @return {ol.Extent} Extent.
 */
ol.extent.createOrUpdateFromFlatCoordinates = function(flatCoordinates, offset, end, stride, opt_extent) {
  var extent = ol.extent.createOrUpdateEmpty(opt_extent);
  return ol.extent.extendFlatCoordinates(
      extent, flatCoordinates, offset, end, stride);
};


/**
 * @param {Array.<Array.<ol.Coordinate>>} rings Rings.
 * @param {ol.Extent=} opt_extent Extent.
 * @return {ol.Extent} Extent.
 */
ol.extent.createOrUpdateFromRings = function(rings, opt_extent) {
  var extent = ol.extent.createOrUpdateEmpty(opt_extent);
  return ol.extent.extendRings(extent, rings);
};


/**
 * Empty an extent in place.
 * @param {ol.Extent} extent Extent.
 * @return {ol.Extent} Extent.
 */
ol.extent.empty = function(extent) {
  extent[0] = extent[1] = Infinity;
  extent[2] = extent[3] = -Infinity;
  return extent;
};


/**
 * Determine if two extents are equivalent.
 * @param {ol.Extent} extent1 Extent 1.
 * @param {ol.Extent} extent2 Extent 2.
 * @return {boolean} The two extents are equivalent.
 * @api stable
 */
ol.extent.equals = function(extent1, extent2) {
  return extent1[0] == extent2[0] && extent1[2] == extent2[2] &&
      extent1[1] == extent2[1] && extent1[3] == extent2[3];
};


/**
 * Modify an extent to include another extent.
 * @param {ol.Extent} extent1 The extent to be modified.
 * @param {ol.Extent} extent2 The extent that will be included in the first.
 * @return {ol.Extent} A reference to the first (extended) extent.
 * @api stable
 */
ol.extent.extend = function(extent1, extent2) {
  if (extent2[0] < extent1[0]) {
    extent1[0] = extent2[0];
  }
  if (extent2[2] > extent1[2]) {
    extent1[2] = extent2[2];
  }
  if (extent2[1] < extent1[1]) {
    extent1[1] = extent2[1];
  }
  if (extent2[3] > extent1[3]) {
    extent1[3] = extent2[3];
  }
  return extent1;
};


/**
 * @param {ol.Extent} extent Extent.
 * @param {ol.Coordinate} coordinate Coordinate.
 */
ol.extent.extendCoordinate = function(extent, coordinate) {
  if (coordinate[0] < extent[0]) {
    extent[0] = coordinate[0];
  }
  if (coordinate[0] > extent[2]) {
    extent[2] = coordinate[0];
  }
  if (coordinate[1] < extent[1]) {
    extent[1] = coordinate[1];
  }
  if (coordinate[1] > extent[3]) {
    extent[3] = coordinate[1];
  }
};


/**
 * @param {ol.Extent} extent Extent.
 * @param {Array.<ol.Coordinate>} coordinates Coordinates.
 * @return {ol.Extent} Extent.
 */
ol.extent.extendCoordinates = function(extent, coordinates) {
  var i, ii;
  for (i = 0, ii = coordinates.length; i < ii; ++i) {
    ol.extent.extendCoordinate(extent, coordinates[i]);
  }
  return extent;
};


/**
 * @param {ol.Extent} extent Extent.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @return {ol.Extent} Extent.
 */
ol.extent.extendFlatCoordinates = function(extent, flatCoordinates, offset, end, stride) {
  for (; offset < end; offset += stride) {
    ol.extent.extendXY(
        extent, flatCoordinates[offset], flatCoordinates[offset + 1]);
  }
  return extent;
};


/**
 * @param {ol.Extent} extent Extent.
 * @param {Array.<Array.<ol.Coordinate>>} rings Rings.
 * @return {ol.Extent} Extent.
 */
ol.extent.extendRings = function(extent, rings) {
  var i, ii;
  for (i = 0, ii = rings.length; i < ii; ++i) {
    ol.extent.extendCoordinates(extent, rings[i]);
  }
  return extent;
};


/**
 * @param {ol.Extent} extent Extent.
 * @param {number} x X.
 * @param {number} y Y.
 */
ol.extent.extendXY = function(extent, x, y) {
  extent[0] = Math.min(extent[0], x);
  extent[1] = Math.min(extent[1], y);
  extent[2] = Math.max(extent[2], x);
  extent[3] = Math.max(extent[3], y);
};


/**
 * This function calls `callback` for each corner of the extent. If the
 * callback returns a truthy value the function returns that value
 * immediately. Otherwise the function returns `false`.
 * @param {ol.Extent} extent Extent.
 * @param {function(this:T, ol.Coordinate): S} callback Callback.
 * @param {T=} opt_this Value to use as `this` when executing `callback`.
 * @return {S|boolean} Value.
 * @template S, T
 */
ol.extent.forEachCorner = function(extent, callback, opt_this) {
  var val;
  val = callback.call(opt_this, ol.extent.getBottomLeft(extent));
  if (val) {
    return val;
  }
  val = callback.call(opt_this, ol.extent.getBottomRight(extent));
  if (val) {
    return val;
  }
  val = callback.call(opt_this, ol.extent.getTopRight(extent));
  if (val) {
    return val;
  }
  val = callback.call(opt_this, ol.extent.getTopLeft(extent));
  if (val) {
    return val;
  }
  return false;
};


/**
 * @param {ol.Extent} extent Extent.
 * @return {number} Area.
 */
ol.extent.getArea = function(extent) {
  var area = 0;
  if (!ol.extent.isEmpty(extent)) {
    area = ol.extent.getWidth(extent) * ol.extent.getHeight(extent);
  }
  return area;
};


/**
 * Get the bottom left coordinate of an extent.
 * @param {ol.Extent} extent Extent.
 * @return {ol.Coordinate} Bottom left coordinate.
 * @api stable
 */
ol.extent.getBottomLeft = function(extent) {
  return [extent[0], extent[1]];
};


/**
 * Get the bottom right coordinate of an extent.
 * @param {ol.Extent} extent Extent.
 * @return {ol.Coordinate} Bottom right coordinate.
 * @api stable
 */
ol.extent.getBottomRight = function(extent) {
  return [extent[2], extent[1]];
};


/**
 * Get the center coordinate of an extent.
 * @param {ol.Extent} extent Extent.
 * @return {ol.Coordinate} Center.
 * @api stable
 */
ol.extent.getCenter = function(extent) {
  return [(extent[0] + extent[2]) / 2, (extent[1] + extent[3]) / 2];
};


/**
 * Get a corner coordinate of an extent.
 * @param {ol.Extent} extent Extent.
 * @param {ol.extent.Corner} corner Corner.
 * @return {ol.Coordinate} Corner coordinate.
 */
ol.extent.getCorner = function(extent, corner) {
  var coordinate;
  if (corner === ol.extent.Corner.BOTTOM_LEFT) {
    coordinate = ol.extent.getBottomLeft(extent);
  } else if (corner === ol.extent.Corner.BOTTOM_RIGHT) {
    coordinate = ol.extent.getBottomRight(extent);
  } else if (corner === ol.extent.Corner.TOP_LEFT) {
    coordinate = ol.extent.getTopLeft(extent);
  } else if (corner === ol.extent.Corner.TOP_RIGHT) {
    coordinate = ol.extent.getTopRight(extent);
  } else {
    goog.asserts.fail('Invalid corner: %s', corner);
  }
  goog.asserts.assert(coordinate, 'coordinate should be defined');
  return coordinate;
};


/**
 * @param {ol.Extent} extent1 Extent 1.
 * @param {ol.Extent} extent2 Extent 2.
 * @return {number} Enlarged area.
 */
ol.extent.getEnlargedArea = function(extent1, extent2) {
  var minX = Math.min(extent1[0], extent2[0]);
  var minY = Math.min(extent1[1], extent2[1]);
  var maxX = Math.max(extent1[2], extent2[2]);
  var maxY = Math.max(extent1[3], extent2[3]);
  return (maxX - minX) * (maxY - minY);
};


/**
 * @param {ol.Coordinate} center Center.
 * @param {number} resolution Resolution.
 * @param {number} rotation Rotation.
 * @param {ol.Size} size Size.
 * @param {ol.Extent=} opt_extent Destination extent.
 * @return {ol.Extent} Extent.
 */
ol.extent.getForViewAndSize = function(center, resolution, rotation, size, opt_extent) {
  var dx = resolution * size[0] / 2;
  var dy = resolution * size[1] / 2;
  var cosRotation = Math.cos(rotation);
  var sinRotation = Math.sin(rotation);
  /** @type {Array.<number>} */
  var xs = [-dx, -dx, dx, dx];
  /** @type {Array.<number>} */
  var ys = [-dy, dy, -dy, dy];
  var i, x, y;
  for (i = 0; i < 4; ++i) {
    x = xs[i];
    y = ys[i];
    xs[i] = center[0] + x * cosRotation - y * sinRotation;
    ys[i] = center[1] + x * sinRotation + y * cosRotation;
  }
  return ol.extent.boundingExtentXYs_(xs, ys, opt_extent);
};


/**
 * Get the height of an extent.
 * @param {ol.Extent} extent Extent.
 * @return {number} Height.
 * @api stable
 */
ol.extent.getHeight = function(extent) {
  return extent[3] - extent[1];
};


/**
 * @param {ol.Extent} extent1 Extent 1.
 * @param {ol.Extent} extent2 Extent 2.
 * @return {number} Intersection area.
 */
ol.extent.getIntersectionArea = function(extent1, extent2) {
  var intersection = ol.extent.getIntersection(extent1, extent2);
  return ol.extent.getArea(intersection);
};


/**
 * Get the intersection of two extents.
 * @param {ol.Extent} extent1 Extent 1.
 * @param {ol.Extent} extent2 Extent 2.
 * @param {ol.Extent=} opt_extent Optional extent to populate with intersection.
 * @return {ol.Extent} Intersecting extent.
 * @api stable
 */
ol.extent.getIntersection = function(extent1, extent2, opt_extent) {
  var intersection = opt_extent ? opt_extent : ol.extent.createEmpty();
  if (ol.extent.intersects(extent1, extent2)) {
    if (extent1[0] > extent2[0]) {
      intersection[0] = extent1[0];
    } else {
      intersection[0] = extent2[0];
    }
    if (extent1[1] > extent2[1]) {
      intersection[1] = extent1[1];
    } else {
      intersection[1] = extent2[1];
    }
    if (extent1[2] < extent2[2]) {
      intersection[2] = extent1[2];
    } else {
      intersection[2] = extent2[2];
    }
    if (extent1[3] < extent2[3]) {
      intersection[3] = extent1[3];
    } else {
      intersection[3] = extent2[3];
    }
  }
  return intersection;
};


/**
 * @param {ol.Extent} extent Extent.
 * @return {number} Margin.
 */
ol.extent.getMargin = function(extent) {
  return ol.extent.getWidth(extent) + ol.extent.getHeight(extent);
};


/**
 * Get the size (width, height) of an extent.
 * @param {ol.Extent} extent The extent.
 * @return {ol.Size} The extent size.
 * @api stable
 */
ol.extent.getSize = function(extent) {
  return [extent[2] - extent[0], extent[3] - extent[1]];
};


/**
 * Get the top left coordinate of an extent.
 * @param {ol.Extent} extent Extent.
 * @return {ol.Coordinate} Top left coordinate.
 * @api stable
 */
ol.extent.getTopLeft = function(extent) {
  return [extent[0], extent[3]];
};


/**
 * Get the top right coordinate of an extent.
 * @param {ol.Extent} extent Extent.
 * @return {ol.Coordinate} Top right coordinate.
 * @api stable
 */
ol.extent.getTopRight = function(extent) {
  return [extent[2], extent[3]];
};


/**
 * Get the width of an extent.
 * @param {ol.Extent} extent Extent.
 * @return {number} Width.
 * @api stable
 */
ol.extent.getWidth = function(extent) {
  return extent[2] - extent[0];
};


/**
 * Determine if one extent intersects another.
 * @param {ol.Extent} extent1 Extent 1.
 * @param {ol.Extent} extent2 Extent.
 * @return {boolean} The two extents intersect.
 * @api stable
 */
ol.extent.intersects = function(extent1, extent2) {
  return extent1[0] <= extent2[2] &&
      extent1[2] >= extent2[0] &&
      extent1[1] <= extent2[3] &&
      extent1[3] >= extent2[1];
};


/**
 * Determine if an extent is empty.
 * @param {ol.Extent} extent Extent.
 * @return {boolean} Is empty.
 * @api stable
 */
ol.extent.isEmpty = function(extent) {
  return extent[2] < extent[0] || extent[3] < extent[1];
};


/**
 * @param {ol.Extent} extent Extent.
 * @return {boolean} Is infinite.
 */
ol.extent.isInfinite = function(extent) {
  return extent[0] == -Infinity || extent[1] == -Infinity ||
      extent[2] == Infinity || extent[3] == Infinity;
};


/**
 * @param {ol.Extent} extent Extent.
 * @param {ol.Coordinate} coordinate Coordinate.
 * @return {ol.Coordinate} Coordinate.
 */
ol.extent.normalize = function(extent, coordinate) {
  return [
    (coordinate[0] - extent[0]) / (extent[2] - extent[0]),
    (coordinate[1] - extent[1]) / (extent[3] - extent[1])
  ];
};


/**
 * @param {ol.Extent} extent Extent.
 * @param {ol.Extent=} opt_extent Extent.
 * @return {ol.Extent} Extent.
 */
ol.extent.returnOrUpdate = function(extent, opt_extent) {
  if (opt_extent) {
    opt_extent[0] = extent[0];
    opt_extent[1] = extent[1];
    opt_extent[2] = extent[2];
    opt_extent[3] = extent[3];
    return opt_extent;
  } else {
    return extent;
  }
};


/**
 * @param {ol.Extent} extent Extent.
 * @param {number} value Value.
 */
ol.extent.scaleFromCenter = function(extent, value) {
  var deltaX = ((extent[2] - extent[0]) / 2) * (value - 1);
  var deltaY = ((extent[3] - extent[1]) / 2) * (value - 1);
  extent[0] -= deltaX;
  extent[2] += deltaX;
  extent[1] -= deltaY;
  extent[3] += deltaY;
};


/**
 * Determine if the segment between two coordinates intersects (crosses,
 * touches, or is contained by) the provided extent.
 * @param {ol.Extent} extent The extent.
 * @param {ol.Coordinate} start Segment start coordinate.
 * @param {ol.Coordinate} end Segment end coordinate.
 * @return {boolean} The segment intersects the extent.
 */
ol.extent.intersectsSegment = function(extent, start, end) {
  var intersects = false;
  var startRel = ol.extent.coordinateRelationship(extent, start);
  var endRel = ol.extent.coordinateRelationship(extent, end);
  if (startRel === ol.extent.Relationship.INTERSECTING ||
      endRel === ol.extent.Relationship.INTERSECTING) {
    intersects = true;
  } else {
    var minX = extent[0];
    var minY = extent[1];
    var maxX = extent[2];
    var maxY = extent[3];
    var startX = start[0];
    var startY = start[1];
    var endX = end[0];
    var endY = end[1];
    var slope = (endY - startY) / (endX - startX);
    var x, y;
    if (!!(endRel & ol.extent.Relationship.ABOVE) &&
        !(startRel & ol.extent.Relationship.ABOVE)) {
      // potentially intersects top
      x = endX - ((endY - maxY) / slope);
      intersects = x >= minX && x <= maxX;
    }
    if (!intersects && !!(endRel & ol.extent.Relationship.RIGHT) &&
        !(startRel & ol.extent.Relationship.RIGHT)) {
      // potentially intersects right
      y = endY - ((endX - maxX) * slope);
      intersects = y >= minY && y <= maxY;
    }
    if (!intersects && !!(endRel & ol.extent.Relationship.BELOW) &&
        !(startRel & ol.extent.Relationship.BELOW)) {
      // potentially intersects bottom
      x = endX - ((endY - minY) / slope);
      intersects = x >= minX && x <= maxX;
    }
    if (!intersects && !!(endRel & ol.extent.Relationship.LEFT) &&
        !(startRel & ol.extent.Relationship.LEFT)) {
      // potentially intersects left
      y = endY - ((endX - minX) * slope);
      intersects = y >= minY && y <= maxY;
    }

  }
  return intersects;
};


/**
 * @param {ol.Extent} extent1 Extent 1.
 * @param {ol.Extent} extent2 Extent 2.
 * @return {boolean} Touches.
 */
ol.extent.touches = function(extent1, extent2) {
  var intersects = ol.extent.intersects(extent1, extent2);
  return intersects &&
      (extent1[0] == extent2[2] || extent1[2] == extent2[0] ||
       extent1[1] == extent2[3] || extent1[3] == extent2[1]);
};


/**
 * Apply a transform function to the extent.
 * @param {ol.Extent} extent Extent.
 * @param {ol.TransformFunction} transformFn Transform function.  Called with
 * [minX, minY, maxX, maxY] extent coordinates.
 * @param {ol.Extent=} opt_extent Destination extent.
 * @return {ol.Extent} Extent.
 * @api stable
 */
ol.extent.applyTransform = function(extent, transformFn, opt_extent) {
  var coordinates = [
    extent[0], extent[1],
    extent[0], extent[3],
    extent[2], extent[1],
    extent[2], extent[3]
  ];
  transformFn(coordinates, coordinates, 2);
  var xs = [coordinates[0], coordinates[2], coordinates[4], coordinates[6]];
  var ys = [coordinates[1], coordinates[3], coordinates[5], coordinates[7]];
  return ol.extent.boundingExtentXYs_(xs, ys, opt_extent);
};


/**
 * Apply a 2d transform to an extent.
 * @param {ol.Extent} extent Input extent.
 * @param {goog.vec.Mat4.Number} transform The transform matrix.
 * @param {ol.Extent=} opt_extent Optional extent for return values.
 * @return {ol.Extent} The transformed extent.
 */
ol.extent.transform2D = function(extent, transform, opt_extent) {
  var dest = opt_extent ? opt_extent : [];
  var m00 = goog.vec.Mat4.getElement(transform, 0, 0);
  var m10 = goog.vec.Mat4.getElement(transform, 1, 0);
  var m01 = goog.vec.Mat4.getElement(transform, 0, 1);
  var m11 = goog.vec.Mat4.getElement(transform, 1, 1);
  var m03 = goog.vec.Mat4.getElement(transform, 0, 3);
  var m13 = goog.vec.Mat4.getElement(transform, 1, 3);
  var xi = [0, 2, 0, 2];
  var yi = [1, 1, 3, 3];
  var xs = [];
  var ys = [];
  var i, x, y;
  for (i = 0; i < 4; ++i) {
    x = extent[xi[i]];
    y = extent[yi[i]];
    xs[i] = m00 * x + m01 * y + m03;
    ys[i] = m10 * x + m11 * y + m13;
  }
  dest[0] = Math.min.apply(null, xs);
  dest[1] = Math.min.apply(null, ys);
  dest[2] = Math.max.apply(null, xs);
  dest[3] = Math.max.apply(null, ys);
  return dest;
};

// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Utilities for creating functions. Loosely inspired by the
 * java classes: http://goo.gl/GM0Hmu and http://goo.gl/6k7nI8.
 *
 * @author nicksantos@google.com (Nick Santos)
 */


goog.provide('goog.functions');


/**
 * Creates a function that always returns the same value.
 * @param {T} retValue The value to return.
 * @return {function():T} The new function.
 * @template T
 */
goog.functions.constant = function(retValue) {
  return function() {
    return retValue;
  };
};


/**
 * Always returns false.
 * @type {function(...): boolean}
 */
goog.functions.FALSE = goog.functions.constant(false);


/**
 * Always returns true.
 * @type {function(...): boolean}
 */
goog.functions.TRUE = goog.functions.constant(true);


/**
 * Always returns NULL.
 * @type {function(...): null}
 */
goog.functions.NULL = goog.functions.constant(null);


/**
 * A simple function that returns the first argument of whatever is passed
 * into it.
 * @param {T=} opt_returnValue The single value that will be returned.
 * @param {...*} var_args Optional trailing arguments. These are ignored.
 * @return {T} The first argument passed in, or undefined if nothing was passed.
 * @template T
 */
goog.functions.identity = function(opt_returnValue, var_args) {
  return opt_returnValue;
};


/**
 * Creates a function that always throws an error with the given message.
 * @param {string} message The error message.
 * @return {!Function} The error-throwing function.
 */
goog.functions.error = function(message) {
  return function() {
    throw Error(message);
  };
};


/**
 * Creates a function that throws the given object.
 * @param {*} err An object to be thrown.
 * @return {!Function} The error-throwing function.
 */
goog.functions.fail = function(err) {
  return function() {
    throw err;
  }
};


/**
 * Given a function, create a function that keeps opt_numArgs arguments and
 * silently discards all additional arguments.
 * @param {Function} f The original function.
 * @param {number=} opt_numArgs The number of arguments to keep. Defaults to 0.
 * @return {!Function} A version of f that only keeps the first opt_numArgs
 *     arguments.
 */
goog.functions.lock = function(f, opt_numArgs) {
  opt_numArgs = opt_numArgs || 0;
  return function() {
    return f.apply(this, Array.prototype.slice.call(arguments, 0, opt_numArgs));
  };
};


/**
 * Creates a function that returns its nth argument.
 * @param {number} n The position of the return argument.
 * @return {!Function} A new function.
 */
goog.functions.nth = function(n) {
  return function() {
    return arguments[n];
  };
};


/**
 * Given a function, create a new function that swallows its return value
 * and replaces it with a new one.
 * @param {Function} f A function.
 * @param {T} retValue A new return value.
 * @return {function(...?):T} A new function.
 * @template T
 */
goog.functions.withReturnValue = function(f, retValue) {
  return goog.functions.sequence(f, goog.functions.constant(retValue));
};


/**
 * Creates a function that returns whether its arguement equals the given value.
 *
 * Example:
 * var key = goog.object.findKey(obj, goog.functions.equalTo('needle'));
 *
 * @param {*} value The value to compare to.
 * @param {boolean=} opt_useLooseComparison Whether to use a loose (==)
 *     comparison rather than a strict (===) one. Defaults to false.
 * @return {function(*):boolean} The new function.
 */
goog.functions.equalTo = function(value, opt_useLooseComparison) {
  return function(other) {
    return opt_useLooseComparison ? (value == other) : (value === other);
  };
};


/**
 * Creates the composition of the functions passed in.
 * For example, (goog.functions.compose(f, g))(a) is equivalent to f(g(a)).
 * @param {function(...?):T} fn The final function.
 * @param {...Function} var_args A list of functions.
 * @return {function(...?):T} The composition of all inputs.
 * @template T
 */
goog.functions.compose = function(fn, var_args) {
  var functions = arguments;
  var length = functions.length;
  return function() {
    var result;
    if (length) {
      result = functions[length - 1].apply(this, arguments);
    }

    for (var i = length - 2; i >= 0; i--) {
      result = functions[i].call(this, result);
    }
    return result;
  };
};


/**
 * Creates a function that calls the functions passed in in sequence, and
 * returns the value of the last function. For example,
 * (goog.functions.sequence(f, g))(x) is equivalent to f(x),g(x).
 * @param {...Function} var_args A list of functions.
 * @return {!Function} A function that calls all inputs in sequence.
 */
goog.functions.sequence = function(var_args) {
  var functions = arguments;
  var length = functions.length;
  return function() {
    var result;
    for (var i = 0; i < length; i++) {
      result = functions[i].apply(this, arguments);
    }
    return result;
  };
};


/**
 * Creates a function that returns true if each of its components evaluates
 * to true. The components are evaluated in order, and the evaluation will be
 * short-circuited as soon as a function returns false.
 * For example, (goog.functions.and(f, g))(x) is equivalent to f(x) && g(x).
 * @param {...Function} var_args A list of functions.
 * @return {function(...?):boolean} A function that ANDs its component
 *      functions.
 */
goog.functions.and = function(var_args) {
  var functions = arguments;
  var length = functions.length;
  return function() {
    for (var i = 0; i < length; i++) {
      if (!functions[i].apply(this, arguments)) {
        return false;
      }
    }
    return true;
  };
};


/**
 * Creates a function that returns true if any of its components evaluates
 * to true. The components are evaluated in order, and the evaluation will be
 * short-circuited as soon as a function returns true.
 * For example, (goog.functions.or(f, g))(x) is equivalent to f(x) || g(x).
 * @param {...Function} var_args A list of functions.
 * @return {function(...?):boolean} A function that ORs its component
 *    functions.
 */
goog.functions.or = function(var_args) {
  var functions = arguments;
  var length = functions.length;
  return function() {
    for (var i = 0; i < length; i++) {
      if (functions[i].apply(this, arguments)) {
        return true;
      }
    }
    return false;
  };
};


/**
 * Creates a function that returns the Boolean opposite of a provided function.
 * For example, (goog.functions.not(f))(x) is equivalent to !f(x).
 * @param {!Function} f The original function.
 * @return {function(...?):boolean} A function that delegates to f and returns
 * opposite.
 */
goog.functions.not = function(f) {
  return function() {
    return !f.apply(this, arguments);
  };
};


/**
 * Generic factory function to construct an object given the constructor
 * and the arguments. Intended to be bound to create object factories.
 *
 * Example:
 *
 * var factory = goog.partial(goog.functions.create, Class);
 *
 * @param {function(new:T, ...)} constructor The constructor for the Object.
 * @param {...*} var_args The arguments to be passed to the constructor.
 * @return {T} A new instance of the class given in {@code constructor}.
 * @template T
 */
goog.functions.create = function(constructor, var_args) {
  /**
   * @constructor
   * @final
   */
  var temp = function() {};
  temp.prototype = constructor.prototype;

  // obj will have constructor's prototype in its chain and
  // 'obj instanceof constructor' will be true.
  var obj = new temp();

  // obj is initialized by constructor.
  // arguments is only array-like so lacks shift(), but can be used with
  // the Array prototype function.
  constructor.apply(obj, Array.prototype.slice.call(arguments, 1));
  return obj;
};


/**
 * @define {boolean} Whether the return value cache should be used.
 *    This should only be used to disable caches when testing.
 */
goog.define('goog.functions.CACHE_RETURN_VALUE', true);


/**
 * Gives a wrapper function that caches the return value of a parameterless
 * function when first called.
 *
 * When called for the first time, the given function is called and its
 * return value is cached (thus this is only appropriate for idempotent
 * functions).  Subsequent calls will return the cached return value. This
 * allows the evaluation of expensive functions to be delayed until first used.
 *
 * To cache the return values of functions with parameters, see goog.memoize.
 *
 * @param {!function():T} fn A function to lazily evaluate.
 * @return {!function():T} A wrapped version the function.
 * @template T
 */
goog.functions.cacheReturnValue = function(fn) {
  var called = false;
  var value;

  return function() {
    if (!goog.functions.CACHE_RETURN_VALUE) {
      return fn();
    }

    if (!called) {
      value = fn();
      called = true;
    }

    return value;
  }
};


/**
 * Wraps a function to allow it to be called, at most, once. All
 * additional calls are no-ops.
 *
 * This is particularly useful for initialization functions
 * that should be called, at most, once.
 *
 * @param {function():*} f Function to call.
 * @return {function():undefined} Wrapped function.
 */
goog.functions.once = function(f) {
  // Keep a reference to the function that we null out when we're done with
  // it -- that way, the function can be GC'd when we're done with it.
  var inner = f;
  return function() {
    if (inner) {
      var tmp = inner;
      inner = null;
      tmp();
    }
  };
};


/**
 * Wraps a function to allow it to be called, at most, once for each sequence of
 * calls fired repeatedly so long as they are fired less than a specified
 * interval apart (in milliseconds). Whether it receives one signal or multiple,
 * it will always wait until a full interval has elapsed since the last signal
 * before performing the action.
 *
 * This is particularly useful for bulking up repeated user actions (e.g. only
 * refreshing a view once a user finishes typing rather than updating with every
 * keystroke). For more stateful debouncing with support for pausing, resuming,
 * and canceling debounced actions, use {@code goog.async.Debouncer}.
 *
 * @param {function(this:SCOPE):*} f Function to call.
 * @param {number} interval Interval over which to debounce. The function will
 *     only be called after the full interval has elapsed since the last call.
 * @param {SCOPE=} opt_scope Object in whose scope to call the function.
 * @return {function():undefined} Wrapped function.
 * @template SCOPE
 */
goog.functions.debounce = function(f, interval, opt_scope) {
  if (opt_scope) {
    f = goog.bind(f, opt_scope);
  }
  var timeout = null;
  return function() {
    goog.global.clearTimeout(timeout);
    timeout = goog.global.setTimeout(f, interval);
  };
};


/**
 * Wraps a function to allow it to be called, at most, once per interval
 * (specified in milliseconds). If it is called multiple times while it is
 * waiting, it will only perform the action once at the end of the interval.
 *
 * This is particularly useful for limiting repeated user requests (e.g.
 * preventing a user from spamming a server with frequent view refreshes). For
 * more stateful throttling with support for pausing, resuming, and canceling
 * throttled actions, use {@code goog.async.Throttle}.
 *
 * @param {function(this:SCOPE):*} f Function to call.
 * @param {number} interval Interval over which to throttle. The function can
 *     only be called once per interval.
 * @param {SCOPE=} opt_scope Object in whose scope to call the function.
 * @return {function():undefined} Wrapped function.
 * @template SCOPE
 */
goog.functions.throttle = function(f, interval, opt_scope) {
  if (opt_scope) {
    f = goog.bind(f, opt_scope);
  }
  var timeout = null;
  var shouldFire = false;
  var fire = function() {
    timeout = goog.global.setTimeout(handleTimeout, interval);
    f();
  };
  var handleTimeout = function() {
    timeout = null;
    if (shouldFire) {
      shouldFire = false;
      fire();
    }
  };

  return function() {
    if (!timeout) {
      fire();
    } else {
      shouldFire = true;
    }
  };
};

/**
 * @license
 * Latitude/longitude spherical geodesy formulae taken from
 * http://www.movable-type.co.uk/scripts/latlong.html
 * Licensed under CC-BY-3.0.
 */

goog.provide('ol.Sphere');

goog.require('ol.math');


/**
 * @classdesc
 * Class to create objects that can be used with {@link
 * ol.geom.Polygon.circular}.
 *
 * For example to create a sphere whose radius is equal to the semi-major
 * axis of the WGS84 ellipsoid:
 *
 * ```js
 * var wgs84Sphere= new ol.Sphere(6378137);
 * ```
 *
 * @constructor
 * @param {number} radius Radius.
 * @api
 */
ol.Sphere = function(radius) {

  /**
   * @type {number}
   */
  this.radius = radius;

};


/**
 * Returns the geodesic area for a list of coordinates.
 *
 * [Reference](http://trs-new.jpl.nasa.gov/dspace/handle/2014/40409)
 * Robert. G. Chamberlain and William H. Duquette, "Some Algorithms for
 * Polygons on a Sphere", JPL Publication 07-03, Jet Propulsion
 * Laboratory, Pasadena, CA, June 2007
 *
 * @param {Array.<ol.Coordinate>} coordinates List of coordinates of a linear
 * ring. If the ring is oriented clockwise, the area will be positive,
 * otherwise it will be negative.
 * @return {number} Area.
 * @api
 */
ol.Sphere.prototype.geodesicArea = function(coordinates) {
  var area = 0, len = coordinates.length;
  var x1 = coordinates[len - 1][0];
  var y1 = coordinates[len - 1][1];
  for (var i = 0; i < len; i++) {
    var x2 = coordinates[i][0], y2 = coordinates[i][1];
    area += ol.math.toRadians(x2 - x1) *
        (2 + Math.sin(ol.math.toRadians(y1)) +
        Math.sin(ol.math.toRadians(y2)));
    x1 = x2;
    y1 = y2;
  }
  return area * this.radius * this.radius / 2.0;
};


/**
 * Returns the distance from c1 to c2 using the haversine formula.
 *
 * @param {ol.Coordinate} c1 Coordinate 1.
 * @param {ol.Coordinate} c2 Coordinate 2.
 * @return {number} Haversine distance.
 * @api
 */
ol.Sphere.prototype.haversineDistance = function(c1, c2) {
  var lat1 = ol.math.toRadians(c1[1]);
  var lat2 = ol.math.toRadians(c2[1]);
  var deltaLatBy2 = (lat2 - lat1) / 2;
  var deltaLonBy2 = ol.math.toRadians(c2[0] - c1[0]) / 2;
  var a = Math.sin(deltaLatBy2) * Math.sin(deltaLatBy2) +
      Math.sin(deltaLonBy2) * Math.sin(deltaLonBy2) *
      Math.cos(lat1) * Math.cos(lat2);
  return 2 * this.radius * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
};


/**
 * Returns the coordinate at the given distance and bearing from `c1`.
 *
 * @param {ol.Coordinate} c1 The origin point (`[lon, lat]` in degrees).
 * @param {number} distance The great-circle distance between the origin
 *     point and the target point.
 * @param {number} bearing The bearing (in radians).
 * @return {ol.Coordinate} The target point.
 */
ol.Sphere.prototype.offset = function(c1, distance, bearing) {
  var lat1 = ol.math.toRadians(c1[1]);
  var lon1 = ol.math.toRadians(c1[0]);
  var dByR = distance / this.radius;
  var lat = Math.asin(
      Math.sin(lat1) * Math.cos(dByR) +
      Math.cos(lat1) * Math.sin(dByR) * Math.cos(bearing));
  var lon = lon1 + Math.atan2(
      Math.sin(bearing) * Math.sin(dByR) * Math.cos(lat1),
      Math.cos(dByR) - Math.sin(lat1) * Math.sin(lat));
  return [ol.math.toDegrees(lon), ol.math.toDegrees(lat)];
};

goog.provide('ol.sphere.NORMAL');

goog.require('ol.Sphere');


/**
 * The normal sphere.
 * @const
 * @type {ol.Sphere}
 */
ol.sphere.NORMAL = new ol.Sphere(6370997);

goog.provide('ol.proj');
goog.provide('ol.proj.METERS_PER_UNIT');
goog.provide('ol.proj.Projection');
goog.provide('ol.proj.ProjectionLike');
goog.provide('ol.proj.Units');

goog.require('goog.asserts');
goog.require('goog.object');
goog.require('ol');
goog.require('ol.Extent');
goog.require('ol.TransformFunction');
goog.require('ol.extent');
goog.require('ol.sphere.NORMAL');


/**
 * A projection as {@link ol.proj.Projection}, SRS identifier string or
 * undefined.
 * @typedef {ol.proj.Projection|string|undefined} ol.proj.ProjectionLike
 * @api stable
 */
ol.proj.ProjectionLike;


/**
 * Projection units: `'degrees'`, `'ft'`, `'m'`, `'pixels'`, `'tile-pixels'` or
 * `'us-ft'`.
 * @enum {string}
 * @api stable
 */
ol.proj.Units = {
  DEGREES: 'degrees',
  FEET: 'ft',
  METERS: 'm',
  PIXELS: 'pixels',
  TILE_PIXELS: 'tile-pixels',
  USFEET: 'us-ft'
};


/**
 * Meters per unit lookup table.
 * @const
 * @type {Object.<ol.proj.Units, number>}
 * @api stable
 */
ol.proj.METERS_PER_UNIT = {};
ol.proj.METERS_PER_UNIT[ol.proj.Units.DEGREES] =
    2 * Math.PI * ol.sphere.NORMAL.radius / 360;
ol.proj.METERS_PER_UNIT[ol.proj.Units.FEET] = 0.3048;
ol.proj.METERS_PER_UNIT[ol.proj.Units.METERS] = 1;
ol.proj.METERS_PER_UNIT[ol.proj.Units.USFEET] = 1200 / 3937;


/**
 * @classdesc
 * Projection definition class. One of these is created for each projection
 * supported in the application and stored in the {@link ol.proj} namespace.
 * You can use these in applications, but this is not required, as API params
 * and options use {@link ol.proj.ProjectionLike} which means the simple string
 * code will suffice.
 *
 * You can use {@link ol.proj.get} to retrieve the object for a particular
 * projection.
 *
 * The library includes definitions for `EPSG:4326` and `EPSG:3857`, together
 * with the following aliases:
 * * `EPSG:4326`: CRS:84, urn:ogc:def:crs:EPSG:6.6:4326,
 *     urn:ogc:def:crs:OGC:1.3:CRS84, urn:ogc:def:crs:OGC:2:84,
 *     http://www.opengis.net/gml/srs/epsg.xml#4326,
 *     urn:x-ogc:def:crs:EPSG:4326
 * * `EPSG:3857`: EPSG:102100, EPSG:102113, EPSG:900913,
 *     urn:ogc:def:crs:EPSG:6.18:3:3857,
 *     http://www.opengis.net/gml/srs/epsg.xml#3857
 *
 * If you use proj4js, aliases can be added using `proj4.defs()`; see
 * [documentation](https://github.com/proj4js/proj4js). To set an alternative
 * namespace for proj4, use {@link ol.proj.setProj4}.
 *
 * @constructor
 * @param {olx.ProjectionOptions} options Projection options.
 * @struct
 * @api stable
 */
ol.proj.Projection = function(options) {

  /**
   * @private
   * @type {string}
   */
  this.code_ = options.code;

  /**
   * @private
   * @type {ol.proj.Units}
   */
  this.units_ = /** @type {ol.proj.Units} */ (options.units);

  /**
   * @private
   * @type {ol.Extent}
   */
  this.extent_ = options.extent !== undefined ? options.extent : null;

  /**
   * @private
   * @type {ol.Extent}
   */
  this.worldExtent_ = options.worldExtent !== undefined ?
      options.worldExtent : null;

  /**
   * @private
   * @type {string}
   */
  this.axisOrientation_ = options.axisOrientation !== undefined ?
      options.axisOrientation : 'enu';

  /**
   * @private
   * @type {boolean}
   */
  this.global_ = options.global !== undefined ? options.global : false;


  /**
   * @private
   * @type {boolean}
   */
  this.canWrapX_ = !!(this.global_ && this.extent_);

  /**
  * @private
  * @type {function(number, ol.Coordinate):number}
  */
  this.getPointResolutionFunc_ = options.getPointResolution !== undefined ?
      options.getPointResolution : this.getPointResolution_;

  /**
   * @private
   * @type {ol.tilegrid.TileGrid}
   */
  this.defaultTileGrid_ = null;

  /**
   * @private
   * @type {number|undefined}
   */
  this.metersPerUnit_ = options.metersPerUnit;

  var projections = ol.proj.projections_;
  var code = options.code;
  goog.asserts.assert(code !== undefined,
      'Option "code" is required for constructing instance');
  if (ol.ENABLE_PROJ4JS) {
    var proj4js = ol.proj.proj4_ || goog.global['proj4'];
    if (typeof proj4js == 'function' && projections[code] === undefined) {
      var def = proj4js.defs(code);
      if (def !== undefined) {
        if (def.axis !== undefined && options.axisOrientation === undefined) {
          this.axisOrientation_ = def.axis;
        }
        if (options.metersPerUnit === undefined) {
          this.metersPerUnit_ = def.to_meter;
        }
        if (options.units === undefined) {
          this.units_ = def.units;
        }
        var currentCode, currentDef, currentProj, proj4Transform;
        for (currentCode in projections) {
          currentDef = proj4js.defs(currentCode);
          if (currentDef !== undefined) {
            currentProj = ol.proj.get(currentCode);
            if (currentDef === def) {
              ol.proj.addEquivalentProjections([currentProj, this]);
            } else {
              proj4Transform = proj4js(currentCode, code);
              ol.proj.addCoordinateTransforms(currentProj, this,
                  proj4Transform.forward, proj4Transform.inverse);
            }
          }
        }
      }
    }
  }

};


/**
 * @return {boolean} The projection is suitable for wrapping the x-axis
 */
ol.proj.Projection.prototype.canWrapX = function() {
  return this.canWrapX_;
};


/**
 * Get the code for this projection, e.g. 'EPSG:4326'.
 * @return {string} Code.
 * @api stable
 */
ol.proj.Projection.prototype.getCode = function() {
  return this.code_;
};


/**
 * Get the validity extent for this projection.
 * @return {ol.Extent} Extent.
 * @api stable
 */
ol.proj.Projection.prototype.getExtent = function() {
  return this.extent_;
};


/**
 * Get the units of this projection.
 * @return {ol.proj.Units} Units.
 * @api stable
 */
ol.proj.Projection.prototype.getUnits = function() {
  return this.units_;
};


/**
 * Get the amount of meters per unit of this projection.  If the projection is
 * not configured with `metersPerUnit` or a units identifier, the return is
 * `undefined`.
 * @return {number|undefined} Meters.
 * @api stable
 */
ol.proj.Projection.prototype.getMetersPerUnit = function() {
  return this.metersPerUnit_ || ol.proj.METERS_PER_UNIT[this.units_];
};


/**
 * Get the world extent for this projection.
 * @return {ol.Extent} Extent.
 * @api
 */
ol.proj.Projection.prototype.getWorldExtent = function() {
  return this.worldExtent_;
};


/**
 * Get the axis orientation of this projection.
 * Example values are:
 * enu - the default easting, northing, elevation.
 * neu - northing, easting, up - useful for "lat/long" geographic coordinates,
 *     or south orientated transverse mercator.
 * wnu - westing, northing, up - some planetary coordinate systems have
 *     "west positive" coordinate systems
 * @return {string} Axis orientation.
 */
ol.proj.Projection.prototype.getAxisOrientation = function() {
  return this.axisOrientation_;
};


/**
 * Is this projection a global projection which spans the whole world?
 * @return {boolean} Whether the projection is global.
 * @api stable
 */
ol.proj.Projection.prototype.isGlobal = function() {
  return this.global_;
};


/**
* Set if the projection is a global projection which spans the whole world
* @param {boolean} global Whether the projection is global.
* @api stable
*/
ol.proj.Projection.prototype.setGlobal = function(global) {
  this.global_ = global;
  this.canWrapX_ = !!(global && this.extent_);
};


/**
 * @return {ol.tilegrid.TileGrid} The default tile grid.
 */
ol.proj.Projection.prototype.getDefaultTileGrid = function() {
  return this.defaultTileGrid_;
};


/**
 * @param {ol.tilegrid.TileGrid} tileGrid The default tile grid.
 */
ol.proj.Projection.prototype.setDefaultTileGrid = function(tileGrid) {
  this.defaultTileGrid_ = tileGrid;
};


/**
 * Set the validity extent for this projection.
 * @param {ol.Extent} extent Extent.
 * @api stable
 */
ol.proj.Projection.prototype.setExtent = function(extent) {
  this.extent_ = extent;
  this.canWrapX_ = !!(this.global_ && extent);
};


/**
 * Set the world extent for this projection.
 * @param {ol.Extent} worldExtent World extent
 *     [minlon, minlat, maxlon, maxlat].
 * @api
 */
ol.proj.Projection.prototype.setWorldExtent = function(worldExtent) {
  this.worldExtent_ = worldExtent;
};


/**
* Set the getPointResolution function for this projection.
* @param {function(number, ol.Coordinate):number} func Function
* @api
*/
ol.proj.Projection.prototype.setGetPointResolution = function(func) {
  this.getPointResolutionFunc_ = func;
};


/**
* Default version.
* Get the resolution of the point in degrees or distance units.
* For projections with degrees as the unit this will simply return the
* provided resolution. For other projections the point resolution is
* estimated by transforming the 'point' pixel to EPSG:4326,
* measuring its width and height on the normal sphere,
* and taking the average of the width and height.
* @param {number} resolution Nominal resolution in projection units.
* @param {ol.Coordinate} point Point to find adjusted resolution at.
* @return {number} Point resolution at point in projection units.
* @private
*/
ol.proj.Projection.prototype.getPointResolution_ = function(resolution, point) {
  var units = this.getUnits();
  if (units == ol.proj.Units.DEGREES) {
    return resolution;
  } else {
    // Estimate point resolution by transforming the center pixel to EPSG:4326,
    // measuring its width and height on the normal sphere, and taking the
    // average of the width and height.
    var toEPSG4326 = ol.proj.getTransformFromProjections(
        this, ol.proj.get('EPSG:4326'));
    var vertices = [
      point[0] - resolution / 2, point[1],
      point[0] + resolution / 2, point[1],
      point[0], point[1] - resolution / 2,
      point[0], point[1] + resolution / 2
    ];
    vertices = toEPSG4326(vertices, vertices, 2);
    var width = ol.sphere.NORMAL.haversineDistance(
        vertices.slice(0, 2), vertices.slice(2, 4));
    var height = ol.sphere.NORMAL.haversineDistance(
        vertices.slice(4, 6), vertices.slice(6, 8));
    var pointResolution = (width + height) / 2;
    var metersPerUnit = this.getMetersPerUnit();
    if (metersPerUnit !== undefined) {
      pointResolution /= metersPerUnit;
    }
    return pointResolution;
  }
};


/**
 * Get the resolution of the point in degrees or distance units.
 * For projections with degrees as the unit this will simply return the
 * provided resolution. The default for other projections is to estimate
 * the point resolution by transforming the 'point' pixel to EPSG:4326,
 * measuring its width and height on the normal sphere,
 * and taking the average of the width and height.
 * An alternative implementation may be given when constructing a
 * projection. For many local projections,
 * such a custom function will return the resolution unchanged.
 * @param {number} resolution Resolution in projection units.
 * @param {ol.Coordinate} point Point.
 * @return {number} Point resolution in projection units.
 * @api
 */
ol.proj.Projection.prototype.getPointResolution = function(resolution, point) {
  return this.getPointResolutionFunc_(resolution, point);
};


/**
 * @private
 * @type {Object.<string, ol.proj.Projection>}
 */
ol.proj.projections_ = {};


/**
 * @private
 * @type {Object.<string, Object.<string, ol.TransformFunction>>}
 */
ol.proj.transforms_ = {};


/**
 * @private
 * @type {proj4}
 */
ol.proj.proj4_ = null;


if (ol.ENABLE_PROJ4JS) {
  /**
   * Register proj4. If not explicitly registered, it will be assumed that
   * proj4js will be loaded in the global namespace. For example in a
   * browserify ES6 environment you could use:
   *
   *     import ol from 'openlayers';
   *     import proj4 from 'proj4';
   *     ol.proj.setProj4(proj4);
   *
   * @param {proj4} proj4 Proj4.
   * @api
   */
  ol.proj.setProj4 = function(proj4) {
    goog.asserts.assert(typeof proj4 == 'function',
        'proj4 argument should be a function');
    ol.proj.proj4_ = proj4;
  };
}


/**
 * Registers transformation functions that don't alter coordinates. Those allow
 * to transform between projections with equal meaning.
 *
 * @param {Array.<ol.proj.Projection>} projections Projections.
 * @api
 */
ol.proj.addEquivalentProjections = function(projections) {
  ol.proj.addProjections(projections);
  projections.forEach(function(source) {
    projections.forEach(function(destination) {
      if (source !== destination) {
        ol.proj.addTransform(source, destination, ol.proj.cloneTransform);
      }
    });
  });
};


/**
 * Registers transformation functions to convert coordinates in any projection
 * in projection1 to any projection in projection2.
 *
 * @param {Array.<ol.proj.Projection>} projections1 Projections with equal
 *     meaning.
 * @param {Array.<ol.proj.Projection>} projections2 Projections with equal
 *     meaning.
 * @param {ol.TransformFunction} forwardTransform Transformation from any
 *   projection in projection1 to any projection in projection2.
 * @param {ol.TransformFunction} inverseTransform Transform from any projection
 *   in projection2 to any projection in projection1..
 */
ol.proj.addEquivalentTransforms = function(projections1, projections2, forwardTransform, inverseTransform) {
  projections1.forEach(function(projection1) {
    projections2.forEach(function(projection2) {
      ol.proj.addTransform(projection1, projection2, forwardTransform);
      ol.proj.addTransform(projection2, projection1, inverseTransform);
    });
  });
};


/**
 * Add a Projection object to the list of supported projections that can be
 * looked up by their code.
 *
 * @param {ol.proj.Projection} projection Projection instance.
 * @api stable
 */
ol.proj.addProjection = function(projection) {
  ol.proj.projections_[projection.getCode()] = projection;
  ol.proj.addTransform(projection, projection, ol.proj.cloneTransform);
};


/**
 * @param {Array.<ol.proj.Projection>} projections Projections.
 */
ol.proj.addProjections = function(projections) {
  var addedProjections = [];
  projections.forEach(function(projection) {
    addedProjections.push(ol.proj.addProjection(projection));
  });
};


/**
 * FIXME empty description for jsdoc
 */
ol.proj.clearAllProjections = function() {
  ol.proj.projections_ = {};
  ol.proj.transforms_ = {};
};


/**
 * @param {ol.proj.Projection|string|undefined} projection Projection.
 * @param {string} defaultCode Default code.
 * @return {ol.proj.Projection} Projection.
 */
ol.proj.createProjection = function(projection, defaultCode) {
  if (!projection) {
    return ol.proj.get(defaultCode);
  } else if (goog.isString(projection)) {
    return ol.proj.get(projection);
  } else {
    goog.asserts.assertInstanceof(projection, ol.proj.Projection,
        'projection should be an ol.proj.Projection');
    return projection;
  }
};


/**
 * Registers a conversion function to convert coordinates from the source
 * projection to the destination projection.
 *
 * @param {ol.proj.Projection} source Source.
 * @param {ol.proj.Projection} destination Destination.
 * @param {ol.TransformFunction} transformFn Transform.
 */
ol.proj.addTransform = function(source, destination, transformFn) {
  var sourceCode = source.getCode();
  var destinationCode = destination.getCode();
  var transforms = ol.proj.transforms_;
  if (!(sourceCode in transforms)) {
    transforms[sourceCode] = {};
  }
  transforms[sourceCode][destinationCode] = transformFn;
};


/**
 * Registers coordinate transform functions to convert coordinates between the
 * source projection and the destination projection.
 * The forward and inverse functions convert coordinate pairs; this function
 * converts these into the functions used internally which also handle
 * extents and coordinate arrays.
 *
 * @param {ol.proj.ProjectionLike} source Source projection.
 * @param {ol.proj.ProjectionLike} destination Destination projection.
 * @param {function(ol.Coordinate): ol.Coordinate} forward The forward transform
 *     function (that is, from the source projection to the destination
 *     projection) that takes a {@link ol.Coordinate} as argument and returns
 *     the transformed {@link ol.Coordinate}.
 * @param {function(ol.Coordinate): ol.Coordinate} inverse The inverse transform
 *     function (that is, from the destination projection to the source
 *     projection) that takes a {@link ol.Coordinate} as argument and returns
 *     the transformed {@link ol.Coordinate}.
 * @api stable
 */
ol.proj.addCoordinateTransforms = function(source, destination, forward, inverse) {
  var sourceProj = ol.proj.get(source);
  var destProj = ol.proj.get(destination);
  ol.proj.addTransform(sourceProj, destProj,
      ol.proj.createTransformFromCoordinateTransform(forward));
  ol.proj.addTransform(destProj, sourceProj,
      ol.proj.createTransformFromCoordinateTransform(inverse));
};


/**
 * Creates a {@link ol.TransformFunction} from a simple 2D coordinate transform
 * function.
 * @param {function(ol.Coordinate): ol.Coordinate} transform Coordinate
 *     transform.
 * @return {ol.TransformFunction} Transform function.
 */
ol.proj.createTransformFromCoordinateTransform = function(transform) {
  return (
      /**
       * @param {Array.<number>} input Input.
       * @param {Array.<number>=} opt_output Output.
       * @param {number=} opt_dimension Dimension.
       * @return {Array.<number>} Output.
       */
      function(input, opt_output, opt_dimension) {
        var length = input.length;
        var dimension = opt_dimension !== undefined ? opt_dimension : 2;
        var output = opt_output !== undefined ? opt_output : new Array(length);
        var point, i, j;
        for (i = 0; i < length; i += dimension) {
          point = transform([input[i], input[i + 1]]);
          output[i] = point[0];
          output[i + 1] = point[1];
          for (j = dimension - 1; j >= 2; --j) {
            output[i + j] = input[i + j];
          }
        }
        return output;
      });
};


/**
 * Unregisters the conversion function to convert coordinates from the source
 * projection to the destination projection.  This method is used to clean up
 * cached transforms during testing.
 *
 * @param {ol.proj.Projection} source Source projection.
 * @param {ol.proj.Projection} destination Destination projection.
 * @return {ol.TransformFunction} transformFn The unregistered transform.
 */
ol.proj.removeTransform = function(source, destination) {
  var sourceCode = source.getCode();
  var destinationCode = destination.getCode();
  var transforms = ol.proj.transforms_;
  goog.asserts.assert(sourceCode in transforms,
      'sourceCode should be in transforms');
  goog.asserts.assert(destinationCode in transforms[sourceCode],
      'destinationCode should be in transforms of sourceCode');
  var transform = transforms[sourceCode][destinationCode];
  delete transforms[sourceCode][destinationCode];
  if (goog.object.isEmpty(transforms[sourceCode])) {
    delete transforms[sourceCode];
  }
  return transform;
};


/**
 * Transforms a coordinate from longitude/latitude to a different projection.
 * @param {ol.Coordinate} coordinate Coordinate as longitude and latitude, i.e.
 *     an array with longitude as 1st and latitude as 2nd element.
 * @param {ol.proj.ProjectionLike=} opt_projection Target projection. The
 *     default is Web Mercator, i.e. 'EPSG:3857'.
 * @return {ol.Coordinate} Coordinate projected to the target projection.
 * @api stable
 */
ol.proj.fromLonLat = function(coordinate, opt_projection) {
  return ol.proj.transform(coordinate, 'EPSG:4326',
      opt_projection !== undefined ? opt_projection : 'EPSG:3857');
};


/**
 * Transforms a coordinate to longitude/latitude.
 * @param {ol.Coordinate} coordinate Projected coordinate.
 * @param {ol.proj.ProjectionLike=} opt_projection Projection of the coordinate.
 *     The default is Web Mercator, i.e. 'EPSG:3857'.
 * @return {ol.Coordinate} Coordinate as longitude and latitude, i.e. an array
 *     with longitude as 1st and latitude as 2nd element.
 * @api stable
 */
ol.proj.toLonLat = function(coordinate, opt_projection) {
  return ol.proj.transform(coordinate,
      opt_projection !== undefined ? opt_projection : 'EPSG:3857', 'EPSG:4326');
};


/**
 * Fetches a Projection object for the code specified.
 *
 * @param {ol.proj.ProjectionLike} projectionLike Either a code string which is
 *     a combination of authority and identifier such as "EPSG:4326", or an
 *     existing projection object, or undefined.
 * @return {ol.proj.Projection} Projection object, or null if not in list.
 * @api stable
 */
ol.proj.get = function(projectionLike) {
  var projection;
  if (projectionLike instanceof ol.proj.Projection) {
    projection = projectionLike;
  } else if (goog.isString(projectionLike)) {
    var code = projectionLike;
    projection = ol.proj.projections_[code];
    if (ol.ENABLE_PROJ4JS) {
      var proj4js = ol.proj.proj4_ || goog.global['proj4'];
      if (projection === undefined && typeof proj4js == 'function' &&
          proj4js.defs(code) !== undefined) {
        projection = new ol.proj.Projection({code: code});
        ol.proj.addProjection(projection);
      }
    }
  } else {
    projection = null;
  }
  return projection;
};


/**
 * Checks if two projections are the same, that is every coordinate in one
 * projection does represent the same geographic point as the same coordinate in
 * the other projection.
 *
 * @param {ol.proj.Projection} projection1 Projection 1.
 * @param {ol.proj.Projection} projection2 Projection 2.
 * @return {boolean} Equivalent.
 */
ol.proj.equivalent = function(projection1, projection2) {
  if (projection1 === projection2) {
    return true;
  }
  var equalUnits = projection1.getUnits() === projection2.getUnits();
  if (projection1.getCode() === projection2.getCode()) {
    return equalUnits;
  } else {
    var transformFn = ol.proj.getTransformFromProjections(
        projection1, projection2);
    return transformFn === ol.proj.cloneTransform && equalUnits;
  }
};


/**
 * Given the projection-like objects, searches for a transformation
 * function to convert a coordinates array from the source projection to the
 * destination projection.
 *
 * @param {ol.proj.ProjectionLike} source Source.
 * @param {ol.proj.ProjectionLike} destination Destination.
 * @return {ol.TransformFunction} Transform function.
 * @api stable
 */
ol.proj.getTransform = function(source, destination) {
  var sourceProjection = ol.proj.get(source);
  var destinationProjection = ol.proj.get(destination);
  return ol.proj.getTransformFromProjections(
      sourceProjection, destinationProjection);
};


/**
 * Searches in the list of transform functions for the function for converting
 * coordinates from the source projection to the destination projection.
 *
 * @param {ol.proj.Projection} sourceProjection Source Projection object.
 * @param {ol.proj.Projection} destinationProjection Destination Projection
 *     object.
 * @return {ol.TransformFunction} Transform function.
 */
ol.proj.getTransformFromProjections = function(sourceProjection, destinationProjection) {
  var transforms = ol.proj.transforms_;
  var sourceCode = sourceProjection.getCode();
  var destinationCode = destinationProjection.getCode();
  var transform;
  if (sourceCode in transforms && destinationCode in transforms[sourceCode]) {
    transform = transforms[sourceCode][destinationCode];
  }
  if (transform === undefined) {
    goog.asserts.assert(transform !== undefined, 'transform should be defined');
    transform = ol.proj.identityTransform;
  }
  return transform;
};


/**
 * @param {Array.<number>} input Input coordinate array.
 * @param {Array.<number>=} opt_output Output array of coordinate values.
 * @param {number=} opt_dimension Dimension.
 * @return {Array.<number>} Input coordinate array (same array as input).
 */
ol.proj.identityTransform = function(input, opt_output, opt_dimension) {
  if (opt_output !== undefined && input !== opt_output) {
    // TODO: consider making this a warning instead
    goog.asserts.fail('This should not be used internally.');
    for (var i = 0, ii = input.length; i < ii; ++i) {
      opt_output[i] = input[i];
    }
    input = opt_output;
  }
  return input;
};


/**
 * @param {Array.<number>} input Input coordinate array.
 * @param {Array.<number>=} opt_output Output array of coordinate values.
 * @param {number=} opt_dimension Dimension.
 * @return {Array.<number>} Output coordinate array (new array, same coordinate
 *     values).
 */
ol.proj.cloneTransform = function(input, opt_output, opt_dimension) {
  var output;
  if (opt_output !== undefined) {
    for (var i = 0, ii = input.length; i < ii; ++i) {
      opt_output[i] = input[i];
    }
    output = opt_output;
  } else {
    output = input.slice();
  }
  return output;
};


/**
 * Transforms a coordinate from source projection to destination projection.
 * This returns a new coordinate (and does not modify the original).
 *
 * See {@link ol.proj.transformExtent} for extent transformation.
 * See the transform method of {@link ol.geom.Geometry} and its subclasses for
 * geometry transforms.
 *
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {ol.proj.ProjectionLike} source Source projection-like.
 * @param {ol.proj.ProjectionLike} destination Destination projection-like.
 * @return {ol.Coordinate} Coordinate.
 * @api stable
 */
ol.proj.transform = function(coordinate, source, destination) {
  var transformFn = ol.proj.getTransform(source, destination);
  return transformFn(coordinate, undefined, coordinate.length);
};


/**
 * Transforms an extent from source projection to destination projection.  This
 * returns a new extent (and does not modify the original).
 *
 * @param {ol.Extent} extent The extent to transform.
 * @param {ol.proj.ProjectionLike} source Source projection-like.
 * @param {ol.proj.ProjectionLike} destination Destination projection-like.
 * @return {ol.Extent} The transformed extent.
 * @api stable
 */
ol.proj.transformExtent = function(extent, source, destination) {
  var transformFn = ol.proj.getTransform(source, destination);
  return ol.extent.applyTransform(extent, transformFn);
};


/**
 * Transforms the given point to the destination projection.
 *
 * @param {ol.Coordinate} point Point.
 * @param {ol.proj.Projection} sourceProjection Source projection.
 * @param {ol.proj.Projection} destinationProjection Destination projection.
 * @return {ol.Coordinate} Point.
 */
ol.proj.transformWithProjections = function(point, sourceProjection, destinationProjection) {
  var transformFn = ol.proj.getTransformFromProjections(
      sourceProjection, destinationProjection);
  return transformFn(point);
};

goog.provide('ol.geom.Geometry');
goog.provide('ol.geom.GeometryLayout');
goog.provide('ol.geom.GeometryType');

goog.require('goog.asserts');
goog.require('goog.functions');
goog.require('ol.Object');
goog.require('ol.extent');
goog.require('ol.proj');
goog.require('ol.proj.Units');


/**
 * The geometry type. One of `'Point'`, `'LineString'`, `'LinearRing'`,
 * `'Polygon'`, `'MultiPoint'`, `'MultiLineString'`, `'MultiPolygon'`,
 * `'GeometryCollection'`, `'Circle'`.
 * @enum {string}
 * @api stable
 */
ol.geom.GeometryType = {
  POINT: 'Point',
  LINE_STRING: 'LineString',
  LINEAR_RING: 'LinearRing',
  POLYGON: 'Polygon',
  MULTI_POINT: 'MultiPoint',
  MULTI_LINE_STRING: 'MultiLineString',
  MULTI_POLYGON: 'MultiPolygon',
  GEOMETRY_COLLECTION: 'GeometryCollection',
  CIRCLE: 'Circle'
};


/**
 * The coordinate layout for geometries, indicating whether a 3rd or 4th z ('Z')
 * or measure ('M') coordinate is available. Supported values are `'XY'`,
 * `'XYZ'`, `'XYM'`, `'XYZM'`.
 * @enum {string}
 * @api stable
 */
ol.geom.GeometryLayout = {
  XY: 'XY',
  XYZ: 'XYZ',
  XYM: 'XYM',
  XYZM: 'XYZM'
};


/**
 * @classdesc
 * Abstract base class; normally only used for creating subclasses and not
 * instantiated in apps.
 * Base class for vector geometries.
 *
 * To get notified of changes to the geometry, register a listener for the
 * generic `change` event on your geometry instance.
 *
 * @constructor
 * @extends {ol.Object}
 * @api stable
 */
ol.geom.Geometry = function() {

  goog.base(this);

  /**
   * @private
   * @type {ol.Extent}
   */
  this.extent_ = ol.extent.createEmpty();

  /**
   * @private
   * @type {number}
   */
  this.extentRevision_ = -1;

  /**
   * @protected
   * @type {Object.<string, ol.geom.Geometry>}
   */
  this.simplifiedGeometryCache = {};

  /**
   * @protected
   * @type {number}
   */
  this.simplifiedGeometryMaxMinSquaredTolerance = 0;

  /**
   * @protected
   * @type {number}
   */
  this.simplifiedGeometryRevision = 0;

};
goog.inherits(ol.geom.Geometry, ol.Object);


/**
 * Make a complete copy of the geometry.
 * @function
 * @return {!ol.geom.Geometry} Clone.
 */
ol.geom.Geometry.prototype.clone = goog.abstractMethod;


/**
 * @param {number} x X.
 * @param {number} y Y.
 * @param {ol.Coordinate} closestPoint Closest point.
 * @param {number} minSquaredDistance Minimum squared distance.
 * @return {number} Minimum squared distance.
 */
ol.geom.Geometry.prototype.closestPointXY = goog.abstractMethod;


/**
 * Return the closest point of the geometry to the passed point as
 * {@link ol.Coordinate coordinate}.
 * @param {ol.Coordinate} point Point.
 * @param {ol.Coordinate=} opt_closestPoint Closest point.
 * @return {ol.Coordinate} Closest point.
 * @api stable
 */
ol.geom.Geometry.prototype.getClosestPoint = function(point, opt_closestPoint) {
  var closestPoint = opt_closestPoint ? opt_closestPoint : [NaN, NaN];
  this.closestPointXY(point[0], point[1], closestPoint, Infinity);
  return closestPoint;
};


/**
 * @param {ol.Coordinate} coordinate Coordinate.
 * @return {boolean} Contains coordinate.
 */
ol.geom.Geometry.prototype.containsCoordinate = function(coordinate) {
  return this.containsXY(coordinate[0], coordinate[1]);
};


/**
 * @param {ol.Extent} extent Extent.
 * @protected
 * @return {ol.Extent} extent Extent.
 */
ol.geom.Geometry.prototype.computeExtent = goog.abstractMethod;


/**
 * @param {number} x X.
 * @param {number} y Y.
 * @return {boolean} Contains (x, y).
 */
ol.geom.Geometry.prototype.containsXY = goog.functions.FALSE;


/**
 * Get the extent of the geometry.
 * @param {ol.Extent=} opt_extent Extent.
 * @return {ol.Extent} extent Extent.
 * @api stable
 */
ol.geom.Geometry.prototype.getExtent = function(opt_extent) {
  if (this.extentRevision_ != this.getRevision()) {
    this.extent_ = this.computeExtent(this.extent_);
    this.extentRevision_ = this.getRevision();
  }
  return ol.extent.returnOrUpdate(this.extent_, opt_extent);
};


/**
 * Create a simplified version of this geometry.  For linestrings, this uses
 * the the {@link
 * https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm
 * Douglas Peucker} algorithm.  For polygons, a quantization-based
 * simplification is used to preserve topology.
 * @function
 * @param {number} tolerance The tolerance distance for simplification.
 * @return {ol.geom.Geometry} A new, simplified version of the original
 *     geometry.
 * @api
 */
ol.geom.Geometry.prototype.simplify = function(tolerance) {
  return this.getSimplifiedGeometry(tolerance * tolerance);
};


/**
 * Create a simplified version of this geometry using the Douglas Peucker
 * algorithm.
 * @see https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm
 * @function
 * @param {number} squaredTolerance Squared tolerance.
 * @return {ol.geom.Geometry} Simplified geometry.
 */
ol.geom.Geometry.prototype.getSimplifiedGeometry = goog.abstractMethod;


/**
 * Get the type of this geometry.
 * @function
 * @return {ol.geom.GeometryType} Geometry type.
 */
ol.geom.Geometry.prototype.getType = goog.abstractMethod;


/**
 * Apply a transform function to each coordinate of the geometry.
 * The geometry is modified in place.
 * If you do not want the geometry modified in place, first clone() it and
 * then use this function on the clone.
 * @function
 * @param {ol.TransformFunction} transformFn Transform.
 */
ol.geom.Geometry.prototype.applyTransform = goog.abstractMethod;


/**
 * Test if the geometry and the passed extent intersect.
 * @param {ol.Extent} extent Extent.
 * @return {boolean} `true` if the geometry and the extent intersect.
 * @function
 */
ol.geom.Geometry.prototype.intersectsExtent = goog.abstractMethod;


/**
 * Translate the geometry.  This modifies the geometry coordinates in place.  If
 * instead you want a new geometry, first `clone()` this geometry.
 * @param {number} deltaX Delta X.
 * @param {number} deltaY Delta Y.
 * @function
 */
ol.geom.Geometry.prototype.translate = goog.abstractMethod;


/**
 * Transform each coordinate of the geometry from one coordinate reference
 * system to another. The geometry is modified in place.
 * For example, a line will be transformed to a line and a circle to a circle.
 * If you do not want the geometry modified in place, first clone() it and
 * then use this function on the clone.
 *
 * @param {ol.proj.ProjectionLike} source The current projection.  Can be a
 *     string identifier or a {@link ol.proj.Projection} object.
 * @param {ol.proj.ProjectionLike} destination The desired projection.  Can be a
 *     string identifier or a {@link ol.proj.Projection} object.
 * @return {ol.geom.Geometry} This geometry.  Note that original geometry is
 *     modified in place.
 * @api stable
 */
ol.geom.Geometry.prototype.transform = function(source, destination) {
  goog.asserts.assert(
      ol.proj.get(source).getUnits() !== ol.proj.Units.TILE_PIXELS &&
      ol.proj.get(destination).getUnits() !== ol.proj.Units.TILE_PIXELS,
      'cannot transform geometries with TILE_PIXELS units');
  this.applyTransform(ol.proj.getTransform(source, destination));
  return this;
};

goog.provide('ol.geom.flat.transform');

goog.require('goog.vec.Mat4');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {goog.vec.Mat4.Number} transform Transform.
 * @param {Array.<number>=} opt_dest Destination.
 * @return {Array.<number>} Transformed coordinates.
 */
ol.geom.flat.transform.transform2D = function(flatCoordinates, offset, end, stride, transform, opt_dest) {
  var m00 = goog.vec.Mat4.getElement(transform, 0, 0);
  var m10 = goog.vec.Mat4.getElement(transform, 1, 0);
  var m01 = goog.vec.Mat4.getElement(transform, 0, 1);
  var m11 = goog.vec.Mat4.getElement(transform, 1, 1);
  var m03 = goog.vec.Mat4.getElement(transform, 0, 3);
  var m13 = goog.vec.Mat4.getElement(transform, 1, 3);
  var dest = opt_dest ? opt_dest : [];
  var i = 0;
  var j;
  for (j = offset; j < end; j += stride) {
    var x = flatCoordinates[j];
    var y = flatCoordinates[j + 1];
    dest[i++] = m00 * x + m01 * y + m03;
    dest[i++] = m10 * x + m11 * y + m13;
  }
  if (opt_dest && dest.length != i) {
    dest.length = i;
  }
  return dest;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {number} deltaX Delta X.
 * @param {number} deltaY Delta Y.
 * @param {Array.<number>=} opt_dest Destination.
 * @return {Array.<number>} Transformed coordinates.
 */
ol.geom.flat.transform.translate = function(flatCoordinates, offset, end, stride, deltaX, deltaY, opt_dest) {
  var dest = opt_dest ? opt_dest : [];
  var i = 0;
  var j, k;
  for (j = offset; j < end; j += stride) {
    dest[i++] = flatCoordinates[j] + deltaX;
    dest[i++] = flatCoordinates[j + 1] + deltaY;
    for (k = j + 2; k < j + stride; ++k) {
      dest[i++] = flatCoordinates[k];
    }
  }
  if (opt_dest && dest.length != i) {
    dest.length = i;
  }
  return dest;
};

goog.provide('ol.geom.SimpleGeometry');

goog.require('goog.asserts');
goog.require('goog.functions');
goog.require('goog.object');
goog.require('ol.extent');
goog.require('ol.geom.Geometry');
goog.require('ol.geom.GeometryLayout');
goog.require('ol.geom.flat.transform');


/**
 * @classdesc
 * Abstract base class; only used for creating subclasses; do not instantiate
 * in apps, as cannot be rendered.
 *
 * @constructor
 * @extends {ol.geom.Geometry}
 * @api stable
 */
ol.geom.SimpleGeometry = function() {

  goog.base(this);

  /**
   * @protected
   * @type {ol.geom.GeometryLayout}
   */
  this.layout = ol.geom.GeometryLayout.XY;

  /**
   * @protected
   * @type {number}
   */
  this.stride = 2;

  /**
   * @protected
   * @type {Array.<number>}
   */
  this.flatCoordinates = null;

};
goog.inherits(ol.geom.SimpleGeometry, ol.geom.Geometry);


/**
 * @param {number} stride Stride.
 * @private
 * @return {ol.geom.GeometryLayout} layout Layout.
 */
ol.geom.SimpleGeometry.getLayoutForStride_ = function(stride) {
  if (stride == 2) {
    return ol.geom.GeometryLayout.XY;
  } else if (stride == 3) {
    return ol.geom.GeometryLayout.XYZ;
  } else if (stride == 4) {
    return ol.geom.GeometryLayout.XYZM;
  } else {
    goog.asserts.fail('unsupported stride: ' + stride);
  }
};


/**
 * @param {ol.geom.GeometryLayout} layout Layout.
 * @return {number} Stride.
 */
ol.geom.SimpleGeometry.getStrideForLayout = function(layout) {
  if (layout == ol.geom.GeometryLayout.XY) {
    return 2;
  } else if (layout == ol.geom.GeometryLayout.XYZ) {
    return 3;
  } else if (layout == ol.geom.GeometryLayout.XYM) {
    return 3;
  } else if (layout == ol.geom.GeometryLayout.XYZM) {
    return 4;
  } else {
    goog.asserts.fail('unsupported layout: ' + layout);
  }
};


/**
 * @inheritDoc
 */
ol.geom.SimpleGeometry.prototype.containsXY = goog.functions.FALSE;


/**
 * @inheritDoc
 */
ol.geom.SimpleGeometry.prototype.computeExtent = function(extent) {
  return ol.extent.createOrUpdateFromFlatCoordinates(
      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
      extent);
};


/**
 * @return {Array} Coordinates.
 */
ol.geom.SimpleGeometry.prototype.getCoordinates = goog.abstractMethod;


/**
 * Return the first coordinate of the geometry.
 * @return {ol.Coordinate} First coordinate.
 * @api stable
 */
ol.geom.SimpleGeometry.prototype.getFirstCoordinate = function() {
  return this.flatCoordinates.slice(0, this.stride);
};


/**
 * @return {Array.<number>} Flat coordinates.
 */
ol.geom.SimpleGeometry.prototype.getFlatCoordinates = function() {
  return this.flatCoordinates;
};


/**
 * Return the last coordinate of the geometry.
 * @return {ol.Coordinate} Last point.
 * @api stable
 */
ol.geom.SimpleGeometry.prototype.getLastCoordinate = function() {
  return this.flatCoordinates.slice(this.flatCoordinates.length - this.stride);
};


/**
 * Return the {@link ol.geom.GeometryLayout layout} of the geometry.
 * @return {ol.geom.GeometryLayout} Layout.
 * @api stable
 */
ol.geom.SimpleGeometry.prototype.getLayout = function() {
  return this.layout;
};


/**
 * @inheritDoc
 */
ol.geom.SimpleGeometry.prototype.getSimplifiedGeometry = function(squaredTolerance) {
  if (this.simplifiedGeometryRevision != this.getRevision()) {
    goog.object.clear(this.simplifiedGeometryCache);
    this.simplifiedGeometryMaxMinSquaredTolerance = 0;
    this.simplifiedGeometryRevision = this.getRevision();
  }
  // If squaredTolerance is negative or if we know that simplification will not
  // have any effect then just return this.
  if (squaredTolerance < 0 ||
      (this.simplifiedGeometryMaxMinSquaredTolerance !== 0 &&
       squaredTolerance <= this.simplifiedGeometryMaxMinSquaredTolerance)) {
    return this;
  }
  var key = squaredTolerance.toString();
  if (this.simplifiedGeometryCache.hasOwnProperty(key)) {
    return this.simplifiedGeometryCache[key];
  } else {
    var simplifiedGeometry =
        this.getSimplifiedGeometryInternal(squaredTolerance);
    var simplifiedFlatCoordinates = simplifiedGeometry.getFlatCoordinates();
    if (simplifiedFlatCoordinates.length < this.flatCoordinates.length) {
      this.simplifiedGeometryCache[key] = simplifiedGeometry;
      return simplifiedGeometry;
    } else {
      // Simplification did not actually remove any coordinates.  We now know
      // that any calls to getSimplifiedGeometry with a squaredTolerance less
      // than or equal to the current squaredTolerance will also not have any
      // effect.  This allows us to short circuit simplification (saving CPU
      // cycles) and prevents the cache of simplified geometries from filling
      // up with useless identical copies of this geometry (saving memory).
      this.simplifiedGeometryMaxMinSquaredTolerance = squaredTolerance;
      return this;
    }
  }
};


/**
 * @param {number} squaredTolerance Squared tolerance.
 * @return {ol.geom.SimpleGeometry} Simplified geometry.
 * @protected
 */
ol.geom.SimpleGeometry.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) {
  return this;
};


/**
 * @return {number} Stride.
 */
ol.geom.SimpleGeometry.prototype.getStride = function() {
  return this.stride;
};


/**
 * @param {ol.geom.GeometryLayout} layout Layout.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @protected
 */
ol.geom.SimpleGeometry.prototype.setFlatCoordinatesInternal = function(layout, flatCoordinates) {
  this.stride = ol.geom.SimpleGeometry.getStrideForLayout(layout);
  this.layout = layout;
  this.flatCoordinates = flatCoordinates;
};


/**
 * @param {Array} coordinates Coordinates.
 * @param {ol.geom.GeometryLayout=} opt_layout Layout.
 */
ol.geom.SimpleGeometry.prototype.setCoordinates = goog.abstractMethod;


/**
 * @param {ol.geom.GeometryLayout|undefined} layout Layout.
 * @param {Array} coordinates Coordinates.
 * @param {number} nesting Nesting.
 * @protected
 */
ol.geom.SimpleGeometry.prototype.setLayout = function(layout, coordinates, nesting) {
  /** @type {number} */
  var stride;
  if (layout) {
    stride = ol.geom.SimpleGeometry.getStrideForLayout(layout);
  } else {
    var i;
    for (i = 0; i < nesting; ++i) {
      if (coordinates.length === 0) {
        this.layout = ol.geom.GeometryLayout.XY;
        this.stride = 2;
        return;
      } else {
        coordinates = /** @type {Array} */ (coordinates[0]);
      }
    }
    stride = (/** @type {Array} */ (coordinates)).length;
    layout = ol.geom.SimpleGeometry.getLayoutForStride_(stride);
  }
  this.layout = layout;
  this.stride = stride;
};


/**
 * @inheritDoc
 * @api stable
 */
ol.geom.SimpleGeometry.prototype.applyTransform = function(transformFn) {
  if (this.flatCoordinates) {
    transformFn(this.flatCoordinates, this.flatCoordinates, this.stride);
    this.changed();
  }
};


/**
 * @inheritDoc
 * @api stable
 */
ol.geom.SimpleGeometry.prototype.translate = function(deltaX, deltaY) {
  var flatCoordinates = this.getFlatCoordinates();
  if (flatCoordinates) {
    var stride = this.getStride();
    ol.geom.flat.transform.translate(
        flatCoordinates, 0, flatCoordinates.length, stride,
        deltaX, deltaY, flatCoordinates);
    this.changed();
  }
};


/**
 * @param {ol.geom.SimpleGeometry} simpleGeometry Simple geometry.
 * @param {goog.vec.Mat4.Number} transform Transform.
 * @param {Array.<number>=} opt_dest Destination.
 * @return {Array.<number>} Transformed flat coordinates.
 */
ol.geom.transformSimpleGeometry2D = function(simpleGeometry, transform, opt_dest) {
  var flatCoordinates = simpleGeometry.getFlatCoordinates();
  if (!flatCoordinates) {
    return null;
  } else {
    var stride = simpleGeometry.getStride();
    return ol.geom.flat.transform.transform2D(
        flatCoordinates, 0, flatCoordinates.length, stride,
        transform, opt_dest);
  }
};

goog.provide('ol.geom.flat.area');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @return {number} Area.
 */
ol.geom.flat.area.linearRing = function(flatCoordinates, offset, end, stride) {
  var twiceArea = 0;
  var x1 = flatCoordinates[end - stride];
  var y1 = flatCoordinates[end - stride + 1];
  for (; offset < end; offset += stride) {
    var x2 = flatCoordinates[offset];
    var y2 = flatCoordinates[offset + 1];
    twiceArea += y1 * x2 - x1 * y2;
    x1 = x2;
    y1 = y2;
  }
  return twiceArea / 2;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<number>} ends Ends.
 * @param {number} stride Stride.
 * @return {number} Area.
 */
ol.geom.flat.area.linearRings = function(flatCoordinates, offset, ends, stride) {
  var area = 0;
  var i, ii;
  for (i = 0, ii = ends.length; i < ii; ++i) {
    var end = ends[i];
    area += ol.geom.flat.area.linearRing(flatCoordinates, offset, end, stride);
    offset = end;
  }
  return area;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<Array.<number>>} endss Endss.
 * @param {number} stride Stride.
 * @return {number} Area.
 */
ol.geom.flat.area.linearRingss = function(flatCoordinates, offset, endss, stride) {
  var area = 0;
  var i, ii;
  for (i = 0, ii = endss.length; i < ii; ++i) {
    var ends = endss[i];
    area +=
        ol.geom.flat.area.linearRings(flatCoordinates, offset, ends, stride);
    offset = ends[ends.length - 1];
  }
  return area;
};

goog.provide('ol.geom.flat.closest');

goog.require('goog.asserts');
goog.require('goog.math');
goog.require('ol.math');


/**
 * Returns the point on the 2D line segment flatCoordinates[offset1] to
 * flatCoordinates[offset2] that is closest to the point (x, y).  Extra
 * dimensions are linearly interpolated.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset1 Offset 1.
 * @param {number} offset2 Offset 2.
 * @param {number} stride Stride.
 * @param {number} x X.
 * @param {number} y Y.
 * @param {Array.<number>} closestPoint Closest point.
 */
ol.geom.flat.closest.point = function(flatCoordinates, offset1, offset2, stride, x, y, closestPoint) {
  var x1 = flatCoordinates[offset1];
  var y1 = flatCoordinates[offset1 + 1];
  var dx = flatCoordinates[offset2] - x1;
  var dy = flatCoordinates[offset2 + 1] - y1;
  var i, offset;
  if (dx === 0 && dy === 0) {
    offset = offset1;
  } else {
    var t = ((x - x1) * dx + (y - y1) * dy) / (dx * dx + dy * dy);
    if (t > 1) {
      offset = offset2;
    } else if (t > 0) {
      for (i = 0; i < stride; ++i) {
        closestPoint[i] = goog.math.lerp(flatCoordinates[offset1 + i],
            flatCoordinates[offset2 + i], t);
      }
      closestPoint.length = stride;
      return;
    } else {
      offset = offset1;
    }
  }
  for (i = 0; i < stride; ++i) {
    closestPoint[i] = flatCoordinates[offset + i];
  }
  closestPoint.length = stride;
};


/**
 * Return the squared of the largest distance between any pair of consecutive
 * coordinates.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {number} maxSquaredDelta Max squared delta.
 * @return {number} Max squared delta.
 */
ol.geom.flat.closest.getMaxSquaredDelta = function(flatCoordinates, offset, end, stride, maxSquaredDelta) {
  var x1 = flatCoordinates[offset];
  var y1 = flatCoordinates[offset + 1];
  for (offset += stride; offset < end; offset += stride) {
    var x2 = flatCoordinates[offset];
    var y2 = flatCoordinates[offset + 1];
    var squaredDelta = ol.math.squaredDistance(x1, y1, x2, y2);
    if (squaredDelta > maxSquaredDelta) {
      maxSquaredDelta = squaredDelta;
    }
    x1 = x2;
    y1 = y2;
  }
  return maxSquaredDelta;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<number>} ends Ends.
 * @param {number} stride Stride.
 * @param {number} maxSquaredDelta Max squared delta.
 * @return {number} Max squared delta.
 */
ol.geom.flat.closest.getsMaxSquaredDelta = function(flatCoordinates, offset, ends, stride, maxSquaredDelta) {
  var i, ii;
  for (i = 0, ii = ends.length; i < ii; ++i) {
    var end = ends[i];
    maxSquaredDelta = ol.geom.flat.closest.getMaxSquaredDelta(
        flatCoordinates, offset, end, stride, maxSquaredDelta);
    offset = end;
  }
  return maxSquaredDelta;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<Array.<number>>} endss Endss.
 * @param {number} stride Stride.
 * @param {number} maxSquaredDelta Max squared delta.
 * @return {number} Max squared delta.
 */
ol.geom.flat.closest.getssMaxSquaredDelta = function(flatCoordinates, offset, endss, stride, maxSquaredDelta) {
  var i, ii;
  for (i = 0, ii = endss.length; i < ii; ++i) {
    var ends = endss[i];
    maxSquaredDelta = ol.geom.flat.closest.getsMaxSquaredDelta(
        flatCoordinates, offset, ends, stride, maxSquaredDelta);
    offset = ends[ends.length - 1];
  }
  return maxSquaredDelta;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {number} maxDelta Max delta.
 * @param {boolean} isRing Is ring.
 * @param {number} x X.
 * @param {number} y Y.
 * @param {Array.<number>} closestPoint Closest point.
 * @param {number} minSquaredDistance Minimum squared distance.
 * @param {Array.<number>=} opt_tmpPoint Temporary point object.
 * @return {number} Minimum squared distance.
 */
ol.geom.flat.closest.getClosestPoint = function(flatCoordinates, offset, end,
    stride, maxDelta, isRing, x, y, closestPoint, minSquaredDistance,
    opt_tmpPoint) {
  if (offset == end) {
    return minSquaredDistance;
  }
  var i, squaredDistance;
  if (maxDelta === 0) {
    // All points are identical, so just test the first point.
    squaredDistance = ol.math.squaredDistance(
        x, y, flatCoordinates[offset], flatCoordinates[offset + 1]);
    if (squaredDistance < minSquaredDistance) {
      for (i = 0; i < stride; ++i) {
        closestPoint[i] = flatCoordinates[offset + i];
      }
      closestPoint.length = stride;
      return squaredDistance;
    } else {
      return minSquaredDistance;
    }
  }
  goog.asserts.assert(maxDelta > 0, 'maxDelta should be larger than 0');
  var tmpPoint = opt_tmpPoint ? opt_tmpPoint : [NaN, NaN];
  var index = offset + stride;
  while (index < end) {
    ol.geom.flat.closest.point(
        flatCoordinates, index - stride, index, stride, x, y, tmpPoint);
    squaredDistance = ol.math.squaredDistance(x, y, tmpPoint[0], tmpPoint[1]);
    if (squaredDistance < minSquaredDistance) {
      minSquaredDistance = squaredDistance;
      for (i = 0; i < stride; ++i) {
        closestPoint[i] = tmpPoint[i];
      }
      closestPoint.length = stride;
      index += stride;
    } else {
      // Skip ahead multiple points, because we know that all the skipped
      // points cannot be any closer than the closest point we have found so
      // far.  We know this because we know how close the current point is, how
      // close the closest point we have found so far is, and the maximum
      // distance between consecutive points.  For example, if we're currently
      // at distance 10, the best we've found so far is 3, and that the maximum
      // distance between consecutive points is 2, then we'll need to skip at
      // least (10 - 3) / 2 == 3 (rounded down) points to have any chance of
      // finding a closer point.  We use Math.max(..., 1) to ensure that we
      // always advance at least one point, to avoid an infinite loop.
      index += stride * Math.max(
          ((Math.sqrt(squaredDistance) -
            Math.sqrt(minSquaredDistance)) / maxDelta) | 0, 1);
    }
  }
  if (isRing) {
    // Check the closing segment.
    ol.geom.flat.closest.point(
        flatCoordinates, end - stride, offset, stride, x, y, tmpPoint);
    squaredDistance = ol.math.squaredDistance(x, y, tmpPoint[0], tmpPoint[1]);
    if (squaredDistance < minSquaredDistance) {
      minSquaredDistance = squaredDistance;
      for (i = 0; i < stride; ++i) {
        closestPoint[i] = tmpPoint[i];
      }
      closestPoint.length = stride;
    }
  }
  return minSquaredDistance;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<number>} ends Ends.
 * @param {number} stride Stride.
 * @param {number} maxDelta Max delta.
 * @param {boolean} isRing Is ring.
 * @param {number} x X.
 * @param {number} y Y.
 * @param {Array.<number>} closestPoint Closest point.
 * @param {number} minSquaredDistance Minimum squared distance.
 * @param {Array.<number>=} opt_tmpPoint Temporary point object.
 * @return {number} Minimum squared distance.
 */
ol.geom.flat.closest.getsClosestPoint = function(flatCoordinates, offset, ends,
    stride, maxDelta, isRing, x, y, closestPoint, minSquaredDistance,
    opt_tmpPoint) {
  var tmpPoint = opt_tmpPoint ? opt_tmpPoint : [NaN, NaN];
  var i, ii;
  for (i = 0, ii = ends.length; i < ii; ++i) {
    var end = ends[i];
    minSquaredDistance = ol.geom.flat.closest.getClosestPoint(
        flatCoordinates, offset, end, stride,
        maxDelta, isRing, x, y, closestPoint, minSquaredDistance, tmpPoint);
    offset = end;
  }
  return minSquaredDistance;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<Array.<number>>} endss Endss.
 * @param {number} stride Stride.
 * @param {number} maxDelta Max delta.
 * @param {boolean} isRing Is ring.
 * @param {number} x X.
 * @param {number} y Y.
 * @param {Array.<number>} closestPoint Closest point.
 * @param {number} minSquaredDistance Minimum squared distance.
 * @param {Array.<number>=} opt_tmpPoint Temporary point object.
 * @return {number} Minimum squared distance.
 */
ol.geom.flat.closest.getssClosestPoint = function(flatCoordinates, offset,
    endss, stride, maxDelta, isRing, x, y, closestPoint, minSquaredDistance,
    opt_tmpPoint) {
  var tmpPoint = opt_tmpPoint ? opt_tmpPoint : [NaN, NaN];
  var i, ii;
  for (i = 0, ii = endss.length; i < ii; ++i) {
    var ends = endss[i];
    minSquaredDistance = ol.geom.flat.closest.getsClosestPoint(
        flatCoordinates, offset, ends, stride,
        maxDelta, isRing, x, y, closestPoint, minSquaredDistance, tmpPoint);
    offset = ends[ends.length - 1];
  }
  return minSquaredDistance;
};

goog.provide('ol.geom.flat.deflate');

goog.require('goog.asserts');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {number} stride Stride.
 * @return {number} offset Offset.
 */
ol.geom.flat.deflate.coordinate = function(flatCoordinates, offset, coordinate, stride) {
  goog.asserts.assert(coordinate.length == stride,
      'length of the coordinate array should match stride');
  var i, ii;
  for (i = 0, ii = coordinate.length; i < ii; ++i) {
    flatCoordinates[offset++] = coordinate[i];
  }
  return offset;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<ol.Coordinate>} coordinates Coordinates.
 * @param {number} stride Stride.
 * @return {number} offset Offset.
 */
ol.geom.flat.deflate.coordinates = function(flatCoordinates, offset, coordinates, stride) {
  var i, ii;
  for (i = 0, ii = coordinates.length; i < ii; ++i) {
    var coordinate = coordinates[i];
    goog.asserts.assert(coordinate.length == stride,
        'length of coordinate array should match stride');
    var j;
    for (j = 0; j < stride; ++j) {
      flatCoordinates[offset++] = coordinate[j];
    }
  }
  return offset;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<Array.<ol.Coordinate>>} coordinatess Coordinatess.
 * @param {number} stride Stride.
 * @param {Array.<number>=} opt_ends Ends.
 * @return {Array.<number>} Ends.
 */
ol.geom.flat.deflate.coordinatess = function(flatCoordinates, offset, coordinatess, stride, opt_ends) {
  var ends = opt_ends ? opt_ends : [];
  var i = 0;
  var j, jj;
  for (j = 0, jj = coordinatess.length; j < jj; ++j) {
    var end = ol.geom.flat.deflate.coordinates(
        flatCoordinates, offset, coordinatess[j], stride);
    ends[i++] = end;
    offset = end;
  }
  ends.length = i;
  return ends;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<Array.<Array.<ol.Coordinate>>>} coordinatesss Coordinatesss.
 * @param {number} stride Stride.
 * @param {Array.<Array.<number>>=} opt_endss Endss.
 * @return {Array.<Array.<number>>} Endss.
 */
ol.geom.flat.deflate.coordinatesss = function(flatCoordinates, offset, coordinatesss, stride, opt_endss) {
  var endss = opt_endss ? opt_endss : [];
  var i = 0;
  var j, jj;
  for (j = 0, jj = coordinatesss.length; j < jj; ++j) {
    var ends = ol.geom.flat.deflate.coordinatess(
        flatCoordinates, offset, coordinatesss[j], stride, endss[i]);
    endss[i++] = ends;
    offset = ends[ends.length - 1];
  }
  endss.length = i;
  return endss;
};

goog.provide('ol.geom.flat.inflate');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {Array.<ol.Coordinate>=} opt_coordinates Coordinates.
 * @return {Array.<ol.Coordinate>} Coordinates.
 */
ol.geom.flat.inflate.coordinates = function(flatCoordinates, offset, end, stride, opt_coordinates) {
  var coordinates = opt_coordinates !== undefined ? opt_coordinates : [];
  var i = 0;
  var j;
  for (j = offset; j < end; j += stride) {
    coordinates[i++] = flatCoordinates.slice(j, j + stride);
  }
  coordinates.length = i;
  return coordinates;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<number>} ends Ends.
 * @param {number} stride Stride.
 * @param {Array.<Array.<ol.Coordinate>>=} opt_coordinatess Coordinatess.
 * @return {Array.<Array.<ol.Coordinate>>} Coordinatess.
 */
ol.geom.flat.inflate.coordinatess = function(flatCoordinates, offset, ends, stride, opt_coordinatess) {
  var coordinatess = opt_coordinatess !== undefined ? opt_coordinatess : [];
  var i = 0;
  var j, jj;
  for (j = 0, jj = ends.length; j < jj; ++j) {
    var end = ends[j];
    coordinatess[i++] = ol.geom.flat.inflate.coordinates(
        flatCoordinates, offset, end, stride, coordinatess[i]);
    offset = end;
  }
  coordinatess.length = i;
  return coordinatess;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<Array.<number>>} endss Endss.
 * @param {number} stride Stride.
 * @param {Array.<Array.<Array.<ol.Coordinate>>>=} opt_coordinatesss
 *     Coordinatesss.
 * @return {Array.<Array.<Array.<ol.Coordinate>>>} Coordinatesss.
 */
ol.geom.flat.inflate.coordinatesss = function(flatCoordinates, offset, endss, stride, opt_coordinatesss) {
  var coordinatesss = opt_coordinatesss !== undefined ? opt_coordinatesss : [];
  var i = 0;
  var j, jj;
  for (j = 0, jj = endss.length; j < jj; ++j) {
    var ends = endss[j];
    coordinatesss[i++] = ol.geom.flat.inflate.coordinatess(
        flatCoordinates, offset, ends, stride, coordinatesss[i]);
    offset = ends[ends.length - 1];
  }
  coordinatesss.length = i;
  return coordinatesss;
};

// Based on simplify-js https://github.com/mourner/simplify-js
// Copyright (c) 2012, Vladimir Agafonkin
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met:
//
//    1. Redistributions of source code must retain the above copyright notice,
//       this list of conditions and the following disclaimer.
//
//    2. Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the following disclaimer in the
//       documentation and/or other materials provided with the distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
// ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
// LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
// CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
// SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
// CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
// POSSIBILITY OF SUCH DAMAGE.

goog.provide('ol.geom.flat.simplify');

goog.require('ol.math');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {number} squaredTolerance Squared tolerance.
 * @param {boolean} highQuality Highest quality.
 * @param {Array.<number>=} opt_simplifiedFlatCoordinates Simplified flat
 *     coordinates.
 * @return {Array.<number>} Simplified line string.
 */
ol.geom.flat.simplify.lineString = function(flatCoordinates, offset, end,
    stride, squaredTolerance, highQuality, opt_simplifiedFlatCoordinates) {
  var simplifiedFlatCoordinates = opt_simplifiedFlatCoordinates !== undefined ?
      opt_simplifiedFlatCoordinates : [];
  if (!highQuality) {
    end = ol.geom.flat.simplify.radialDistance(flatCoordinates, offset, end,
        stride, squaredTolerance,
        simplifiedFlatCoordinates, 0);
    flatCoordinates = simplifiedFlatCoordinates;
    offset = 0;
    stride = 2;
  }
  simplifiedFlatCoordinates.length = ol.geom.flat.simplify.douglasPeucker(
      flatCoordinates, offset, end, stride, squaredTolerance,
      simplifiedFlatCoordinates, 0);
  return simplifiedFlatCoordinates;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {number} squaredTolerance Squared tolerance.
 * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
 *     coordinates.
 * @param {number} simplifiedOffset Simplified offset.
 * @return {number} Simplified offset.
 */
ol.geom.flat.simplify.douglasPeucker = function(flatCoordinates, offset, end,
    stride, squaredTolerance, simplifiedFlatCoordinates, simplifiedOffset) {
  var n = (end - offset) / stride;
  if (n < 3) {
    for (; offset < end; offset += stride) {
      simplifiedFlatCoordinates[simplifiedOffset++] =
          flatCoordinates[offset];
      simplifiedFlatCoordinates[simplifiedOffset++] =
          flatCoordinates[offset + 1];
    }
    return simplifiedOffset;
  }
  /** @type {Array.<number>} */
  var markers = new Array(n);
  markers[0] = 1;
  markers[n - 1] = 1;
  /** @type {Array.<number>} */
  var stack = [offset, end - stride];
  var index = 0;
  var i;
  while (stack.length > 0) {
    var last = stack.pop();
    var first = stack.pop();
    var maxSquaredDistance = 0;
    var x1 = flatCoordinates[first];
    var y1 = flatCoordinates[first + 1];
    var x2 = flatCoordinates[last];
    var y2 = flatCoordinates[last + 1];
    for (i = first + stride; i < last; i += stride) {
      var x = flatCoordinates[i];
      var y = flatCoordinates[i + 1];
      var squaredDistance = ol.math.squaredSegmentDistance(
          x, y, x1, y1, x2, y2);
      if (squaredDistance > maxSquaredDistance) {
        index = i;
        maxSquaredDistance = squaredDistance;
      }
    }
    if (maxSquaredDistance > squaredTolerance) {
      markers[(index - offset) / stride] = 1;
      if (first + stride < index) {
        stack.push(first, index);
      }
      if (index + stride < last) {
        stack.push(index, last);
      }
    }
  }
  for (i = 0; i < n; ++i) {
    if (markers[i]) {
      simplifiedFlatCoordinates[simplifiedOffset++] =
          flatCoordinates[offset + i * stride];
      simplifiedFlatCoordinates[simplifiedOffset++] =
          flatCoordinates[offset + i * stride + 1];
    }
  }
  return simplifiedOffset;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<number>} ends Ends.
 * @param {number} stride Stride.
 * @param {number} squaredTolerance Squared tolerance.
 * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
 *     coordinates.
 * @param {number} simplifiedOffset Simplified offset.
 * @param {Array.<number>} simplifiedEnds Simplified ends.
 * @return {number} Simplified offset.
 */
ol.geom.flat.simplify.douglasPeuckers = function(flatCoordinates, offset,
    ends, stride, squaredTolerance, simplifiedFlatCoordinates,
    simplifiedOffset, simplifiedEnds) {
  var i, ii;
  for (i = 0, ii = ends.length; i < ii; ++i) {
    var end = ends[i];
    simplifiedOffset = ol.geom.flat.simplify.douglasPeucker(
        flatCoordinates, offset, end, stride, squaredTolerance,
        simplifiedFlatCoordinates, simplifiedOffset);
    simplifiedEnds.push(simplifiedOffset);
    offset = end;
  }
  return simplifiedOffset;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<Array.<number>>} endss Endss.
 * @param {number} stride Stride.
 * @param {number} squaredTolerance Squared tolerance.
 * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
 *     coordinates.
 * @param {number} simplifiedOffset Simplified offset.
 * @param {Array.<Array.<number>>} simplifiedEndss Simplified endss.
 * @return {number} Simplified offset.
 */
ol.geom.flat.simplify.douglasPeuckerss = function(
    flatCoordinates, offset, endss, stride, squaredTolerance,
    simplifiedFlatCoordinates, simplifiedOffset, simplifiedEndss) {
  var i, ii;
  for (i = 0, ii = endss.length; i < ii; ++i) {
    var ends = endss[i];
    var simplifiedEnds = [];
    simplifiedOffset = ol.geom.flat.simplify.douglasPeuckers(
        flatCoordinates, offset, ends, stride, squaredTolerance,
        simplifiedFlatCoordinates, simplifiedOffset, simplifiedEnds);
    simplifiedEndss.push(simplifiedEnds);
    offset = ends[ends.length - 1];
  }
  return simplifiedOffset;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {number} squaredTolerance Squared tolerance.
 * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
 *     coordinates.
 * @param {number} simplifiedOffset Simplified offset.
 * @return {number} Simplified offset.
 */
ol.geom.flat.simplify.radialDistance = function(flatCoordinates, offset, end,
    stride, squaredTolerance, simplifiedFlatCoordinates, simplifiedOffset) {
  if (end <= offset + stride) {
    // zero or one point, no simplification possible, so copy and return
    for (; offset < end; offset += stride) {
      simplifiedFlatCoordinates[simplifiedOffset++] = flatCoordinates[offset];
      simplifiedFlatCoordinates[simplifiedOffset++] =
          flatCoordinates[offset + 1];
    }
    return simplifiedOffset;
  }
  var x1 = flatCoordinates[offset];
  var y1 = flatCoordinates[offset + 1];
  // copy first point
  simplifiedFlatCoordinates[simplifiedOffset++] = x1;
  simplifiedFlatCoordinates[simplifiedOffset++] = y1;
  var x2 = x1;
  var y2 = y1;
  for (offset += stride; offset < end; offset += stride) {
    x2 = flatCoordinates[offset];
    y2 = flatCoordinates[offset + 1];
    if (ol.math.squaredDistance(x1, y1, x2, y2) > squaredTolerance) {
      // copy point at offset
      simplifiedFlatCoordinates[simplifiedOffset++] = x2;
      simplifiedFlatCoordinates[simplifiedOffset++] = y2;
      x1 = x2;
      y1 = y2;
    }
  }
  if (x2 != x1 || y2 != y1) {
    // copy last point
    simplifiedFlatCoordinates[simplifiedOffset++] = x2;
    simplifiedFlatCoordinates[simplifiedOffset++] = y2;
  }
  return simplifiedOffset;
};


/**
 * @param {number} value Value.
 * @param {number} tolerance Tolerance.
 * @return {number} Rounded value.
 */
ol.geom.flat.simplify.snap = function(value, tolerance) {
  return tolerance * Math.round(value / tolerance);
};


/**
 * Simplifies a line string using an algorithm designed by Tim Schaub.
 * Coordinates are snapped to the nearest value in a virtual grid and
 * consecutive duplicate coordinates are discarded.  This effectively preserves
 * topology as the simplification of any subsection of a line string is
 * independent of the rest of the line string.  This means that, for examples,
 * the common edge between two polygons will be simplified to the same line
 * string independently in both polygons.  This implementation uses a single
 * pass over the coordinates and eliminates intermediate collinear points.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {number} tolerance Tolerance.
 * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
 *     coordinates.
 * @param {number} simplifiedOffset Simplified offset.
 * @return {number} Simplified offset.
 */
ol.geom.flat.simplify.quantize = function(flatCoordinates, offset, end, stride,
    tolerance, simplifiedFlatCoordinates, simplifiedOffset) {
  // do nothing if the line is empty
  if (offset == end) {
    return simplifiedOffset;
  }
  // snap the first coordinate (P1)
  var x1 = ol.geom.flat.simplify.snap(flatCoordinates[offset], tolerance);
  var y1 = ol.geom.flat.simplify.snap(flatCoordinates[offset + 1], tolerance);
  offset += stride;
  // add the first coordinate to the output
  simplifiedFlatCoordinates[simplifiedOffset++] = x1;
  simplifiedFlatCoordinates[simplifiedOffset++] = y1;
  // find the next coordinate that does not snap to the same value as the first
  // coordinate (P2)
  var x2, y2;
  do {
    x2 = ol.geom.flat.simplify.snap(flatCoordinates[offset], tolerance);
    y2 = ol.geom.flat.simplify.snap(flatCoordinates[offset + 1], tolerance);
    offset += stride;
    if (offset == end) {
      // all coordinates snap to the same value, the line collapses to a point
      // push the last snapped value anyway to ensure that the output contains
      // at least two points
      // FIXME should we really return at least two points anyway?
      simplifiedFlatCoordinates[simplifiedOffset++] = x2;
      simplifiedFlatCoordinates[simplifiedOffset++] = y2;
      return simplifiedOffset;
    }
  } while (x2 == x1 && y2 == y1);
  while (offset < end) {
    var x3, y3;
    // snap the next coordinate (P3)
    x3 = ol.geom.flat.simplify.snap(flatCoordinates[offset], tolerance);
    y3 = ol.geom.flat.simplify.snap(flatCoordinates[offset + 1], tolerance);
    offset += stride;
    // skip P3 if it is equal to P2
    if (x3 == x2 && y3 == y2) {
      continue;
    }
    // calculate the delta between P1 and P2
    var dx1 = x2 - x1;
    var dy1 = y2 - y1;
    // calculate the delta between P3 and P1
    var dx2 = x3 - x1;
    var dy2 = y3 - y1;
    // if P1, P2, and P3 are colinear and P3 is further from P1 than P2 is from
    // P1 in the same direction then P2 is on the straight line between P1 and
    // P3
    if ((dx1 * dy2 == dy1 * dx2) &&
        ((dx1 < 0 && dx2 < dx1) || dx1 == dx2 || (dx1 > 0 && dx2 > dx1)) &&
        ((dy1 < 0 && dy2 < dy1) || dy1 == dy2 || (dy1 > 0 && dy2 > dy1))) {
      // discard P2 and set P2 = P3
      x2 = x3;
      y2 = y3;
      continue;
    }
    // either P1, P2, and P3 are not colinear, or they are colinear but P3 is
    // between P3 and P1 or on the opposite half of the line to P2.  add P2,
    // and continue with P1 = P2 and P2 = P3
    simplifiedFlatCoordinates[simplifiedOffset++] = x2;
    simplifiedFlatCoordinates[simplifiedOffset++] = y2;
    x1 = x2;
    y1 = y2;
    x2 = x3;
    y2 = y3;
  }
  // add the last point (P2)
  simplifiedFlatCoordinates[simplifiedOffset++] = x2;
  simplifiedFlatCoordinates[simplifiedOffset++] = y2;
  return simplifiedOffset;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<number>} ends Ends.
 * @param {number} stride Stride.
 * @param {number} tolerance Tolerance.
 * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
 *     coordinates.
 * @param {number} simplifiedOffset Simplified offset.
 * @param {Array.<number>} simplifiedEnds Simplified ends.
 * @return {number} Simplified offset.
 */
ol.geom.flat.simplify.quantizes = function(
    flatCoordinates, offset, ends, stride,
    tolerance,
    simplifiedFlatCoordinates, simplifiedOffset, simplifiedEnds) {
  var i, ii;
  for (i = 0, ii = ends.length; i < ii; ++i) {
    var end = ends[i];
    simplifiedOffset = ol.geom.flat.simplify.quantize(
        flatCoordinates, offset, end, stride,
        tolerance,
        simplifiedFlatCoordinates, simplifiedOffset);
    simplifiedEnds.push(simplifiedOffset);
    offset = end;
  }
  return simplifiedOffset;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<Array.<number>>} endss Endss.
 * @param {number} stride Stride.
 * @param {number} tolerance Tolerance.
 * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
 *     coordinates.
 * @param {number} simplifiedOffset Simplified offset.
 * @param {Array.<Array.<number>>} simplifiedEndss Simplified endss.
 * @return {number} Simplified offset.
 */
ol.geom.flat.simplify.quantizess = function(
    flatCoordinates, offset, endss, stride,
    tolerance,
    simplifiedFlatCoordinates, simplifiedOffset, simplifiedEndss) {
  var i, ii;
  for (i = 0, ii = endss.length; i < ii; ++i) {
    var ends = endss[i];
    var simplifiedEnds = [];
    simplifiedOffset = ol.geom.flat.simplify.quantizes(
        flatCoordinates, offset, ends, stride,
        tolerance,
        simplifiedFlatCoordinates, simplifiedOffset, simplifiedEnds);
    simplifiedEndss.push(simplifiedEnds);
    offset = ends[ends.length - 1];
  }
  return simplifiedOffset;
};

goog.provide('ol.geom.LinearRing');

goog.require('ol.extent');
goog.require('ol.geom.GeometryLayout');
goog.require('ol.geom.GeometryType');
goog.require('ol.geom.SimpleGeometry');
goog.require('ol.geom.flat.area');
goog.require('ol.geom.flat.closest');
goog.require('ol.geom.flat.deflate');
goog.require('ol.geom.flat.inflate');
goog.require('ol.geom.flat.simplify');


/**
 * @classdesc
 * Linear ring geometry. Only used as part of polygon; cannot be rendered
 * on its own.
 *
 * @constructor
 * @extends {ol.geom.SimpleGeometry}
 * @param {Array.<ol.Coordinate>} coordinates Coordinates.
 * @param {ol.geom.GeometryLayout=} opt_layout Layout.
 * @api stable
 */
ol.geom.LinearRing = function(coordinates, opt_layout) {

  goog.base(this);

  /**
   * @private
   * @type {number}
   */
  this.maxDelta_ = -1;

  /**
   * @private
   * @type {number}
   */
  this.maxDeltaRevision_ = -1;

  this.setCoordinates(coordinates, opt_layout);

};
goog.inherits(ol.geom.LinearRing, ol.geom.SimpleGeometry);


/**
 * Make a complete copy of the geometry.
 * @return {!ol.geom.LinearRing} Clone.
 * @api stable
 */
ol.geom.LinearRing.prototype.clone = function() {
  var linearRing = new ol.geom.LinearRing(null);
  linearRing.setFlatCoordinates(this.layout, this.flatCoordinates.slice());
  return linearRing;
};


/**
 * @inheritDoc
 */
ol.geom.LinearRing.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
  if (minSquaredDistance <
      ol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
    return minSquaredDistance;
  }
  if (this.maxDeltaRevision_ != this.getRevision()) {
    this.maxDelta_ = Math.sqrt(ol.geom.flat.closest.getMaxSquaredDelta(
        this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, 0));
    this.maxDeltaRevision_ = this.getRevision();
  }
  return ol.geom.flat.closest.getClosestPoint(
      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
      this.maxDelta_, true, x, y, closestPoint, minSquaredDistance);
};


/**
 * Return the area of the linear ring on projected plane.
 * @return {number} Area (on projected plane).
 * @api stable
 */
ol.geom.LinearRing.prototype.getArea = function() {
  return ol.geom.flat.area.linearRing(
      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride);
};


/**
 * Return the coordinates of the linear ring.
 * @return {Array.<ol.Coordinate>} Coordinates.
 * @api stable
 */
ol.geom.LinearRing.prototype.getCoordinates = function() {
  return ol.geom.flat.inflate.coordinates(
      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride);
};


/**
 * @inheritDoc
 */
ol.geom.LinearRing.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) {
  var simplifiedFlatCoordinates = [];
  simplifiedFlatCoordinates.length = ol.geom.flat.simplify.douglasPeucker(
      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
      squaredTolerance, simplifiedFlatCoordinates, 0);
  var simplifiedLinearRing = new ol.geom.LinearRing(null);
  simplifiedLinearRing.setFlatCoordinates(
      ol.geom.GeometryLayout.XY, simplifiedFlatCoordinates);
  return simplifiedLinearRing;
};


/**
 * @inheritDoc
 * @api stable
 */
ol.geom.LinearRing.prototype.getType = function() {
  return ol.geom.GeometryType.LINEAR_RING;
};


/**
 * Set the coordinates of the linear ring.
 * @param {Array.<ol.Coordinate>} coordinates Coordinates.
 * @param {ol.geom.GeometryLayout=} opt_layout Layout.
 * @api stable
 */
ol.geom.LinearRing.prototype.setCoordinates = function(coordinates, opt_layout) {
  if (!coordinates) {
    this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null);
  } else {
    this.setLayout(opt_layout, coordinates, 1);
    if (!this.flatCoordinates) {
      this.flatCoordinates = [];
    }
    this.flatCoordinates.length = ol.geom.flat.deflate.coordinates(
        this.flatCoordinates, 0, coordinates, this.stride);
    this.changed();
  }
};


/**
 * @param {ol.geom.GeometryLayout} layout Layout.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 */
ol.geom.LinearRing.prototype.setFlatCoordinates = function(layout, flatCoordinates) {
  this.setFlatCoordinatesInternal(layout, flatCoordinates);
  this.changed();
};

goog.provide('ol.geom.Point');

goog.require('ol.extent');
goog.require('ol.geom.GeometryLayout');
goog.require('ol.geom.GeometryType');
goog.require('ol.geom.SimpleGeometry');
goog.require('ol.geom.flat.deflate');
goog.require('ol.math');


/**
 * @classdesc
 * Point geometry.
 *
 * @constructor
 * @extends {ol.geom.SimpleGeometry}
 * @param {ol.Coordinate} coordinates Coordinates.
 * @param {ol.geom.GeometryLayout=} opt_layout Layout.
 * @api stable
 */
ol.geom.Point = function(coordinates, opt_layout) {
  goog.base(this);
  this.setCoordinates(coordinates, opt_layout);
};
goog.inherits(ol.geom.Point, ol.geom.SimpleGeometry);


/**
 * Make a complete copy of the geometry.
 * @return {!ol.geom.Point} Clone.
 * @api stable
 */
ol.geom.Point.prototype.clone = function() {
  var point = new ol.geom.Point(null);
  point.setFlatCoordinates(this.layout, this.flatCoordinates.slice());
  return point;
};


/**
 * @inheritDoc
 */
ol.geom.Point.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
  var flatCoordinates = this.flatCoordinates;
  var squaredDistance = ol.math.squaredDistance(
      x, y, flatCoordinates[0], flatCoordinates[1]);
  if (squaredDistance < minSquaredDistance) {
    var stride = this.stride;
    var i;
    for (i = 0; i < stride; ++i) {
      closestPoint[i] = flatCoordinates[i];
    }
    closestPoint.length = stride;
    return squaredDistance;
  } else {
    return minSquaredDistance;
  }
};


/**
 * Return the coordinate of the point.
 * @return {ol.Coordinate} Coordinates.
 * @api stable
 */
ol.geom.Point.prototype.getCoordinates = function() {
  return !this.flatCoordinates ? [] : this.flatCoordinates.slice();
};


/**
 * @inheritDoc
 */
ol.geom.Point.prototype.computeExtent = function(extent) {
  return ol.extent.createOrUpdateFromCoordinate(this.flatCoordinates, extent);
};


/**
 * @inheritDoc
 * @api stable
 */
ol.geom.Point.prototype.getType = function() {
  return ol.geom.GeometryType.POINT;
};


/**
 * @inheritDoc
 * @api stable
 */
ol.geom.Point.prototype.intersectsExtent = function(extent) {
  return ol.extent.containsXY(extent,
      this.flatCoordinates[0], this.flatCoordinates[1]);
};


/**
 * Set the coordinate of the point.
 * @param {ol.Coordinate} coordinates Coordinates.
 * @param {ol.geom.GeometryLayout=} opt_layout Layout.
 * @api stable
 */
ol.geom.Point.prototype.setCoordinates = function(coordinates, opt_layout) {
  if (!coordinates) {
    this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null);
  } else {
    this.setLayout(opt_layout, coordinates, 0);
    if (!this.flatCoordinates) {
      this.flatCoordinates = [];
    }
    this.flatCoordinates.length = ol.geom.flat.deflate.coordinate(
        this.flatCoordinates, 0, coordinates, this.stride);
    this.changed();
  }
};


/**
 * @param {ol.geom.GeometryLayout} layout Layout.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 */
ol.geom.Point.prototype.setFlatCoordinates = function(layout, flatCoordinates) {
  this.setFlatCoordinatesInternal(layout, flatCoordinates);
  this.changed();
};

goog.provide('ol.geom.flat.contains');

goog.require('goog.asserts');
goog.require('ol.extent');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {ol.Extent} extent Extent.
 * @return {boolean} Contains extent.
 */
ol.geom.flat.contains.linearRingContainsExtent = function(flatCoordinates, offset, end, stride, extent) {
  var outside = ol.extent.forEachCorner(extent,
      /**
       * @param {ol.Coordinate} coordinate Coordinate.
       */
      function(coordinate) {
        return !ol.geom.flat.contains.linearRingContainsXY(flatCoordinates,
            offset, end, stride, coordinate[0], coordinate[1]);
      });
  return !outside;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {number} x X.
 * @param {number} y Y.
 * @return {boolean} Contains (x, y).
 */
ol.geom.flat.contains.linearRingContainsXY = function(flatCoordinates, offset, end, stride, x, y) {
  // http://www.ecse.rpi.edu/Homepages/wrf/Research/Short_Notes/pnpoly.html
  var contains = false;
  var x1 = flatCoordinates[end - stride];
  var y1 = flatCoordinates[end - stride + 1];
  for (; offset < end; offset += stride) {
    var x2 = flatCoordinates[offset];
    var y2 = flatCoordinates[offset + 1];
    var intersect = ((y1 > y) != (y2 > y)) &&
        (x < (x2 - x1) * (y - y1) / (y2 - y1) + x1);
    if (intersect) {
      contains = !contains;
    }
    x1 = x2;
    y1 = y2;
  }
  return contains;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<number>} ends Ends.
 * @param {number} stride Stride.
 * @param {number} x X.
 * @param {number} y Y.
 * @return {boolean} Contains (x, y).
 */
ol.geom.flat.contains.linearRingsContainsXY = function(flatCoordinates, offset, ends, stride, x, y) {
  goog.asserts.assert(ends.length > 0, 'ends should not be an empty array');
  if (ends.length === 0) {
    return false;
  }
  if (!ol.geom.flat.contains.linearRingContainsXY(
      flatCoordinates, offset, ends[0], stride, x, y)) {
    return false;
  }
  var i, ii;
  for (i = 1, ii = ends.length; i < ii; ++i) {
    if (ol.geom.flat.contains.linearRingContainsXY(
        flatCoordinates, ends[i - 1], ends[i], stride, x, y)) {
      return false;
    }
  }
  return true;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<Array.<number>>} endss Endss.
 * @param {number} stride Stride.
 * @param {number} x X.
 * @param {number} y Y.
 * @return {boolean} Contains (x, y).
 */
ol.geom.flat.contains.linearRingssContainsXY = function(flatCoordinates, offset, endss, stride, x, y) {
  goog.asserts.assert(endss.length > 0, 'endss should not be an empty array');
  if (endss.length === 0) {
    return false;
  }
  var i, ii;
  for (i = 0, ii = endss.length; i < ii; ++i) {
    var ends = endss[i];
    if (ol.geom.flat.contains.linearRingsContainsXY(
        flatCoordinates, offset, ends, stride, x, y)) {
      return true;
    }
    offset = ends[ends.length - 1];
  }
  return false;
};

goog.provide('ol.geom.flat.interiorpoint');

goog.require('goog.asserts');
goog.require('ol.array');
goog.require('ol.geom.flat.contains');


/**
 * Calculates a point that is likely to lie in the interior of the linear rings.
 * Inspired by JTS's com.vividsolutions.jts.geom.Geometry#getInteriorPoint.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<number>} ends Ends.
 * @param {number} stride Stride.
 * @param {Array.<number>} flatCenters Flat centers.
 * @param {number} flatCentersOffset Flat center offset.
 * @param {Array.<number>=} opt_dest Destination.
 * @return {Array.<number>} Destination.
 */
ol.geom.flat.interiorpoint.linearRings = function(flatCoordinates, offset,
    ends, stride, flatCenters, flatCentersOffset, opt_dest) {
  var i, ii, x, x1, x2, y1, y2;
  var y = flatCenters[flatCentersOffset + 1];
  /** @type {Array.<number>} */
  var intersections = [];
  // Calculate intersections with the horizontal line
  var end = ends[0];
  x1 = flatCoordinates[end - stride];
  y1 = flatCoordinates[end - stride + 1];
  for (i = offset; i < end; i += stride) {
    x2 = flatCoordinates[i];
    y2 = flatCoordinates[i + 1];
    if ((y <= y1 && y2 <= y) || (y1 <= y && y <= y2)) {
      x = (y - y1) / (y2 - y1) * (x2 - x1) + x1;
      intersections.push(x);
    }
    x1 = x2;
    y1 = y2;
  }
  // Find the longest segment of the horizontal line that has its center point
  // inside the linear ring.
  var pointX = NaN;
  var maxSegmentLength = -Infinity;
  intersections.sort(ol.array.numberSafeCompareFunction);
  x1 = intersections[0];
  for (i = 1, ii = intersections.length; i < ii; ++i) {
    x2 = intersections[i];
    var segmentLength = Math.abs(x2 - x1);
    if (segmentLength > maxSegmentLength) {
      x = (x1 + x2) / 2;
      if (ol.geom.flat.contains.linearRingsContainsXY(
          flatCoordinates, offset, ends, stride, x, y)) {
        pointX = x;
        maxSegmentLength = segmentLength;
      }
    }
    x1 = x2;
  }
  if (isNaN(pointX)) {
    // There is no horizontal line that has its center point inside the linear
    // ring.  Use the center of the the linear ring's extent.
    pointX = flatCenters[flatCentersOffset];
  }
  if (opt_dest) {
    opt_dest.push(pointX, y);
    return opt_dest;
  } else {
    return [pointX, y];
  }
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<Array.<number>>} endss Endss.
 * @param {number} stride Stride.
 * @param {Array.<number>} flatCenters Flat centers.
 * @return {Array.<number>} Interior points.
 */
ol.geom.flat.interiorpoint.linearRingss = function(flatCoordinates, offset, endss, stride, flatCenters) {
  goog.asserts.assert(2 * endss.length == flatCenters.length,
      'endss.length times 2 should be flatCenters.length');
  var interiorPoints = [];
  var i, ii;
  for (i = 0, ii = endss.length; i < ii; ++i) {
    var ends = endss[i];
    interiorPoints = ol.geom.flat.interiorpoint.linearRings(flatCoordinates,
        offset, ends, stride, flatCenters, 2 * i, interiorPoints);
    offset = ends[ends.length - 1];
  }
  return interiorPoints;
};

goog.provide('ol.geom.flat.segments');


/**
 * This function calls `callback` for each segment of the flat coordinates
 * array. If the callback returns a truthy value the function returns that
 * value immediately. Otherwise the function returns `false`.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {function(this: S, ol.Coordinate, ol.Coordinate): T} callback Function
 *     called for each segment.
 * @param {S=} opt_this The object to be used as the value of 'this'
 *     within callback.
 * @return {T|boolean} Value.
 * @template T,S
 */
ol.geom.flat.segments.forEach = function(flatCoordinates, offset, end, stride, callback, opt_this) {
  var point1 = [flatCoordinates[offset], flatCoordinates[offset + 1]];
  var point2 = [];
  var ret;
  for (; (offset + stride) < end; offset += stride) {
    point2[0] = flatCoordinates[offset + stride];
    point2[1] = flatCoordinates[offset + stride + 1];
    ret = callback.call(opt_this, point1, point2);
    if (ret) {
      return ret;
    }
    point1[0] = point2[0];
    point1[1] = point2[1];
  }
  return false;
};

goog.provide('ol.geom.flat.intersectsextent');

goog.require('goog.asserts');
goog.require('ol.extent');
goog.require('ol.geom.flat.contains');
goog.require('ol.geom.flat.segments');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {ol.Extent} extent Extent.
 * @return {boolean} True if the geometry and the extent intersect.
 */
ol.geom.flat.intersectsextent.lineString = function(flatCoordinates, offset, end, stride, extent) {
  var coordinatesExtent = ol.extent.extendFlatCoordinates(
      ol.extent.createEmpty(), flatCoordinates, offset, end, stride);
  if (!ol.extent.intersects(extent, coordinatesExtent)) {
    return false;
  }
  if (ol.extent.containsExtent(extent, coordinatesExtent)) {
    return true;
  }
  if (coordinatesExtent[0] >= extent[0] &&
      coordinatesExtent[2] <= extent[2]) {
    return true;
  }
  if (coordinatesExtent[1] >= extent[1] &&
      coordinatesExtent[3] <= extent[3]) {
    return true;
  }
  return ol.geom.flat.segments.forEach(flatCoordinates, offset, end, stride,
      /**
       * @param {ol.Coordinate} point1 Start point.
       * @param {ol.Coordinate} point2 End point.
       * @return {boolean} `true` if the segment and the extent intersect,
       *     `false` otherwise.
       */
      function(point1, point2) {
        return ol.extent.intersectsSegment(extent, point1, point2);
      });
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<number>} ends Ends.
 * @param {number} stride Stride.
 * @param {ol.Extent} extent Extent.
 * @return {boolean} True if the geometry and the extent intersect.
 */
ol.geom.flat.intersectsextent.lineStrings = function(flatCoordinates, offset, ends, stride, extent) {
  var i, ii;
  for (i = 0, ii = ends.length; i < ii; ++i) {
    if (ol.geom.flat.intersectsextent.lineString(
        flatCoordinates, offset, ends[i], stride, extent)) {
      return true;
    }
    offset = ends[i];
  }
  return false;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {ol.Extent} extent Extent.
 * @return {boolean} True if the geometry and the extent intersect.
 */
ol.geom.flat.intersectsextent.linearRing = function(flatCoordinates, offset, end, stride, extent) {
  if (ol.geom.flat.intersectsextent.lineString(
      flatCoordinates, offset, end, stride, extent)) {
    return true;
  }
  if (ol.geom.flat.contains.linearRingContainsXY(
      flatCoordinates, offset, end, stride, extent[0], extent[1])) {
    return true;
  }
  if (ol.geom.flat.contains.linearRingContainsXY(
      flatCoordinates, offset, end, stride, extent[0], extent[3])) {
    return true;
  }
  if (ol.geom.flat.contains.linearRingContainsXY(
      flatCoordinates, offset, end, stride, extent[2], extent[1])) {
    return true;
  }
  if (ol.geom.flat.contains.linearRingContainsXY(
      flatCoordinates, offset, end, stride, extent[2], extent[3])) {
    return true;
  }
  return false;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<number>} ends Ends.
 * @param {number} stride Stride.
 * @param {ol.Extent} extent Extent.
 * @return {boolean} True if the geometry and the extent intersect.
 */
ol.geom.flat.intersectsextent.linearRings = function(flatCoordinates, offset, ends, stride, extent) {
  goog.asserts.assert(ends.length > 0, 'ends should not be an empty array');
  if (!ol.geom.flat.intersectsextent.linearRing(
      flatCoordinates, offset, ends[0], stride, extent)) {
    return false;
  }
  if (ends.length === 1) {
    return true;
  }
  var i, ii;
  for (i = 1, ii = ends.length; i < ii; ++i) {
    if (ol.geom.flat.contains.linearRingContainsExtent(
        flatCoordinates, ends[i - 1], ends[i], stride, extent)) {
      return false;
    }
  }
  return true;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<Array.<number>>} endss Endss.
 * @param {number} stride Stride.
 * @param {ol.Extent} extent Extent.
 * @return {boolean} True if the geometry and the extent intersect.
 */
ol.geom.flat.intersectsextent.linearRingss = function(flatCoordinates, offset, endss, stride, extent) {
  goog.asserts.assert(endss.length > 0, 'endss should not be an empty array');
  var i, ii;
  for (i = 0, ii = endss.length; i < ii; ++i) {
    var ends = endss[i];
    if (ol.geom.flat.intersectsextent.linearRings(
        flatCoordinates, offset, ends, stride, extent)) {
      return true;
    }
    offset = ends[ends.length - 1];
  }
  return false;
};

goog.provide('ol.geom.flat.reverse');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 */
ol.geom.flat.reverse.coordinates = function(flatCoordinates, offset, end, stride) {
  while (offset < end - stride) {
    var i;
    for (i = 0; i < stride; ++i) {
      var tmp = flatCoordinates[offset + i];
      flatCoordinates[offset + i] = flatCoordinates[end - stride + i];
      flatCoordinates[end - stride + i] = tmp;
    }
    offset += stride;
    end -= stride;
  }
};

goog.provide('ol.geom.flat.orient');

goog.require('ol');
goog.require('ol.geom.flat.reverse');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @return {boolean} Is clockwise.
 */
ol.geom.flat.orient.linearRingIsClockwise = function(flatCoordinates, offset, end, stride) {
  // http://tinyurl.com/clockwise-method
  // https://github.com/OSGeo/gdal/blob/trunk/gdal/ogr/ogrlinearring.cpp
  var edge = 0;
  var x1 = flatCoordinates[end - stride];
  var y1 = flatCoordinates[end - stride + 1];
  for (; offset < end; offset += stride) {
    var x2 = flatCoordinates[offset];
    var y2 = flatCoordinates[offset + 1];
    edge += (x2 - x1) * (y2 + y1);
    x1 = x2;
    y1 = y2;
  }
  return edge > 0;
};


/**
 * Determines if linear rings are oriented.  By default, left-hand orientation
 * is tested (first ring must be clockwise, remaining rings counter-clockwise).
 * To test for right-hand orientation, use the `opt_right` argument.
 *
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<number>} ends Array of end indexes.
 * @param {number} stride Stride.
 * @param {boolean=} opt_right Test for right-hand orientation
 *     (counter-clockwise exterior ring and clockwise interior rings).
 * @return {boolean} Rings are correctly oriented.
 */
ol.geom.flat.orient.linearRingsAreOriented = function(flatCoordinates, offset, ends, stride, opt_right) {
  var right = opt_right !== undefined ? opt_right : false;
  var i, ii;
  for (i = 0, ii = ends.length; i < ii; ++i) {
    var end = ends[i];
    var isClockwise = ol.geom.flat.orient.linearRingIsClockwise(
        flatCoordinates, offset, end, stride);
    if (i === 0) {
      if ((right && isClockwise) || (!right && !isClockwise)) {
        return false;
      }
    } else {
      if ((right && !isClockwise) || (!right && isClockwise)) {
        return false;
      }
    }
    offset = end;
  }
  return true;
};


/**
 * Determines if linear rings are oriented.  By default, left-hand orientation
 * is tested (first ring must be clockwise, remaining rings counter-clockwise).
 * To test for right-hand orientation, use the `opt_right` argument.
 *
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<Array.<number>>} endss Array of array of end indexes.
 * @param {number} stride Stride.
 * @param {boolean=} opt_right Test for right-hand orientation
 *     (counter-clockwise exterior ring and clockwise interior rings).
 * @return {boolean} Rings are correctly oriented.
 */
ol.geom.flat.orient.linearRingssAreOriented = function(flatCoordinates, offset, endss, stride, opt_right) {
  var i, ii;
  for (i = 0, ii = endss.length; i < ii; ++i) {
    if (!ol.geom.flat.orient.linearRingsAreOriented(
        flatCoordinates, offset, endss[i], stride, opt_right)) {
      return false;
    }
  }
  return true;
};


/**
 * Orient coordinates in a flat array of linear rings.  By default, rings
 * are oriented following the left-hand rule (clockwise for exterior and
 * counter-clockwise for interior rings).  To orient according to the
 * right-hand rule, use the `opt_right` argument.
 *
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<number>} ends Ends.
 * @param {number} stride Stride.
 * @param {boolean=} opt_right Follow the right-hand rule for orientation.
 * @return {number} End.
 */
ol.geom.flat.orient.orientLinearRings = function(flatCoordinates, offset, ends, stride, opt_right) {
  var right = opt_right !== undefined ? opt_right : false;
  var i, ii;
  for (i = 0, ii = ends.length; i < ii; ++i) {
    var end = ends[i];
    var isClockwise = ol.geom.flat.orient.linearRingIsClockwise(
        flatCoordinates, offset, end, stride);
    var reverse = i === 0 ?
        (right && isClockwise) || (!right && !isClockwise) :
        (right && !isClockwise) || (!right && isClockwise);
    if (reverse) {
      ol.geom.flat.reverse.coordinates(flatCoordinates, offset, end, stride);
    }
    offset = end;
  }
  return offset;
};


/**
 * Orient coordinates in a flat array of linear rings.  By default, rings
 * are oriented following the left-hand rule (clockwise for exterior and
 * counter-clockwise for interior rings).  To orient according to the
 * right-hand rule, use the `opt_right` argument.
 *
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<Array.<number>>} endss Array of array of end indexes.
 * @param {number} stride Stride.
 * @param {boolean=} opt_right Follow the right-hand rule for orientation.
 * @return {number} End.
 */
ol.geom.flat.orient.orientLinearRingss = function(flatCoordinates, offset, endss, stride, opt_right) {
  var i, ii;
  for (i = 0, ii = endss.length; i < ii; ++i) {
    offset = ol.geom.flat.orient.orientLinearRings(
        flatCoordinates, offset, endss[i], stride, opt_right);
  }
  return offset;
};

goog.provide('ol.geom.Polygon');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.math');
goog.require('ol');
goog.require('ol.extent');
goog.require('ol.geom.GeometryLayout');
goog.require('ol.geom.GeometryType');
goog.require('ol.geom.LinearRing');
goog.require('ol.geom.Point');
goog.require('ol.geom.SimpleGeometry');
goog.require('ol.geom.flat.area');
goog.require('ol.geom.flat.closest');
goog.require('ol.geom.flat.contains');
goog.require('ol.geom.flat.deflate');
goog.require('ol.geom.flat.inflate');
goog.require('ol.geom.flat.interiorpoint');
goog.require('ol.geom.flat.intersectsextent');
goog.require('ol.geom.flat.orient');
goog.require('ol.geom.flat.simplify');


/**
 * @classdesc
 * Polygon geometry.
 *
 * @constructor
 * @extends {ol.geom.SimpleGeometry}
 * @param {Array.<Array.<ol.Coordinate>>} coordinates Coordinates.
 * @param {ol.geom.GeometryLayout=} opt_layout Layout.
 * @api stable
 */
ol.geom.Polygon = function(coordinates, opt_layout) {

  goog.base(this);

  /**
   * @type {Array.<number>}
   * @private
   */
  this.ends_ = [];

  /**
   * @private
   * @type {number}
   */
  this.flatInteriorPointRevision_ = -1;

  /**
   * @private
   * @type {ol.Coordinate}
   */
  this.flatInteriorPoint_ = null;

  /**
   * @private
   * @type {number}
   */
  this.maxDelta_ = -1;

  /**
   * @private
   * @type {number}
   */
  this.maxDeltaRevision_ = -1;

  /**
   * @private
   * @type {number}
   */
  this.orientedRevision_ = -1;

  /**
   * @private
   * @type {Array.<number>}
   */
  this.orientedFlatCoordinates_ = null;

  this.setCoordinates(coordinates, opt_layout);

};
goog.inherits(ol.geom.Polygon, ol.geom.SimpleGeometry);


/**
 * Append the passed linear ring to this polygon.
 * @param {ol.geom.LinearRing} linearRing Linear ring.
 * @api stable
 */
ol.geom.Polygon.prototype.appendLinearRing = function(linearRing) {
  goog.asserts.assert(linearRing.getLayout() == this.layout,
      'layout of linearRing should match layout');
  if (!this.flatCoordinates) {
    this.flatCoordinates = linearRing.getFlatCoordinates().slice();
  } else {
    goog.array.extend(this.flatCoordinates, linearRing.getFlatCoordinates());
  }
  this.ends_.push(this.flatCoordinates.length);
  this.changed();
};


/**
 * Make a complete copy of the geometry.
 * @return {!ol.geom.Polygon} Clone.
 * @api stable
 */
ol.geom.Polygon.prototype.clone = function() {
  var polygon = new ol.geom.Polygon(null);
  polygon.setFlatCoordinates(
      this.layout, this.flatCoordinates.slice(), this.ends_.slice());
  return polygon;
};


/**
 * @inheritDoc
 */
ol.geom.Polygon.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
  if (minSquaredDistance <
      ol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
    return minSquaredDistance;
  }
  if (this.maxDeltaRevision_ != this.getRevision()) {
    this.maxDelta_ = Math.sqrt(ol.geom.flat.closest.getsMaxSquaredDelta(
        this.flatCoordinates, 0, this.ends_, this.stride, 0));
    this.maxDeltaRevision_ = this.getRevision();
  }
  return ol.geom.flat.closest.getsClosestPoint(
      this.flatCoordinates, 0, this.ends_, this.stride,
      this.maxDelta_, true, x, y, closestPoint, minSquaredDistance);
};


/**
 * @inheritDoc
 */
ol.geom.Polygon.prototype.containsXY = function(x, y) {
  return ol.geom.flat.contains.linearRingsContainsXY(
      this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride, x, y);
};


/**
 * Return the area of the polygon on projected plane.
 * @return {number} Area (on projected plane).
 * @api stable
 */
ol.geom.Polygon.prototype.getArea = function() {
  return ol.geom.flat.area.linearRings(
      this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride);
};


/**
 * Get the coordinate array for this geometry.  This array has the structure
 * of a GeoJSON coordinate array for polygons.
 *
 * @param {boolean=} opt_right Orient coordinates according to the right-hand
 *     rule (counter-clockwise for exterior and clockwise for interior rings).
 *     If `false`, coordinates will be oriented according to the left-hand rule
 *     (clockwise for exterior and counter-clockwise for interior rings).
 *     By default, coordinate orientation will depend on how the geometry was
 *     constructed.
 * @return {Array.<Array.<ol.Coordinate>>} Coordinates.
 * @api stable
 */
ol.geom.Polygon.prototype.getCoordinates = function(opt_right) {
  var flatCoordinates;
  if (opt_right !== undefined) {
    flatCoordinates = this.getOrientedFlatCoordinates().slice();
    ol.geom.flat.orient.orientLinearRings(
        flatCoordinates, 0, this.ends_, this.stride, opt_right);
  } else {
    flatCoordinates = this.flatCoordinates;
  }

  return ol.geom.flat.inflate.coordinatess(
      flatCoordinates, 0, this.ends_, this.stride);
};


/**
 * @return {Array.<number>} Ends.
 */
ol.geom.Polygon.prototype.getEnds = function() {
  return this.ends_;
};


/**
 * @return {Array.<number>} Interior point.
 */
ol.geom.Polygon.prototype.getFlatInteriorPoint = function() {
  if (this.flatInteriorPointRevision_ != this.getRevision()) {
    var flatCenter = ol.extent.getCenter(this.getExtent());
    this.flatInteriorPoint_ = ol.geom.flat.interiorpoint.linearRings(
        this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride,
        flatCenter, 0);
    this.flatInteriorPointRevision_ = this.getRevision();
  }
  return this.flatInteriorPoint_;
};


/**
 * Return an interior point of the polygon.
 * @return {ol.geom.Point} Interior point.
 * @api stable
 */
ol.geom.Polygon.prototype.getInteriorPoint = function() {
  return new ol.geom.Point(this.getFlatInteriorPoint());
};


/**
 * Return the number of rings of the polygon,  this includes the exterior
 * ring and any interior rings.
 *
 * @return {number} Number of rings.
 * @api
 */
ol.geom.Polygon.prototype.getLinearRingCount = function() {
  return this.ends_.length;
};


/**
 * Return the Nth linear ring of the polygon geometry. Return `null` if the
 * given index is out of range.
 * The exterior linear ring is available at index `0` and the interior rings
 * at index `1` and beyond.
 *
 * @param {number} index Index.
 * @return {ol.geom.LinearRing} Linear ring.
 * @api stable
 */
ol.geom.Polygon.prototype.getLinearRing = function(index) {
  goog.asserts.assert(0 <= index && index < this.ends_.length,
      'index should be in between 0 and and length of this.ends_');
  if (index < 0 || this.ends_.length <= index) {
    return null;
  }
  var linearRing = new ol.geom.LinearRing(null);
  linearRing.setFlatCoordinates(this.layout, this.flatCoordinates.slice(
      index === 0 ? 0 : this.ends_[index - 1], this.ends_[index]));
  return linearRing;
};


/**
 * Return the linear rings of the polygon.
 * @return {Array.<ol.geom.LinearRing>} Linear rings.
 * @api stable
 */
ol.geom.Polygon.prototype.getLinearRings = function() {
  var layout = this.layout;
  var flatCoordinates = this.flatCoordinates;
  var ends = this.ends_;
  var linearRings = [];
  var offset = 0;
  var i, ii;
  for (i = 0, ii = ends.length; i < ii; ++i) {
    var end = ends[i];
    var linearRing = new ol.geom.LinearRing(null);
    linearRing.setFlatCoordinates(layout, flatCoordinates.slice(offset, end));
    linearRings.push(linearRing);
    offset = end;
  }
  return linearRings;
};


/**
 * @return {Array.<number>} Oriented flat coordinates.
 */
ol.geom.Polygon.prototype.getOrientedFlatCoordinates = function() {
  if (this.orientedRevision_ != this.getRevision()) {
    var flatCoordinates = this.flatCoordinates;
    if (ol.geom.flat.orient.linearRingsAreOriented(
        flatCoordinates, 0, this.ends_, this.stride)) {
      this.orientedFlatCoordinates_ = flatCoordinates;
    } else {
      this.orientedFlatCoordinates_ = flatCoordinates.slice();
      this.orientedFlatCoordinates_.length =
          ol.geom.flat.orient.orientLinearRings(
              this.orientedFlatCoordinates_, 0, this.ends_, this.stride);
    }
    this.orientedRevision_ = this.getRevision();
  }
  return this.orientedFlatCoordinates_;
};


/**
 * @inheritDoc
 */
ol.geom.Polygon.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) {
  var simplifiedFlatCoordinates = [];
  var simplifiedEnds = [];
  simplifiedFlatCoordinates.length = ol.geom.flat.simplify.quantizes(
      this.flatCoordinates, 0, this.ends_, this.stride,
      Math.sqrt(squaredTolerance),
      simplifiedFlatCoordinates, 0, simplifiedEnds);
  var simplifiedPolygon = new ol.geom.Polygon(null);
  simplifiedPolygon.setFlatCoordinates(
      ol.geom.GeometryLayout.XY, simplifiedFlatCoordinates, simplifiedEnds);
  return simplifiedPolygon;
};


/**
 * @inheritDoc
 * @api stable
 */
ol.geom.Polygon.prototype.getType = function() {
  return ol.geom.GeometryType.POLYGON;
};


/**
 * @inheritDoc
 * @api stable
 */
ol.geom.Polygon.prototype.intersectsExtent = function(extent) {
  return ol.geom.flat.intersectsextent.linearRings(
      this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride, extent);
};


/**
 * Set the coordinates of the polygon.
 * @param {Array.<Array.<ol.Coordinate>>} coordinates Coordinates.
 * @param {ol.geom.GeometryLayout=} opt_layout Layout.
 * @api stable
 */
ol.geom.Polygon.prototype.setCoordinates = function(coordinates, opt_layout) {
  if (!coordinates) {
    this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null, this.ends_);
  } else {
    this.setLayout(opt_layout, coordinates, 2);
    if (!this.flatCoordinates) {
      this.flatCoordinates = [];
    }
    var ends = ol.geom.flat.deflate.coordinatess(
        this.flatCoordinates, 0, coordinates, this.stride, this.ends_);
    this.flatCoordinates.length = ends.length === 0 ? 0 : ends[ends.length - 1];
    this.changed();
  }
};


/**
 * @param {ol.geom.GeometryLayout} layout Layout.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {Array.<number>} ends Ends.
 */
ol.geom.Polygon.prototype.setFlatCoordinates = function(layout, flatCoordinates, ends) {
  if (!flatCoordinates) {
    goog.asserts.assert(ends && ends.length === 0,
        'ends must be an empty array');
  } else if (ends.length === 0) {
    goog.asserts.assert(flatCoordinates.length === 0,
        'flatCoordinates should be an empty array');
  } else {
    goog.asserts.assert(flatCoordinates.length == ends[ends.length - 1],
        'the length of flatCoordinates should be the last entry of ends');
  }
  this.setFlatCoordinatesInternal(layout, flatCoordinates);
  this.ends_ = ends;
  this.changed();
};


/**
 * Create an approximation of a circle on the surface of a sphere.
 * @param {ol.Sphere} sphere The sphere.
 * @param {ol.Coordinate} center Center (`[lon, lat]` in degrees).
 * @param {number} radius The great-circle distance from the center to
 *     the polygon vertices.
 * @param {number=} opt_n Optional number of vertices for the resulting
 *     polygon. Default is `32`.
 * @return {ol.geom.Polygon} The "circular" polygon.
 * @api stable
 */
ol.geom.Polygon.circular = function(sphere, center, radius, opt_n) {
  var n = opt_n ? opt_n : 32;
  /** @type {Array.<number>} */
  var flatCoordinates = [];
  var i;
  for (i = 0; i < n; ++i) {
    goog.array.extend(
        flatCoordinates, sphere.offset(center, radius, 2 * Math.PI * i / n));
  }
  flatCoordinates.push(flatCoordinates[0], flatCoordinates[1]);
  var polygon = new ol.geom.Polygon(null);
  polygon.setFlatCoordinates(
      ol.geom.GeometryLayout.XY, flatCoordinates, [flatCoordinates.length]);
  return polygon;
};


/**
 * Create a polygon from an extent. The layout used is `XY`.
 * @param {ol.Extent} extent The extent.
 * @return {ol.geom.Polygon} The polygon.
 * @api
 */
ol.geom.Polygon.fromExtent = function(extent) {
  var minX = extent[0];
  var minY = extent[1];
  var maxX = extent[2];
  var maxY = extent[3];
  var flatCoordinates =
      [minX, minY, minX, maxY, maxX, maxY, maxX, minY, minX, minY];
  var polygon = new ol.geom.Polygon(null);
  polygon.setFlatCoordinates(
      ol.geom.GeometryLayout.XY, flatCoordinates, [flatCoordinates.length]);
  return polygon;
};


/**
 * Create a regular polygon from a circle.
 * @param {ol.geom.Circle} circle Circle geometry.
 * @param {number=} opt_sides Number of sides of the polygon. Default is 32.
 * @param {number=} opt_angle Start angle for the first vertex of the polygon in
 *     radians. Default is 0.
 * @return {ol.geom.Polygon} Polygon geometry.
 * @api
 */
ol.geom.Polygon.fromCircle = function(circle, opt_sides, opt_angle) {
  var sides = opt_sides ? opt_sides : 32;
  var stride = circle.getStride();
  var layout = circle.getLayout();
  var polygon = new ol.geom.Polygon(null, layout);
  var flatCoordinates = goog.array.repeat(0, stride * (sides + 1));
  var ends = [flatCoordinates.length];
  polygon.setFlatCoordinates(layout, flatCoordinates, ends);
  ol.geom.Polygon.makeRegular(
      polygon, circle.getCenter(), circle.getRadius(), opt_angle);
  return polygon;
};


/**
 * Modify the coordinates of a polygon to make it a regular polygon.
 * @param {ol.geom.Polygon} polygon Polygon geometry.
 * @param {ol.Coordinate} center Center of the regular polygon.
 * @param {number} radius Radius of the regular polygon.
 * @param {number=} opt_angle Start angle for the first vertex of the polygon in
 *     radians. Default is 0.
 */
ol.geom.Polygon.makeRegular = function(polygon, center, radius, opt_angle) {
  var flatCoordinates = polygon.getFlatCoordinates();
  var layout = polygon.getLayout();
  var stride = polygon.getStride();
  var ends = polygon.getEnds();
  goog.asserts.assert(ends.length === 1, 'only 1 ring is supported');
  var sides = flatCoordinates.length / stride - 1;
  var startAngle = opt_angle ? opt_angle : 0;
  var angle, offset;
  for (var i = 0; i <= sides; ++i) {
    offset = i * stride;
    angle = startAngle + (goog.math.modulo(i, sides) * 2 * Math.PI / sides);
    flatCoordinates[offset] = center[0] + (radius * Math.cos(angle));
    flatCoordinates[offset + 1] = center[1] + (radius * Math.sin(angle));
  }
  polygon.setFlatCoordinates(layout, flatCoordinates, ends);
};

goog.provide('ol.View');
goog.provide('ol.ViewHint');
goog.provide('ol.ViewProperty');

goog.require('goog.asserts');
goog.require('ol');
goog.require('ol.CenterConstraint');
goog.require('ol.Constraints');
goog.require('ol.Object');
goog.require('ol.ResolutionConstraint');
goog.require('ol.RotationConstraint');
goog.require('ol.RotationConstraintType');
goog.require('ol.Size');
goog.require('ol.coordinate');
goog.require('ol.extent');
goog.require('ol.geom.Polygon');
goog.require('ol.geom.SimpleGeometry');
goog.require('ol.proj');
goog.require('ol.proj.METERS_PER_UNIT');
goog.require('ol.proj.Projection');
goog.require('ol.proj.Units');


/**
 * @enum {string}
 */
ol.ViewProperty = {
  CENTER: 'center',
  RESOLUTION: 'resolution',
  ROTATION: 'rotation'
};


/**
 * @enum {number}
 */
ol.ViewHint = {
  ANIMATING: 0,
  INTERACTING: 1
};


/**
 * @classdesc
 * An ol.View object represents a simple 2D view of the map.
 *
 * This is the object to act upon to change the center, resolution,
 * and rotation of the map.
 *
 * ### The view states
 *
 * An `ol.View` is determined by three states: `center`, `resolution`,
 * and `rotation`. Each state has a corresponding getter and setter, e.g.
 * `getCenter` and `setCenter` for the `center` state.
 *
 * An `ol.View` has a `projection`. The projection determines the
 * coordinate system of the center, and its units determine the units of the
 * resolution (projection units per pixel). The default projection is
 * Spherical Mercator (EPSG:3857).
 *
 * ### The constraints
 *
 * `setCenter`, `setResolution` and `setRotation` can be used to change the
 * states of the view. Any value can be passed to the setters. And the value
 * that is passed to a setter will effectively be the value set in the view,
 * and returned by the corresponding getter.
 *
 * But an `ol.View` object also has a *resolution constraint*, a
 * *rotation constraint* and a *center constraint*.
 *
 * As said above, no constraints are applied when the setters are used to set
 * new states for the view. Applying constraints is done explicitly through
 * the use of the `constrain*` functions (`constrainResolution` and
 * `constrainRotation` and `constrainCenter`).
 *
 * The main users of the constraints are the interactions and the
 * controls. For example, double-clicking on the map changes the view to
 * the "next" resolution. And releasing the fingers after pinch-zooming
 * snaps to the closest resolution (with an animation).
 *
 * The *resolution constraint* snaps to specific resolutions. It is
 * determined by the following options: `resolutions`, `maxResolution`,
 * `maxZoom`, and `zoomFactor`. If `resolutions` is set, the other three
 * options are ignored. See documentation for each option for more
 * information.
 *
 * The *rotation constraint* snaps to specific angles. It is determined
 * by the following options: `enableRotation` and `constrainRotation`.
 * By default the rotation value is snapped to zero when approaching the
 * horizontal.
 *
 * The *center constraint* is determined by the `extent` option. By
 * default the center is not constrained at all.
 *
 * @constructor
 * @extends {ol.Object}
 * @param {olx.ViewOptions=} opt_options View options.
 * @api stable
 */
ol.View = function(opt_options) {
  goog.base(this);
  var options = opt_options || {};

  /**
   * @private
   * @type {Array.<number>}
   */
  this.hints_ = [0, 0];

  /**
   * @type {Object.<string, *>}
   */
  var properties = {};
  properties[ol.ViewProperty.CENTER] = options.center !== undefined ?
      options.center : null;

  /**
   * @private
   * @const
   * @type {ol.proj.Projection}
   */
  this.projection_ = ol.proj.createProjection(options.projection, 'EPSG:3857');

  var resolutionConstraintInfo = ol.View.createResolutionConstraint_(
      options);

  /**
   * @private
   * @type {number}
   */
  this.maxResolution_ = resolutionConstraintInfo.maxResolution;

  /**
   * @private
   * @type {number}
   */
  this.minResolution_ = resolutionConstraintInfo.minResolution;

  /**
   * @private
   * @type {number}
   */
  this.minZoom_ = resolutionConstraintInfo.minZoom;

  var centerConstraint = ol.View.createCenterConstraint_(options);
  var resolutionConstraint = resolutionConstraintInfo.constraint;
  var rotationConstraint = ol.View.createRotationConstraint_(options);

  /**
   * @private
   * @type {ol.Constraints}
   */
  this.constraints_ = new ol.Constraints(
      centerConstraint, resolutionConstraint, rotationConstraint);

  if (options.resolution !== undefined) {
    properties[ol.ViewProperty.RESOLUTION] = options.resolution;
  } else if (options.zoom !== undefined) {
    properties[ol.ViewProperty.RESOLUTION] = this.constrainResolution(
        this.maxResolution_, options.zoom - this.minZoom_);
  }
  properties[ol.ViewProperty.ROTATION] =
      options.rotation !== undefined ? options.rotation : 0;
  this.setProperties(properties);
};
goog.inherits(ol.View, ol.Object);


/**
 * @param {number} rotation Target rotation.
 * @param {ol.Coordinate} anchor Rotation anchor.
 * @return {ol.Coordinate|undefined} Center for rotation and anchor.
 */
ol.View.prototype.calculateCenterRotate = function(rotation, anchor) {
  var center;
  var currentCenter = this.getCenter();
  if (currentCenter !== undefined) {
    center = [currentCenter[0] - anchor[0], currentCenter[1] - anchor[1]];
    ol.coordinate.rotate(center, rotation - this.getRotation());
    ol.coordinate.add(center, anchor);
  }
  return center;
};


/**
 * @param {number} resolution Target resolution.
 * @param {ol.Coordinate} anchor Zoom anchor.
 * @return {ol.Coordinate|undefined} Center for resolution and anchor.
 */
ol.View.prototype.calculateCenterZoom = function(resolution, anchor) {
  var center;
  var currentCenter = this.getCenter();
  var currentResolution = this.getResolution();
  if (currentCenter !== undefined && currentResolution !== undefined) {
    var x = anchor[0] -
        resolution * (anchor[0] - currentCenter[0]) / currentResolution;
    var y = anchor[1] -
        resolution * (anchor[1] - currentCenter[1]) / currentResolution;
    center = [x, y];
  }
  return center;
};


/**
 * Get the constrained center of this view.
 * @param {ol.Coordinate|undefined} center Center.
 * @return {ol.Coordinate|undefined} Constrained center.
 * @api
 */
ol.View.prototype.constrainCenter = function(center) {
  return this.constraints_.center(center);
};


/**
 * Get the constrained resolution of this view.
 * @param {number|undefined} resolution Resolution.
 * @param {number=} opt_delta Delta. Default is `0`.
 * @param {number=} opt_direction Direction. Default is `0`.
 * @return {number|undefined} Constrained resolution.
 * @api
 */
ol.View.prototype.constrainResolution = function(
    resolution, opt_delta, opt_direction) {
  var delta = opt_delta || 0;
  var direction = opt_direction || 0;
  return this.constraints_.resolution(resolution, delta, direction);
};


/**
 * Get the constrained rotation of this view.
 * @param {number|undefined} rotation Rotation.
 * @param {number=} opt_delta Delta. Default is `0`.
 * @return {number|undefined} Constrained rotation.
 * @api
 */
ol.View.prototype.constrainRotation = function(rotation, opt_delta) {
  var delta = opt_delta || 0;
  return this.constraints_.rotation(rotation, delta);
};


/**
 * Get the view center.
 * @return {ol.Coordinate|undefined} The center of the view.
 * @observable
 * @api stable
 */
ol.View.prototype.getCenter = function() {
  return /** @type {ol.Coordinate|undefined} */ (
      this.get(ol.ViewProperty.CENTER));
};


/**
 * @return {Array.<number>} Hint.
 */
ol.View.prototype.getHints = function() {
  return this.hints_.slice();
};


/**
 * Calculate the extent for the current view state and the passed size.
 * The size is the pixel dimensions of the box into which the calculated extent
 * should fit. In most cases you want to get the extent of the entire map,
 * that is `map.getSize()`.
 * @param {ol.Size} size Box pixel size.
 * @return {ol.Extent} Extent.
 * @api stable
 */
ol.View.prototype.calculateExtent = function(size) {
  var center = this.getCenter();
  goog.asserts.assert(center, 'The view center is not defined');
  var resolution = this.getResolution();
  goog.asserts.assert(resolution !== undefined,
      'The view resolution is not defined');
  var rotation = this.getRotation();
  goog.asserts.assert(rotation !== undefined,
      'The view rotation is not defined');

  return ol.extent.getForViewAndSize(center, resolution, rotation, size);
};


/**
 * Get the view projection.
 * @return {ol.proj.Projection} The projection of the view.
 * @api stable
 */
ol.View.prototype.getProjection = function() {
  return this.projection_;
};


/**
 * Get the view resolution.
 * @return {number|undefined} The resolution of the view.
 * @observable
 * @api stable
 */
ol.View.prototype.getResolution = function() {
  return /** @type {number|undefined} */ (
      this.get(ol.ViewProperty.RESOLUTION));
};


/**
 * Get the resolution for a provided extent (in map units) and size (in pixels).
 * @param {ol.Extent} extent Extent.
 * @param {ol.Size} size Box pixel size.
 * @return {number} The resolution at which the provided extent will render at
 *     the given size.
 */
ol.View.prototype.getResolutionForExtent = function(extent, size) {
  var xResolution = ol.extent.getWidth(extent) / size[0];
  var yResolution = ol.extent.getHeight(extent) / size[1];
  return Math.max(xResolution, yResolution);
};


/**
 * Return a function that returns a value between 0 and 1 for a
 * resolution. Exponential scaling is assumed.
 * @param {number=} opt_power Power.
 * @return {function(number): number} Resolution for value function.
 */
ol.View.prototype.getResolutionForValueFunction = function(opt_power) {
  var power = opt_power || 2;
  var maxResolution = this.maxResolution_;
  var minResolution = this.minResolution_;
  var max = Math.log(maxResolution / minResolution) / Math.log(power);
  return (
      /**
       * @param {number} value Value.
       * @return {number} Resolution.
       */
      function(value) {
        var resolution = maxResolution / Math.pow(power, value * max);
        goog.asserts.assert(resolution >= minResolution &&
            resolution <= maxResolution,
            'calculated resolution outside allowed bounds (%s <= %s <= %s)',
            minResolution, resolution, maxResolution);
        return resolution;
      });
};


/**
 * Get the view rotation.
 * @return {number} The rotation of the view in radians.
 * @observable
 * @api stable
 */
ol.View.prototype.getRotation = function() {
  return /** @type {number} */ (this.get(ol.ViewProperty.ROTATION));
};


/**
 * Return a function that returns a resolution for a value between
 * 0 and 1. Exponential scaling is assumed.
 * @param {number=} opt_power Power.
 * @return {function(number): number} Value for resolution function.
 */
ol.View.prototype.getValueForResolutionFunction = function(opt_power) {
  var power = opt_power || 2;
  var maxResolution = this.maxResolution_;
  var minResolution = this.minResolution_;
  var max = Math.log(maxResolution / minResolution) / Math.log(power);
  return (
      /**
       * @param {number} resolution Resolution.
       * @return {number} Value.
       */
      function(resolution) {
        var value =
            (Math.log(maxResolution / resolution) / Math.log(power)) / max;
        goog.asserts.assert(value >= 0 && value <= 1,
            'calculated value (%s) ouside allowed range (0-1)', value);
        return value;
      });
};


/**
 * @return {olx.ViewState} View state.
 */
ol.View.prototype.getState = function() {
  goog.asserts.assert(this.isDef(),
      'the view was not defined (had no center and/or resolution)');
  var center = /** @type {ol.Coordinate} */ (this.getCenter());
  var projection = this.getProjection();
  var resolution = /** @type {number} */ (this.getResolution());
  var rotation = this.getRotation();
  return /** @type {olx.ViewState} */ ({
    // Snap center to closest pixel
    center: [
      Math.round(center[0] / resolution) * resolution,
      Math.round(center[1] / resolution) * resolution
    ],
    projection: projection !== undefined ? projection : null,
    resolution: resolution,
    rotation: rotation
  });
};


/**
 * Get the current zoom level. Return undefined if the current
 * resolution is undefined or not a "constrained resolution".
 * @return {number|undefined} Zoom.
 * @api stable
 */
ol.View.prototype.getZoom = function() {
  var offset;
  var resolution = this.getResolution();

  if (resolution !== undefined) {
    var res, z = 0;
    do {
      res = this.constrainResolution(this.maxResolution_, z);
      if (res == resolution) {
        offset = z;
        break;
      }
      ++z;
    } while (res > this.minResolution_);
  }

  return offset !== undefined ? this.minZoom_ + offset : offset;
};


/**
 * Fit the given geometry or extent based on the given map size and border.
 * The size is pixel dimensions of the box to fit the extent into.
 * In most cases you will want to use the map size, that is `map.getSize()`.
 * Takes care of the map angle.
 * @param {ol.geom.SimpleGeometry|ol.Extent} geometry Geometry.
 * @param {ol.Size} size Box pixel size.
 * @param {olx.view.FitOptions=} opt_options Options.
 * @api
 */
ol.View.prototype.fit = function(geometry, size, opt_options) {
  if (!(geometry instanceof ol.geom.SimpleGeometry)) {
    goog.asserts.assert(goog.isArray(geometry),
        'invalid extent or geometry');
    goog.asserts.assert(!ol.extent.isEmpty(geometry),
        'cannot fit empty extent');
    geometry = ol.geom.Polygon.fromExtent(geometry);
  }

  var options = opt_options || {};

  var padding = options.padding !== undefined ? options.padding : [0, 0, 0, 0];
  var constrainResolution = options.constrainResolution !== undefined ?
      options.constrainResolution : true;
  var nearest = options.nearest !== undefined ? options.nearest : false;
  var minResolution;
  if (options.minResolution !== undefined) {
    minResolution = options.minResolution;
  } else if (options.maxZoom !== undefined) {
    minResolution = this.constrainResolution(
        this.maxResolution_, options.maxZoom - this.minZoom_, 0);
  } else {
    minResolution = 0;
  }
  var coords = geometry.getFlatCoordinates();

  // calculate rotated extent
  var rotation = this.getRotation();
  goog.asserts.assert(rotation !== undefined, 'rotation was not defined');
  var cosAngle = Math.cos(-rotation);
  var sinAngle = Math.sin(-rotation);
  var minRotX = +Infinity;
  var minRotY = +Infinity;
  var maxRotX = -Infinity;
  var maxRotY = -Infinity;
  var stride = geometry.getStride();
  for (var i = 0, ii = coords.length; i < ii; i += stride) {
    var rotX = coords[i] * cosAngle - coords[i + 1] * sinAngle;
    var rotY = coords[i] * sinAngle + coords[i + 1] * cosAngle;
    minRotX = Math.min(minRotX, rotX);
    minRotY = Math.min(minRotY, rotY);
    maxRotX = Math.max(maxRotX, rotX);
    maxRotY = Math.max(maxRotY, rotY);
  }

  // calculate resolution
  var resolution = this.getResolutionForExtent(
      [minRotX, minRotY, maxRotX, maxRotY],
      [size[0] - padding[1] - padding[3], size[1] - padding[0] - padding[2]]);
  resolution = isNaN(resolution) ? minResolution :
      Math.max(resolution, minResolution);
  if (constrainResolution) {
    var constrainedResolution = this.constrainResolution(resolution, 0, 0);
    if (!nearest && constrainedResolution < resolution) {
      constrainedResolution = this.constrainResolution(
          constrainedResolution, -1, 0);
    }
    resolution = constrainedResolution;
  }
  this.setResolution(resolution);

  // calculate center
  sinAngle = -sinAngle; // go back to original rotation
  var centerRotX = (minRotX + maxRotX) / 2;
  var centerRotY = (minRotY + maxRotY) / 2;
  centerRotX += (padding[1] - padding[3]) / 2 * resolution;
  centerRotY += (padding[0] - padding[2]) / 2 * resolution;
  var centerX = centerRotX * cosAngle - centerRotY * sinAngle;
  var centerY = centerRotY * cosAngle + centerRotX * sinAngle;

  this.setCenter([centerX, centerY]);
};


/**
 * Center on coordinate and view position.
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {ol.Size} size Box pixel size.
 * @param {ol.Pixel} position Position on the view to center on.
 * @api
 */
ol.View.prototype.centerOn = function(coordinate, size, position) {
  // calculate rotated position
  var rotation = this.getRotation();
  var cosAngle = Math.cos(-rotation);
  var sinAngle = Math.sin(-rotation);
  var rotX = coordinate[0] * cosAngle - coordinate[1] * sinAngle;
  var rotY = coordinate[1] * cosAngle + coordinate[0] * sinAngle;
  var resolution = this.getResolution();
  rotX += (size[0] / 2 - position[0]) * resolution;
  rotY += (position[1] - size[1] / 2) * resolution;

  // go back to original angle
  sinAngle = -sinAngle; // go back to original rotation
  var centerX = rotX * cosAngle - rotY * sinAngle;
  var centerY = rotY * cosAngle + rotX * sinAngle;

  this.setCenter([centerX, centerY]);
};


/**
 * @return {boolean} Is defined.
 */
ol.View.prototype.isDef = function() {
  return !!this.getCenter() && this.getResolution() !== undefined;
};


/**
 * Rotate the view around a given coordinate.
 * @param {number} rotation New rotation value for the view.
 * @param {ol.Coordinate=} opt_anchor The rotation center.
 * @api stable
 */
ol.View.prototype.rotate = function(rotation, opt_anchor) {
  if (opt_anchor !== undefined) {
    var center = this.calculateCenterRotate(rotation, opt_anchor);
    this.setCenter(center);
  }
  this.setRotation(rotation);
};


/**
 * Set the center of the current view.
 * @param {ol.Coordinate|undefined} center The center of the view.
 * @observable
 * @api stable
 */
ol.View.prototype.setCenter = function(center) {
  this.set(ol.ViewProperty.CENTER, center);
};


/**
 * @param {ol.ViewHint} hint Hint.
 * @param {number} delta Delta.
 * @return {number} New value.
 */
ol.View.prototype.setHint = function(hint, delta) {
  goog.asserts.assert(0 <= hint && hint < this.hints_.length,
      'illegal hint (%s), must be between 0 and %s', hint, this.hints_.length);
  this.hints_[hint] += delta;
  goog.asserts.assert(this.hints_[hint] >= 0,
      'Hint at %s must be positive, was %s', hint, this.hints_[hint]);
  return this.hints_[hint];
};


/**
 * Set the resolution for this view.
 * @param {number|undefined} resolution The resolution of the view.
 * @observable
 * @api stable
 */
ol.View.prototype.setResolution = function(resolution) {
  this.set(ol.ViewProperty.RESOLUTION, resolution);
};


/**
 * Set the rotation for this view.
 * @param {number} rotation The rotation of the view in radians.
 * @observable
 * @api stable
 */
ol.View.prototype.setRotation = function(rotation) {
  this.set(ol.ViewProperty.ROTATION, rotation);
};


/**
 * Zoom to a specific zoom level.
 * @param {number} zoom Zoom level.
 * @api stable
 */
ol.View.prototype.setZoom = function(zoom) {
  var resolution = this.constrainResolution(
      this.maxResolution_, zoom - this.minZoom_, 0);
  this.setResolution(resolution);
};


/**
 * @param {olx.ViewOptions} options View options.
 * @private
 * @return {ol.CenterConstraintType} The constraint.
 */
ol.View.createCenterConstraint_ = function(options) {
  if (options.extent !== undefined) {
    return ol.CenterConstraint.createExtent(options.extent);
  } else {
    return ol.CenterConstraint.none;
  }
};


/**
 * @private
 * @param {olx.ViewOptions} options View options.
 * @return {{constraint: ol.ResolutionConstraintType, maxResolution: number,
 *     minResolution: number}} The constraint.
 */
ol.View.createResolutionConstraint_ = function(options) {
  var resolutionConstraint;
  var maxResolution;
  var minResolution;

  // TODO: move these to be ol constants
  // see https://github.com/openlayers/ol3/issues/2076
  var defaultMaxZoom = 28;
  var defaultZoomFactor = 2;

  var minZoom = options.minZoom !== undefined ?
      options.minZoom : ol.DEFAULT_MIN_ZOOM;

  var maxZoom = options.maxZoom !== undefined ?
      options.maxZoom : defaultMaxZoom;

  var zoomFactor = options.zoomFactor !== undefined ?
      options.zoomFactor : defaultZoomFactor;

  if (options.resolutions !== undefined) {
    var resolutions = options.resolutions;
    maxResolution = resolutions[0];
    minResolution = resolutions[resolutions.length - 1];
    resolutionConstraint = ol.ResolutionConstraint.createSnapToResolutions(
        resolutions);
  } else {
    // calculate the default min and max resolution
    var projection = ol.proj.createProjection(options.projection, 'EPSG:3857');
    var extent = projection.getExtent();
    var size = !extent ?
        // use an extent that can fit the whole world if need be
        360 * ol.proj.METERS_PER_UNIT[ol.proj.Units.DEGREES] /
            projection.getMetersPerUnit() :
        Math.max(ol.extent.getWidth(extent), ol.extent.getHeight(extent));

    var defaultMaxResolution = size / ol.DEFAULT_TILE_SIZE / Math.pow(
        defaultZoomFactor, ol.DEFAULT_MIN_ZOOM);

    var defaultMinResolution = defaultMaxResolution / Math.pow(
        defaultZoomFactor, defaultMaxZoom - ol.DEFAULT_MIN_ZOOM);

    // user provided maxResolution takes precedence
    maxResolution = options.maxResolution;
    if (maxResolution !== undefined) {
      minZoom = 0;
    } else {
      maxResolution = defaultMaxResolution / Math.pow(zoomFactor, minZoom);
    }

    // user provided minResolution takes precedence
    minResolution = options.minResolution;
    if (minResolution === undefined) {
      if (options.maxZoom !== undefined) {
        if (options.maxResolution !== undefined) {
          minResolution = maxResolution / Math.pow(zoomFactor, maxZoom);
        } else {
          minResolution = defaultMaxResolution / Math.pow(zoomFactor, maxZoom);
        }
      } else {
        minResolution = defaultMinResolution;
      }
    }

    // given discrete zoom levels, minResolution may be different than provided
    maxZoom = minZoom + Math.floor(
        Math.log(maxResolution / minResolution) / Math.log(zoomFactor));
    minResolution = maxResolution / Math.pow(zoomFactor, maxZoom - minZoom);

    resolutionConstraint = ol.ResolutionConstraint.createSnapToPower(
        zoomFactor, maxResolution, maxZoom - minZoom);
  }
  return {constraint: resolutionConstraint, maxResolution: maxResolution,
    minResolution: minResolution, minZoom: minZoom};
};


/**
 * @private
 * @param {olx.ViewOptions} options View options.
 * @return {ol.RotationConstraintType} Rotation constraint.
 */
ol.View.createRotationConstraint_ = function(options) {
  var enableRotation = options.enableRotation !== undefined ?
      options.enableRotation : true;
  if (enableRotation) {
    var constrainRotation = options.constrainRotation;
    if (constrainRotation === undefined || constrainRotation === true) {
      return ol.RotationConstraint.createSnapToZero();
    } else if (constrainRotation === false) {
      return ol.RotationConstraint.none;
    } else if (goog.isNumber(constrainRotation)) {
      return ol.RotationConstraint.createSnapToN(constrainRotation);
    } else {
      goog.asserts.fail(
          'illegal option for constrainRotation (%s)', constrainRotation);
      return ol.RotationConstraint.none;
    }
  } else {
    return ol.RotationConstraint.disable;
  }
};

goog.provide('ol.easing');


/**
 * Start slow and speed up.
 * @param {number} t Input between 0 and 1.
 * @return {number} Output between 0 and 1.
 * @api
 */
ol.easing.easeIn = function(t) {
  return Math.pow(t, 3);
};


/**
 * Start fast and slow down.
 * @param {number} t Input between 0 and 1.
 * @return {number} Output between 0 and 1.
 * @api
 */
ol.easing.easeOut = function(t) {
  return 1 - ol.easing.easeIn(1 - t);
};


/**
 * Start slow, speed up, and then slow down again.
 * @param {number} t Input between 0 and 1.
 * @return {number} Output between 0 and 1.
 * @api
 */
ol.easing.inAndOut = function(t) {
  return 3 * t * t - 2 * t * t * t;
};


/**
 * Maintain a constant speed over time.
 * @param {number} t Input between 0 and 1.
 * @return {number} Output between 0 and 1.
 * @api
 */
ol.easing.linear = function(t) {
  return t;
};


/**
 * Start slow, speed up, and at the very end slow down again.  This has the
 * same general behavior as {@link ol.easing.inAndOut}, but the final slowdown
 * is delayed.
 * @param {number} t Input between 0 and 1.
 * @return {number} Output between 0 and 1.
 * @api
 */
ol.easing.upAndDown = function(t) {
  if (t < 0.5) {
    return ol.easing.inAndOut(2 * t);
  } else {
    return 1 - ol.easing.inAndOut(2 * (t - 0.5));
  }
};

goog.provide('ol.animation');

goog.require('ol');
goog.require('ol.PreRenderFunction');
goog.require('ol.ViewHint');
goog.require('ol.coordinate');
goog.require('ol.easing');


/**
 * Generate an animated transition that will "bounce" the resolution as it
 * approaches the final value.
 * @param {olx.animation.BounceOptions} options Bounce options.
 * @return {ol.PreRenderFunction} Pre-render function.
 * @api
 */
ol.animation.bounce = function(options) {
  var resolution = options.resolution;
  var start = options.start ? options.start : Date.now();
  var duration = options.duration !== undefined ? options.duration : 1000;
  var easing = options.easing ?
      options.easing : ol.easing.upAndDown;
  return (
      /**
       * @param {ol.Map} map Map.
       * @param {?olx.FrameState} frameState Frame state.
       */
      function(map, frameState) {
        if (frameState.time < start) {
          frameState.animate = true;
          frameState.viewHints[ol.ViewHint.ANIMATING] += 1;
          return true;
        } else if (frameState.time < start + duration) {
          var delta = easing((frameState.time - start) / duration);
          var deltaResolution = resolution - frameState.viewState.resolution;
          frameState.animate = true;
          frameState.viewState.resolution += delta * deltaResolution;
          frameState.viewHints[ol.ViewHint.ANIMATING] += 1;
          return true;
        } else {
          return false;
        }
      });
};


/**
 * Generate an animated transition while updating the view center.
 * @param {olx.animation.PanOptions} options Pan options.
 * @return {ol.PreRenderFunction} Pre-render function.
 * @api
 */
ol.animation.pan = function(options) {
  var source = options.source;
  var start = options.start ? options.start : Date.now();
  var sourceX = source[0];
  var sourceY = source[1];
  var duration = options.duration !== undefined ? options.duration : 1000;
  var easing = options.easing ?
      options.easing : ol.easing.inAndOut;
  return (
      /**
       * @param {ol.Map} map Map.
       * @param {?olx.FrameState} frameState Frame state.
       */
      function(map, frameState) {
        if (frameState.time < start) {
          frameState.animate = true;
          frameState.viewHints[ol.ViewHint.ANIMATING] += 1;
          return true;
        } else if (frameState.time < start + duration) {
          var delta = 1 - easing((frameState.time - start) / duration);
          var deltaX = sourceX - frameState.viewState.center[0];
          var deltaY = sourceY - frameState.viewState.center[1];
          frameState.animate = true;
          frameState.viewState.center[0] += delta * deltaX;
          frameState.viewState.center[1] += delta * deltaY;
          frameState.viewHints[ol.ViewHint.ANIMATING] += 1;
          return true;
        } else {
          return false;
        }
      });
};


/**
 * Generate an animated transition while updating the view rotation.
 * @param {olx.animation.RotateOptions} options Rotate options.
 * @return {ol.PreRenderFunction} Pre-render function.
 * @api
 */
ol.animation.rotate = function(options) {
  var sourceRotation = options.rotation ? options.rotation : 0;
  var start = options.start ? options.start : Date.now();
  var duration = options.duration !== undefined ? options.duration : 1000;
  var easing = options.easing ?
      options.easing : ol.easing.inAndOut;
  var anchor = options.anchor ?
      options.anchor : null;

  return (
      /**
       * @param {ol.Map} map Map.
       * @param {?olx.FrameState} frameState Frame state.
       */
      function(map, frameState) {
        if (frameState.time < start) {
          frameState.animate = true;
          frameState.viewHints[ol.ViewHint.ANIMATING] += 1;
          return true;
        } else if (frameState.time < start + duration) {
          var delta = 1 - easing((frameState.time - start) / duration);
          var deltaRotation =
              (sourceRotation - frameState.viewState.rotation) * delta;
          frameState.animate = true;
          frameState.viewState.rotation += deltaRotation;
          if (anchor) {
            var center = frameState.viewState.center;
            ol.coordinate.sub(center, anchor);
            ol.coordinate.rotate(center, deltaRotation);
            ol.coordinate.add(center, anchor);
          }
          frameState.viewHints[ol.ViewHint.ANIMATING] += 1;
          return true;
        } else {
          return false;
        }
      });
};


/**
 * Generate an animated transition while updating the view resolution.
 * @param {olx.animation.ZoomOptions} options Zoom options.
 * @return {ol.PreRenderFunction} Pre-render function.
 * @api
 */
ol.animation.zoom = function(options) {
  var sourceResolution = options.resolution;
  var start = options.start ? options.start : Date.now();
  var duration = options.duration !== undefined ? options.duration : 1000;
  var easing = options.easing ?
      options.easing : ol.easing.inAndOut;
  return (
      /**
       * @param {ol.Map} map Map.
       * @param {?olx.FrameState} frameState Frame state.
       */
      function(map, frameState) {
        if (frameState.time < start) {
          frameState.animate = true;
          frameState.viewHints[ol.ViewHint.ANIMATING] += 1;
          return true;
        } else if (frameState.time < start + duration) {
          var delta = 1 - easing((frameState.time - start) / duration);
          var deltaResolution =
              sourceResolution - frameState.viewState.resolution;
          frameState.animate = true;
          frameState.viewState.resolution += delta * deltaResolution;
          frameState.viewHints[ol.ViewHint.ANIMATING] += 1;
          return true;
        } else {
          return false;
        }
      });
};

goog.provide('ol.TileCoord');
goog.provide('ol.tilecoord');

goog.require('goog.asserts');
goog.require('ol.extent');


/**
 * An array of three numbers representing the location of a tile in a tile
 * grid. The order is `z`, `x`, and `y`. `z` is the zoom level.
 * @typedef {Array.<number>} ol.TileCoord
 * @api
 */
ol.TileCoord;


/**
 * @enum {number}
 */
ol.QuadKeyCharCode = {
  ZERO: '0'.charCodeAt(0),
  ONE: '1'.charCodeAt(0),
  TWO: '2'.charCodeAt(0),
  THREE: '3'.charCodeAt(0)
};


/**
 * @param {string} str String that follows pattern “z/x/y” where x, y and z are
 *   numbers.
 * @return {ol.TileCoord} Tile coord.
 */
ol.tilecoord.createFromString = function(str) {
  var v = str.split('/');
  goog.asserts.assert(v.length === 3,
      'must provide a string in "z/x/y" format, got "%s"', str);
  return v.map(function(e) {
    return parseInt(e, 10);
  });
};


/**
 * @param {number} z Z.
 * @param {number} x X.
 * @param {number} y Y.
 * @param {ol.TileCoord=} opt_tileCoord Tile coordinate.
 * @return {ol.TileCoord} Tile coordinate.
 */
ol.tilecoord.createOrUpdate = function(z, x, y, opt_tileCoord) {
  if (opt_tileCoord !== undefined) {
    opt_tileCoord[0] = z;
    opt_tileCoord[1] = x;
    opt_tileCoord[2] = y;
    return opt_tileCoord;
  } else {
    return [z, x, y];
  }
};


/**
 * @param {number} z Z.
 * @param {number} x X.
 * @param {number} y Y.
 * @return {string} Key.
 */
ol.tilecoord.getKeyZXY = function(z, x, y) {
  return z + '/' + x + '/' + y;
};


/**
 * @param {ol.TileCoord} tileCoord Tile coord.
 * @return {number} Hash.
 */
ol.tilecoord.hash = function(tileCoord) {
  return (tileCoord[1] << tileCoord[0]) + tileCoord[2];
};


/**
 * @param {ol.TileCoord} tileCoord Tile coord.
 * @return {string} Quad key.
 */
ol.tilecoord.quadKey = function(tileCoord) {
  var z = tileCoord[0];
  var digits = new Array(z);
  var mask = 1 << (z - 1);
  var i, charCode;
  for (i = 0; i < z; ++i) {
    charCode = ol.QuadKeyCharCode.ZERO;
    if (tileCoord[1] & mask) {
      charCode += 1;
    }
    if (tileCoord[2] & mask) {
      charCode += 2;
    }
    digits[i] = String.fromCharCode(charCode);
    mask >>= 1;
  }
  return digits.join('');
};


/**
 * @param {ol.TileCoord} tileCoord Tile coordinate.
 * @param {ol.tilegrid.TileGrid} tileGrid Tile grid.
 * @param {ol.proj.Projection} projection Projection.
 * @return {ol.TileCoord} Tile coordinate.
 */
ol.tilecoord.wrapX = function(tileCoord, tileGrid, projection) {
  var z = tileCoord[0];
  var center = tileGrid.getTileCoordCenter(tileCoord);
  var projectionExtent = ol.tilegrid.extentFromProjection(projection);
  if (!ol.extent.containsCoordinate(projectionExtent, center)) {
    var worldWidth = ol.extent.getWidth(projectionExtent);
    var worldsAway = Math.ceil((projectionExtent[0] - center[0]) / worldWidth);
    center[0] += worldWidth * worldsAway;
    return tileGrid.getTileCoordForCoordAndZ(center, z);
  } else {
    return tileCoord;
  }
};


/**
 * @param {ol.TileCoord} tileCoord Tile coordinate.
 * @param {!ol.tilegrid.TileGrid} tileGrid Tile grid.
 * @return {boolean} Tile coordinate is within extent and zoom level range.
 */
ol.tilecoord.withinExtentAndZ = function(tileCoord, tileGrid) {
  var z = tileCoord[0];
  var x = tileCoord[1];
  var y = tileCoord[2];

  if (tileGrid.getMinZoom() > z || z > tileGrid.getMaxZoom()) {
    return false;
  }
  var extent = tileGrid.getExtent();
  var tileRange;
  if (!extent) {
    tileRange = tileGrid.getFullTileRange(z);
  } else {
    tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z);
  }
  if (!tileRange) {
    return true;
  } else {
    return tileRange.containsXY(x, y);
  }
};

goog.provide('ol.TileRange');

goog.require('goog.asserts');
goog.require('ol.Size');
goog.require('ol.TileCoord');


/**
 * A representation of a contiguous block of tiles.  A tile range is specified
 * by its min/max tile coordinates and is inclusive of coordinates.
 *
 * @constructor
 * @param {number} minX Minimum X.
 * @param {number} maxX Maximum X.
 * @param {number} minY Minimum Y.
 * @param {number} maxY Maximum Y.
 * @struct
 */
ol.TileRange = function(minX, maxX, minY, maxY) {

  /**
   * @type {number}
   */
  this.minX = minX;

  /**
   * @type {number}
   */
  this.maxX = maxX;

  /**
   * @type {number}
   */
  this.minY = minY;

  /**
   * @type {number}
   */
  this.maxY = maxY;

};


/**
 * @param {...ol.TileCoord} var_args Tile coordinates.
 * @return {!ol.TileRange} Bounding tile box.
 */
ol.TileRange.boundingTileRange = function(var_args) {
  var tileCoord0 = /** @type {ol.TileCoord} */ (arguments[0]);
  var tileCoord0Z = tileCoord0[0];
  var tileCoord0X = tileCoord0[1];
  var tileCoord0Y = tileCoord0[2];
  var tileRange = new ol.TileRange(tileCoord0X, tileCoord0X,
                                   tileCoord0Y, tileCoord0Y);
  var i, ii, tileCoord, tileCoordX, tileCoordY, tileCoordZ;
  for (i = 1, ii = arguments.length; i < ii; ++i) {
    tileCoord = /** @type {ol.TileCoord} */ (arguments[i]);
    tileCoordZ = tileCoord[0];
    tileCoordX = tileCoord[1];
    tileCoordY = tileCoord[2];
    goog.asserts.assert(tileCoordZ == tileCoord0Z,
        'passed tilecoords all have the same Z-value');
    tileRange.minX = Math.min(tileRange.minX, tileCoordX);
    tileRange.maxX = Math.max(tileRange.maxX, tileCoordX);
    tileRange.minY = Math.min(tileRange.minY, tileCoordY);
    tileRange.maxY = Math.max(tileRange.maxY, tileCoordY);
  }
  return tileRange;
};


/**
 * @param {number} minX Minimum X.
 * @param {number} maxX Maximum X.
 * @param {number} minY Minimum Y.
 * @param {number} maxY Maximum Y.
 * @param {ol.TileRange|undefined} tileRange TileRange.
 * @return {ol.TileRange} Tile range.
 */
ol.TileRange.createOrUpdate = function(minX, maxX, minY, maxY, tileRange) {
  if (tileRange !== undefined) {
    tileRange.minX = minX;
    tileRange.maxX = maxX;
    tileRange.minY = minY;
    tileRange.maxY = maxY;
    return tileRange;
  } else {
    return new ol.TileRange(minX, maxX, minY, maxY);
  }
};


/**
 * @param {ol.TileCoord} tileCoord Tile coordinate.
 * @return {boolean} Contains tile coordinate.
 */
ol.TileRange.prototype.contains = function(tileCoord) {
  return this.containsXY(tileCoord[1], tileCoord[2]);
};


/**
 * @param {ol.TileRange} tileRange Tile range.
 * @return {boolean} Contains.
 */
ol.TileRange.prototype.containsTileRange = function(tileRange) {
  return this.minX <= tileRange.minX && tileRange.maxX <= this.maxX &&
      this.minY <= tileRange.minY && tileRange.maxY <= this.maxY;
};


/**
 * @param {number} x Tile coordinate x.
 * @param {number} y Tile coordinate y.
 * @return {boolean} Contains coordinate.
 */
ol.TileRange.prototype.containsXY = function(x, y) {
  return this.minX <= x && x <= this.maxX && this.minY <= y && y <= this.maxY;
};


/**
 * @param {ol.TileRange} tileRange Tile range.
 * @return {boolean} Equals.
 */
ol.TileRange.prototype.equals = function(tileRange) {
  return this.minX == tileRange.minX && this.minY == tileRange.minY &&
      this.maxX == tileRange.maxX && this.maxY == tileRange.maxY;
};


/**
 * @param {ol.TileRange} tileRange Tile range.
 */
ol.TileRange.prototype.extend = function(tileRange) {
  if (tileRange.minX < this.minX) {
    this.minX = tileRange.minX;
  }
  if (tileRange.maxX > this.maxX) {
    this.maxX = tileRange.maxX;
  }
  if (tileRange.minY < this.minY) {
    this.minY = tileRange.minY;
  }
  if (tileRange.maxY > this.maxY) {
    this.maxY = tileRange.maxY;
  }
};


/**
 * @return {number} Height.
 */
ol.TileRange.prototype.getHeight = function() {
  return this.maxY - this.minY + 1;
};


/**
 * @return {ol.Size} Size.
 */
ol.TileRange.prototype.getSize = function() {
  return [this.getWidth(), this.getHeight()];
};


/**
 * @return {number} Width.
 */
ol.TileRange.prototype.getWidth = function() {
  return this.maxX - this.minX + 1;
};


/**
 * @param {ol.TileRange} tileRange Tile range.
 * @return {boolean} Intersects.
 */
ol.TileRange.prototype.intersects = function(tileRange) {
  return this.minX <= tileRange.maxX &&
      this.maxX >= tileRange.minX &&
      this.minY <= tileRange.maxY &&
      this.maxY >= tileRange.minY;
};

goog.provide('ol.Attribution');

goog.require('goog.math');
goog.require('ol.TileRange');


/**
 * @classdesc
 * An attribution for a layer source.
 *
 * Example:
 *
 *     source: new ol.source.OSM({
 *       attributions: [
 *         new ol.Attribution({
 *           html: 'All maps &copy; ' +
 *               '<a href="http://www.opencyclemap.org/">OpenCycleMap</a>'
 *         }),
 *         ol.source.OSM.ATTRIBUTION
 *       ],
 *     ..
 *
 * @constructor
 * @param {olx.AttributionOptions} options Attribution options.
 * @struct
 * @api stable
 */
ol.Attribution = function(options) {

  /**
   * @private
   * @type {string}
   */
  this.html_ = options.html;

  /**
   * @private
   * @type {Object.<string, Array.<ol.TileRange>>}
   */
  this.tileRanges_ = options.tileRanges ? options.tileRanges : null;

};


/**
 * Get the attribution markup.
 * @return {string} The attribution HTML.
 * @api stable
 */
ol.Attribution.prototype.getHTML = function() {
  return this.html_;
};


/**
 * @param {Object.<string, ol.TileRange>} tileRanges Tile ranges.
 * @param {!ol.tilegrid.TileGrid} tileGrid Tile grid.
 * @param {!ol.proj.Projection} projection Projection.
 * @return {boolean} Intersects any tile range.
 */
ol.Attribution.prototype.intersectsAnyTileRange = function(tileRanges, tileGrid, projection) {
  if (!this.tileRanges_) {
    return true;
  }
  var i, ii, tileRange, zKey;
  for (zKey in tileRanges) {
    if (!(zKey in this.tileRanges_)) {
      continue;
    }
    tileRange = tileRanges[zKey];
    var testTileRange;
    for (i = 0, ii = this.tileRanges_[zKey].length; i < ii; ++i) {
      testTileRange = this.tileRanges_[zKey][i];
      if (testTileRange.intersects(tileRange)) {
        return true;
      }
      var extentTileRange = tileGrid.getTileRangeForExtentAndZ(
          ol.tilegrid.extentFromProjection(projection), parseInt(zKey, 10));
      var width = extentTileRange.getWidth();
      if (tileRange.minX < extentTileRange.minX ||
          tileRange.maxX > extentTileRange.maxX) {
        if (testTileRange.intersects(new ol.TileRange(
            goog.math.modulo(tileRange.minX, width),
            goog.math.modulo(tileRange.maxX, width),
            tileRange.minY, tileRange.maxY))) {
          return true;
        }
        if (tileRange.getWidth() > width &&
            testTileRange.intersects(extentTileRange)) {
          return true;
        }
      }
    }
  }
  return false;
};

goog.provide('ol.CanvasFunctionType');


/**
 * A function returning the canvas element (`{HTMLCanvasElement}`)
 * used by the source as an image. The arguments passed to the function are:
 * {@link ol.Extent} the image extent, `{number}` the image resolution,
 * `{number}` the device pixel ratio, {@link ol.Size} the image size, and
 * {@link ol.proj.Projection} the image projection. The canvas returned by
 * this function is cached by the source. The this keyword inside the function
 * references the {@link ol.source.ImageCanvas}.
 *
 * @typedef {function(this:ol.source.ImageCanvas, ol.Extent, number,
 *     number, ol.Size, ol.proj.Projection): HTMLCanvasElement}
 * @api
 */
ol.CanvasFunctionType;

/**
 * An implementation of Google Maps' MVCArray.
 * @see https://developers.google.com/maps/documentation/javascript/reference
 */

goog.provide('ol.Collection');
goog.provide('ol.CollectionEvent');
goog.provide('ol.CollectionEventType');

goog.require('goog.array');
goog.require('goog.events.Event');
goog.require('ol.Object');


/**
 * @enum {string}
 */
ol.CollectionEventType = {
  /**
   * Triggered when an item is added to the collection.
   * @event ol.CollectionEvent#add
   * @api stable
   */
  ADD: 'add',
  /**
   * Triggered when an item is removed from the collection.
   * @event ol.CollectionEvent#remove
   * @api stable
   */
  REMOVE: 'remove'
};


/**
 * @classdesc
 * Events emitted by {@link ol.Collection} instances are instances of this
 * type.
 *
 * @constructor
 * @extends {goog.events.Event}
 * @implements {oli.CollectionEvent}
 * @param {ol.CollectionEventType} type Type.
 * @param {*=} opt_element Element.
 * @param {Object=} opt_target Target.
 */
ol.CollectionEvent = function(type, opt_element, opt_target) {

  goog.base(this, type, opt_target);

  /**
   * The element that is added to or removed from the collection.
   * @type {*}
   * @api stable
   */
  this.element = opt_element;

};
goog.inherits(ol.CollectionEvent, goog.events.Event);


/**
 * @enum {string}
 */
ol.CollectionProperty = {
  LENGTH: 'length'
};


/**
 * @classdesc
 * An expanded version of standard JS Array, adding convenience methods for
 * manipulation. Add and remove changes to the Collection trigger a Collection
 * event. Note that this does not cover changes to the objects _within_ the
 * Collection; they trigger events on the appropriate object, not on the
 * Collection as a whole.
 *
 * @constructor
 * @extends {ol.Object}
 * @fires ol.CollectionEvent
 * @param {!Array.<T>=} opt_array Array.
 * @template T
 * @api stable
 */
ol.Collection = function(opt_array) {

  goog.base(this);

  /**
   * @private
   * @type {!Array.<T>}
   */
  this.array_ = opt_array ? opt_array : [];

  this.updateLength_();

};
goog.inherits(ol.Collection, ol.Object);


/**
 * Remove all elements from the collection.
 * @api stable
 */
ol.Collection.prototype.clear = function() {
  while (this.getLength() > 0) {
    this.pop();
  }
};


/**
 * Add elements to the collection.  This pushes each item in the provided array
 * to the end of the collection.
 * @param {!Array.<T>} arr Array.
 * @return {ol.Collection.<T>} This collection.
 * @api stable
 */
ol.Collection.prototype.extend = function(arr) {
  var i, ii;
  for (i = 0, ii = arr.length; i < ii; ++i) {
    this.push(arr[i]);
  }
  return this;
};


/**
 * Iterate over each element, calling the provided callback.
 * @param {function(this: S, T, number, Array.<T>): *} f The function to call
 *     for every element. This function takes 3 arguments (the element, the
 *     index and the array). The return value is ignored.
 * @param {S=} opt_this The object to use as `this` in `f`.
 * @template S
 * @api stable
 */
ol.Collection.prototype.forEach = function(f, opt_this) {
  this.array_.forEach(f, opt_this);
};


/**
 * Get a reference to the underlying Array object. Warning: if the array
 * is mutated, no events will be dispatched by the collection, and the
 * collection's "length" property won't be in sync with the actual length
 * of the array.
 * @return {!Array.<T>} Array.
 * @api stable
 */
ol.Collection.prototype.getArray = function() {
  return this.array_;
};


/**
 * Get the element at the provided index.
 * @param {number} index Index.
 * @return {T} Element.
 * @api stable
 */
ol.Collection.prototype.item = function(index) {
  return this.array_[index];
};


/**
 * Get the length of this collection.
 * @return {number} The length of the array.
 * @observable
 * @api stable
 */
ol.Collection.prototype.getLength = function() {
  return /** @type {number} */ (this.get(ol.CollectionProperty.LENGTH));
};


/**
 * Insert an element at the provided index.
 * @param {number} index Index.
 * @param {T} elem Element.
 * @api stable
 */
ol.Collection.prototype.insertAt = function(index, elem) {
  goog.array.insertAt(this.array_, elem, index);
  this.updateLength_();
  this.dispatchEvent(
      new ol.CollectionEvent(ol.CollectionEventType.ADD, elem, this));
};


/**
 * Remove the last element of the collection and return it.
 * Return `undefined` if the collection is empty.
 * @return {T|undefined} Element.
 * @api stable
 */
ol.Collection.prototype.pop = function() {
  return this.removeAt(this.getLength() - 1);
};


/**
 * Insert the provided element at the end of the collection.
 * @param {T} elem Element.
 * @return {number} Length.
 * @api stable
 */
ol.Collection.prototype.push = function(elem) {
  var n = this.array_.length;
  this.insertAt(n, elem);
  return n;
};


/**
 * Remove the first occurrence of an element from the collection.
 * @param {T} elem Element.
 * @return {T|undefined} The removed element or undefined if none found.
 * @api stable
 */
ol.Collection.prototype.remove = function(elem) {
  var arr = this.array_;
  var i, ii;
  for (i = 0, ii = arr.length; i < ii; ++i) {
    if (arr[i] === elem) {
      return this.removeAt(i);
    }
  }
  return undefined;
};


/**
 * Remove the element at the provided index and return it.
 * Return `undefined` if the collection does not contain this index.
 * @param {number} index Index.
 * @return {T|undefined} Value.
 * @api stable
 */
ol.Collection.prototype.removeAt = function(index) {
  var prev = this.array_[index];
  goog.array.removeAt(this.array_, index);
  this.updateLength_();
  this.dispatchEvent(
      new ol.CollectionEvent(ol.CollectionEventType.REMOVE, prev, this));
  return prev;
};


/**
 * Set the element at the provided index.
 * @param {number} index Index.
 * @param {T} elem Element.
 * @api stable
 */
ol.Collection.prototype.setAt = function(index, elem) {
  var n = this.getLength();
  if (index < n) {
    var prev = this.array_[index];
    this.array_[index] = elem;
    this.dispatchEvent(
        new ol.CollectionEvent(ol.CollectionEventType.REMOVE, prev, this));
    this.dispatchEvent(
        new ol.CollectionEvent(ol.CollectionEventType.ADD, elem, this));
  } else {
    var j;
    for (j = n; j < index; ++j) {
      this.insertAt(j, undefined);
    }
    this.insertAt(index, elem);
  }
};


/**
 * @private
 */
ol.Collection.prototype.updateLength_ = function() {
  this.set(ol.CollectionProperty.LENGTH, this.array_.length);
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Names of standard colors with their associated hex values.
 */

goog.provide('goog.color.names');


/**
 * A map that contains a lot of colors that are recognised by various browsers.
 * This list is way larger than the minimal one dictated by W3C.
 * The keys of this map are the lowercase "readable" names of the colors, while
 * the values are the "hex" values.
 *
 * @type {!Object<string, string>}
 */
goog.color.names = {
  'aliceblue': '#f0f8ff',
  'antiquewhite': '#faebd7',
  'aqua': '#00ffff',
  'aquamarine': '#7fffd4',
  'azure': '#f0ffff',
  'beige': '#f5f5dc',
  'bisque': '#ffe4c4',
  'black': '#000000',
  'blanchedalmond': '#ffebcd',
  'blue': '#0000ff',
  'blueviolet': '#8a2be2',
  'brown': '#a52a2a',
  'burlywood': '#deb887',
  'cadetblue': '#5f9ea0',
  'chartreuse': '#7fff00',
  'chocolate': '#d2691e',
  'coral': '#ff7f50',
  'cornflowerblue': '#6495ed',
  'cornsilk': '#fff8dc',
  'crimson': '#dc143c',
  'cyan': '#00ffff',
  'darkblue': '#00008b',
  'darkcyan': '#008b8b',
  'darkgoldenrod': '#b8860b',
  'darkgray': '#a9a9a9',
  'darkgreen': '#006400',
  'darkgrey': '#a9a9a9',
  'darkkhaki': '#bdb76b',
  'darkmagenta': '#8b008b',
  'darkolivegreen': '#556b2f',
  'darkorange': '#ff8c00',
  'darkorchid': '#9932cc',
  'darkred': '#8b0000',
  'darksalmon': '#e9967a',
  'darkseagreen': '#8fbc8f',
  'darkslateblue': '#483d8b',
  'darkslategray': '#2f4f4f',
  'darkslategrey': '#2f4f4f',
  'darkturquoise': '#00ced1',
  'darkviolet': '#9400d3',
  'deeppink': '#ff1493',
  'deepskyblue': '#00bfff',
  'dimgray': '#696969',
  'dimgrey': '#696969',
  'dodgerblue': '#1e90ff',
  'firebrick': '#b22222',
  'floralwhite': '#fffaf0',
  'forestgreen': '#228b22',
  'fuchsia': '#ff00ff',
  'gainsboro': '#dcdcdc',
  'ghostwhite': '#f8f8ff',
  'gold': '#ffd700',
  'goldenrod': '#daa520',
  'gray': '#808080',
  'green': '#008000',
  'greenyellow': '#adff2f',
  'grey': '#808080',
  'honeydew': '#f0fff0',
  'hotpink': '#ff69b4',
  'indianred': '#cd5c5c',
  'indigo': '#4b0082',
  'ivory': '#fffff0',
  'khaki': '#f0e68c',
  'lavender': '#e6e6fa',
  'lavenderblush': '#fff0f5',
  'lawngreen': '#7cfc00',
  'lemonchiffon': '#fffacd',
  'lightblue': '#add8e6',
  'lightcoral': '#f08080',
  'lightcyan': '#e0ffff',
  'lightgoldenrodyellow': '#fafad2',
  'lightgray': '#d3d3d3',
  'lightgreen': '#90ee90',
  'lightgrey': '#d3d3d3',
  'lightpink': '#ffb6c1',
  'lightsalmon': '#ffa07a',
  'lightseagreen': '#20b2aa',
  'lightskyblue': '#87cefa',
  'lightslategray': '#778899',
  'lightslategrey': '#778899',
  'lightsteelblue': '#b0c4de',
  'lightyellow': '#ffffe0',
  'lime': '#00ff00',
  'limegreen': '#32cd32',
  'linen': '#faf0e6',
  'magenta': '#ff00ff',
  'maroon': '#800000',
  'mediumaquamarine': '#66cdaa',
  'mediumblue': '#0000cd',
  'mediumorchid': '#ba55d3',
  'mediumpurple': '#9370db',
  'mediumseagreen': '#3cb371',
  'mediumslateblue': '#7b68ee',
  'mediumspringgreen': '#00fa9a',
  'mediumturquoise': '#48d1cc',
  'mediumvioletred': '#c71585',
  'midnightblue': '#191970',
  'mintcream': '#f5fffa',
  'mistyrose': '#ffe4e1',
  'moccasin': '#ffe4b5',
  'navajowhite': '#ffdead',
  'navy': '#000080',
  'oldlace': '#fdf5e6',
  'olive': '#808000',
  'olivedrab': '#6b8e23',
  'orange': '#ffa500',
  'orangered': '#ff4500',
  'orchid': '#da70d6',
  'palegoldenrod': '#eee8aa',
  'palegreen': '#98fb98',
  'paleturquoise': '#afeeee',
  'palevioletred': '#db7093',
  'papayawhip': '#ffefd5',
  'peachpuff': '#ffdab9',
  'peru': '#cd853f',
  'pink': '#ffc0cb',
  'plum': '#dda0dd',
  'powderblue': '#b0e0e6',
  'purple': '#800080',
  'red': '#ff0000',
  'rosybrown': '#bc8f8f',
  'royalblue': '#4169e1',
  'saddlebrown': '#8b4513',
  'salmon': '#fa8072',
  'sandybrown': '#f4a460',
  'seagreen': '#2e8b57',
  'seashell': '#fff5ee',
  'sienna': '#a0522d',
  'silver': '#c0c0c0',
  'skyblue': '#87ceeb',
  'slateblue': '#6a5acd',
  'slategray': '#708090',
  'slategrey': '#708090',
  'snow': '#fffafa',
  'springgreen': '#00ff7f',
  'steelblue': '#4682b4',
  'tan': '#d2b48c',
  'teal': '#008080',
  'thistle': '#d8bfd8',
  'tomato': '#ff6347',
  'turquoise': '#40e0d0',
  'violet': '#ee82ee',
  'wheat': '#f5deb3',
  'white': '#ffffff',
  'whitesmoke': '#f5f5f5',
  'yellow': '#ffff00',
  'yellowgreen': '#9acd32'
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Utilities related to color and color conversion.
 */

goog.provide('goog.color');
goog.provide('goog.color.Hsl');
goog.provide('goog.color.Hsv');
goog.provide('goog.color.Rgb');

goog.require('goog.color.names');
goog.require('goog.math');


/**
 * RGB color representation. An array containing three elements [r, g, b],
 * each an integer in [0, 255], representing the red, green, and blue components
 * of the color respectively.
 * @typedef {Array<number>}
 */
goog.color.Rgb;


/**
 * HSV color representation. An array containing three elements [h, s, v]:
 * h (hue) must be an integer in [0, 360], cyclic.
 * s (saturation) must be a number in [0, 1].
 * v (value/brightness) must be an integer in [0, 255].
 * @typedef {Array<number>}
 */
goog.color.Hsv;


/**
 * HSL color representation. An array containing three elements [h, s, l]:
 * h (hue) must be an integer in [0, 360], cyclic.
 * s (saturation) must be a number in [0, 1].
 * l (lightness) must be a number in [0, 1].
 * @typedef {Array<number>}
 */
goog.color.Hsl;


/**
 * Parses a color out of a string.
 * @param {string} str Color in some format.
 * @return {{hex: string, type: string}} 'hex' is a string containing a hex
 *     representation of the color, 'type' is a string containing the type
 *     of color format passed in ('hex', 'rgb', 'named').
 */
goog.color.parse = function(str) {
  var result = {};
  str = String(str);

  var maybeHex = goog.color.prependHashIfNecessaryHelper(str);
  if (goog.color.isValidHexColor_(maybeHex)) {
    result.hex = goog.color.normalizeHex(maybeHex);
    result.type = 'hex';
    return result;
  } else {
    var rgb = goog.color.isValidRgbColor_(str);
    if (rgb.length) {
      result.hex = goog.color.rgbArrayToHex(rgb);
      result.type = 'rgb';
      return result;
    } else if (goog.color.names) {
      var hex = goog.color.names[str.toLowerCase()];
      if (hex) {
        result.hex = hex;
        result.type = 'named';
        return result;
      }
    }
  }
  throw Error(str + ' is not a valid color string');
};


/**
 * Determines if the given string can be parsed as a color.
 *     {@see goog.color.parse}.
 * @param {string} str Potential color string.
 * @return {boolean} True if str is in a format that can be parsed to a color.
 */
goog.color.isValidColor = function(str) {
  var maybeHex = goog.color.prependHashIfNecessaryHelper(str);
  return !!(goog.color.isValidHexColor_(maybeHex) ||
            goog.color.isValidRgbColor_(str).length ||
            goog.color.names && goog.color.names[str.toLowerCase()]);
};


/**
 * Parses red, green, blue components out of a valid rgb color string.
 * Throws Error if the color string is invalid.
 * @param {string} str RGB representation of a color.
 *    {@see goog.color.isValidRgbColor_}.
 * @return {!goog.color.Rgb} rgb representation of the color.
 */
goog.color.parseRgb = function(str) {
  var rgb = goog.color.isValidRgbColor_(str);
  if (!rgb.length) {
    throw Error(str + ' is not a valid RGB color');
  }
  return rgb;
};


/**
 * Converts a hex representation of a color to RGB.
 * @param {string} hexColor Color to convert.
 * @return {string} string of the form 'rgb(R,G,B)' which can be used in
 *    styles.
 */
goog.color.hexToRgbStyle = function(hexColor) {
  return goog.color.rgbStyle_(goog.color.hexToRgb(hexColor));
};


/**
 * Regular expression for extracting the digits in a hex color triplet.
 * @type {RegExp}
 * @private
 */
goog.color.hexTripletRe_ = /#(.)(.)(.)/;


/**
 * Normalize an hex representation of a color
 * @param {string} hexColor an hex color string.
 * @return {string} hex color in the format '#rrggbb' with all lowercase
 *     literals.
 */
goog.color.normalizeHex = function(hexColor) {
  if (!goog.color.isValidHexColor_(hexColor)) {
    throw Error("'" + hexColor + "' is not a valid hex color");
  }
  if (hexColor.length == 4) { // of the form #RGB
    hexColor = hexColor.replace(goog.color.hexTripletRe_, '#$1$1$2$2$3$3');
  }
  return hexColor.toLowerCase();
};


/**
 * Converts a hex representation of a color to RGB.
 * @param {string} hexColor Color to convert.
 * @return {!goog.color.Rgb} rgb representation of the color.
 */
goog.color.hexToRgb = function(hexColor) {
  hexColor = goog.color.normalizeHex(hexColor);
  var r = parseInt(hexColor.substr(1, 2), 16);
  var g = parseInt(hexColor.substr(3, 2), 16);
  var b = parseInt(hexColor.substr(5, 2), 16);

  return [r, g, b];
};


/**
 * Converts a color from RGB to hex representation.
 * @param {number} r Amount of red, int between 0 and 255.
 * @param {number} g Amount of green, int between 0 and 255.
 * @param {number} b Amount of blue, int between 0 and 255.
 * @return {string} hex representation of the color.
 */
goog.color.rgbToHex = function(r, g, b) {
  r = Number(r);
  g = Number(g);
  b = Number(b);
  if (isNaN(r) || r < 0 || r > 255 ||
      isNaN(g) || g < 0 || g > 255 ||
      isNaN(b) || b < 0 || b > 255) {
    throw Error('"(' + r + ',' + g + ',' + b + '") is not a valid RGB color');
  }
  var hexR = goog.color.prependZeroIfNecessaryHelper(r.toString(16));
  var hexG = goog.color.prependZeroIfNecessaryHelper(g.toString(16));
  var hexB = goog.color.prependZeroIfNecessaryHelper(b.toString(16));
  return '#' + hexR + hexG + hexB;
};


/**
 * Converts a color from RGB to hex representation.
 * @param {goog.color.Rgb} rgb rgb representation of the color.
 * @return {string} hex representation of the color.
 */
goog.color.rgbArrayToHex = function(rgb) {
  return goog.color.rgbToHex(rgb[0], rgb[1], rgb[2]);
};


/**
 * Converts a color from RGB color space to HSL color space.
 * Modified from {@link http://en.wikipedia.org/wiki/HLS_color_space}.
 * @param {number} r Value of red, in [0, 255].
 * @param {number} g Value of green, in [0, 255].
 * @param {number} b Value of blue, in [0, 255].
 * @return {!goog.color.Hsl} hsl representation of the color.
 */
goog.color.rgbToHsl = function(r, g, b) {
  // First must normalize r, g, b to be between 0 and 1.
  var normR = r / 255;
  var normG = g / 255;
  var normB = b / 255;
  var max = Math.max(normR, normG, normB);
  var min = Math.min(normR, normG, normB);
  var h = 0;
  var s = 0;

  // Luminosity is the average of the max and min rgb color intensities.
  var l = 0.5 * (max + min);

  // The hue and saturation are dependent on which color intensity is the max.
  // If max and min are equal, the color is gray and h and s should be 0.
  if (max != min) {
    if (max == normR) {
      h = 60 * (normG - normB) / (max - min);
    } else if (max == normG) {
      h = 60 * (normB - normR) / (max - min) + 120;
    } else if (max == normB) {
      h = 60 * (normR - normG) / (max - min) + 240;
    }

    if (0 < l && l <= 0.5) {
      s = (max - min) / (2 * l);
    } else {
      s = (max - min) / (2 - 2 * l);
    }
  }

  // Make sure the hue falls between 0 and 360.
  return [Math.round(h + 360) % 360, s, l];
};


/**
 * Converts a color from RGB color space to HSL color space.
 * @param {goog.color.Rgb} rgb rgb representation of the color.
 * @return {!goog.color.Hsl} hsl representation of the color.
 */
goog.color.rgbArrayToHsl = function(rgb) {
  return goog.color.rgbToHsl(rgb[0], rgb[1], rgb[2]);
};


/**
 * Helper for hslToRgb.
 * @param {number} v1 Helper variable 1.
 * @param {number} v2 Helper variable 2.
 * @param {number} vH Helper variable 3.
 * @return {number} Appropriate RGB value, given the above.
 * @private
 */
goog.color.hueToRgb_ = function(v1, v2, vH) {
  if (vH < 0) {
    vH += 1;
  } else if (vH > 1) {
    vH -= 1;
  }
  if ((6 * vH) < 1) {
    return (v1 + (v2 - v1) * 6 * vH);
  } else if (2 * vH < 1) {
    return v2;
  } else if (3 * vH < 2) {
    return (v1 + (v2 - v1) * ((2 / 3) - vH) * 6);
  }
  return v1;
};


/**
 * Converts a color from HSL color space to RGB color space.
 * Modified from {@link http://www.easyrgb.com/math.html}
 * @param {number} h Hue, in [0, 360].
 * @param {number} s Saturation, in [0, 1].
 * @param {number} l Luminosity, in [0, 1].
 * @return {!goog.color.Rgb} rgb representation of the color.
 */
goog.color.hslToRgb = function(h, s, l) {
  var r = 0;
  var g = 0;
  var b = 0;
  var normH = h / 360; // normalize h to fall in [0, 1]

  if (s == 0) {
    r = g = b = l * 255;
  } else {
    var temp1 = 0;
    var temp2 = 0;
    if (l < 0.5) {
      temp2 = l * (1 + s);
    } else {
      temp2 = l + s - (s * l);
    }
    temp1 = 2 * l - temp2;
    r = 255 * goog.color.hueToRgb_(temp1, temp2, normH + (1 / 3));
    g = 255 * goog.color.hueToRgb_(temp1, temp2, normH);
    b = 255 * goog.color.hueToRgb_(temp1, temp2, normH - (1 / 3));
  }

  return [Math.round(r), Math.round(g), Math.round(b)];
};


/**
 * Converts a color from HSL color space to RGB color space.
 * @param {goog.color.Hsl} hsl hsl representation of the color.
 * @return {!goog.color.Rgb} rgb representation of the color.
 */
goog.color.hslArrayToRgb = function(hsl) {
  return goog.color.hslToRgb(hsl[0], hsl[1], hsl[2]);
};


/**
 * Helper for isValidHexColor_.
 * @type {RegExp}
 * @private
 */
goog.color.validHexColorRe_ = /^#(?:[0-9a-f]{3}){1,2}$/i;


/**
 * Checks if a string is a valid hex color.  We expect strings of the format
 * #RRGGBB (ex: #1b3d5f) or #RGB (ex: #3CA == #33CCAA).
 * @param {string} str String to check.
 * @return {boolean} Whether the string is a valid hex color.
 * @private
 */
goog.color.isValidHexColor_ = function(str) {
  return goog.color.validHexColorRe_.test(str);
};


/**
 * Helper for isNormalizedHexColor_.
 * @type {RegExp}
 * @private
 */
goog.color.normalizedHexColorRe_ = /^#[0-9a-f]{6}$/;


/**
 * Checks if a string is a normalized hex color.
 * We expect strings of the format #RRGGBB (ex: #1b3d5f)
 * using only lowercase letters.
 * @param {string} str String to check.
 * @return {boolean} Whether the string is a normalized hex color.
 * @private
 */
goog.color.isNormalizedHexColor_ = function(str) {
  return goog.color.normalizedHexColorRe_.test(str);
};


/**
 * Regular expression for matching and capturing RGB style strings. Helper for
 * isValidRgbColor_.
 * @type {RegExp}
 * @private
 */
goog.color.rgbColorRe_ =
    /^(?:rgb)?\((0|[1-9]\d{0,2}),\s?(0|[1-9]\d{0,2}),\s?(0|[1-9]\d{0,2})\)$/i;


/**
 * Checks if a string is a valid rgb color.  We expect strings of the format
 * '(r, g, b)', or 'rgb(r, g, b)', where each color component is an int in
 * [0, 255].
 * @param {string} str String to check.
 * @return {!goog.color.Rgb} the rgb representation of the color if it is
 *     a valid color, or the empty array otherwise.
 * @private
 */
goog.color.isValidRgbColor_ = function(str) {
  // Each component is separate (rather than using a repeater) so we can
  // capture the match. Also, we explicitly set each component to be either 0,
  // or start with a non-zero, to prevent octal numbers from slipping through.
  var regExpResultArray = str.match(goog.color.rgbColorRe_);
  if (regExpResultArray) {
    var r = Number(regExpResultArray[1]);
    var g = Number(regExpResultArray[2]);
    var b = Number(regExpResultArray[3]);
    if (r >= 0 && r <= 255 &&
        g >= 0 && g <= 255 &&
        b >= 0 && b <= 255) {
      return [r, g, b];
    }
  }
  return [];
};


/**
 * Takes a hex value and prepends a zero if it's a single digit.
 * Small helper method for use by goog.color and friends.
 * @param {string} hex Hex value to prepend if single digit.
 * @return {string} hex value prepended with zero if it was single digit,
 *     otherwise the same value that was passed in.
 */
goog.color.prependZeroIfNecessaryHelper = function(hex) {
  return hex.length == 1 ? '0' + hex : hex;
};


/**
 * Takes a string a prepends a '#' sign if one doesn't exist.
 * Small helper method for use by goog.color and friends.
 * @param {string} str String to check.
 * @return {string} The value passed in, prepended with a '#' if it didn't
 *     already have one.
 */
goog.color.prependHashIfNecessaryHelper = function(str) {
  return str.charAt(0) == '#' ? str : '#' + str;
};


/**
 * Takes an array of [r, g, b] and converts it into a string appropriate for
 * CSS styles.
 * @param {goog.color.Rgb} rgb rgb representation of the color.
 * @return {string} string of the form 'rgb(r,g,b)'.
 * @private
 */
goog.color.rgbStyle_ = function(rgb) {
  return 'rgb(' + rgb.join(',') + ')';
};


/**
 * Converts an HSV triplet to an RGB array.  V is brightness because b is
 *   reserved for blue in RGB.
 * @param {number} h Hue value in [0, 360].
 * @param {number} s Saturation value in [0, 1].
 * @param {number} brightness brightness in [0, 255].
 * @return {!goog.color.Rgb} rgb representation of the color.
 */
goog.color.hsvToRgb = function(h, s, brightness) {
  var red = 0;
  var green = 0;
  var blue = 0;
  if (s == 0) {
    red = brightness;
    green = brightness;
    blue = brightness;
  } else {
    var sextant = Math.floor(h / 60);
    var remainder = (h / 60) - sextant;
    var val1 = brightness * (1 - s);
    var val2 = brightness * (1 - (s * remainder));
    var val3 = brightness * (1 - (s * (1 - remainder)));
    switch (sextant) {
      case 1:
        red = val2;
        green = brightness;
        blue = val1;
        break;
      case 2:
        red = val1;
        green = brightness;
        blue = val3;
        break;
      case 3:
        red = val1;
        green = val2;
        blue = brightness;
        break;
      case 4:
        red = val3;
        green = val1;
        blue = brightness;
        break;
      case 5:
        red = brightness;
        green = val1;
        blue = val2;
        break;
      case 6:
      case 0:
        red = brightness;
        green = val3;
        blue = val1;
        break;
    }
  }

  return [Math.floor(red), Math.floor(green), Math.floor(blue)];
};


/**
 * Converts from RGB values to an array of HSV values.
 * @param {number} red Red value in [0, 255].
 * @param {number} green Green value in [0, 255].
 * @param {number} blue Blue value in [0, 255].
 * @return {!goog.color.Hsv} hsv representation of the color.
 */
goog.color.rgbToHsv = function(red, green, blue) {

  var max = Math.max(Math.max(red, green), blue);
  var min = Math.min(Math.min(red, green), blue);
  var hue;
  var saturation;
  var value = max;
  if (min == max) {
    hue = 0;
    saturation = 0;
  } else {
    var delta = (max - min);
    saturation = delta / max;

    if (red == max) {
      hue = (green - blue) / delta;
    } else if (green == max) {
      hue = 2 + ((blue - red) / delta);
    } else {
      hue = 4 + ((red - green) / delta);
    }
    hue *= 60;
    if (hue < 0) {
      hue += 360;
    }
    if (hue > 360) {
      hue -= 360;
    }
  }

  return [hue, saturation, value];
};


/**
 * Converts from an array of RGB values to an array of HSV values.
 * @param {goog.color.Rgb} rgb rgb representation of the color.
 * @return {!goog.color.Hsv} hsv representation of the color.
 */
goog.color.rgbArrayToHsv = function(rgb) {
  return goog.color.rgbToHsv(rgb[0], rgb[1], rgb[2]);
};


/**
 * Converts an HSV triplet to an RGB array.
 * @param {goog.color.Hsv} hsv hsv representation of the color.
 * @return {!goog.color.Rgb} rgb representation of the color.
 */
goog.color.hsvArrayToRgb = function(hsv) {
  return goog.color.hsvToRgb(hsv[0], hsv[1], hsv[2]);
};


/**
 * Converts a hex representation of a color to HSL.
 * @param {string} hex Color to convert.
 * @return {!goog.color.Hsv} hsv representation of the color.
 */
goog.color.hexToHsl = function(hex) {
  var rgb = goog.color.hexToRgb(hex);
  return goog.color.rgbToHsl(rgb[0], rgb[1], rgb[2]);
};


/**
 * Converts from h,s,l values to a hex string
 * @param {number} h Hue, in [0, 360].
 * @param {number} s Saturation, in [0, 1].
 * @param {number} l Luminosity, in [0, 1].
 * @return {string} hex representation of the color.
 */
goog.color.hslToHex = function(h, s, l) {
  return goog.color.rgbArrayToHex(goog.color.hslToRgb(h, s, l));
};


/**
 * Converts from an hsl array to a hex string
 * @param {goog.color.Hsl} hsl hsl representation of the color.
 * @return {string} hex representation of the color.
 */
goog.color.hslArrayToHex = function(hsl) {
  return goog.color.rgbArrayToHex(goog.color.hslToRgb(hsl[0], hsl[1], hsl[2]));
};


/**
 * Converts a hex representation of a color to HSV
 * @param {string} hex Color to convert.
 * @return {!goog.color.Hsv} hsv representation of the color.
 */
goog.color.hexToHsv = function(hex) {
  return goog.color.rgbArrayToHsv(goog.color.hexToRgb(hex));
};


/**
 * Converts from h,s,v values to a hex string
 * @param {number} h Hue, in [0, 360].
 * @param {number} s Saturation, in [0, 1].
 * @param {number} v Value, in [0, 255].
 * @return {string} hex representation of the color.
 */
goog.color.hsvToHex = function(h, s, v) {
  return goog.color.rgbArrayToHex(goog.color.hsvToRgb(h, s, v));
};


/**
 * Converts from an HSV array to a hex string
 * @param {goog.color.Hsv} hsv hsv representation of the color.
 * @return {string} hex representation of the color.
 */
goog.color.hsvArrayToHex = function(hsv) {
  return goog.color.hsvToHex(hsv[0], hsv[1], hsv[2]);
};


/**
 * Calculates the Euclidean distance between two color vectors on an HSL sphere.
 * A demo of the sphere can be found at:
 * http://en.wikipedia.org/wiki/HSL_color_space
 * In short, a vector for color (H, S, L) in this system can be expressed as
 * (S*L'*cos(2*PI*H), S*L'*sin(2*PI*H), L), where L' = abs(L - 0.5), and we
 * simply calculate the 1-2 distance using these coordinates
 * @param {goog.color.Hsl} hsl1 First color in hsl representation.
 * @param {goog.color.Hsl} hsl2 Second color in hsl representation.
 * @return {number} Distance between the two colors, in the range [0, 1].
 */
goog.color.hslDistance = function(hsl1, hsl2) {
  var sl1, sl2;
  if (hsl1[2] <= 0.5) {
    sl1 = hsl1[1] * hsl1[2];
  } else {
    sl1 = hsl1[1] * (1.0 - hsl1[2]);
  }

  if (hsl2[2] <= 0.5) {
    sl2 = hsl2[1] * hsl2[2];
  } else {
    sl2 = hsl2[1] * (1.0 - hsl2[2]);
  }

  var h1 = hsl1[0] / 360.0;
  var h2 = hsl2[0] / 360.0;
  var dh = (h1 - h2) * 2.0 * Math.PI;
  return (hsl1[2] - hsl2[2]) * (hsl1[2] - hsl2[2]) +
      sl1 * sl1 + sl2 * sl2 - 2 * sl1 * sl2 * Math.cos(dh);
};


/**
 * Blend two colors together, using the specified factor to indicate the weight
 * given to the first color
 * @param {goog.color.Rgb} rgb1 First color represented in rgb.
 * @param {goog.color.Rgb} rgb2 Second color represented in rgb.
 * @param {number} factor The weight to be given to rgb1 over rgb2. Values
 *     should be in the range [0, 1]. If less than 0, factor will be set to 0.
 *     If greater than 1, factor will be set to 1.
 * @return {!goog.color.Rgb} Combined color represented in rgb.
 */
goog.color.blend = function(rgb1, rgb2, factor) {
  factor = goog.math.clamp(factor, 0, 1);

  return [
    Math.round(factor * rgb1[0] + (1.0 - factor) * rgb2[0]),
    Math.round(factor * rgb1[1] + (1.0 - factor) * rgb2[1]),
    Math.round(factor * rgb1[2] + (1.0 - factor) * rgb2[2])
  ];
};


/**
 * Adds black to the specified color, darkening it
 * @param {goog.color.Rgb} rgb rgb representation of the color.
 * @param {number} factor Number in the range [0, 1]. 0 will do nothing, while
 *     1 will return black. If less than 0, factor will be set to 0. If greater
 *     than 1, factor will be set to 1.
 * @return {!goog.color.Rgb} Combined rgb color.
 */
goog.color.darken = function(rgb, factor) {
  var black = [0, 0, 0];
  return goog.color.blend(black, rgb, factor);
};


/**
 * Adds white to the specified color, lightening it
 * @param {goog.color.Rgb} rgb rgb representation of the color.
 * @param {number} factor Number in the range [0, 1].  0 will do nothing, while
 *     1 will return white. If less than 0, factor will be set to 0. If greater
 *     than 1, factor will be set to 1.
 * @return {!goog.color.Rgb} Combined rgb color.
 */
goog.color.lighten = function(rgb, factor) {
  var white = [255, 255, 255];
  return goog.color.blend(white, rgb, factor);
};


/**
 * Find the "best" (highest-contrast) of the suggested colors for the prime
 * color. Uses W3C formula for judging readability and visual accessibility:
 * http://www.w3.org/TR/AERT#color-contrast
 * @param {goog.color.Rgb} prime Color represented as a rgb array.
 * @param {Array<goog.color.Rgb>} suggestions Array of colors,
 *     each representing a rgb array.
 * @return {!goog.color.Rgb} Highest-contrast color represented by an array..
 */
goog.color.highContrast = function(prime, suggestions) {
  var suggestionsWithDiff = [];
  for (var i = 0; i < suggestions.length; i++) {
    suggestionsWithDiff.push({
      color: suggestions[i],
      diff: goog.color.yiqBrightnessDiff_(suggestions[i], prime) +
          goog.color.colorDiff_(suggestions[i], prime)
    });
  }
  suggestionsWithDiff.sort(function(a, b) {
    return b.diff - a.diff;
  });
  return suggestionsWithDiff[0].color;
};


/**
 * Calculate brightness of a color according to YIQ formula (brightness is Y).
 * More info on YIQ here: http://en.wikipedia.org/wiki/YIQ. Helper method for
 * goog.color.highContrast()
 * @param {goog.color.Rgb} rgb Color represented by a rgb array.
 * @return {number} brightness (Y).
 * @private
 */
goog.color.yiqBrightness_ = function(rgb) {
  return Math.round((rgb[0] * 299 + rgb[1] * 587 + rgb[2] * 114) / 1000);
};


/**
 * Calculate difference in brightness of two colors. Helper method for
 * goog.color.highContrast()
 * @param {goog.color.Rgb} rgb1 Color represented by a rgb array.
 * @param {goog.color.Rgb} rgb2 Color represented by a rgb array.
 * @return {number} Brightness difference.
 * @private
 */
goog.color.yiqBrightnessDiff_ = function(rgb1, rgb2) {
  return Math.abs(goog.color.yiqBrightness_(rgb1) -
                  goog.color.yiqBrightness_(rgb2));
};


/**
 * Calculate color difference between two colors. Helper method for
 * goog.color.highContrast()
 * @param {goog.color.Rgb} rgb1 Color represented by a rgb array.
 * @param {goog.color.Rgb} rgb2 Color represented by a rgb array.
 * @return {number} Color difference.
 * @private
 */
goog.color.colorDiff_ = function(rgb1, rgb2) {
  return Math.abs(rgb1[0] - rgb2[0]) + Math.abs(rgb1[1] - rgb2[1]) +
      Math.abs(rgb1[2] - rgb2[2]);
};

// We can't use goog.color or goog.color.alpha because they interally use a hex
// string representation that encodes each channel in a single byte.  This
// causes occasional loss of precision and rounding errors, especially in the
// alpha channel.

goog.provide('ol.Color');
goog.provide('ol.color');

goog.require('goog.asserts');
goog.require('goog.color');
goog.require('goog.color.names');
goog.require('goog.vec.Mat4');
goog.require('ol');
goog.require('ol.math');


/**
 * A color represented as a short array [red, green, blue, alpha].
 * red, green, and blue should be integers in the range 0..255 inclusive.
 * alpha should be a float in the range 0..1 inclusive.
 * @typedef {Array.<number>}
 * @api
 */
ol.Color;


/**
 * This RegExp matches # followed by 3 or 6 hex digits.
 * @const
 * @type {RegExp}
 * @private
 */
ol.color.hexColorRe_ = /^#(?:[0-9a-f]{3}){1,2}$/i;


/**
 * @see goog.color.rgbColorRe_
 * @const
 * @type {RegExp}
 * @private
 */
ol.color.rgbColorRe_ =
    /^(?:rgb)?\((0|[1-9]\d{0,2}),\s?(0|[1-9]\d{0,2}),\s?(0|[1-9]\d{0,2})\)$/i;


/**
 * @see goog.color.alpha.rgbaColorRe_
 * @const
 * @type {RegExp}
 * @private
 */
ol.color.rgbaColorRe_ =
    /^(?:rgba)?\((0|[1-9]\d{0,2}),\s?(0|[1-9]\d{0,2}),\s?(0|[1-9]\d{0,2}),\s?(0|1|0\.\d{0,10})\)$/i;


/**
 * @param {ol.Color} dst Destination.
 * @param {ol.Color} src Source.
 * @param {ol.Color=} opt_color Color.
 * @return {ol.Color} Color.
 */
ol.color.blend = function(dst, src, opt_color) {
  // http://en.wikipedia.org/wiki/Alpha_compositing
  // FIXME do we need to scale by 255?
  var out = opt_color ? opt_color : [];
  var dstA = dst[3];
  var srcA = src[3];
  if (dstA == 1) {
    out[0] = (src[0] * srcA + dst[0] * (1 - srcA) + 0.5) | 0;
    out[1] = (src[1] * srcA + dst[1] * (1 - srcA) + 0.5) | 0;
    out[2] = (src[2] * srcA + dst[2] * (1 - srcA) + 0.5) | 0;
    out[3] = 1;
  } else if (srcA === 0) {
    out[0] = dst[0];
    out[1] = dst[1];
    out[2] = dst[2];
    out[3] = dstA;
  } else {
    var outA = srcA + dstA * (1 - srcA);
    if (outA === 0) {
      out[0] = 0;
      out[1] = 0;
      out[2] = 0;
      out[3] = 0;
    } else {
      out[0] = ((src[0] * srcA + dst[0] * dstA * (1 - srcA)) / outA + 0.5) | 0;
      out[1] = ((src[1] * srcA + dst[1] * dstA * (1 - srcA)) / outA + 0.5) | 0;
      out[2] = ((src[2] * srcA + dst[2] * dstA * (1 - srcA)) / outA + 0.5) | 0;
      out[3] = outA;
    }
  }
  goog.asserts.assert(ol.color.isValid(out),
      'Output color of blend should be a valid color');
  return out;
};


/**
 * Return the color as an array. This function maintains a cache of calculated
 * arrays which means the result should not be modified.
 * @param {ol.Color|string} color Color.
 * @return {ol.Color} Color.
 * @api
 */
ol.color.asArray = function(color) {
  if (goog.isArray(color)) {
    return color;
  } else {
    goog.asserts.assert(goog.isString(color), 'Color should be a string');
    return ol.color.fromString(color);
  }
};


/**
 * Return the color as an rgba string.
 * @param {ol.Color|string} color Color.
 * @return {string} Rgba string.
 * @api
 */
ol.color.asString = function(color) {
  if (goog.isString(color)) {
    return color;
  } else {
    goog.asserts.assert(goog.isArray(color), 'Color should be an array');
    return ol.color.toString(color);
  }
};


/**
 * @param {ol.Color} color1 Color1.
 * @param {ol.Color} color2 Color2.
 * @return {boolean} Equals.
 */
ol.color.equals = function(color1, color2) {
  return color1 === color2 || (
      color1[0] == color2[0] && color1[1] == color2[1] &&
      color1[2] == color2[2] && color1[3] == color2[3]);
};


/**
 * @param {string} s String.
 * @return {ol.Color} Color.
 */
ol.color.fromString = (
    function() {

      // We maintain a small cache of parsed strings.  To provide cheap LRU-like
      // semantics, whenever the cache grows too large we simply delete an
      // arbitrary 25% of the entries.

      /**
       * @const
       * @type {number}
       */
      var MAX_CACHE_SIZE = 1024;

      /**
       * @type {Object.<string, ol.Color>}
       */
      var cache = {};

      /**
       * @type {number}
       */
      var cacheSize = 0;

      return (
          /**
           * @param {string} s String.
           * @return {ol.Color} Color.
           */
          function(s) {
            var color;
            if (cache.hasOwnProperty(s)) {
              color = cache[s];
            } else {
              if (cacheSize >= MAX_CACHE_SIZE) {
                var i = 0;
                var key;
                for (key in cache) {
                  if ((i++ & 3) === 0) {
                    delete cache[key];
                    --cacheSize;
                  }
                }
              }
              color = ol.color.fromStringInternal_(s);
              cache[s] = color;
              ++cacheSize;
            }
            return color;
          });

    })();


/**
 * @param {string} s String.
 * @private
 * @return {ol.Color} Color.
 */
ol.color.fromStringInternal_ = function(s) {

  var isHex = false;
  if (ol.ENABLE_NAMED_COLORS && goog.color.names.hasOwnProperty(s)) {
    s = goog.color.names[s];
    isHex = true;
  }

  var r, g, b, a, color, match;
  if (isHex || (match = ol.color.hexColorRe_.exec(s))) { // hex
    var n = s.length - 1; // number of hex digits
    goog.asserts.assert(n == 3 || n == 6,
        'Color string length should be 3 or 6');
    var d = n == 3 ? 1 : 2; // number of digits per channel
    r = parseInt(s.substr(1 + 0 * d, d), 16);
    g = parseInt(s.substr(1 + 1 * d, d), 16);
    b = parseInt(s.substr(1 + 2 * d, d), 16);
    if (d == 1) {
      r = (r << 4) + r;
      g = (g << 4) + g;
      b = (b << 4) + b;
    }
    a = 1;
    color = [r, g, b, a];
    goog.asserts.assert(ol.color.isValid(color),
        'Color should be a valid color');
    return color;
  } else if ((match = ol.color.rgbaColorRe_.exec(s))) { // rgba()
    r = Number(match[1]);
    g = Number(match[2]);
    b = Number(match[3]);
    a = Number(match[4]);
    color = [r, g, b, a];
    return ol.color.normalize(color, color);
  } else if ((match = ol.color.rgbColorRe_.exec(s))) { // rgb()
    r = Number(match[1]);
    g = Number(match[2]);
    b = Number(match[3]);
    color = [r, g, b, 1];
    return ol.color.normalize(color, color);
  } else {
    goog.asserts.fail(s + ' is not a valid color');
  }

};


/**
 * @param {ol.Color} color Color.
 * @return {boolean} Is valid.
 */
ol.color.isValid = function(color) {
  return 0 <= color[0] && color[0] < 256 &&
      0 <= color[1] && color[1] < 256 &&
      0 <= color[2] && color[2] < 256 &&
      0 <= color[3] && color[3] <= 1;
};


/**
 * @param {ol.Color} color Color.
 * @param {ol.Color=} opt_color Color.
 * @return {ol.Color} Clamped color.
 */
ol.color.normalize = function(color, opt_color) {
  var result = opt_color || [];
  result[0] = ol.math.clamp((color[0] + 0.5) | 0, 0, 255);
  result[1] = ol.math.clamp((color[1] + 0.5) | 0, 0, 255);
  result[2] = ol.math.clamp((color[2] + 0.5) | 0, 0, 255);
  result[3] = ol.math.clamp(color[3], 0, 1);
  return result;
};


/**
 * @param {ol.Color} color Color.
 * @return {string} String.
 */
ol.color.toString = function(color) {
  var r = color[0];
  if (r != (r | 0)) {
    r = (r + 0.5) | 0;
  }
  var g = color[1];
  if (g != (g | 0)) {
    g = (g + 0.5) | 0;
  }
  var b = color[2];
  if (b != (b | 0)) {
    b = (b + 0.5) | 0;
  }
  var a = color[3];
  return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
};


/**
 * @param {!ol.Color} color Color.
 * @param {goog.vec.Mat4.Number} transform Transform.
 * @param {!ol.Color=} opt_color Color.
 * @return {ol.Color} Transformed color.
 */
ol.color.transform = function(color, transform, opt_color) {
  var result = opt_color ? opt_color : [];
  result = goog.vec.Mat4.multVec3(transform, color, result);
  goog.asserts.assert(goog.isArray(result), 'result should be an array');
  result[3] = color[3];
  return ol.color.normalize(result, result);
};


/**
 * @param {ol.Color|string} color1 Color2.
 * @param {ol.Color|string} color2 Color2.
 * @return {boolean} Equals.
 */
ol.color.stringOrColorEquals = function(color1, color2) {
  if (color1 === color2 || color1 == color2) {
    return true;
  }
  if (goog.isString(color1)) {
    color1 = ol.color.fromString(color1);
  }
  if (goog.isString(color2)) {
    color2 = ol.color.fromString(color2);
  }
  return ol.color.equals(color1, color2);
};

// Copyright 2010 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Browser capability checks for the dom package.
 *
 */


goog.provide('goog.dom.BrowserFeature');

goog.require('goog.userAgent');


/**
 * Enum of browser capabilities.
 * @enum {boolean}
 */
goog.dom.BrowserFeature = {
  /**
   * Whether attributes 'name' and 'type' can be added to an element after it's
   * created. False in Internet Explorer prior to version 9.
   */
  CAN_ADD_NAME_OR_TYPE_ATTRIBUTES: !goog.userAgent.IE ||
      goog.userAgent.isDocumentModeOrHigher(9),

  /**
   * Whether we can use element.children to access an element's Element
   * children. Available since Gecko 1.9.1, IE 9. (IE<9 also includes comment
   * nodes in the collection.)
   */
  CAN_USE_CHILDREN_ATTRIBUTE: !goog.userAgent.GECKO && !goog.userAgent.IE ||
      goog.userAgent.IE && goog.userAgent.isDocumentModeOrHigher(9) ||
      goog.userAgent.GECKO && goog.userAgent.isVersionOrHigher('1.9.1'),

  /**
   * Opera, Safari 3, and Internet Explorer 9 all support innerText but they
   * include text nodes in script and style tags. Not document-mode-dependent.
   */
  CAN_USE_INNER_TEXT: (
      goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('9')),

  /**
   * MSIE, Opera, and Safari>=4 support element.parentElement to access an
   * element's parent if it is an Element.
   */
  CAN_USE_PARENT_ELEMENT_PROPERTY: goog.userAgent.IE || goog.userAgent.OPERA ||
      goog.userAgent.WEBKIT,

  /**
   * Whether NoScope elements need a scoped element written before them in
   * innerHTML.
   * MSDN: http://msdn.microsoft.com/en-us/library/ms533897(VS.85).aspx#1
   */
  INNER_HTML_NEEDS_SCOPED_ELEMENT: goog.userAgent.IE,

  /**
   * Whether we use legacy IE range API.
   */
  LEGACY_IE_RANGES: goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)
};

// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Defines the goog.dom.TagName enum.  This enumerates
 * all HTML tag names specified in either the the W3C HTML 4.01 index of
 * elements or the HTML5 draft specification.
 *
 * References:
 * http://www.w3.org/TR/html401/index/elements.html
 * http://dev.w3.org/html5/spec/section-index.html
 *
 */
goog.provide('goog.dom.TagName');


/**
 * Enum of all html tag names specified by the W3C HTML4.01 and HTML5
 * specifications.
 * @enum {string}
 */
goog.dom.TagName = {
  A: 'A',
  ABBR: 'ABBR',
  ACRONYM: 'ACRONYM',
  ADDRESS: 'ADDRESS',
  APPLET: 'APPLET',
  AREA: 'AREA',
  ARTICLE: 'ARTICLE',
  ASIDE: 'ASIDE',
  AUDIO: 'AUDIO',
  B: 'B',
  BASE: 'BASE',
  BASEFONT: 'BASEFONT',
  BDI: 'BDI',
  BDO: 'BDO',
  BIG: 'BIG',
  BLOCKQUOTE: 'BLOCKQUOTE',
  BODY: 'BODY',
  BR: 'BR',
  BUTTON: 'BUTTON',
  CANVAS: 'CANVAS',
  CAPTION: 'CAPTION',
  CENTER: 'CENTER',
  CITE: 'CITE',
  CODE: 'CODE',
  COL: 'COL',
  COLGROUP: 'COLGROUP',
  COMMAND: 'COMMAND',
  DATA: 'DATA',
  DATALIST: 'DATALIST',
  DD: 'DD',
  DEL: 'DEL',
  DETAILS: 'DETAILS',
  DFN: 'DFN',
  DIALOG: 'DIALOG',
  DIR: 'DIR',
  DIV: 'DIV',
  DL: 'DL',
  DT: 'DT',
  EM: 'EM',
  EMBED: 'EMBED',
  FIELDSET: 'FIELDSET',
  FIGCAPTION: 'FIGCAPTION',
  FIGURE: 'FIGURE',
  FONT: 'FONT',
  FOOTER: 'FOOTER',
  FORM: 'FORM',
  FRAME: 'FRAME',
  FRAMESET: 'FRAMESET',
  H1: 'H1',
  H2: 'H2',
  H3: 'H3',
  H4: 'H4',
  H5: 'H5',
  H6: 'H6',
  HEAD: 'HEAD',
  HEADER: 'HEADER',
  HGROUP: 'HGROUP',
  HR: 'HR',
  HTML: 'HTML',
  I: 'I',
  IFRAME: 'IFRAME',
  IMG: 'IMG',
  INPUT: 'INPUT',
  INS: 'INS',
  ISINDEX: 'ISINDEX',
  KBD: 'KBD',
  KEYGEN: 'KEYGEN',
  LABEL: 'LABEL',
  LEGEND: 'LEGEND',
  LI: 'LI',
  LINK: 'LINK',
  MAP: 'MAP',
  MARK: 'MARK',
  MATH: 'MATH',
  MENU: 'MENU',
  META: 'META',
  METER: 'METER',
  NAV: 'NAV',
  NOFRAMES: 'NOFRAMES',
  NOSCRIPT: 'NOSCRIPT',
  OBJECT: 'OBJECT',
  OL: 'OL',
  OPTGROUP: 'OPTGROUP',
  OPTION: 'OPTION',
  OUTPUT: 'OUTPUT',
  P: 'P',
  PARAM: 'PARAM',
  PRE: 'PRE',
  PROGRESS: 'PROGRESS',
  Q: 'Q',
  RP: 'RP',
  RT: 'RT',
  RUBY: 'RUBY',
  S: 'S',
  SAMP: 'SAMP',
  SCRIPT: 'SCRIPT',
  SECTION: 'SECTION',
  SELECT: 'SELECT',
  SMALL: 'SMALL',
  SOURCE: 'SOURCE',
  SPAN: 'SPAN',
  STRIKE: 'STRIKE',
  STRONG: 'STRONG',
  STYLE: 'STYLE',
  SUB: 'SUB',
  SUMMARY: 'SUMMARY',
  SUP: 'SUP',
  SVG: 'SVG',
  TABLE: 'TABLE',
  TBODY: 'TBODY',
  TD: 'TD',
  TEMPLATE: 'TEMPLATE',
  TEXTAREA: 'TEXTAREA',
  TFOOT: 'TFOOT',
  TH: 'TH',
  THEAD: 'THEAD',
  TIME: 'TIME',
  TITLE: 'TITLE',
  TR: 'TR',
  TRACK: 'TRACK',
  TT: 'TT',
  U: 'U',
  UL: 'UL',
  VAR: 'VAR',
  VIDEO: 'VIDEO',
  WBR: 'WBR'
};

// Copyright 2014 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Utilities for HTML element tag names.
 */
goog.provide('goog.dom.tags');

goog.require('goog.object');


/**
 * The void elements specified by
 * http://www.w3.org/TR/html-markup/syntax.html#void-elements.
 * @const @private {!Object<string, boolean>}
 */
goog.dom.tags.VOID_TAGS_ = goog.object.createSet(
    'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input',
    'keygen', 'link', 'meta', 'param', 'source', 'track', 'wbr');


/**
 * Checks whether the tag is void (with no contents allowed and no legal end
 * tag), for example 'br'.
 * @param {string} tagName The tag name in lower case.
 * @return {boolean}
 */
goog.dom.tags.isVoidTag = function(tagName) {
  return goog.dom.tags.VOID_TAGS_[tagName] === true;
};

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

goog.provide('goog.string.TypedString');



/**
 * Wrapper for strings that conform to a data type or language.
 *
 * Implementations of this interface are wrappers for strings, and typically
 * associate a type contract with the wrapped string.  Concrete implementations
 * of this interface may choose to implement additional run-time type checking,
 * see for example {@code goog.html.SafeHtml}. If available, client code that
 * needs to ensure type membership of an object should use the type's function
 * to assert type membership, such as {@code goog.html.SafeHtml.unwrap}.
 * @interface
 */
goog.string.TypedString = function() {};


/**
 * Interface marker of the TypedString interface.
 *
 * This property can be used to determine at runtime whether or not an object
 * implements this interface.  All implementations of this interface set this
 * property to {@code true}.
 * @type {boolean}
 */
goog.string.TypedString.prototype.implementsGoogStringTypedString;


/**
 * Retrieves this wrapped string's value.
 * @return {!string} The wrapped string's value.
 */
goog.string.TypedString.prototype.getTypedStringValue;

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

goog.provide('goog.string.Const');

goog.require('goog.asserts');
goog.require('goog.string.TypedString');



/**
 * Wrapper for compile-time-constant strings.
 *
 * Const is a wrapper for strings that can only be created from program
 * constants (i.e., string literals).  This property relies on a custom Closure
 * compiler check that {@code goog.string.Const.from} is only invoked on
 * compile-time-constant expressions.
 *
 * Const is useful in APIs whose correct and secure use requires that certain
 * arguments are not attacker controlled: Compile-time constants are inherently
 * under the control of the application and not under control of external
 * attackers, and hence are safe to use in such contexts.
 *
 * Instances of this type must be created via its factory method
 * {@code goog.string.Const.from} and not by invoking its constructor.  The
 * constructor intentionally takes no parameters and the type is immutable;
 * hence only a default instance corresponding to the empty string can be
 * obtained via constructor invocation.
 *
 * @see goog.string.Const#from
 * @constructor
 * @final
 * @struct
 * @implements {goog.string.TypedString}
 */
goog.string.Const = function() {
  /**
   * The wrapped value of this Const object.  The field has a purposely ugly
   * name to make (non-compiled) code that attempts to directly access this
   * field stand out.
   * @private {string}
   */
  this.stringConstValueWithSecurityContract__googStringSecurityPrivate_ = '';

  /**
   * A type marker used to implement additional run-time type checking.
   * @see goog.string.Const#unwrap
   * @const
   * @private
   */
  this.STRING_CONST_TYPE_MARKER__GOOG_STRING_SECURITY_PRIVATE_ =
      goog.string.Const.TYPE_MARKER_;
};


/**
 * @override
 * @const
 */
goog.string.Const.prototype.implementsGoogStringTypedString = true;


/**
 * Returns this Const's value a string.
 *
 * IMPORTANT: In code where it is security-relevant that an object's type is
 * indeed {@code goog.string.Const}, use {@code goog.string.Const.unwrap}
 * instead of this method.
 *
 * @see goog.string.Const#unwrap
 * @override
 */
goog.string.Const.prototype.getTypedStringValue = function() {
  return this.stringConstValueWithSecurityContract__googStringSecurityPrivate_;
};


/**
 * Returns a debug-string representation of this value.
 *
 * To obtain the actual string value wrapped inside an object of this type,
 * use {@code goog.string.Const.unwrap}.
 *
 * @see goog.string.Const#unwrap
 * @override
 */
goog.string.Const.prototype.toString = function() {
  return 'Const{' +
         this.stringConstValueWithSecurityContract__googStringSecurityPrivate_ +
         '}';
};


/**
 * Performs a runtime check that the provided object is indeed an instance
 * of {@code goog.string.Const}, and returns its value.
 * @param {!goog.string.Const} stringConst The object to extract from.
 * @return {string} The Const object's contained string, unless the run-time
 *     type check fails. In that case, {@code unwrap} returns an innocuous
 *     string, or, if assertions are enabled, throws
 *     {@code goog.asserts.AssertionError}.
 */
goog.string.Const.unwrap = function(stringConst) {
  // Perform additional run-time type-checking to ensure that stringConst is
  // indeed an instance of the expected type.  This provides some additional
  // protection against security bugs due to application code that disables type
  // checks.
  if (stringConst instanceof goog.string.Const &&
      stringConst.constructor === goog.string.Const &&
      stringConst.STRING_CONST_TYPE_MARKER__GOOG_STRING_SECURITY_PRIVATE_ ===
          goog.string.Const.TYPE_MARKER_) {
    return stringConst.
        stringConstValueWithSecurityContract__googStringSecurityPrivate_;
  } else {
    goog.asserts.fail('expected object of type Const, got \'' +
                      stringConst + '\'');
    return 'type_error:Const';
  }
};


/**
 * Creates a Const object from a compile-time constant string.
 *
 * It is illegal to invoke this function on an expression whose
 * compile-time-contant value cannot be determined by the Closure compiler.
 *
 * Correct invocations include,
 * <pre>
 *   var s = goog.string.Const.from('hello');
 *   var t = goog.string.Const.from('hello' + 'world');
 * </pre>
 *
 * In contrast, the following are illegal:
 * <pre>
 *   var s = goog.string.Const.from(getHello());
 *   var t = goog.string.Const.from('hello' + world);
 * </pre>
 *
 * TODO(xtof): Compile-time checks that this function is only called
 * with compile-time constant expressions.
 *
 * @param {string} s A constant string from which to create a Const.
 * @return {!goog.string.Const} A Const object initialized to stringConst.
 */
goog.string.Const.from = function(s) {
  return goog.string.Const.create__googStringSecurityPrivate_(s);
};


/**
 * Type marker for the Const type, used to implement additional run-time
 * type checking.
 * @const {!Object}
 * @private
 */
goog.string.Const.TYPE_MARKER_ = {};


/**
 * Utility method to create Const instances.
 * @param {string} s The string to initialize the Const object with.
 * @return {!goog.string.Const} The initialized Const object.
 * @private
 */
goog.string.Const.create__googStringSecurityPrivate_ = function(s) {
  var stringConst = new goog.string.Const();
  stringConst.stringConstValueWithSecurityContract__googStringSecurityPrivate_ =
      s;
  return stringConst;
};

// Copyright 2014 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview The SafeStyle type and its builders.
 *
 * TODO(xtof): Link to document stating type contract.
 */

goog.provide('goog.html.SafeStyle');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.string');
goog.require('goog.string.Const');
goog.require('goog.string.TypedString');



/**
 * A string-like object which represents a sequence of CSS declarations
 * ({@code propertyName1: propertyvalue1; propertyName2: propertyValue2; ...})
 * and that carries the security type contract that its value, as a string,
 * will not cause untrusted script execution (XSS) when evaluated as CSS in a
 * browser.
 *
 * Instances of this type must be created via the factory methods
 * ({@code goog.html.SafeStyle.create} or
 * {@code goog.html.SafeStyle.fromConstant}) and not by invoking its
 * constructor. The constructor intentionally takes no parameters and the type
 * is immutable; hence only a default instance corresponding to the empty string
 * can be obtained via constructor invocation.
 *
 * A SafeStyle's string representation ({@link #getTypedStringValue()}) can
 * safely:
 * <ul>
 *   <li>Be interpolated as the entire content of a *quoted* HTML style
 *       attribute, or before already existing properties. The SafeStyle string
 *       *must be HTML-attribute-escaped* (where " and ' are escaped) before
 *       interpolation.
 *   <li>Be interpolated as the entire content of a {}-wrapped block within a
 *       stylesheet, or before already existing properties. The SafeStyle string
 *       should not be escaped before interpolation. SafeStyle's contract also
 *       guarantees that the string will not be able to introduce new properties
 *       or elide existing ones.
 *   <li>Be assigned to the style property of a DOM node. The SafeStyle string
 *       should not be escaped before being assigned to the property.
 * </ul>
 *
 * A SafeStyle may never contain literal angle brackets. Otherwise, it could
 * be unsafe to place a SafeStyle into a &lt;style&gt; tag (where it can't
 * be HTML escaped). For example, if the SafeStyle containing
 * "{@code font: 'foo &lt;style/&gt;&lt;script&gt;evil&lt;/script&gt;'}" were
 * interpolated within a &lt;style&gt; tag, this would then break out of the
 * style context into HTML.
 *
 * A SafeStyle may contain literal single or double quotes, and as such the
 * entire style string must be escaped when used in a style attribute (if
 * this were not the case, the string could contain a matching quote that
 * would escape from the style attribute).
 *
 * Values of this type must be composable, i.e. for any two values
 * {@code style1} and {@code style2} of this type,
 * {@code goog.html.SafeStyle.unwrap(style1) +
 * goog.html.SafeStyle.unwrap(style2)} must itself be a value that satisfies
 * the SafeStyle type constraint. This requirement implies that for any value
 * {@code style} of this type, {@code goog.html.SafeStyle.unwrap(style)} must
 * not end in a "property value" or "property name" context. For example,
 * a value of {@code background:url("} or {@code font-} would not satisfy the
 * SafeStyle contract. This is because concatenating such strings with a
 * second value that itself does not contain unsafe CSS can result in an
 * overall string that does. For example, if {@code javascript:evil())"} is
 * appended to {@code background:url("}, the resulting string may result in
 * the execution of a malicious script.
 *
 * TODO(user): Consider whether we should implement UTF-8 interchange
 * validity checks and blacklisting of newlines (including Unicode ones) and
 * other whitespace characters (\t, \f). Document here if so and also update
 * SafeStyle.fromConstant().
 *
 * The following example values comply with this type's contract:
 * <ul>
 *   <li><pre>width: 1em;</pre>
 *   <li><pre>height:1em;</pre>
 *   <li><pre>width: 1em;height: 1em;</pre>
 *   <li><pre>background:url('http://url');</pre>
 * </ul>
 * In addition, the empty string is safe for use in a CSS attribute.
 *
 * The following example values do NOT comply with this type's contract:
 * <ul>
 *   <li><pre>background: red</pre> (missing a trailing semi-colon)
 *   <li><pre>background:</pre> (missing a value and a trailing semi-colon)
 *   <li><pre>1em</pre> (missing an attribute name, which provides context for
 *       the value)
 * </ul>
 *
 * @see goog.html.SafeStyle#create
 * @see goog.html.SafeStyle#fromConstant
 * @see http://www.w3.org/TR/css3-syntax/
 * @constructor
 * @final
 * @struct
 * @implements {goog.string.TypedString}
 */
goog.html.SafeStyle = function() {
  /**
   * The contained value of this SafeStyle.  The field has a purposely
   * ugly name to make (non-compiled) code that attempts to directly access this
   * field stand out.
   * @private {string}
   */
  this.privateDoNotAccessOrElseSafeStyleWrappedValue_ = '';

  /**
   * A type marker used to implement additional run-time type checking.
   * @see goog.html.SafeStyle#unwrap
   * @const
   * @private
   */
  this.SAFE_STYLE_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ =
      goog.html.SafeStyle.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_;
};


/**
 * @override
 * @const
 */
goog.html.SafeStyle.prototype.implementsGoogStringTypedString = true;


/**
 * Type marker for the SafeStyle type, used to implement additional
 * run-time type checking.
 * @const {!Object}
 * @private
 */
goog.html.SafeStyle.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ = {};


/**
 * Creates a SafeStyle object from a compile-time constant string.
 *
 * {@code style} should be in the format
 * {@code name: value; [name: value; ...]} and must not have any < or >
 * characters in it. This is so that SafeStyle's contract is preserved,
 * allowing the SafeStyle to correctly be interpreted as a sequence of CSS
 * declarations and without affecting the syntactic structure of any
 * surrounding CSS and HTML.
 *
 * This method performs basic sanity checks on the format of {@code style}
 * but does not constrain the format of {@code name} and {@code value}, except
 * for disallowing tag characters.
 *
 * @param {!goog.string.Const} style A compile-time-constant string from which
 *     to create a SafeStyle.
 * @return {!goog.html.SafeStyle} A SafeStyle object initialized to
 *     {@code style}.
 */
goog.html.SafeStyle.fromConstant = function(style) {
  var styleString = goog.string.Const.unwrap(style);
  if (styleString.length === 0) {
    return goog.html.SafeStyle.EMPTY;
  }
  goog.html.SafeStyle.checkStyle_(styleString);
  goog.asserts.assert(goog.string.endsWith(styleString, ';'),
      'Last character of style string is not \';\': ' + styleString);
  goog.asserts.assert(goog.string.contains(styleString, ':'),
      'Style string must contain at least one \':\', to ' +
      'specify a "name: value" pair: ' + styleString);
  return goog.html.SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse(
      styleString);
};


/**
 * Checks if the style definition is valid.
 * @param {string} style
 * @private
 */
goog.html.SafeStyle.checkStyle_ = function(style) {
  goog.asserts.assert(!/[<>]/.test(style),
      'Forbidden characters in style string: ' + style);
};


/**
 * Returns this SafeStyle's value as a string.
 *
 * IMPORTANT: In code where it is security relevant that an object's type is
 * indeed {@code SafeStyle}, use {@code goog.html.SafeStyle.unwrap} instead of
 * this method. If in doubt, assume that it's security relevant. In particular,
 * note that goog.html functions which return a goog.html type do not guarantee
 * the returned instance is of the right type. For example:
 *
 * <pre>
 * var fakeSafeHtml = new String('fake');
 * fakeSafeHtml.__proto__ = goog.html.SafeHtml.prototype;
 * var newSafeHtml = goog.html.SafeHtml.htmlEscape(fakeSafeHtml);
 * // newSafeHtml is just an alias for fakeSafeHtml, it's passed through by
 * // goog.html.SafeHtml.htmlEscape() as fakeSafeHtml
 * // instanceof goog.html.SafeHtml.
 * </pre>
 *
 * @see goog.html.SafeStyle#unwrap
 * @override
 */
goog.html.SafeStyle.prototype.getTypedStringValue = function() {
  return this.privateDoNotAccessOrElseSafeStyleWrappedValue_;
};


if (goog.DEBUG) {
  /**
   * Returns a debug string-representation of this value.
   *
   * To obtain the actual string value wrapped in a SafeStyle, use
   * {@code goog.html.SafeStyle.unwrap}.
   *
   * @see goog.html.SafeStyle#unwrap
   * @override
   */
  goog.html.SafeStyle.prototype.toString = function() {
    return 'SafeStyle{' +
        this.privateDoNotAccessOrElseSafeStyleWrappedValue_ + '}';
  };
}


/**
 * Performs a runtime check that the provided object is indeed a
 * SafeStyle object, and returns its value.
 *
 * @param {!goog.html.SafeStyle} safeStyle The object to extract from.
 * @return {string} The safeStyle object's contained string, unless
 *     the run-time type check fails. In that case, {@code unwrap} returns an
 *     innocuous string, or, if assertions are enabled, throws
 *     {@code goog.asserts.AssertionError}.
 */
goog.html.SafeStyle.unwrap = function(safeStyle) {
  // Perform additional Run-time type-checking to ensure that
  // safeStyle is indeed an instance of the expected type.  This
  // provides some additional protection against security bugs due to
  // application code that disables type checks.
  // Specifically, the following checks are performed:
  // 1. The object is an instance of the expected type.
  // 2. The object is not an instance of a subclass.
  // 3. The object carries a type marker for the expected type. "Faking" an
  // object requires a reference to the type marker, which has names intended
  // to stand out in code reviews.
  if (safeStyle instanceof goog.html.SafeStyle &&
      safeStyle.constructor === goog.html.SafeStyle &&
      safeStyle.SAFE_STYLE_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ ===
          goog.html.SafeStyle.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_) {
    return safeStyle.privateDoNotAccessOrElseSafeStyleWrappedValue_;
  } else {
    goog.asserts.fail(
        'expected object of type SafeStyle, got \'' + safeStyle + '\'');
    return 'type_error:SafeStyle';
  }
};


/**
 * Package-internal utility method to create SafeStyle instances.
 *
 * @param {string} style The string to initialize the SafeStyle object with.
 * @return {!goog.html.SafeStyle} The initialized SafeStyle object.
 * @package
 */
goog.html.SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse =
    function(style) {
  return new goog.html.SafeStyle().initSecurityPrivateDoNotAccessOrElse_(style);
};


/**
 * Called from createSafeStyleSecurityPrivateDoNotAccessOrElse(). This
 * method exists only so that the compiler can dead code eliminate static
 * fields (like EMPTY) when they're not accessed.
 * @param {string} style
 * @return {!goog.html.SafeStyle}
 * @private
 */
goog.html.SafeStyle.prototype.initSecurityPrivateDoNotAccessOrElse_ = function(
    style) {
  this.privateDoNotAccessOrElseSafeStyleWrappedValue_ = style;
  return this;
};


/**
 * A SafeStyle instance corresponding to the empty string.
 * @const {!goog.html.SafeStyle}
 */
goog.html.SafeStyle.EMPTY =
    goog.html.SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse('');


/**
 * The innocuous string generated by goog.html.SafeUrl.create when passed
 * an unsafe value.
 * @const {string}
 */
goog.html.SafeStyle.INNOCUOUS_STRING = 'zClosurez';


/**
 * Mapping of property names to their values.
 * @typedef {!Object<string, goog.string.Const|string>}
 */
goog.html.SafeStyle.PropertyMap;


/**
 * Creates a new SafeStyle object from the properties specified in the map.
 * @param {goog.html.SafeStyle.PropertyMap} map Mapping of property names to
 *     their values, for example {'margin': '1px'}. Names must consist of
 *     [-_a-zA-Z0-9]. Values might be strings consisting of
 *     [-,.'"%_!# a-zA-Z0-9], where " and ' must be properly balanced.
 *     Other values must be wrapped in goog.string.Const. Null value causes
 *     skipping the property.
 * @return {!goog.html.SafeStyle}
 * @throws {Error} If invalid name is provided.
 * @throws {goog.asserts.AssertionError} If invalid value is provided. With
 *     disabled assertions, invalid value is replaced by
 *     goog.html.SafeStyle.INNOCUOUS_STRING.
 */
goog.html.SafeStyle.create = function(map) {
  var style = '';
  for (var name in map) {
    if (!/^[-_a-zA-Z0-9]+$/.test(name)) {
      throw Error('Name allows only [-_a-zA-Z0-9], got: ' + name);
    }
    var value = map[name];
    if (value == null) {
      continue;
    }
    if (value instanceof goog.string.Const) {
      value = goog.string.Const.unwrap(value);
      // These characters can be used to change context and we don't want that
      // even with const values.
      goog.asserts.assert(!/[{;}]/.test(value), 'Value does not allow [{;}].');
    } else if (!goog.html.SafeStyle.VALUE_RE_.test(value)) {
      goog.asserts.fail(
          'String value allows only [-,."\'%_!# a-zA-Z0-9], got: ' + value);
      value = goog.html.SafeStyle.INNOCUOUS_STRING;
    } else if (!goog.html.SafeStyle.hasBalancedQuotes_(value)) {
      goog.asserts.fail('String value requires balanced quotes, got: ' + value);
      value = goog.html.SafeStyle.INNOCUOUS_STRING;
    }
    style += name + ':' + value + ';';
  }
  if (!style) {
    return goog.html.SafeStyle.EMPTY;
  }
  goog.html.SafeStyle.checkStyle_(style);
  return goog.html.SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse(
      style);
};


/**
 * Checks that quotes (" and ') are properly balanced inside a string. Assumes
 * that neither escape (\) nor any other character that could result in
 * breaking out of a string parsing context are allowed;
 * see http://www.w3.org/TR/css3-syntax/#string-token-diagram.
 * @param {string} value Untrusted CSS property value.
 * @return {boolean} True if property value is safe with respect to quote
 *     balancedness.
 * @private
 */
goog.html.SafeStyle.hasBalancedQuotes_ = function(value) {
  var outsideSingle = true;
  var outsideDouble = true;
  for (var i = 0; i < value.length; i++) {
    var c = value.charAt(i);
    if (c == "'" && outsideDouble) {
      outsideSingle = !outsideSingle;
    } else if (c == '"' && outsideSingle) {
      outsideDouble = !outsideDouble;
    }
  }
  return outsideSingle && outsideDouble;
};


// Keep in sync with the error string in create().
/**
 * Regular expression for safe values.
 *
 * Quotes (" and ') are allowed, but a check must be done elsewhere to ensure
 * they're balanced.
 *
 * ',' allows multiple values to be assigned to the same property
 * (e.g. background-attachment or font-family) and hence could allow
 * multiple values to get injected, but that should pose no risk of XSS.
 * @const {!RegExp}
 * @private
 */
goog.html.SafeStyle.VALUE_RE_ = /^[-,."'%_!# a-zA-Z0-9]+$/;


/**
 * Creates a new SafeStyle object by concatenating the values.
 * @param {...(!goog.html.SafeStyle|!Array<!goog.html.SafeStyle>)} var_args
 *     SafeStyles to concatenate.
 * @return {!goog.html.SafeStyle}
 */
goog.html.SafeStyle.concat = function(var_args) {
  var style = '';

  /**
   * @param {!goog.html.SafeStyle|!Array<!goog.html.SafeStyle>} argument
   */
  var addArgument = function(argument) {
    if (goog.isArray(argument)) {
      goog.array.forEach(argument, addArgument);
    } else {
      style += goog.html.SafeStyle.unwrap(argument);
    }
  };

  goog.array.forEach(arguments, addArgument);
  if (!style) {
    return goog.html.SafeStyle.EMPTY;
  }
  return goog.html.SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse(
      style);
};

// Copyright 2014 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview The SafeStyleSheet type and its builders.
 *
 * TODO(xtof): Link to document stating type contract.
 */

goog.provide('goog.html.SafeStyleSheet');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.string');
goog.require('goog.string.Const');
goog.require('goog.string.TypedString');



/**
 * A string-like object which represents a CSS style sheet and that carries the
 * security type contract that its value, as a string, will not cause untrusted
 * script execution (XSS) when evaluated as CSS in a browser.
 *
 * Instances of this type must be created via the factory method
 * {@code goog.html.SafeStyleSheet.fromConstant} and not by invoking its
 * constructor. The constructor intentionally takes no parameters and the type
 * is immutable; hence only a default instance corresponding to the empty string
 * can be obtained via constructor invocation.
 *
 * A SafeStyleSheet's string representation can safely be interpolated as the
 * content of a style element within HTML. The SafeStyleSheet string should
 * not be escaped before interpolation.
 *
 * Values of this type must be composable, i.e. for any two values
 * {@code styleSheet1} and {@code styleSheet2} of this type,
 * {@code goog.html.SafeStyleSheet.unwrap(styleSheet1) +
 * goog.html.SafeStyleSheet.unwrap(styleSheet2)} must itself be a value that
 * satisfies the SafeStyleSheet type constraint. This requirement implies that
 * for any value {@code styleSheet} of this type,
 * {@code goog.html.SafeStyleSheet.unwrap(styleSheet1)} must end in
 * "beginning of rule" context.

 * A SafeStyleSheet can be constructed via security-reviewed unchecked
 * conversions. In this case producers of SafeStyleSheet must ensure themselves
 * that the SafeStyleSheet does not contain unsafe script. Note in particular
 * that {@code &lt;} is dangerous, even when inside CSS strings, and so should
 * always be forbidden or CSS-escaped in user controlled input. For example, if
 * {@code &lt;/style&gt;&lt;script&gt;evil&lt;/script&gt;"} were interpolated
 * inside a CSS string, it would break out of the context of the original
 * style element and {@code evil} would execute. Also note that within an HTML
 * style (raw text) element, HTML character references, such as
 * {@code &amp;lt;}, are not allowed. See
 * http://www.w3.org/TR/html5/scripting-1.html#restrictions-for-contents-of-script-elements
 * (similar considerations apply to the style element).
 *
 * @see goog.html.SafeStyleSheet#fromConstant
 * @constructor
 * @final
 * @struct
 * @implements {goog.string.TypedString}
 */
goog.html.SafeStyleSheet = function() {
  /**
   * The contained value of this SafeStyleSheet.  The field has a purposely
   * ugly name to make (non-compiled) code that attempts to directly access this
   * field stand out.
   * @private {string}
   */
  this.privateDoNotAccessOrElseSafeStyleSheetWrappedValue_ = '';

  /**
   * A type marker used to implement additional run-time type checking.
   * @see goog.html.SafeStyleSheet#unwrap
   * @const
   * @private
   */
  this.SAFE_SCRIPT_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ =
      goog.html.SafeStyleSheet.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_;
};


/**
 * @override
 * @const
 */
goog.html.SafeStyleSheet.prototype.implementsGoogStringTypedString = true;


/**
 * Type marker for the SafeStyleSheet type, used to implement additional
 * run-time type checking.
 * @const {!Object}
 * @private
 */
goog.html.SafeStyleSheet.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ = {};


/**
 * Creates a new SafeStyleSheet object by concatenating values.
 * @param {...(!goog.html.SafeStyleSheet|!Array<!goog.html.SafeStyleSheet>)}
 *     var_args Values to concatenate.
 * @return {!goog.html.SafeStyleSheet}
 */
goog.html.SafeStyleSheet.concat = function(var_args) {
  var result = '';

  /**
   * @param {!goog.html.SafeStyleSheet|!Array<!goog.html.SafeStyleSheet>}
   *     argument
   */
  var addArgument = function(argument) {
    if (goog.isArray(argument)) {
      goog.array.forEach(argument, addArgument);
    } else {
      result += goog.html.SafeStyleSheet.unwrap(argument);
    }
  };

  goog.array.forEach(arguments, addArgument);
  return goog.html.SafeStyleSheet
      .createSafeStyleSheetSecurityPrivateDoNotAccessOrElse(result);
};


/**
 * Creates a SafeStyleSheet object from a compile-time constant string.
 *
 * {@code styleSheet} must not have any &lt; characters in it, so that
 * the syntactic structure of the surrounding HTML is not affected.
 *
 * @param {!goog.string.Const} styleSheet A compile-time-constant string from
 *     which to create a SafeStyleSheet.
 * @return {!goog.html.SafeStyleSheet} A SafeStyleSheet object initialized to
 *     {@code styleSheet}.
 */
goog.html.SafeStyleSheet.fromConstant = function(styleSheet) {
  var styleSheetString = goog.string.Const.unwrap(styleSheet);
  if (styleSheetString.length === 0) {
    return goog.html.SafeStyleSheet.EMPTY;
  }
  // > is a valid character in CSS selectors and there's no strict need to
  // block it if we already block <.
  goog.asserts.assert(!goog.string.contains(styleSheetString, '<'),
      "Forbidden '<' character in style sheet string: " + styleSheetString);
  return goog.html.SafeStyleSheet.
      createSafeStyleSheetSecurityPrivateDoNotAccessOrElse(styleSheetString);
};


/**
 * Returns this SafeStyleSheet's value as a string.
 *
 * IMPORTANT: In code where it is security relevant that an object's type is
 * indeed {@code SafeStyleSheet}, use {@code goog.html.SafeStyleSheet.unwrap}
 * instead of this method. If in doubt, assume that it's security relevant. In
 * particular, note that goog.html functions which return a goog.html type do
 * not guarantee the returned instance is of the right type. For example:
 *
 * <pre>
 * var fakeSafeHtml = new String('fake');
 * fakeSafeHtml.__proto__ = goog.html.SafeHtml.prototype;
 * var newSafeHtml = goog.html.SafeHtml.htmlEscape(fakeSafeHtml);
 * // newSafeHtml is just an alias for fakeSafeHtml, it's passed through by
 * // goog.html.SafeHtml.htmlEscape() as fakeSafeHtml
 * // instanceof goog.html.SafeHtml.
 * </pre>
 *
 * @see goog.html.SafeStyleSheet#unwrap
 * @override
 */
goog.html.SafeStyleSheet.prototype.getTypedStringValue = function() {
  return this.privateDoNotAccessOrElseSafeStyleSheetWrappedValue_;
};


if (goog.DEBUG) {
  /**
   * Returns a debug string-representation of this value.
   *
   * To obtain the actual string value wrapped in a SafeStyleSheet, use
   * {@code goog.html.SafeStyleSheet.unwrap}.
   *
   * @see goog.html.SafeStyleSheet#unwrap
   * @override
   */
  goog.html.SafeStyleSheet.prototype.toString = function() {
    return 'SafeStyleSheet{' +
        this.privateDoNotAccessOrElseSafeStyleSheetWrappedValue_ + '}';
  };
}


/**
 * Performs a runtime check that the provided object is indeed a
 * SafeStyleSheet object, and returns its value.
 *
 * @param {!goog.html.SafeStyleSheet} safeStyleSheet The object to extract from.
 * @return {string} The safeStyleSheet object's contained string, unless
 *     the run-time type check fails. In that case, {@code unwrap} returns an
 *     innocuous string, or, if assertions are enabled, throws
 *     {@code goog.asserts.AssertionError}.
 */
goog.html.SafeStyleSheet.unwrap = function(safeStyleSheet) {
  // Perform additional Run-time type-checking to ensure that
  // safeStyleSheet is indeed an instance of the expected type.  This
  // provides some additional protection against security bugs due to
  // application code that disables type checks.
  // Specifically, the following checks are performed:
  // 1. The object is an instance of the expected type.
  // 2. The object is not an instance of a subclass.
  // 3. The object carries a type marker for the expected type. "Faking" an
  // object requires a reference to the type marker, which has names intended
  // to stand out in code reviews.
  if (safeStyleSheet instanceof goog.html.SafeStyleSheet &&
      safeStyleSheet.constructor === goog.html.SafeStyleSheet &&
      safeStyleSheet.SAFE_SCRIPT_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ ===
          goog.html.SafeStyleSheet.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_) {
    return safeStyleSheet.privateDoNotAccessOrElseSafeStyleSheetWrappedValue_;
  } else {
    goog.asserts.fail(
        "expected object of type SafeStyleSheet, got '" + safeStyleSheet +
        "'");
    return 'type_error:SafeStyleSheet';
  }
};


/**
 * Package-internal utility method to create SafeStyleSheet instances.
 *
 * @param {string} styleSheet The string to initialize the SafeStyleSheet
 *     object with.
 * @return {!goog.html.SafeStyleSheet} The initialized SafeStyleSheet object.
 * @package
 */
goog.html.SafeStyleSheet.createSafeStyleSheetSecurityPrivateDoNotAccessOrElse =
    function(styleSheet) {
  return new goog.html.SafeStyleSheet().initSecurityPrivateDoNotAccessOrElse_(
      styleSheet);
};


/**
 * Called from createSafeStyleSheetSecurityPrivateDoNotAccessOrElse(). This
 * method exists only so that the compiler can dead code eliminate static
 * fields (like EMPTY) when they're not accessed.
 * @param {string} styleSheet
 * @return {!goog.html.SafeStyleSheet}
 * @private
 */
goog.html.SafeStyleSheet.prototype.initSecurityPrivateDoNotAccessOrElse_ =
    function(styleSheet) {
  this.privateDoNotAccessOrElseSafeStyleSheetWrappedValue_ = styleSheet;
  return this;
};


/**
 * A SafeStyleSheet instance corresponding to the empty string.
 * @const {!goog.html.SafeStyleSheet}
 */
goog.html.SafeStyleSheet.EMPTY =
    goog.html.SafeStyleSheet.
        createSafeStyleSheetSecurityPrivateDoNotAccessOrElse('');

// Copyright 2015 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Wrapper for URL and its createObjectUrl and revokeObjectUrl
 * methods that are part of the HTML5 File API.
 */

goog.provide('goog.fs.url');


/**
 * Creates a blob URL for a blob object.
 * Throws an error if the browser does not support Object Urls.
 *
 * @param {!Blob} blob The object for which to create the URL.
 * @return {string} The URL for the object.
 */
goog.fs.url.createObjectUrl = function(blob) {
  return goog.fs.url.getUrlObject_().createObjectURL(blob);
};


/**
 * Revokes a URL created by {@link goog.fs.url.createObjectUrl}.
 * Throws an error if the browser does not support Object Urls.
 *
 * @param {string} url The URL to revoke.
 */
goog.fs.url.revokeObjectUrl = function(url) {
  goog.fs.url.getUrlObject_().revokeObjectURL(url);
};


/**
 * @typedef {{createObjectURL: (function(!Blob): string),
 *            revokeObjectURL: function(string): void}}
 */
goog.fs.url.UrlObject_;


/**
 * Get the object that has the createObjectURL and revokeObjectURL functions for
 * this browser.
 *
 * @return {goog.fs.url.UrlObject_} The object for this browser.
 * @private
 */
goog.fs.url.getUrlObject_ = function() {
  var urlObject = goog.fs.url.findUrlObject_();
  if (urlObject != null) {
    return urlObject;
  } else {
    throw Error('This browser doesn\'t seem to support blob URLs');
  }
};


/**
 * Finds the object that has the createObjectURL and revokeObjectURL functions
 * for this browser.
 *
 * @return {?goog.fs.url.UrlObject_} The object for this browser or null if the
 *     browser does not support Object Urls.
 * @suppress {unnecessaryCasts} Depending on how the code is compiled, casting
 *     goog.global to UrlObject_ may result in unnecessary cast warning.
 *     However, the cast cannot be removed because with different set of
 *     compiler flags, the cast is indeed necessary.  As such, silencing it.
 * @private
 */
goog.fs.url.findUrlObject_ = function() {
  // This is what the spec says to do
  // http://dev.w3.org/2006/webapi/FileAPI/#dfn-createObjectURL
  if (goog.isDef(goog.global.URL) &&
      goog.isDef(goog.global.URL.createObjectURL)) {
    return /** @type {goog.fs.url.UrlObject_} */ (goog.global.URL);
  // This is what Chrome does (as of 10.0.648.6 dev)
  } else if (goog.isDef(goog.global.webkitURL) &&
             goog.isDef(goog.global.webkitURL.createObjectURL)) {
    return /** @type {goog.fs.url.UrlObject_} */ (goog.global.webkitURL);
  // This is what the spec used to say to do
  } else if (goog.isDef(goog.global.createObjectURL)) {
    return /** @type {goog.fs.url.UrlObject_} */ (goog.global);
  } else {
    return null;
  }
};


/**
 * Checks whether this browser supports Object Urls. If not, calls to
 * createObjectUrl and revokeObjectUrl will result in an error.
 *
 * @return {boolean} True if this browser supports Object Urls.
 */
goog.fs.url.browserSupportsObjectUrls = function() {
  return goog.fs.url.findUrlObject_() != null;
};

// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Utility functions for supporting Bidi issues.
 */


/**
 * Namespace for bidi supporting functions.
 */
goog.provide('goog.i18n.bidi');
goog.provide('goog.i18n.bidi.Dir');
goog.provide('goog.i18n.bidi.DirectionalString');
goog.provide('goog.i18n.bidi.Format');


/**
 * @define {boolean} FORCE_RTL forces the {@link goog.i18n.bidi.IS_RTL} constant
 * to say that the current locale is a RTL locale.  This should only be used
 * if you want to override the default behavior for deciding whether the
 * current locale is RTL or not.
 *
 * {@see goog.i18n.bidi.IS_RTL}
 */
goog.define('goog.i18n.bidi.FORCE_RTL', false);


/**
 * Constant that defines whether or not the current locale is a RTL locale.
 * If {@link goog.i18n.bidi.FORCE_RTL} is not true, this constant will default
 * to check that {@link goog.LOCALE} is one of a few major RTL locales.
 *
 * <p>This is designed to be a maximally efficient compile-time constant. For
 * example, for the default goog.LOCALE, compiling
 * "if (goog.i18n.bidi.IS_RTL) alert('rtl') else {}" should produce no code. It
 * is this design consideration that limits the implementation to only
 * supporting a few major RTL locales, as opposed to the broader repertoire of
 * something like goog.i18n.bidi.isRtlLanguage.
 *
 * <p>Since this constant refers to the directionality of the locale, it is up
 * to the caller to determine if this constant should also be used for the
 * direction of the UI.
 *
 * {@see goog.LOCALE}
 *
 * @type {boolean}
 *
 * TODO(user): write a test that checks that this is a compile-time constant.
 */
goog.i18n.bidi.IS_RTL = goog.i18n.bidi.FORCE_RTL ||
    (
        (goog.LOCALE.substring(0, 2).toLowerCase() == 'ar' ||
         goog.LOCALE.substring(0, 2).toLowerCase() == 'fa' ||
         goog.LOCALE.substring(0, 2).toLowerCase() == 'he' ||
         goog.LOCALE.substring(0, 2).toLowerCase() == 'iw' ||
         goog.LOCALE.substring(0, 2).toLowerCase() == 'ps' ||
         goog.LOCALE.substring(0, 2).toLowerCase() == 'sd' ||
         goog.LOCALE.substring(0, 2).toLowerCase() == 'ug' ||
         goog.LOCALE.substring(0, 2).toLowerCase() == 'ur' ||
         goog.LOCALE.substring(0, 2).toLowerCase() == 'yi') &&
        (goog.LOCALE.length == 2 ||
         goog.LOCALE.substring(2, 3) == '-' ||
         goog.LOCALE.substring(2, 3) == '_')
    ) || (
        goog.LOCALE.length >= 3 &&
        goog.LOCALE.substring(0, 3).toLowerCase() == 'ckb' &&
        (goog.LOCALE.length == 3 ||
         goog.LOCALE.substring(3, 4) == '-' ||
         goog.LOCALE.substring(3, 4) == '_')
    );


/**
 * Unicode formatting characters and directionality string constants.
 * @enum {string}
 */
goog.i18n.bidi.Format = {
  /** Unicode "Left-To-Right Embedding" (LRE) character. */
  LRE: '\u202A',
  /** Unicode "Right-To-Left Embedding" (RLE) character. */
  RLE: '\u202B',
  /** Unicode "Pop Directional Formatting" (PDF) character. */
  PDF: '\u202C',
  /** Unicode "Left-To-Right Mark" (LRM) character. */
  LRM: '\u200E',
  /** Unicode "Right-To-Left Mark" (RLM) character. */
  RLM: '\u200F'
};


/**
 * Directionality enum.
 * @enum {number}
 */
goog.i18n.bidi.Dir = {
  /**
   * Left-to-right.
   */
  LTR: 1,

  /**
   * Right-to-left.
   */
  RTL: -1,

  /**
   * Neither left-to-right nor right-to-left.
   */
  NEUTRAL: 0
};


/**
 * 'right' string constant.
 * @type {string}
 */
goog.i18n.bidi.RIGHT = 'right';


/**
 * 'left' string constant.
 * @type {string}
 */
goog.i18n.bidi.LEFT = 'left';


/**
 * 'left' if locale is RTL, 'right' if not.
 * @type {string}
 */
goog.i18n.bidi.I18N_RIGHT = goog.i18n.bidi.IS_RTL ? goog.i18n.bidi.LEFT :
    goog.i18n.bidi.RIGHT;


/**
 * 'right' if locale is RTL, 'left' if not.
 * @type {string}
 */
goog.i18n.bidi.I18N_LEFT = goog.i18n.bidi.IS_RTL ? goog.i18n.bidi.RIGHT :
    goog.i18n.bidi.LEFT;


/**
 * Convert a directionality given in various formats to a goog.i18n.bidi.Dir
 * constant. Useful for interaction with different standards of directionality
 * representation.
 *
 * @param {goog.i18n.bidi.Dir|number|boolean|null} givenDir Directionality given
 *     in one of the following formats:
 *     1. A goog.i18n.bidi.Dir constant.
 *     2. A number (positive = LTR, negative = RTL, 0 = neutral).
 *     3. A boolean (true = RTL, false = LTR).
 *     4. A null for unknown directionality.
 * @param {boolean=} opt_noNeutral Whether a givenDir of zero or
 *     goog.i18n.bidi.Dir.NEUTRAL should be treated as null, i.e. unknown, in
 *     order to preserve legacy behavior.
 * @return {?goog.i18n.bidi.Dir} A goog.i18n.bidi.Dir constant matching the
 *     given directionality. If given null, returns null (i.e. unknown).
 */
goog.i18n.bidi.toDir = function(givenDir, opt_noNeutral) {
  if (typeof givenDir == 'number') {
    // This includes the non-null goog.i18n.bidi.Dir case.
    return givenDir > 0 ? goog.i18n.bidi.Dir.LTR :
        givenDir < 0 ? goog.i18n.bidi.Dir.RTL :
        opt_noNeutral ? null : goog.i18n.bidi.Dir.NEUTRAL;
  } else if (givenDir == null) {
    return null;
  } else {
    // Must be typeof givenDir == 'boolean'.
    return givenDir ? goog.i18n.bidi.Dir.RTL : goog.i18n.bidi.Dir.LTR;
  }
};


/**
 * A practical pattern to identify strong LTR characters. This pattern is not
 * theoretically correct according to the Unicode standard. It is simplified for
 * performance and small code size.
 * @type {string}
 * @private
 */
goog.i18n.bidi.ltrChars_ =
    'A-Za-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02B8\u0300-\u0590\u0800-\u1FFF' +
    '\u200E\u2C00-\uFB1C\uFE00-\uFE6F\uFEFD-\uFFFF';


/**
 * A practical pattern to identify strong RTL character. This pattern is not
 * theoretically correct according to the Unicode standard. It is simplified
 * for performance and small code size.
 * @type {string}
 * @private
 */
goog.i18n.bidi.rtlChars_ =
    '\u0591-\u06EF\u06FA-\u07FF\u200F\uFB1D-\uFDFF\uFE70-\uFEFC';


/**
 * Simplified regular expression for an HTML tag (opening or closing) or an HTML
 * escape. We might want to skip over such expressions when estimating the text
 * directionality.
 * @type {RegExp}
 * @private
 */
goog.i18n.bidi.htmlSkipReg_ = /<[^>]*>|&[^;]+;/g;


/**
 * Returns the input text with spaces instead of HTML tags or HTML escapes, if
 * opt_isStripNeeded is true. Else returns the input as is.
 * Useful for text directionality estimation.
 * Note: the function should not be used in other contexts; it is not 100%
 * correct, but rather a good-enough implementation for directionality
 * estimation purposes.
 * @param {string} str The given string.
 * @param {boolean=} opt_isStripNeeded Whether to perform the stripping.
 *     Default: false (to retain consistency with calling functions).
 * @return {string} The given string cleaned of HTML tags / escapes.
 * @private
 */
goog.i18n.bidi.stripHtmlIfNeeded_ = function(str, opt_isStripNeeded) {
  return opt_isStripNeeded ? str.replace(goog.i18n.bidi.htmlSkipReg_, '') :
      str;
};


/**
 * Regular expression to check for RTL characters.
 * @type {RegExp}
 * @private
 */
goog.i18n.bidi.rtlCharReg_ = new RegExp('[' + goog.i18n.bidi.rtlChars_ + ']');


/**
 * Regular expression to check for LTR characters.
 * @type {RegExp}
 * @private
 */
goog.i18n.bidi.ltrCharReg_ = new RegExp('[' + goog.i18n.bidi.ltrChars_ + ']');


/**
 * Test whether the given string has any RTL characters in it.
 * @param {string} str The given string that need to be tested.
 * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
 *     Default: false.
 * @return {boolean} Whether the string contains RTL characters.
 */
goog.i18n.bidi.hasAnyRtl = function(str, opt_isHtml) {
  return goog.i18n.bidi.rtlCharReg_.test(goog.i18n.bidi.stripHtmlIfNeeded_(
      str, opt_isHtml));
};


/**
 * Test whether the given string has any RTL characters in it.
 * @param {string} str The given string that need to be tested.
 * @return {boolean} Whether the string contains RTL characters.
 * @deprecated Use hasAnyRtl.
 */
goog.i18n.bidi.hasRtlChar = goog.i18n.bidi.hasAnyRtl;


/**
 * Test whether the given string has any LTR characters in it.
 * @param {string} str The given string that need to be tested.
 * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
 *     Default: false.
 * @return {boolean} Whether the string contains LTR characters.
 */
goog.i18n.bidi.hasAnyLtr = function(str, opt_isHtml) {
  return goog.i18n.bidi.ltrCharReg_.test(goog.i18n.bidi.stripHtmlIfNeeded_(
      str, opt_isHtml));
};


/**
 * Regular expression pattern to check if the first character in the string
 * is LTR.
 * @type {RegExp}
 * @private
 */
goog.i18n.bidi.ltrRe_ = new RegExp('^[' + goog.i18n.bidi.ltrChars_ + ']');


/**
 * Regular expression pattern to check if the first character in the string
 * is RTL.
 * @type {RegExp}
 * @private
 */
goog.i18n.bidi.rtlRe_ = new RegExp('^[' + goog.i18n.bidi.rtlChars_ + ']');


/**
 * Check if the first character in the string is RTL or not.
 * @param {string} str The given string that need to be tested.
 * @return {boolean} Whether the first character in str is an RTL char.
 */
goog.i18n.bidi.isRtlChar = function(str) {
  return goog.i18n.bidi.rtlRe_.test(str);
};


/**
 * Check if the first character in the string is LTR or not.
 * @param {string} str The given string that need to be tested.
 * @return {boolean} Whether the first character in str is an LTR char.
 */
goog.i18n.bidi.isLtrChar = function(str) {
  return goog.i18n.bidi.ltrRe_.test(str);
};


/**
 * Check if the first character in the string is neutral or not.
 * @param {string} str The given string that need to be tested.
 * @return {boolean} Whether the first character in str is a neutral char.
 */
goog.i18n.bidi.isNeutralChar = function(str) {
  return !goog.i18n.bidi.isLtrChar(str) && !goog.i18n.bidi.isRtlChar(str);
};


/**
 * Regular expressions to check if a piece of text is of LTR directionality
 * on first character with strong directionality.
 * @type {RegExp}
 * @private
 */
goog.i18n.bidi.ltrDirCheckRe_ = new RegExp(
    '^[^' + goog.i18n.bidi.rtlChars_ + ']*[' + goog.i18n.bidi.ltrChars_ + ']');


/**
 * Regular expressions to check if a piece of text is of RTL directionality
 * on first character with strong directionality.
 * @type {RegExp}
 * @private
 */
goog.i18n.bidi.rtlDirCheckRe_ = new RegExp(
    '^[^' + goog.i18n.bidi.ltrChars_ + ']*[' + goog.i18n.bidi.rtlChars_ + ']');


/**
 * Check whether the first strongly directional character (if any) is RTL.
 * @param {string} str String being checked.
 * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
 *     Default: false.
 * @return {boolean} Whether RTL directionality is detected using the first
 *     strongly-directional character method.
 */
goog.i18n.bidi.startsWithRtl = function(str, opt_isHtml) {
  return goog.i18n.bidi.rtlDirCheckRe_.test(goog.i18n.bidi.stripHtmlIfNeeded_(
      str, opt_isHtml));
};


/**
 * Check whether the first strongly directional character (if any) is RTL.
 * @param {string} str String being checked.
 * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
 *     Default: false.
 * @return {boolean} Whether RTL directionality is detected using the first
 *     strongly-directional character method.
 * @deprecated Use startsWithRtl.
 */
goog.i18n.bidi.isRtlText = goog.i18n.bidi.startsWithRtl;


/**
 * Check whether the first strongly directional character (if any) is LTR.
 * @param {string} str String being checked.
 * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
 *     Default: false.
 * @return {boolean} Whether LTR directionality is detected using the first
 *     strongly-directional character method.
 */
goog.i18n.bidi.startsWithLtr = function(str, opt_isHtml) {
  return goog.i18n.bidi.ltrDirCheckRe_.test(goog.i18n.bidi.stripHtmlIfNeeded_(
      str, opt_isHtml));
};


/**
 * Check whether the first strongly directional character (if any) is LTR.
 * @param {string} str String being checked.
 * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
 *     Default: false.
 * @return {boolean} Whether LTR directionality is detected using the first
 *     strongly-directional character method.
 * @deprecated Use startsWithLtr.
 */
goog.i18n.bidi.isLtrText = goog.i18n.bidi.startsWithLtr;


/**
 * Regular expression to check if a string looks like something that must
 * always be LTR even in RTL text, e.g. a URL. When estimating the
 * directionality of text containing these, we treat these as weakly LTR,
 * like numbers.
 * @type {RegExp}
 * @private
 */
goog.i18n.bidi.isRequiredLtrRe_ = /^http:\/\/.*/;


/**
 * Check whether the input string either contains no strongly directional
 * characters or looks like a url.
 * @param {string} str String being checked.
 * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
 *     Default: false.
 * @return {boolean} Whether neutral directionality is detected.
 */
goog.i18n.bidi.isNeutralText = function(str, opt_isHtml) {
  str = goog.i18n.bidi.stripHtmlIfNeeded_(str, opt_isHtml);
  return goog.i18n.bidi.isRequiredLtrRe_.test(str) ||
      !goog.i18n.bidi.hasAnyLtr(str) && !goog.i18n.bidi.hasAnyRtl(str);
};


/**
 * Regular expressions to check if the last strongly-directional character in a
 * piece of text is LTR.
 * @type {RegExp}
 * @private
 */
goog.i18n.bidi.ltrExitDirCheckRe_ = new RegExp(
    '[' + goog.i18n.bidi.ltrChars_ + '][^' + goog.i18n.bidi.rtlChars_ + ']*$');


/**
 * Regular expressions to check if the last strongly-directional character in a
 * piece of text is RTL.
 * @type {RegExp}
 * @private
 */
goog.i18n.bidi.rtlExitDirCheckRe_ = new RegExp(
    '[' + goog.i18n.bidi.rtlChars_ + '][^' + goog.i18n.bidi.ltrChars_ + ']*$');


/**
 * Check if the exit directionality a piece of text is LTR, i.e. if the last
 * strongly-directional character in the string is LTR.
 * @param {string} str String being checked.
 * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
 *     Default: false.
 * @return {boolean} Whether LTR exit directionality was detected.
 */
goog.i18n.bidi.endsWithLtr = function(str, opt_isHtml) {
  return goog.i18n.bidi.ltrExitDirCheckRe_.test(
      goog.i18n.bidi.stripHtmlIfNeeded_(str, opt_isHtml));
};


/**
 * Check if the exit directionality a piece of text is LTR, i.e. if the last
 * strongly-directional character in the string is LTR.
 * @param {string} str String being checked.
 * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
 *     Default: false.
 * @return {boolean} Whether LTR exit directionality was detected.
 * @deprecated Use endsWithLtr.
 */
goog.i18n.bidi.isLtrExitText = goog.i18n.bidi.endsWithLtr;


/**
 * Check if the exit directionality a piece of text is RTL, i.e. if the last
 * strongly-directional character in the string is RTL.
 * @param {string} str String being checked.
 * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
 *     Default: false.
 * @return {boolean} Whether RTL exit directionality was detected.
 */
goog.i18n.bidi.endsWithRtl = function(str, opt_isHtml) {
  return goog.i18n.bidi.rtlExitDirCheckRe_.test(
      goog.i18n.bidi.stripHtmlIfNeeded_(str, opt_isHtml));
};


/**
 * Check if the exit directionality a piece of text is RTL, i.e. if the last
 * strongly-directional character in the string is RTL.
 * @param {string} str String being checked.
 * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
 *     Default: false.
 * @return {boolean} Whether RTL exit directionality was detected.
 * @deprecated Use endsWithRtl.
 */
goog.i18n.bidi.isRtlExitText = goog.i18n.bidi.endsWithRtl;


/**
 * A regular expression for matching right-to-left language codes.
 * See {@link #isRtlLanguage} for the design.
 * @type {RegExp}
 * @private
 */
goog.i18n.bidi.rtlLocalesRe_ = new RegExp(
    '^(ar|ckb|dv|he|iw|fa|nqo|ps|sd|ug|ur|yi|' +
    '.*[-_](Arab|Hebr|Thaa|Nkoo|Tfng))' +
    '(?!.*[-_](Latn|Cyrl)($|-|_))($|-|_)',
    'i');


/**
 * Check if a BCP 47 / III language code indicates an RTL language, i.e. either:
 * - a language code explicitly specifying one of the right-to-left scripts,
 *   e.g. "az-Arab", or<p>
 * - a language code specifying one of the languages normally written in a
 *   right-to-left script, e.g. "fa" (Farsi), except ones explicitly specifying
 *   Latin or Cyrillic script (which are the usual LTR alternatives).<p>
 * The list of right-to-left scripts appears in the 100-199 range in
 * http://www.unicode.org/iso15924/iso15924-num.html, of which Arabic and
 * Hebrew are by far the most widely used. We also recognize Thaana, N'Ko, and
 * Tifinagh, which also have significant modern usage. The rest (Syriac,
 * Samaritan, Mandaic, etc.) seem to have extremely limited or no modern usage
 * and are not recognized to save on code size.
 * The languages usually written in a right-to-left script are taken as those
 * with Suppress-Script: Hebr|Arab|Thaa|Nkoo|Tfng  in
 * http://www.iana.org/assignments/language-subtag-registry,
 * as well as Central (or Sorani) Kurdish (ckb), Sindhi (sd) and Uyghur (ug).
 * Other subtags of the language code, e.g. regions like EG (Egypt), are
 * ignored.
 * @param {string} lang BCP 47 (a.k.a III) language code.
 * @return {boolean} Whether the language code is an RTL language.
 */
goog.i18n.bidi.isRtlLanguage = function(lang) {
  return goog.i18n.bidi.rtlLocalesRe_.test(lang);
};


/**
 * Regular expression for bracket guard replacement in html.
 * @type {RegExp}
 * @private
 */
goog.i18n.bidi.bracketGuardHtmlRe_ =
    /(\(.*?\)+)|(\[.*?\]+)|(\{.*?\}+)|(&lt;.*?(&gt;)+)/g;


/**
 * Regular expression for bracket guard replacement in text.
 * @type {RegExp}
 * @private
 */
goog.i18n.bidi.bracketGuardTextRe_ =
    /(\(.*?\)+)|(\[.*?\]+)|(\{.*?\}+)|(<.*?>+)/g;


/**
 * Apply bracket guard using html span tag. This is to address the problem of
 * messy bracket display frequently happens in RTL layout.
 * @param {string} s The string that need to be processed.
 * @param {boolean=} opt_isRtlContext specifies default direction (usually
 *     direction of the UI).
 * @return {string} The processed string, with all bracket guarded.
 */
goog.i18n.bidi.guardBracketInHtml = function(s, opt_isRtlContext) {
  var useRtl = opt_isRtlContext === undefined ?
      goog.i18n.bidi.hasAnyRtl(s) : opt_isRtlContext;
  if (useRtl) {
    return s.replace(goog.i18n.bidi.bracketGuardHtmlRe_,
        '<span dir=rtl>$&</span>');
  }
  return s.replace(goog.i18n.bidi.bracketGuardHtmlRe_,
      '<span dir=ltr>$&</span>');
};


/**
 * Apply bracket guard using LRM and RLM. This is to address the problem of
 * messy bracket display frequently happens in RTL layout.
 * This version works for both plain text and html. But it does not work as
 * good as guardBracketInHtml in some cases.
 * @param {string} s The string that need to be processed.
 * @param {boolean=} opt_isRtlContext specifies default direction (usually
 *     direction of the UI).
 * @return {string} The processed string, with all bracket guarded.
 */
goog.i18n.bidi.guardBracketInText = function(s, opt_isRtlContext) {
  var useRtl = opt_isRtlContext === undefined ?
      goog.i18n.bidi.hasAnyRtl(s) : opt_isRtlContext;
  var mark = useRtl ? goog.i18n.bidi.Format.RLM : goog.i18n.bidi.Format.LRM;
  return s.replace(goog.i18n.bidi.bracketGuardTextRe_, mark + '$&' + mark);
};


/**
 * Enforce the html snippet in RTL directionality regardless overall context.
 * If the html piece was enclosed by tag, dir will be applied to existing
 * tag, otherwise a span tag will be added as wrapper. For this reason, if
 * html snippet start with with tag, this tag must enclose the whole piece. If
 * the tag already has a dir specified, this new one will override existing
 * one in behavior (tested on FF and IE).
 * @param {string} html The string that need to be processed.
 * @return {string} The processed string, with directionality enforced to RTL.
 */
goog.i18n.bidi.enforceRtlInHtml = function(html) {
  if (html.charAt(0) == '<') {
    return html.replace(/<\w+/, '$& dir=rtl');
  }
  // '\n' is important for FF so that it won't incorrectly merge span groups
  return '\n<span dir=rtl>' + html + '</span>';
};


/**
 * Enforce RTL on both end of the given text piece using unicode BiDi formatting
 * characters RLE and PDF.
 * @param {string} text The piece of text that need to be wrapped.
 * @return {string} The wrapped string after process.
 */
goog.i18n.bidi.enforceRtlInText = function(text) {
  return goog.i18n.bidi.Format.RLE + text + goog.i18n.bidi.Format.PDF;
};


/**
 * Enforce the html snippet in RTL directionality regardless overall context.
 * If the html piece was enclosed by tag, dir will be applied to existing
 * tag, otherwise a span tag will be added as wrapper. For this reason, if
 * html snippet start with with tag, this tag must enclose the whole piece. If
 * the tag already has a dir specified, this new one will override existing
 * one in behavior (tested on FF and IE).
 * @param {string} html The string that need to be processed.
 * @return {string} The processed string, with directionality enforced to RTL.
 */
goog.i18n.bidi.enforceLtrInHtml = function(html) {
  if (html.charAt(0) == '<') {
    return html.replace(/<\w+/, '$& dir=ltr');
  }
  // '\n' is important for FF so that it won't incorrectly merge span groups
  return '\n<span dir=ltr>' + html + '</span>';
};


/**
 * Enforce LTR on both end of the given text piece using unicode BiDi formatting
 * characters LRE and PDF.
 * @param {string} text The piece of text that need to be wrapped.
 * @return {string} The wrapped string after process.
 */
goog.i18n.bidi.enforceLtrInText = function(text) {
  return goog.i18n.bidi.Format.LRE + text + goog.i18n.bidi.Format.PDF;
};


/**
 * Regular expression to find dimensions such as "padding: .3 0.4ex 5px 6;"
 * @type {RegExp}
 * @private
 */
goog.i18n.bidi.dimensionsRe_ =
    /:\s*([.\d][.\w]*)\s+([.\d][.\w]*)\s+([.\d][.\w]*)\s+([.\d][.\w]*)/g;


/**
 * Regular expression for left.
 * @type {RegExp}
 * @private
 */
goog.i18n.bidi.leftRe_ = /left/gi;


/**
 * Regular expression for right.
 * @type {RegExp}
 * @private
 */
goog.i18n.bidi.rightRe_ = /right/gi;


/**
 * Placeholder regular expression for swapping.
 * @type {RegExp}
 * @private
 */
goog.i18n.bidi.tempRe_ = /%%%%/g;


/**
 * Swap location parameters and 'left'/'right' in CSS specification. The
 * processed string will be suited for RTL layout. Though this function can
 * cover most cases, there are always exceptions. It is suggested to put
 * those exceptions in separate group of CSS string.
 * @param {string} cssStr CSS spefication string.
 * @return {string} Processed CSS specification string.
 */
goog.i18n.bidi.mirrorCSS = function(cssStr) {
  return cssStr.
      // reverse dimensions
      replace(goog.i18n.bidi.dimensionsRe_, ':$1 $4 $3 $2').
      replace(goog.i18n.bidi.leftRe_, '%%%%').          // swap left and right
      replace(goog.i18n.bidi.rightRe_, goog.i18n.bidi.LEFT).
      replace(goog.i18n.bidi.tempRe_, goog.i18n.bidi.RIGHT);
};


/**
 * Regular expression for hebrew double quote substitution, finding quote
 * directly after hebrew characters.
 * @type {RegExp}
 * @private
 */
goog.i18n.bidi.doubleQuoteSubstituteRe_ = /([\u0591-\u05f2])"/g;


/**
 * Regular expression for hebrew single quote substitution, finding quote
 * directly after hebrew characters.
 * @type {RegExp}
 * @private
 */
goog.i18n.bidi.singleQuoteSubstituteRe_ = /([\u0591-\u05f2])'/g;


/**
 * Replace the double and single quote directly after a Hebrew character with
 * GERESH and GERSHAYIM. In such case, most likely that's user intention.
 * @param {string} str String that need to be processed.
 * @return {string} Processed string with double/single quote replaced.
 */
goog.i18n.bidi.normalizeHebrewQuote = function(str) {
  return str.
      replace(goog.i18n.bidi.doubleQuoteSubstituteRe_, '$1\u05f4').
      replace(goog.i18n.bidi.singleQuoteSubstituteRe_, '$1\u05f3');
};


/**
 * Regular expression to split a string into "words" for directionality
 * estimation based on relative word counts.
 * @type {RegExp}
 * @private
 */
goog.i18n.bidi.wordSeparatorRe_ = /\s+/;


/**
 * Regular expression to check if a string contains any numerals. Used to
 * differentiate between completely neutral strings and those containing
 * numbers, which are weakly LTR.
 *
 * Native Arabic digits (\u0660 - \u0669) are not included because although they
 * do flow left-to-right inside a number, this is the case even if the  overall
 * directionality is RTL, and a mathematical expression using these digits is
 * supposed to flow right-to-left overall, including unary plus and minus
 * appearing to the right of a number, and this does depend on the overall
 * directionality being RTL. The digits used in Farsi (\u06F0 - \u06F9), on the
 * other hand, are included, since Farsi math (including unary plus and minus)
 * does flow left-to-right.
 *
 * @type {RegExp}
 * @private
 */
goog.i18n.bidi.hasNumeralsRe_ = /[\d\u06f0-\u06f9]/;


/**
 * This constant controls threshold of RTL directionality.
 * @type {number}
 * @private
 */
goog.i18n.bidi.rtlDetectionThreshold_ = 0.40;


/**
 * Estimates the directionality of a string based on relative word counts.
 * If the number of RTL words is above a certain percentage of the total number
 * of strongly directional words, returns RTL.
 * Otherwise, if any words are strongly or weakly LTR, returns LTR.
 * Otherwise, returns UNKNOWN, which is used to mean "neutral".
 * Numbers are counted as weakly LTR.
 * @param {string} str The string to be checked.
 * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
 *     Default: false.
 * @return {goog.i18n.bidi.Dir} Estimated overall directionality of {@code str}.
 */
goog.i18n.bidi.estimateDirection = function(str, opt_isHtml) {
  var rtlCount = 0;
  var totalCount = 0;
  var hasWeaklyLtr = false;
  var tokens = goog.i18n.bidi.stripHtmlIfNeeded_(str, opt_isHtml).
      split(goog.i18n.bidi.wordSeparatorRe_);
  for (var i = 0; i < tokens.length; i++) {
    var token = tokens[i];
    if (goog.i18n.bidi.startsWithRtl(token)) {
      rtlCount++;
      totalCount++;
    } else if (goog.i18n.bidi.isRequiredLtrRe_.test(token)) {
      hasWeaklyLtr = true;
    } else if (goog.i18n.bidi.hasAnyLtr(token)) {
      totalCount++;
    } else if (goog.i18n.bidi.hasNumeralsRe_.test(token)) {
      hasWeaklyLtr = true;
    }
  }

  return totalCount == 0 ?
      (hasWeaklyLtr ? goog.i18n.bidi.Dir.LTR : goog.i18n.bidi.Dir.NEUTRAL) :
      (rtlCount / totalCount > goog.i18n.bidi.rtlDetectionThreshold_ ?
          goog.i18n.bidi.Dir.RTL : goog.i18n.bidi.Dir.LTR);
};


/**
 * Check the directionality of a piece of text, return true if the piece of
 * text should be laid out in RTL direction.
 * @param {string} str The piece of text that need to be detected.
 * @param {boolean=} opt_isHtml Whether str is HTML / HTML-escaped.
 *     Default: false.
 * @return {boolean} Whether this piece of text should be laid out in RTL.
 */
goog.i18n.bidi.detectRtlDirectionality = function(str, opt_isHtml) {
  return goog.i18n.bidi.estimateDirection(str, opt_isHtml) ==
      goog.i18n.bidi.Dir.RTL;
};


/**
 * Sets text input element's directionality and text alignment based on a
 * given directionality. Does nothing if the given directionality is unknown or
 * neutral.
 * @param {Element} element Input field element to set directionality to.
 * @param {goog.i18n.bidi.Dir|number|boolean|null} dir Desired directionality,
 *     given in one of the following formats:
 *     1. A goog.i18n.bidi.Dir constant.
 *     2. A number (positive = LRT, negative = RTL, 0 = neutral).
 *     3. A boolean (true = RTL, false = LTR).
 *     4. A null for unknown directionality.
 */
goog.i18n.bidi.setElementDirAndAlign = function(element, dir) {
  if (element) {
    dir = goog.i18n.bidi.toDir(dir);
    if (dir) {
      element.style.textAlign =
          dir == goog.i18n.bidi.Dir.RTL ?
          goog.i18n.bidi.RIGHT : goog.i18n.bidi.LEFT;
      element.dir = dir == goog.i18n.bidi.Dir.RTL ? 'rtl' : 'ltr';
    }
  }
};


/**
 * Sets element dir based on estimated directionality of the given text.
 * @param {!Element} element
 * @param {string} text
 */
goog.i18n.bidi.setElementDirByTextDirectionality = function(element, text) {
  switch (goog.i18n.bidi.estimateDirection(text)) {
    case (goog.i18n.bidi.Dir.LTR):
      element.dir = 'ltr';
      break;
    case (goog.i18n.bidi.Dir.RTL):
      element.dir = 'rtl';
      break;
    default:
      // Default for no direction, inherit from document.
      element.removeAttribute('dir');
  }
};



/**
 * Strings that have an (optional) known direction.
 *
 * Implementations of this interface are string-like objects that carry an
 * attached direction, if known.
 * @interface
 */
goog.i18n.bidi.DirectionalString = function() {};


/**
 * Interface marker of the DirectionalString interface.
 *
 * This property can be used to determine at runtime whether or not an object
 * implements this interface.  All implementations of this interface set this
 * property to {@code true}.
 * @type {boolean}
 */
goog.i18n.bidi.DirectionalString.prototype.
    implementsGoogI18nBidiDirectionalString;


/**
 * Retrieves this object's known direction (if any).
 * @return {?goog.i18n.bidi.Dir} The known direction. Null if unknown.
 */
goog.i18n.bidi.DirectionalString.prototype.getDirection;

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview The SafeUrl type and its builders.
 *
 * TODO(xtof): Link to document stating type contract.
 */

goog.provide('goog.html.SafeUrl');

goog.require('goog.asserts');
goog.require('goog.fs.url');
goog.require('goog.i18n.bidi.Dir');
goog.require('goog.i18n.bidi.DirectionalString');
goog.require('goog.string.Const');
goog.require('goog.string.TypedString');



/**
 * A string that is safe to use in URL context in DOM APIs and HTML documents.
 *
 * A SafeUrl is a string-like object that carries the security type contract
 * that its value as a string will not cause untrusted script execution
 * when evaluated as a hyperlink URL in a browser.
 *
 * Values of this type are guaranteed to be safe to use in URL/hyperlink
 * contexts, such as, assignment to URL-valued DOM properties, or
 * interpolation into a HTML template in URL context (e.g., inside a href
 * attribute), in the sense that the use will not result in a
 * Cross-Site-Scripting vulnerability.
 *
 * Note that, as documented in {@code goog.html.SafeUrl.unwrap}, this type's
 * contract does not guarantee that instances are safe to interpolate into HTML
 * without appropriate escaping.
 *
 * Note also that this type's contract does not imply any guarantees regarding
 * the resource the URL refers to.  In particular, SafeUrls are <b>not</b>
 * safe to use in a context where the referred-to resource is interpreted as
 * trusted code, e.g., as the src of a script tag.
 *
 * Instances of this type must be created via the factory methods
 * ({@code goog.html.SafeUrl.fromConstant}, {@code goog.html.SafeUrl.sanitize}),
 * etc and not by invoking its constructor.  The constructor intentionally
 * takes no parameters and the type is immutable; hence only a default instance
 * corresponding to the empty string can be obtained via constructor invocation.
 *
 * @see goog.html.SafeUrl#fromConstant
 * @see goog.html.SafeUrl#from
 * @see goog.html.SafeUrl#sanitize
 * @constructor
 * @final
 * @struct
 * @implements {goog.i18n.bidi.DirectionalString}
 * @implements {goog.string.TypedString}
 */
goog.html.SafeUrl = function() {
  /**
   * The contained value of this SafeUrl.  The field has a purposely ugly
   * name to make (non-compiled) code that attempts to directly access this
   * field stand out.
   * @private {string}
   */
  this.privateDoNotAccessOrElseSafeHtmlWrappedValue_ = '';

  /**
   * A type marker used to implement additional run-time type checking.
   * @see goog.html.SafeUrl#unwrap
   * @const
   * @private
   */
  this.SAFE_URL_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ =
      goog.html.SafeUrl.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_;
};


/**
 * The innocuous string generated by goog.html.SafeUrl.sanitize when passed
 * an unsafe URL.
 *
 * about:invalid is registered in
 * http://www.w3.org/TR/css3-values/#about-invalid.
 * http://tools.ietf.org/html/rfc6694#section-2.2.1 permits about URLs to
 * contain a fragment, which is not to be considered when determining if an
 * about URL is well-known.
 *
 * Using about:invalid seems preferable to using a fixed data URL, since
 * browsers might choose to not report CSP violations on it, as legitimate
 * CSS function calls to attr() can result in this URL being produced. It is
 * also a standard URL which matches exactly the semantics we need:
 * "The about:invalid URI references a non-existent document with a generic
 * error condition. It can be used when a URI is necessary, but the default
 * value shouldn't be resolveable as any type of document".
 *
 * @const {string}
 */
goog.html.SafeUrl.INNOCUOUS_STRING = 'about:invalid#zClosurez';


/**
 * @override
 * @const
 */
goog.html.SafeUrl.prototype.implementsGoogStringTypedString = true;


/**
 * Returns this SafeUrl's value a string.
 *
 * IMPORTANT: In code where it is security relevant that an object's type is
 * indeed {@code SafeUrl}, use {@code goog.html.SafeUrl.unwrap} instead of this
 * method. If in doubt, assume that it's security relevant. In particular, note
 * that goog.html functions which return a goog.html type do not guarantee that
 * the returned instance is of the right type. For example:
 *
 * <pre>
 * var fakeSafeHtml = new String('fake');
 * fakeSafeHtml.__proto__ = goog.html.SafeHtml.prototype;
 * var newSafeHtml = goog.html.SafeHtml.htmlEscape(fakeSafeHtml);
 * // newSafeHtml is just an alias for fakeSafeHtml, it's passed through by
 * // goog.html.SafeHtml.htmlEscape() as fakeSafeHtml instanceof
 * // goog.html.SafeHtml.
 * </pre>
 *
 * IMPORTANT: The guarantees of the SafeUrl type contract only extend to the
 * behavior of browsers when interpreting URLs. Values of SafeUrl objects MUST
 * be appropriately escaped before embedding in a HTML document. Note that the
 * required escaping is context-sensitive (e.g. a different escaping is
 * required for embedding a URL in a style property within a style
 * attribute, as opposed to embedding in a href attribute).
 *
 * @see goog.html.SafeUrl#unwrap
 * @override
 */
goog.html.SafeUrl.prototype.getTypedStringValue = function() {
  return this.privateDoNotAccessOrElseSafeHtmlWrappedValue_;
};


/**
 * @override
 * @const
 */
goog.html.SafeUrl.prototype.implementsGoogI18nBidiDirectionalString = true;


/**
 * Returns this URLs directionality, which is always {@code LTR}.
 * @override
 */
goog.html.SafeUrl.prototype.getDirection = function() {
  return goog.i18n.bidi.Dir.LTR;
};


if (goog.DEBUG) {
  /**
   * Returns a debug string-representation of this value.
   *
   * To obtain the actual string value wrapped in a SafeUrl, use
   * {@code goog.html.SafeUrl.unwrap}.
   *
   * @see goog.html.SafeUrl#unwrap
   * @override
   */
  goog.html.SafeUrl.prototype.toString = function() {
    return 'SafeUrl{' + this.privateDoNotAccessOrElseSafeHtmlWrappedValue_ +
        '}';
  };
}


/**
 * Performs a runtime check that the provided object is indeed a SafeUrl
 * object, and returns its value.
 *
 * IMPORTANT: The guarantees of the SafeUrl type contract only extend to the
 * behavior of  browsers when interpreting URLs. Values of SafeUrl objects MUST
 * be appropriately escaped before embedding in a HTML document. Note that the
 * required escaping is context-sensitive (e.g. a different escaping is
 * required for embedding a URL in a style property within a style
 * attribute, as opposed to embedding in a href attribute).
 *
 * @param {!goog.html.SafeUrl} safeUrl The object to extract from.
 * @return {string} The SafeUrl object's contained string, unless the run-time
 *     type check fails. In that case, {@code unwrap} returns an innocuous
 *     string, or, if assertions are enabled, throws
 *     {@code goog.asserts.AssertionError}.
 */
goog.html.SafeUrl.unwrap = function(safeUrl) {
  // Perform additional Run-time type-checking to ensure that safeUrl is indeed
  // an instance of the expected type.  This provides some additional protection
  // against security bugs due to application code that disables type checks.
  // Specifically, the following checks are performed:
  // 1. The object is an instance of the expected type.
  // 2. The object is not an instance of a subclass.
  // 3. The object carries a type marker for the expected type. "Faking" an
  // object requires a reference to the type marker, which has names intended
  // to stand out in code reviews.
  if (safeUrl instanceof goog.html.SafeUrl &&
      safeUrl.constructor === goog.html.SafeUrl &&
      safeUrl.SAFE_URL_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ ===
          goog.html.SafeUrl.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_) {
    return safeUrl.privateDoNotAccessOrElseSafeHtmlWrappedValue_;
  } else {
    goog.asserts.fail('expected object of type SafeUrl, got \'' +
                      safeUrl + '\'');
    return 'type_error:SafeUrl';

  }
};


/**
 * Creates a SafeUrl object from a compile-time constant string.
 *
 * Compile-time constant strings are inherently program-controlled and hence
 * trusted.
 *
 * @param {!goog.string.Const} url A compile-time-constant string from which to
 *         create a SafeUrl.
 * @return {!goog.html.SafeUrl} A SafeUrl object initialized to {@code url}.
 */
goog.html.SafeUrl.fromConstant = function(url) {
  return goog.html.SafeUrl.createSafeUrlSecurityPrivateDoNotAccessOrElse(
      goog.string.Const.unwrap(url));
};


/**
 * A pattern that matches Blob or data types that can have SafeUrls created
 * from URL.createObjectURL(blob) or via a data: URI.  Only matches image and
 * video types, currently.
 * @const
 * @private
 */
goog.html.SAFE_MIME_TYPE_PATTERN_ =
    /^(?:image\/(?:bmp|gif|jpeg|jpg|png|tiff|webp)|video\/(?:mpeg|mp4|ogg|webm))$/i;


/**
 * Creates a SafeUrl wrapping a blob URL for the given {@code blob}.
 *
 * The blob URL is created with {@code URL.createObjectURL}. If the MIME type
 * for {@code blob} is not of a known safe image or video MIME type, then the
 * SafeUrl will wrap {@link #INNOCUOUS_STRING}.
 *
 * @see http://www.w3.org/TR/FileAPI/#url
 * @param {!Blob} blob
 * @return {!goog.html.SafeUrl} The blob URL, or an innocuous string wrapped
 *   as a SafeUrl.
 */
goog.html.SafeUrl.fromBlob = function(blob) {
  var url = goog.html.SAFE_MIME_TYPE_PATTERN_.test(blob.type) ?
      goog.fs.url.createObjectUrl(blob) : goog.html.SafeUrl.INNOCUOUS_STRING;
  return goog.html.SafeUrl.createSafeUrlSecurityPrivateDoNotAccessOrElse(url);
};


/**
 * Matches a base-64 data URL, with the first match group being the MIME type.
 * @const
 * @private
 */
goog.html.DATA_URL_PATTERN_ = /^data:([^;,]*);base64,[a-z0-9+\/]+=*$/i;


/**
 * Creates a SafeUrl wrapping a data: URL, after validating it matches a
 * known-safe image or video MIME type.
 *
 * @param {string} dataUrl A valid base64 data URL with one of the whitelisted
 *     image or video MIME types.
 * @return {!goog.html.SafeUrl} A matching safe URL, or {@link INNOCUOUS_STRING}
 *     wrapped as a SafeUrl if it does not pass.
 */
goog.html.SafeUrl.fromDataUrl = function(dataUrl) {
  // There's a slight risk here that a browser sniffs the content type if it
  // doesn't know the MIME type and executes HTML within the data: URL. For this
  // to cause XSS it would also have to execute the HTML in the same origin
  // of the page with the link. It seems unlikely that both of these will
  // happen, particularly in not really old IEs.
  var match = dataUrl.match(goog.html.DATA_URL_PATTERN_);
  var valid = match && goog.html.SAFE_MIME_TYPE_PATTERN_.test(match[1]);
  return goog.html.SafeUrl.createSafeUrlSecurityPrivateDoNotAccessOrElse(
      valid ? dataUrl : goog.html.SafeUrl.INNOCUOUS_STRING);
};


/**
 * A pattern that recognizes a commonly useful subset of URLs that satisfy
 * the SafeUrl contract.
 *
 * This regular expression matches a subset of URLs that will not cause script
 * execution if used in URL context within a HTML document. Specifically, this
 * regular expression matches if (comment from here on and regex copied from
 * Soy's EscapingConventions):
 * (1) Either a protocol in a whitelist (http, https, mailto or ftp).
 * (2) or no protocol.  A protocol must be followed by a colon. The below
 *     allows that by allowing colons only after one of the characters [/?#].
 *     A colon after a hash (#) must be in the fragment.
 *     Otherwise, a colon after a (?) must be in a query.
 *     Otherwise, a colon after a single solidus (/) must be in a path.
 *     Otherwise, a colon after a double solidus (//) must be in the authority
 *     (before port).
 *
 * The pattern disallows &, used in HTML entity declarations before
 * one of the characters in [/?#]. This disallows HTML entities used in the
 * protocol name, which should never happen, e.g. "h&#116;tp" for "http".
 * It also disallows HTML entities in the first path part of a relative path,
 * e.g. "foo&lt;bar/baz".  Our existing escaping functions should not produce
 * that. More importantly, it disallows masking of a colon,
 * e.g. "javascript&#58;...".
 *
 * @private
 * @const {!RegExp}
 */
goog.html.SAFE_URL_PATTERN_ =
    /^(?:(?:https?|mailto|ftp):|[^&:/?#]*(?:[/?#]|$))/i;


/**
 * Creates a SafeUrl object from {@code url}. If {@code url} is a
 * goog.html.SafeUrl then it is simply returned. Otherwise the input string is
 * validated to match a pattern of commonly used safe URLs. The string is
 * converted to UTF-8 and non-whitelisted characters are percent-encoded. The
 * string wrapped by the created SafeUrl will thus contain only ASCII printable
 * characters.
 *
 * {@code url} may be a URL with the http, https, mailto or ftp scheme,
 * or a relative URL (i.e., a URL without a scheme; specifically, a
 * scheme-relative, absolute-path-relative, or path-relative URL).
 *
 * {@code url} is converted to UTF-8 and non-whitelisted characters are
 * percent-encoded. Whitelisted characters are '%' and, from RFC 3986,
 * unreserved characters and reserved characters, with the exception of '\'',
 * '(' and ')'. This ensures the the SafeUrl contains only ASCII-printable
 * characters and reduces the chance of security bugs were it to be
 * interpolated into a specific context without the necessary escaping.
 *
 * If {@code url} fails validation or does not UTF-16 decode correctly
 * (JavaScript strings are UTF-16 encoded), this function returns a SafeUrl
 * object containing an innocuous string, goog.html.SafeUrl.INNOCUOUS_STRING.
 *
 * @see http://url.spec.whatwg.org/#concept-relative-url
 * @param {string|!goog.string.TypedString} url The URL to validate.
 * @return {!goog.html.SafeUrl} The validated URL, wrapped as a SafeUrl.
 */
goog.html.SafeUrl.sanitize = function(url) {
  if (url instanceof goog.html.SafeUrl) {
    return url;
  }
  else if (url.implementsGoogStringTypedString) {
    url = url.getTypedStringValue();
  } else {
    url = String(url);
  }
  if (!goog.html.SAFE_URL_PATTERN_.test(url)) {
    url = goog.html.SafeUrl.INNOCUOUS_STRING;
  }
  return goog.html.SafeUrl.createSafeUrlSecurityPrivateDoNotAccessOrElse(url);
};


/**
 * Type marker for the SafeUrl type, used to implement additional run-time
 * type checking.
 * @const {!Object}
 * @private
 */
goog.html.SafeUrl.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ = {};


/**
 * Package-internal utility method to create SafeUrl instances.
 *
 * @param {string} url The string to initialize the SafeUrl object with.
 * @return {!goog.html.SafeUrl} The initialized SafeUrl object.
 * @package
 */
goog.html.SafeUrl.createSafeUrlSecurityPrivateDoNotAccessOrElse = function(
    url) {
  var safeUrl = new goog.html.SafeUrl();
  safeUrl.privateDoNotAccessOrElseSafeHtmlWrappedValue_ = url;
  return safeUrl;
};

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview The TrustedResourceUrl type and its builders.
 *
 * TODO(xtof): Link to document stating type contract.
 */

goog.provide('goog.html.TrustedResourceUrl');

goog.require('goog.asserts');
goog.require('goog.i18n.bidi.Dir');
goog.require('goog.i18n.bidi.DirectionalString');
goog.require('goog.string.Const');
goog.require('goog.string.TypedString');



/**
 * A URL which is under application control and from which script, CSS, and
 * other resources that represent executable code, can be fetched.
 *
 * Given that the URL can only be constructed from strings under application
 * control and is used to load resources, bugs resulting in a malformed URL
 * should not have a security impact and are likely to be easily detectable
 * during testing. Given the wide number of non-RFC compliant URLs in use,
 * stricter validation could prevent some applications from being able to use
 * this type.
 *
 * Instances of this type must be created via the factory method,
 * ({@code goog.html.TrustedResourceUrl.fromConstant}), and not by invoking its
 * constructor. The constructor intentionally takes no parameters and the type
 * is immutable; hence only a default instance corresponding to the empty
 * string can be obtained via constructor invocation.
 *
 * @see goog.html.TrustedResourceUrl#fromConstant
 * @constructor
 * @final
 * @struct
 * @implements {goog.i18n.bidi.DirectionalString}
 * @implements {goog.string.TypedString}
 */
goog.html.TrustedResourceUrl = function() {
  /**
   * The contained value of this TrustedResourceUrl.  The field has a purposely
   * ugly name to make (non-compiled) code that attempts to directly access this
   * field stand out.
   * @private {string}
   */
  this.privateDoNotAccessOrElseTrustedResourceUrlWrappedValue_ = '';

  /**
   * A type marker used to implement additional run-time type checking.
   * @see goog.html.TrustedResourceUrl#unwrap
   * @const
   * @private
   */
  this.TRUSTED_RESOURCE_URL_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ =
      goog.html.TrustedResourceUrl.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_;
};


/**
 * @override
 * @const
 */
goog.html.TrustedResourceUrl.prototype.implementsGoogStringTypedString = true;


/**
 * Returns this TrustedResourceUrl's value as a string.
 *
 * IMPORTANT: In code where it is security relevant that an object's type is
 * indeed {@code TrustedResourceUrl}, use
 * {@code goog.html.TrustedResourceUrl.unwrap} instead of this method. If in
 * doubt, assume that it's security relevant. In particular, note that
 * goog.html functions which return a goog.html type do not guarantee that
 * the returned instance is of the right type. For example:
 *
 * <pre>
 * var fakeSafeHtml = new String('fake');
 * fakeSafeHtml.__proto__ = goog.html.SafeHtml.prototype;
 * var newSafeHtml = goog.html.SafeHtml.htmlEscape(fakeSafeHtml);
 * // newSafeHtml is just an alias for fakeSafeHtml, it's passed through by
 * // goog.html.SafeHtml.htmlEscape() as fakeSafeHtml instanceof
 * // goog.html.SafeHtml.
 * </pre>
 *
 * @see goog.html.TrustedResourceUrl#unwrap
 * @override
 */
goog.html.TrustedResourceUrl.prototype.getTypedStringValue = function() {
  return this.privateDoNotAccessOrElseTrustedResourceUrlWrappedValue_;
};


/**
 * @override
 * @const
 */
goog.html.TrustedResourceUrl.prototype.implementsGoogI18nBidiDirectionalString =
    true;


/**
 * Returns this URLs directionality, which is always {@code LTR}.
 * @override
 */
goog.html.TrustedResourceUrl.prototype.getDirection = function() {
  return goog.i18n.bidi.Dir.LTR;
};


if (goog.DEBUG) {
  /**
   * Returns a debug string-representation of this value.
   *
   * To obtain the actual string value wrapped in a TrustedResourceUrl, use
   * {@code goog.html.TrustedResourceUrl.unwrap}.
   *
   * @see goog.html.TrustedResourceUrl#unwrap
   * @override
   */
  goog.html.TrustedResourceUrl.prototype.toString = function() {
    return 'TrustedResourceUrl{' +
        this.privateDoNotAccessOrElseTrustedResourceUrlWrappedValue_ + '}';
  };
}


/**
 * Performs a runtime check that the provided object is indeed a
 * TrustedResourceUrl object, and returns its value.
 *
 * @param {!goog.html.TrustedResourceUrl} trustedResourceUrl The object to
 *     extract from.
 * @return {string} The trustedResourceUrl object's contained string, unless
 *     the run-time type check fails. In that case, {@code unwrap} returns an
 *     innocuous string, or, if assertions are enabled, throws
 *     {@code goog.asserts.AssertionError}.
 */
goog.html.TrustedResourceUrl.unwrap = function(trustedResourceUrl) {
  // Perform additional Run-time type-checking to ensure that
  // trustedResourceUrl is indeed an instance of the expected type.  This
  // provides some additional protection against security bugs due to
  // application code that disables type checks.
  // Specifically, the following checks are performed:
  // 1. The object is an instance of the expected type.
  // 2. The object is not an instance of a subclass.
  // 3. The object carries a type marker for the expected type. "Faking" an
  // object requires a reference to the type marker, which has names intended
  // to stand out in code reviews.
  if (trustedResourceUrl instanceof goog.html.TrustedResourceUrl &&
      trustedResourceUrl.constructor === goog.html.TrustedResourceUrl &&
      trustedResourceUrl
          .TRUSTED_RESOURCE_URL_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ ===
              goog.html.TrustedResourceUrl
                  .TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_) {
    return trustedResourceUrl
        .privateDoNotAccessOrElseTrustedResourceUrlWrappedValue_;
  } else {
    goog.asserts.fail('expected object of type TrustedResourceUrl, got \'' +
                      trustedResourceUrl + '\'');
    return 'type_error:TrustedResourceUrl';

  }
};


/**
 * Creates a TrustedResourceUrl object from a compile-time constant string.
 *
 * Compile-time constant strings are inherently program-controlled and hence
 * trusted.
 *
 * @param {!goog.string.Const} url A compile-time-constant string from which to
 *     create a TrustedResourceUrl.
 * @return {!goog.html.TrustedResourceUrl} A TrustedResourceUrl object
 *     initialized to {@code url}.
 */
goog.html.TrustedResourceUrl.fromConstant = function(url) {
  return goog.html.TrustedResourceUrl
      .createTrustedResourceUrlSecurityPrivateDoNotAccessOrElse(
          goog.string.Const.unwrap(url));
};


/**
 * Type marker for the TrustedResourceUrl type, used to implement additional
 * run-time type checking.
 * @const {!Object}
 * @private
 */
goog.html.TrustedResourceUrl.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ = {};


/**
 * Package-internal utility method to create TrustedResourceUrl instances.
 *
 * @param {string} url The string to initialize the TrustedResourceUrl object
 *     with.
 * @return {!goog.html.TrustedResourceUrl} The initialized TrustedResourceUrl
 *     object.
 * @package
 */
goog.html.TrustedResourceUrl.
    createTrustedResourceUrlSecurityPrivateDoNotAccessOrElse = function(url) {
  var trustedResourceUrl = new goog.html.TrustedResourceUrl();
  trustedResourceUrl.privateDoNotAccessOrElseTrustedResourceUrlWrappedValue_ =
      url;
  return trustedResourceUrl;
};

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.


/**
 * @fileoverview The SafeHtml type and its builders.
 *
 * TODO(xtof): Link to document stating type contract.
 */

goog.provide('goog.html.SafeHtml');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom.TagName');
goog.require('goog.dom.tags');
goog.require('goog.html.SafeStyle');
goog.require('goog.html.SafeStyleSheet');
goog.require('goog.html.SafeUrl');
goog.require('goog.html.TrustedResourceUrl');
goog.require('goog.i18n.bidi.Dir');
goog.require('goog.i18n.bidi.DirectionalString');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.string.Const');
goog.require('goog.string.TypedString');



/**
 * A string that is safe to use in HTML context in DOM APIs and HTML documents.
 *
 * A SafeHtml is a string-like object that carries the security type contract
 * that its value as a string will not cause untrusted script execution when
 * evaluated as HTML in a browser.
 *
 * Values of this type are guaranteed to be safe to use in HTML contexts,
 * such as, assignment to the innerHTML DOM property, or interpolation into
 * a HTML template in HTML PC_DATA context, in the sense that the use will not
 * result in a Cross-Site-Scripting vulnerability.
 *
 * Instances of this type must be created via the factory methods
 * ({@code goog.html.SafeHtml.create}, {@code goog.html.SafeHtml.htmlEscape}),
 * etc and not by invoking its constructor.  The constructor intentionally
 * takes no parameters and the type is immutable; hence only a default instance
 * corresponding to the empty string can be obtained via constructor invocation.
 *
 * @see goog.html.SafeHtml#create
 * @see goog.html.SafeHtml#htmlEscape
 * @constructor
 * @final
 * @struct
 * @implements {goog.i18n.bidi.DirectionalString}
 * @implements {goog.string.TypedString}
 */
goog.html.SafeHtml = function() {
  /**
   * The contained value of this SafeHtml.  The field has a purposely ugly
   * name to make (non-compiled) code that attempts to directly access this
   * field stand out.
   * @private {string}
   */
  this.privateDoNotAccessOrElseSafeHtmlWrappedValue_ = '';

  /**
   * A type marker used to implement additional run-time type checking.
   * @see goog.html.SafeHtml#unwrap
   * @const
   * @private
   */
  this.SAFE_HTML_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ =
      goog.html.SafeHtml.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_;

  /**
   * This SafeHtml's directionality, or null if unknown.
   * @private {?goog.i18n.bidi.Dir}
   */
  this.dir_ = null;
};


/**
 * @override
 * @const
 */
goog.html.SafeHtml.prototype.implementsGoogI18nBidiDirectionalString = true;


/** @override */
goog.html.SafeHtml.prototype.getDirection = function() {
  return this.dir_;
};


/**
 * @override
 * @const
 */
goog.html.SafeHtml.prototype.implementsGoogStringTypedString = true;


/**
 * Returns this SafeHtml's value a string.
 *
 * IMPORTANT: In code where it is security relevant that an object's type is
 * indeed {@code SafeHtml}, use {@code goog.html.SafeHtml.unwrap} instead of
 * this method. If in doubt, assume that it's security relevant. In particular,
 * note that goog.html functions which return a goog.html type do not guarantee
 * that the returned instance is of the right type. For example:
 *
 * <pre>
 * var fakeSafeHtml = new String('fake');
 * fakeSafeHtml.__proto__ = goog.html.SafeHtml.prototype;
 * var newSafeHtml = goog.html.SafeHtml.htmlEscape(fakeSafeHtml);
 * // newSafeHtml is just an alias for fakeSafeHtml, it's passed through by
 * // goog.html.SafeHtml.htmlEscape() as fakeSafeHtml
 * // instanceof goog.html.SafeHtml.
 * </pre>
 *
 * @see goog.html.SafeHtml#unwrap
 * @override
 */
goog.html.SafeHtml.prototype.getTypedStringValue = function() {
  return this.privateDoNotAccessOrElseSafeHtmlWrappedValue_;
};


if (goog.DEBUG) {
  /**
   * Returns a debug string-representation of this value.
   *
   * To obtain the actual string value wrapped in a SafeHtml, use
   * {@code goog.html.SafeHtml.unwrap}.
   *
   * @see goog.html.SafeHtml#unwrap
   * @override
   */
  goog.html.SafeHtml.prototype.toString = function() {
    return 'SafeHtml{' + this.privateDoNotAccessOrElseSafeHtmlWrappedValue_ +
        '}';
  };
}


/**
 * Performs a runtime check that the provided object is indeed a SafeHtml
 * object, and returns its value.
 * @param {!goog.html.SafeHtml} safeHtml The object to extract from.
 * @return {string} The SafeHtml object's contained string, unless the run-time
 *     type check fails. In that case, {@code unwrap} returns an innocuous
 *     string, or, if assertions are enabled, throws
 *     {@code goog.asserts.AssertionError}.
 */
goog.html.SafeHtml.unwrap = function(safeHtml) {
  // Perform additional run-time type-checking to ensure that safeHtml is indeed
  // an instance of the expected type.  This provides some additional protection
  // against security bugs due to application code that disables type checks.
  // Specifically, the following checks are performed:
  // 1. The object is an instance of the expected type.
  // 2. The object is not an instance of a subclass.
  // 3. The object carries a type marker for the expected type. "Faking" an
  // object requires a reference to the type marker, which has names intended
  // to stand out in code reviews.
  if (safeHtml instanceof goog.html.SafeHtml &&
      safeHtml.constructor === goog.html.SafeHtml &&
      safeHtml.SAFE_HTML_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ ===
          goog.html.SafeHtml.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_) {
    return safeHtml.privateDoNotAccessOrElseSafeHtmlWrappedValue_;
  } else {
    goog.asserts.fail('expected object of type SafeHtml, got \'' +
                      safeHtml + '\'');
    return 'type_error:SafeHtml';
  }
};


/**
 * Shorthand for union of types that can sensibly be converted to strings
 * or might already be SafeHtml (as SafeHtml is a goog.string.TypedString).
 * @private
 * @typedef {string|number|boolean|!goog.string.TypedString|
 *           !goog.i18n.bidi.DirectionalString}
 */
goog.html.SafeHtml.TextOrHtml_;


/**
 * Returns HTML-escaped text as a SafeHtml object.
 *
 * If text is of a type that implements
 * {@code goog.i18n.bidi.DirectionalString}, the directionality of the new
 * {@code SafeHtml} object is set to {@code text}'s directionality, if known.
 * Otherwise, the directionality of the resulting SafeHtml is unknown (i.e.,
 * {@code null}).
 *
 * @param {!goog.html.SafeHtml.TextOrHtml_} textOrHtml The text to escape. If
 *     the parameter is of type SafeHtml it is returned directly (no escaping
 *     is done).
 * @return {!goog.html.SafeHtml} The escaped text, wrapped as a SafeHtml.
 */
goog.html.SafeHtml.htmlEscape = function(textOrHtml) {
  if (textOrHtml instanceof goog.html.SafeHtml) {
    return textOrHtml;
  }
  var dir = null;
  if (textOrHtml.implementsGoogI18nBidiDirectionalString) {
    dir = textOrHtml.getDirection();
  }
  var textAsString;
  if (textOrHtml.implementsGoogStringTypedString) {
    textAsString = textOrHtml.getTypedStringValue();
  } else {
    textAsString = String(textOrHtml);
  }
  return goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
      goog.string.htmlEscape(textAsString), dir);
};


/**
 * Returns HTML-escaped text as a SafeHtml object, with newlines changed to
 * &lt;br&gt;.
 * @param {!goog.html.SafeHtml.TextOrHtml_} textOrHtml The text to escape. If
 *     the parameter is of type SafeHtml it is returned directly (no escaping
 *     is done).
 * @return {!goog.html.SafeHtml} The escaped text, wrapped as a SafeHtml.
 */
goog.html.SafeHtml.htmlEscapePreservingNewlines = function(textOrHtml) {
  if (textOrHtml instanceof goog.html.SafeHtml) {
    return textOrHtml;
  }
  var html = goog.html.SafeHtml.htmlEscape(textOrHtml);
  return goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
      goog.string.newLineToBr(goog.html.SafeHtml.unwrap(html)),
      html.getDirection());
};


/**
 * Returns HTML-escaped text as a SafeHtml object, with newlines changed to
 * &lt;br&gt; and escaping whitespace to preserve spatial formatting. Character
 * entity #160 is used to make it safer for XML.
 * @param {!goog.html.SafeHtml.TextOrHtml_} textOrHtml The text to escape. If
 *     the parameter is of type SafeHtml it is returned directly (no escaping
 *     is done).
 * @return {!goog.html.SafeHtml} The escaped text, wrapped as a SafeHtml.
 */
goog.html.SafeHtml.htmlEscapePreservingNewlinesAndSpaces = function(
    textOrHtml) {
  if (textOrHtml instanceof goog.html.SafeHtml) {
    return textOrHtml;
  }
  var html = goog.html.SafeHtml.htmlEscape(textOrHtml);
  return goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
      goog.string.whitespaceEscape(goog.html.SafeHtml.unwrap(html)),
      html.getDirection());
};


/**
 * Coerces an arbitrary object into a SafeHtml object.
 *
 * If {@code textOrHtml} is already of type {@code goog.html.SafeHtml}, the same
 * object is returned. Otherwise, {@code textOrHtml} is coerced to string, and
 * HTML-escaped. If {@code textOrHtml} is of a type that implements
 * {@code goog.i18n.bidi.DirectionalString}, its directionality, if known, is
 * preserved.
 *
 * @param {!goog.html.SafeHtml.TextOrHtml_} textOrHtml The text or SafeHtml to
 *     coerce.
 * @return {!goog.html.SafeHtml} The resulting SafeHtml object.
 * @deprecated Use goog.html.SafeHtml.htmlEscape.
 */
goog.html.SafeHtml.from = goog.html.SafeHtml.htmlEscape;


/**
 * @const
 * @private
 */
goog.html.SafeHtml.VALID_NAMES_IN_TAG_ = /^[a-zA-Z0-9-]+$/;


/**
 * Set of attributes containing URL as defined at
 * http://www.w3.org/TR/html5/index.html#attributes-1.
 * @private @const {!Object<string,boolean>}
 */
goog.html.SafeHtml.URL_ATTRIBUTES_ = goog.object.createSet('action', 'cite',
    'data', 'formaction', 'href', 'manifest', 'poster', 'src');


/**
 * Tags which are unsupported via create(). They might be supported via a
 * tag-specific create method. These are tags which might require a
 * TrustedResourceUrl in one of their attributes or a restricted type for
 * their content.
 * @private @const {!Object<string,boolean>}
 */
goog.html.SafeHtml.NOT_ALLOWED_TAG_NAMES_ = goog.object.createSet(
    goog.dom.TagName.EMBED, goog.dom.TagName.IFRAME, goog.dom.TagName.LINK,
    goog.dom.TagName.OBJECT, goog.dom.TagName.SCRIPT, goog.dom.TagName.STYLE,
    goog.dom.TagName.TEMPLATE);


/**
 * @typedef {string|number|goog.string.TypedString|
 *     goog.html.SafeStyle.PropertyMap}
 * @private
 */
goog.html.SafeHtml.AttributeValue_;


/**
 * Creates a SafeHtml content consisting of a tag with optional attributes and
 * optional content.
 *
 * For convenience tag names and attribute names are accepted as regular
 * strings, instead of goog.string.Const. Nevertheless, you should not pass
 * user-controlled values to these parameters. Note that these parameters are
 * syntactically validated at runtime, and invalid values will result in
 * an exception.
 *
 * Example usage:
 *
 * goog.html.SafeHtml.create('br');
 * goog.html.SafeHtml.create('div', {'class': 'a'});
 * goog.html.SafeHtml.create('p', {}, 'a');
 * goog.html.SafeHtml.create('p', {}, goog.html.SafeHtml.create('br'));
 *
 * goog.html.SafeHtml.create('span', {
 *   'style': {'margin': '0'}
 * });
 *
 * To guarantee SafeHtml's type contract is upheld there are restrictions on
 * attribute values and tag names.
 *
 * - For attributes which contain script code (on*), a goog.string.Const is
 *   required.
 * - For attributes which contain style (style), a goog.html.SafeStyle or a
 *   goog.html.SafeStyle.PropertyMap is required.
 * - For attributes which are interpreted as URLs (e.g. src, href) a
 *   goog.html.SafeUrl, goog.string.Const or string is required. If a string
 *   is passed, it will be sanitized with SafeUrl.sanitize().
 * - For tags which can load code, more specific goog.html.SafeHtml.create*()
 *   functions must be used. Tags which can load code and are not supported by
 *   this function are embed, iframe, link, object, script, style, and template.
 *
 * @param {string} tagName The name of the tag. Only tag names consisting of
 *     [a-zA-Z0-9-] are allowed. Tag names documented above are disallowed.
 * @param {!Object<string, goog.html.SafeHtml.AttributeValue_>=}
 *     opt_attributes Mapping from attribute names to their values. Only
 *     attribute names consisting of [a-zA-Z0-9-] are allowed. Value of null or
 *     undefined causes the attribute to be omitted.
 * @param {!goog.html.SafeHtml.TextOrHtml_|
 *     !Array<!goog.html.SafeHtml.TextOrHtml_>=} opt_content Content to
 *     HTML-escape and put inside the tag. This must be empty for void tags
 *     like <br>. Array elements are concatenated.
 * @return {!goog.html.SafeHtml} The SafeHtml content with the tag.
 * @throws {Error} If invalid tag name, attribute name, or attribute value is
 *     provided.
 * @throws {goog.asserts.AssertionError} If content for void tag is provided.
 */
goog.html.SafeHtml.create = function(tagName, opt_attributes, opt_content) {
  if (!goog.html.SafeHtml.VALID_NAMES_IN_TAG_.test(tagName)) {
    throw Error('Invalid tag name <' + tagName + '>.');
  }
  if (tagName.toUpperCase() in goog.html.SafeHtml.NOT_ALLOWED_TAG_NAMES_) {
    throw Error('Tag name <' + tagName + '> is not allowed for SafeHtml.');
  }
  return goog.html.SafeHtml.createSafeHtmlTagSecurityPrivateDoNotAccessOrElse(
      tagName, opt_attributes, opt_content);
};


/**
 * Creates a SafeHtml representing an iframe tag.
 *
 * By default the sandbox attribute is set to an empty value, which is the most
 * secure option, as it confers the iframe the least privileges. If this
 * is too restrictive then granting individual privileges is the preferable
 * option. Unsetting the attribute entirely is the least secure option and
 * should never be done unless it's stricly necessary.
 *
 * @param {goog.html.TrustedResourceUrl=} opt_src The value of the src
 *     attribute. If null or undefined src will not be set.
 * @param {goog.html.SafeHtml=} opt_srcdoc The value of the srcdoc attribute.
 *     If null or undefined srcdoc will not be set.
 * @param {!Object<string, goog.html.SafeHtml.AttributeValue_>=}
 *     opt_attributes Mapping from attribute names to their values. Only
 *     attribute names consisting of [a-zA-Z0-9-] are allowed. Value of null or
 *     undefined causes the attribute to be omitted.
 * @param {!goog.html.SafeHtml.TextOrHtml_|
 *     !Array<!goog.html.SafeHtml.TextOrHtml_>=} opt_content Content to
 *     HTML-escape and put inside the tag. Array elements are concatenated.
 * @return {!goog.html.SafeHtml} The SafeHtml content with the tag.
 * @throws {Error} If invalid tag name, attribute name, or attribute value is
 *     provided. If opt_attributes contains the src or srcdoc attributes.
 */
goog.html.SafeHtml.createIframe = function(
    opt_src, opt_srcdoc, opt_attributes, opt_content) {
  var fixedAttributes = {};
  fixedAttributes['src'] = opt_src || null;
  fixedAttributes['srcdoc'] = opt_srcdoc || null;
  var defaultAttributes = {'sandbox': ''};
  var attributes = goog.html.SafeHtml.combineAttributes(
      fixedAttributes, defaultAttributes, opt_attributes);
  return goog.html.SafeHtml.createSafeHtmlTagSecurityPrivateDoNotAccessOrElse(
      'iframe', attributes, opt_content);
};


/**
 * Creates a SafeHtml representing a style tag. The type attribute is set
 * to "text/css".
 * @param {!goog.html.SafeStyleSheet|!Array<!goog.html.SafeStyleSheet>}
 *     styleSheet Content to put inside the tag. Array elements are
 *     concatenated.
 * @param {!Object<string, goog.html.SafeHtml.AttributeValue_>=}
 *     opt_attributes Mapping from attribute names to their values. Only
 *     attribute names consisting of [a-zA-Z0-9-] are allowed. Value of null or
 *     undefined causes the attribute to be omitted.
 * @return {!goog.html.SafeHtml} The SafeHtml content with the tag.
 * @throws {Error} If invalid attribute name or attribute value is provided. If
 *     opt_attributes contains the type attribute.
 */
goog.html.SafeHtml.createStyle = function(styleSheet, opt_attributes) {
  var fixedAttributes = {'type': 'text/css'};
  var defaultAttributes = {};
  var attributes = goog.html.SafeHtml.combineAttributes(
      fixedAttributes, defaultAttributes, opt_attributes);

  var content = '';
  styleSheet = goog.array.concat(styleSheet);
  for (var i = 0; i < styleSheet.length; i++) {
    content += goog.html.SafeStyleSheet.unwrap(styleSheet[i]);
  }
  // Convert to SafeHtml so that it's not HTML-escaped.
  var htmlContent = goog.html.SafeHtml
      .createSafeHtmlSecurityPrivateDoNotAccessOrElse(
          content, goog.i18n.bidi.Dir.NEUTRAL);
  return goog.html.SafeHtml.createSafeHtmlTagSecurityPrivateDoNotAccessOrElse(
      'style', attributes, htmlContent);
};


/**
 * @param {string} tagName The tag name.
 * @param {string} name The attribute name.
 * @param {!goog.html.SafeHtml.AttributeValue_} value The attribute value.
 * @return {string} A "name=value" string.
 * @throws {Error} If attribute value is unsafe for the given tag and attribute.
 * @private
 */
goog.html.SafeHtml.getAttrNameAndValue_ = function(tagName, name, value) {
  // If it's goog.string.Const, allow any valid attribute name.
  if (value instanceof goog.string.Const) {
    value = goog.string.Const.unwrap(value);
  } else if (name.toLowerCase() == 'style') {
    value = goog.html.SafeHtml.getStyleValue_(value);
  } else if (/^on/i.test(name)) {
    // TODO(jakubvrana): Disallow more attributes with a special meaning.
    throw Error('Attribute "' + name +
        '" requires goog.string.Const value, "' + value + '" given.');
  // URL attributes handled differently accroding to tag.
  } else if (name.toLowerCase() in goog.html.SafeHtml.URL_ATTRIBUTES_) {
    if (value instanceof goog.html.TrustedResourceUrl) {
      value = goog.html.TrustedResourceUrl.unwrap(value);
    } else if (value instanceof goog.html.SafeUrl) {
      value = goog.html.SafeUrl.unwrap(value);
    } else if (goog.isString(value)) {
      value = goog.html.SafeUrl.sanitize(value).getTypedStringValue();
    } else {
      throw Error('Attribute "' + name + '" on tag "' + tagName +
          '" requires goog.html.SafeUrl, goog.string.Const, or string,' +
          ' value "' + value + '" given.');
    }
  }

  // Accept SafeUrl, TrustedResourceUrl, etc. for attributes which only require
  // HTML-escaping.
  if (value.implementsGoogStringTypedString) {
    // Ok to call getTypedStringValue() since there's no reliance on the type
    // contract for security here.
    value = value.getTypedStringValue();
  }

  goog.asserts.assert(goog.isString(value) || goog.isNumber(value),
      'String or number value expected, got ' +
      (typeof value) + ' with value: ' + value);
  return name + '="' + goog.string.htmlEscape(String(value)) + '"';
};


/**
 * Gets value allowed in "style" attribute.
 * @param {goog.html.SafeHtml.AttributeValue_} value It could be SafeStyle or a
 *     map which will be passed to goog.html.SafeStyle.create.
 * @return {string} Unwrapped value.
 * @throws {Error} If string value is given.
 * @private
 */
goog.html.SafeHtml.getStyleValue_ = function(value) {
  if (!goog.isObject(value)) {
    throw Error('The "style" attribute requires goog.html.SafeStyle or map ' +
        'of style properties, ' + (typeof value) + ' given: ' + value);
  }
  if (!(value instanceof goog.html.SafeStyle)) {
    // Process the property bag into a style object.
    value = goog.html.SafeStyle.create(value);
  }
  return goog.html.SafeStyle.unwrap(value);
};


/**
 * Creates a SafeHtml content with known directionality consisting of a tag with
 * optional attributes and optional content.
 * @param {!goog.i18n.bidi.Dir} dir Directionality.
 * @param {string} tagName
 * @param {!Object<string, goog.html.SafeHtml.AttributeValue_>=} opt_attributes
 * @param {!goog.html.SafeHtml.TextOrHtml_|
 *     !Array<!goog.html.SafeHtml.TextOrHtml_>=} opt_content
 * @return {!goog.html.SafeHtml} The SafeHtml content with the tag.
 */
goog.html.SafeHtml.createWithDir = function(dir, tagName, opt_attributes,
    opt_content) {
  var html = goog.html.SafeHtml.create(tagName, opt_attributes, opt_content);
  html.dir_ = dir;
  return html;
};


/**
 * Creates a new SafeHtml object by concatenating values.
 * @param {...(!goog.html.SafeHtml.TextOrHtml_|
 *     !Array<!goog.html.SafeHtml.TextOrHtml_>)} var_args Values to concatenate.
 * @return {!goog.html.SafeHtml}
 */
goog.html.SafeHtml.concat = function(var_args) {
  var dir = goog.i18n.bidi.Dir.NEUTRAL;
  var content = '';

  /**
   * @param {!goog.html.SafeHtml.TextOrHtml_|
   *     !Array<!goog.html.SafeHtml.TextOrHtml_>} argument
   */
  var addArgument = function(argument) {
    if (goog.isArray(argument)) {
      goog.array.forEach(argument, addArgument);
    } else {
      var html = goog.html.SafeHtml.htmlEscape(argument);
      content += goog.html.SafeHtml.unwrap(html);
      var htmlDir = html.getDirection();
      if (dir == goog.i18n.bidi.Dir.NEUTRAL) {
        dir = htmlDir;
      } else if (htmlDir != goog.i18n.bidi.Dir.NEUTRAL && dir != htmlDir) {
        dir = null;
      }
    }
  };

  goog.array.forEach(arguments, addArgument);
  return goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
      content, dir);
};


/**
 * Creates a new SafeHtml object with known directionality by concatenating the
 * values.
 * @param {!goog.i18n.bidi.Dir} dir Directionality.
 * @param {...(!goog.html.SafeHtml.TextOrHtml_|
 *     !Array<!goog.html.SafeHtml.TextOrHtml_>)} var_args Elements of array
 *     arguments would be processed recursively.
 * @return {!goog.html.SafeHtml}
 */
goog.html.SafeHtml.concatWithDir = function(dir, var_args) {
  var html = goog.html.SafeHtml.concat(goog.array.slice(arguments, 1));
  html.dir_ = dir;
  return html;
};


/**
 * Type marker for the SafeHtml type, used to implement additional run-time
 * type checking.
 * @const {!Object}
 * @private
 */
goog.html.SafeHtml.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ = {};


/**
 * Package-internal utility method to create SafeHtml instances.
 *
 * @param {string} html The string to initialize the SafeHtml object with.
 * @param {?goog.i18n.bidi.Dir} dir The directionality of the SafeHtml to be
 *     constructed, or null if unknown.
 * @return {!goog.html.SafeHtml} The initialized SafeHtml object.
 * @package
 */
goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse = function(
    html, dir) {
  return new goog.html.SafeHtml().initSecurityPrivateDoNotAccessOrElse_(
      html, dir);
};


/**
 * Called from createSafeHtmlSecurityPrivateDoNotAccessOrElse(). This
 * method exists only so that the compiler can dead code eliminate static
 * fields (like EMPTY) when they're not accessed.
 * @param {string} html
 * @param {?goog.i18n.bidi.Dir} dir
 * @return {!goog.html.SafeHtml}
 * @private
 */
goog.html.SafeHtml.prototype.initSecurityPrivateDoNotAccessOrElse_ = function(
    html, dir) {
  this.privateDoNotAccessOrElseSafeHtmlWrappedValue_ = html;
  this.dir_ = dir;
  return this;
};


/**
 * Like create() but does not restrict which tags can be constructed.
 *
 * @param {string} tagName Tag name. Set or validated by caller.
 * @param {!Object<string, goog.html.SafeHtml.AttributeValue_>=} opt_attributes
 * @param {(!goog.html.SafeHtml.TextOrHtml_|
 *     !Array<!goog.html.SafeHtml.TextOrHtml_>)=} opt_content
 * @return {!goog.html.SafeHtml}
 * @throws {Error} If invalid or unsafe attribute name or value is provided.
 * @throws {goog.asserts.AssertionError} If content for void tag is provided.
 * @package
 */
goog.html.SafeHtml.createSafeHtmlTagSecurityPrivateDoNotAccessOrElse =
    function(tagName, opt_attributes, opt_content) {
  var dir = null;
  var result = '<' + tagName;

  if (opt_attributes) {
    for (var name in opt_attributes) {
      if (!goog.html.SafeHtml.VALID_NAMES_IN_TAG_.test(name)) {
        throw Error('Invalid attribute name "' + name + '".');
      }
      var value = opt_attributes[name];
      if (!goog.isDefAndNotNull(value)) {
        continue;
      }
      result += ' ' +
          goog.html.SafeHtml.getAttrNameAndValue_(tagName, name, value);
    }
  }

  var content = opt_content;
  if (!goog.isDefAndNotNull(content)) {
    content = [];
  } else if (!goog.isArray(content)) {
    content = [content];
  }

  if (goog.dom.tags.isVoidTag(tagName.toLowerCase())) {
    goog.asserts.assert(!content.length,
        'Void tag <' + tagName + '> does not allow content.');
    result += '>';
  } else {
    var html = goog.html.SafeHtml.concat(content);
    result += '>' + goog.html.SafeHtml.unwrap(html) + '</' + tagName + '>';
    dir = html.getDirection();
  }

  var dirAttribute = opt_attributes && opt_attributes['dir'];
  if (dirAttribute) {
    if (/^(ltr|rtl|auto)$/i.test(dirAttribute)) {
      // If the tag has the "dir" attribute specified then its direction is
      // neutral because it can be safely used in any context.
      dir = goog.i18n.bidi.Dir.NEUTRAL;
    } else {
      dir = null;
    }
  }

  return goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
      result, dir);
};


/**
 * @param {!Object<string, string>} fixedAttributes
 * @param {!Object<string, string>} defaultAttributes
 * @param {!Object<string, goog.html.SafeHtml.AttributeValue_>=}
 *     opt_attributes Optional attributes passed to create*().
 * @return {!Object<string, goog.html.SafeHtml.AttributeValue_>}
 * @throws {Error} If opt_attributes contains an attribute with the same name
 *     as an attribute in fixedAttributes.
 * @package
 */
goog.html.SafeHtml.combineAttributes = function(
    fixedAttributes, defaultAttributes, opt_attributes) {
  var combinedAttributes = {};
  var name;

  for (name in fixedAttributes) {
    goog.asserts.assert(name.toLowerCase() == name, 'Must be lower case');
    combinedAttributes[name] = fixedAttributes[name];
  }
  for (name in defaultAttributes) {
    goog.asserts.assert(name.toLowerCase() == name, 'Must be lower case');
    combinedAttributes[name] = defaultAttributes[name];
  }

  for (name in opt_attributes) {
    var nameLower = name.toLowerCase();
    if (nameLower in fixedAttributes) {
      throw Error('Cannot override "' + nameLower + '" attribute, got "' +
          name + '" with value "' + opt_attributes[name] + '"');
    }
    if (nameLower in defaultAttributes) {
      delete combinedAttributes[nameLower];
    }
    combinedAttributes[name] = opt_attributes[name];
  }

  return combinedAttributes;
};


/**
 * A SafeHtml instance corresponding to the HTML doctype: "<!DOCTYPE html>".
 * @const {!goog.html.SafeHtml}
 */
goog.html.SafeHtml.DOCTYPE_HTML =
    goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
        '<!DOCTYPE html>', goog.i18n.bidi.Dir.NEUTRAL);


/**
 * A SafeHtml instance corresponding to the empty string.
 * @const {!goog.html.SafeHtml}
 */
goog.html.SafeHtml.EMPTY =
    goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
        '', goog.i18n.bidi.Dir.NEUTRAL);

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Type-safe wrappers for unsafe DOM APIs.
 *
 * This file provides type-safe wrappers for DOM APIs that can result in
 * cross-site scripting (XSS) vulnerabilities, if the API is supplied with
 * untrusted (attacker-controlled) input.  Instead of plain strings, the type
 * safe wrappers consume values of types from the goog.html package whose
 * contract promises that values are safe to use in the corresponding context.
 *
 * Hence, a program that exclusively uses the wrappers in this file (i.e., whose
 * only reference to security-sensitive raw DOM APIs are in this file) is
 * guaranteed to be free of XSS due to incorrect use of such DOM APIs (modulo
 * correctness of code that produces values of the respective goog.html types,
 * and absent code that violates type safety).
 *
 * For example, assigning to an element's .innerHTML property a string that is
 * derived (even partially) from untrusted input typically results in an XSS
 * vulnerability. The type-safe wrapper goog.html.setInnerHtml consumes a value
 * of type goog.html.SafeHtml, whose contract states that using its values in a
 * HTML context will not result in XSS. Hence a program that is free of direct
 * assignments to any element's innerHTML property (with the exception of the
 * assignment to .innerHTML in this file) is guaranteed to be free of XSS due to
 * assignment of untrusted strings to the innerHTML property.
 */

goog.provide('goog.dom.safe');
goog.provide('goog.dom.safe.InsertAdjacentHtmlPosition');

goog.require('goog.asserts');
goog.require('goog.html.SafeHtml');
goog.require('goog.html.SafeUrl');
goog.require('goog.html.TrustedResourceUrl');
goog.require('goog.string');
goog.require('goog.string.Const');


/** @enum {string} */
goog.dom.safe.InsertAdjacentHtmlPosition = {
  AFTERBEGIN: 'afterbegin',
  AFTEREND: 'afterend',
  BEFOREBEGIN: 'beforebegin',
  BEFOREEND: 'beforeend'
};


/**
 * Inserts known-safe HTML into a Node, at the specified position.
 * @param {!Node} node The node on which to call insertAdjacentHTML.
 * @param {!goog.dom.safe.InsertAdjacentHtmlPosition} position Position where
 *     to insert the HTML.
 * @param {!goog.html.SafeHtml} html The known-safe HTML to insert.
 */
goog.dom.safe.insertAdjacentHtml = function(node, position, html) {
  node.insertAdjacentHTML(position, goog.html.SafeHtml.unwrap(html));
};


/**
 * Assigns known-safe HTML to an element's innerHTML property.
 * @param {!Element} elem The element whose innerHTML is to be assigned to.
 * @param {!goog.html.SafeHtml} html The known-safe HTML to assign.
 */
goog.dom.safe.setInnerHtml = function(elem, html) {
  elem.innerHTML = goog.html.SafeHtml.unwrap(html);
};


/**
 * Assigns known-safe HTML to an element's outerHTML property.
 * @param {!Element} elem The element whose outerHTML is to be assigned to.
 * @param {!goog.html.SafeHtml} html The known-safe HTML to assign.
 */
goog.dom.safe.setOuterHtml = function(elem, html) {
  elem.outerHTML = goog.html.SafeHtml.unwrap(html);
};


/**
 * Writes known-safe HTML to a document.
 * @param {!Document} doc The document to be written to.
 * @param {!goog.html.SafeHtml} html The known-safe HTML to assign.
 */
goog.dom.safe.documentWrite = function(doc, html) {
  doc.write(goog.html.SafeHtml.unwrap(html));
};


/**
 * Safely assigns a URL to an anchor element's href property.
 *
 * If url is of type goog.html.SafeUrl, its value is unwrapped and assigned to
 * anchor's href property.  If url is of type string however, it is first
 * sanitized using goog.html.SafeUrl.sanitize.
 *
 * Example usage:
 *   goog.dom.safe.setAnchorHref(anchorEl, url);
 * which is a safe alternative to
 *   anchorEl.href = url;
 * The latter can result in XSS vulnerabilities if url is a
 * user-/attacker-controlled value.
 *
 * @param {!HTMLAnchorElement} anchor The anchor element whose href property
 *     is to be assigned to.
 * @param {string|!goog.html.SafeUrl} url The URL to assign.
 * @see goog.html.SafeUrl#sanitize
 */
goog.dom.safe.setAnchorHref = function(anchor, url) {
  /** @type {!goog.html.SafeUrl} */
  var safeUrl;
  if (url instanceof goog.html.SafeUrl) {
    safeUrl = url;
  } else {
    safeUrl = goog.html.SafeUrl.sanitize(url);
  }
  anchor.href = goog.html.SafeUrl.unwrap(safeUrl);
};


/**
 * Safely assigns a URL to an embed element's src property.
 *
 * Example usage:
 *   goog.dom.safe.setEmbedSrc(embedEl, url);
 * which is a safe alternative to
 *   embedEl.src = url;
 * The latter can result in loading untrusted code unless it is ensured that
 * the URL refers to a trustworthy resource.
 *
 * @param {!HTMLEmbedElement} embed The embed element whose src property
 *     is to be assigned to.
 * @param {!goog.html.TrustedResourceUrl} url The URL to assign.
 */
goog.dom.safe.setEmbedSrc = function(embed, url) {
  embed.src = goog.html.TrustedResourceUrl.unwrap(url);
};


/**
 * Safely assigns a URL to a frame element's src property.
 *
 * Example usage:
 *   goog.dom.safe.setFrameSrc(frameEl, url);
 * which is a safe alternative to
 *   frameEl.src = url;
 * The latter can result in loading untrusted code unless it is ensured that
 * the URL refers to a trustworthy resource.
 *
 * @param {!HTMLFrameElement} frame The frame element whose src property
 *     is to be assigned to.
 * @param {!goog.html.TrustedResourceUrl} url The URL to assign.
 */
goog.dom.safe.setFrameSrc = function(frame, url) {
  frame.src = goog.html.TrustedResourceUrl.unwrap(url);
};


/**
 * Safely assigns a URL to an iframe element's src property.
 *
 * Example usage:
 *   goog.dom.safe.setIframeSrc(iframeEl, url);
 * which is a safe alternative to
 *   iframeEl.src = url;
 * The latter can result in loading untrusted code unless it is ensured that
 * the URL refers to a trustworthy resource.
 *
 * @param {!HTMLIFrameElement} iframe The iframe element whose src property
 *     is to be assigned to.
 * @param {!goog.html.TrustedResourceUrl} url The URL to assign.
 */
goog.dom.safe.setIframeSrc = function(iframe, url) {
  iframe.src = goog.html.TrustedResourceUrl.unwrap(url);
};


/**
 * Safely sets a link element's href and rel properties. Whether or not
 * the URL assigned to href has to be a goog.html.TrustedResourceUrl
 * depends on the value of the rel property. If rel contains "stylesheet"
 * then a TrustedResourceUrl is required.
 *
 * Example usage:
 *   goog.dom.safe.setLinkHrefAndRel(linkEl, url, 'stylesheet');
 * which is a safe alternative to
 *   linkEl.rel = 'stylesheet';
 *   linkEl.href = url;
 * The latter can result in loading untrusted code unless it is ensured that
 * the URL refers to a trustworthy resource.
 *
 * @param {!HTMLLinkElement} link The link element whose href property
 *     is to be assigned to.
 * @param {string|!goog.html.SafeUrl|!goog.html.TrustedResourceUrl} url The URL
 *     to assign to the href property. Must be a TrustedResourceUrl if the
 *     value assigned to rel contains "stylesheet". A string value is
 *     sanitized with goog.html.SafeUrl.sanitize.
 * @param {string} rel The value to assign to the rel property.
 * @throws {Error} if rel contains "stylesheet" and url is not a
 *     TrustedResourceUrl
 * @see goog.html.SafeUrl#sanitize
 */
goog.dom.safe.setLinkHrefAndRel = function(link, url, rel) {
  link.rel = rel;
  if (goog.string.caseInsensitiveContains(rel, 'stylesheet')) {
    goog.asserts.assert(
        url instanceof goog.html.TrustedResourceUrl,
        'URL must be TrustedResourceUrl because "rel" contains "stylesheet"');
    link.href = goog.html.TrustedResourceUrl.unwrap(url);
  } else if (url instanceof goog.html.TrustedResourceUrl) {
    link.href = goog.html.TrustedResourceUrl.unwrap(url);
  } else if (url instanceof goog.html.SafeUrl) {
    link.href = goog.html.SafeUrl.unwrap(url);
  } else {  // string
    // SafeUrl.sanitize must return legitimate SafeUrl when passed a string.
    link.href = goog.html.SafeUrl.sanitize(url).getTypedStringValue();
  }
};


/**
 * Safely assigns a URL to an object element's data property.
 *
 * Example usage:
 *   goog.dom.safe.setObjectData(objectEl, url);
 * which is a safe alternative to
 *   objectEl.data = url;
 * The latter can result in loading untrusted code unless setit is ensured that
 * the URL refers to a trustworthy resource.
 *
 * @param {!HTMLObjectElement} object The object element whose data property
 *     is to be assigned to.
 * @param {!goog.html.TrustedResourceUrl} url The URL to assign.
 */
goog.dom.safe.setObjectData = function(object, url) {
  object.data = goog.html.TrustedResourceUrl.unwrap(url);
};


/**
 * Safely assigns a URL to an iframe element's src property.
 *
 * Example usage:
 *   goog.dom.safe.setScriptSrc(scriptEl, url);
 * which is a safe alternative to
 *   scriptEl.src = url;
 * The latter can result in loading untrusted code unless it is ensured that
 * the URL refers to a trustworthy resource.
 *
 * @param {!HTMLScriptElement} script The script element whose src property
 *     is to be assigned to.
 * @param {!goog.html.TrustedResourceUrl} url The URL to assign.
 */
goog.dom.safe.setScriptSrc = function(script, url) {
  script.src = goog.html.TrustedResourceUrl.unwrap(url);
};


/**
 * Safely assigns a URL to a Location object's href property.
 *
 * If url is of type goog.html.SafeUrl, its value is unwrapped and assigned to
 * loc's href property.  If url is of type string however, it is first sanitized
 * using goog.html.SafeUrl.sanitize.
 *
 * Example usage:
 *   goog.dom.safe.setLocationHref(document.location, redirectUrl);
 * which is a safe alternative to
 *   document.location.href = redirectUrl;
 * The latter can result in XSS vulnerabilities if redirectUrl is a
 * user-/attacker-controlled value.
 *
 * @param {!Location} loc The Location object whose href property is to be
 *     assigned to.
 * @param {string|!goog.html.SafeUrl} url The URL to assign.
 * @see goog.html.SafeUrl#sanitize
 */
goog.dom.safe.setLocationHref = function(loc, url) {
  /** @type {!goog.html.SafeUrl} */
  var safeUrl;
  if (url instanceof goog.html.SafeUrl) {
    safeUrl = url;
  } else {
    safeUrl = goog.html.SafeUrl.sanitize(url);
  }
  loc.href = goog.html.SafeUrl.unwrap(safeUrl);
};


/**
 * Safely opens a URL in a new window (via window.open).
 *
 * If url is of type goog.html.SafeUrl, its value is unwrapped and passed in to
 * window.open.  If url is of type string however, it is first sanitized
 * using goog.html.SafeUrl.sanitize.
 *
 * Note that this function does not prevent leakages via the referer that is
 * sent by window.open. It is advised to only use this to open 1st party URLs.
 *
 * Example usage:
 *   goog.dom.safe.openInWindow(url);
 * which is a safe alternative to
 *   window.open(url);
 * The latter can result in XSS vulnerabilities if redirectUrl is a
 * user-/attacker-controlled value.
 *
 * @param {string|!goog.html.SafeUrl} url The URL to open.
 * @param {Window=} opt_openerWin Window of which to call the .open() method.
 *     Defaults to the global window.
 * @param {!goog.string.Const=} opt_name Name of the window to open in. Can be
 *     _top, etc as allowed by window.open().
 * @param {string=} opt_specs Comma-separated list of specifications, same as
 *     in window.open().
 * @param {boolean=} opt_replace Whether to replace the current entry in browser
 *     history, same as in window.open().
 * @return {Window} Window the url was opened in.
 */
goog.dom.safe.openInWindow = function(
    url, opt_openerWin, opt_name, opt_specs, opt_replace) {
  /** @type {!goog.html.SafeUrl} */
  var safeUrl;
  if (url instanceof goog.html.SafeUrl) {
    safeUrl = url;
  } else {
    safeUrl = goog.html.SafeUrl.sanitize(url);
  }
  var win = opt_openerWin || window;
  return win.open(goog.html.SafeUrl.unwrap(safeUrl),
      // If opt_name is undefined, simply passing that in to open() causes IE to
      // reuse the current window instead of opening a new one. Thus we pass ''
      // in instead, which according to spec opens a new window. See
      // https://html.spec.whatwg.org/multipage/browsers.html#dom-open .
      opt_name ? goog.string.Const.unwrap(opt_name) : '',
      opt_specs, opt_replace);
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview A utility class for representing two-dimensional positions.
 */


goog.provide('goog.math.Coordinate');

goog.require('goog.math');



/**
 * Class for representing coordinates and positions.
 * @param {number=} opt_x Left, defaults to 0.
 * @param {number=} opt_y Top, defaults to 0.
 * @struct
 * @constructor
 */
goog.math.Coordinate = function(opt_x, opt_y) {
  /**
   * X-value
   * @type {number}
   */
  this.x = goog.isDef(opt_x) ? opt_x : 0;

  /**
   * Y-value
   * @type {number}
   */
  this.y = goog.isDef(opt_y) ? opt_y : 0;
};


/**
 * Returns a new copy of the coordinate.
 * @return {!goog.math.Coordinate} A clone of this coordinate.
 */
goog.math.Coordinate.prototype.clone = function() {
  return new goog.math.Coordinate(this.x, this.y);
};


if (goog.DEBUG) {
  /**
   * Returns a nice string representing the coordinate.
   * @return {string} In the form (50, 73).
   * @override
   */
  goog.math.Coordinate.prototype.toString = function() {
    return '(' + this.x + ', ' + this.y + ')';
  };
}


/**
 * Compares coordinates for equality.
 * @param {goog.math.Coordinate} a A Coordinate.
 * @param {goog.math.Coordinate} b A Coordinate.
 * @return {boolean} True iff the coordinates are equal, or if both are null.
 */
goog.math.Coordinate.equals = function(a, b) {
  if (a == b) {
    return true;
  }
  if (!a || !b) {
    return false;
  }
  return a.x == b.x && a.y == b.y;
};


/**
 * Returns the distance between two coordinates.
 * @param {!goog.math.Coordinate} a A Coordinate.
 * @param {!goog.math.Coordinate} b A Coordinate.
 * @return {number} The distance between {@code a} and {@code b}.
 */
goog.math.Coordinate.distance = function(a, b) {
  var dx = a.x - b.x;
  var dy = a.y - b.y;
  return Math.sqrt(dx * dx + dy * dy);
};


/**
 * Returns the magnitude of a coordinate.
 * @param {!goog.math.Coordinate} a A Coordinate.
 * @return {number} The distance between the origin and {@code a}.
 */
goog.math.Coordinate.magnitude = function(a) {
  return Math.sqrt(a.x * a.x + a.y * a.y);
};


/**
 * Returns the angle from the origin to a coordinate.
 * @param {!goog.math.Coordinate} a A Coordinate.
 * @return {number} The angle, in degrees, clockwise from the positive X
 *     axis to {@code a}.
 */
goog.math.Coordinate.azimuth = function(a) {
  return goog.math.angle(0, 0, a.x, a.y);
};


/**
 * Returns the squared distance between two coordinates. Squared distances can
 * be used for comparisons when the actual value is not required.
 *
 * Performance note: eliminating the square root is an optimization often used
 * in lower-level languages, but the speed difference is not nearly as
 * pronounced in JavaScript (only a few percent.)
 *
 * @param {!goog.math.Coordinate} a A Coordinate.
 * @param {!goog.math.Coordinate} b A Coordinate.
 * @return {number} The squared distance between {@code a} and {@code b}.
 */
goog.math.Coordinate.squaredDistance = function(a, b) {
  var dx = a.x - b.x;
  var dy = a.y - b.y;
  return dx * dx + dy * dy;
};


/**
 * Returns the difference between two coordinates as a new
 * goog.math.Coordinate.
 * @param {!goog.math.Coordinate} a A Coordinate.
 * @param {!goog.math.Coordinate} b A Coordinate.
 * @return {!goog.math.Coordinate} A Coordinate representing the difference
 *     between {@code a} and {@code b}.
 */
goog.math.Coordinate.difference = function(a, b) {
  return new goog.math.Coordinate(a.x - b.x, a.y - b.y);
};


/**
 * Returns the sum of two coordinates as a new goog.math.Coordinate.
 * @param {!goog.math.Coordinate} a A Coordinate.
 * @param {!goog.math.Coordinate} b A Coordinate.
 * @return {!goog.math.Coordinate} A Coordinate representing the sum of the two
 *     coordinates.
 */
goog.math.Coordinate.sum = function(a, b) {
  return new goog.math.Coordinate(a.x + b.x, a.y + b.y);
};


/**
 * Rounds the x and y fields to the next larger integer values.
 * @return {!goog.math.Coordinate} This coordinate with ceil'd fields.
 */
goog.math.Coordinate.prototype.ceil = function() {
  this.x = Math.ceil(this.x);
  this.y = Math.ceil(this.y);
  return this;
};


/**
 * Rounds the x and y fields to the next smaller integer values.
 * @return {!goog.math.Coordinate} This coordinate with floored fields.
 */
goog.math.Coordinate.prototype.floor = function() {
  this.x = Math.floor(this.x);
  this.y = Math.floor(this.y);
  return this;
};


/**
 * Rounds the x and y fields to the nearest integer values.
 * @return {!goog.math.Coordinate} This coordinate with rounded fields.
 */
goog.math.Coordinate.prototype.round = function() {
  this.x = Math.round(this.x);
  this.y = Math.round(this.y);
  return this;
};


/**
 * Translates this box by the given offsets. If a {@code goog.math.Coordinate}
 * is given, then the x and y values are translated by the coordinate's x and y.
 * Otherwise, x and y are translated by {@code tx} and {@code opt_ty}
 * respectively.
 * @param {number|goog.math.Coordinate} tx The value to translate x by or the
 *     the coordinate to translate this coordinate by.
 * @param {number=} opt_ty The value to translate y by.
 * @return {!goog.math.Coordinate} This coordinate after translating.
 */
goog.math.Coordinate.prototype.translate = function(tx, opt_ty) {
  if (tx instanceof goog.math.Coordinate) {
    this.x += tx.x;
    this.y += tx.y;
  } else {
    this.x += tx;
    if (goog.isNumber(opt_ty)) {
      this.y += opt_ty;
    }
  }
  return this;
};


/**
 * Scales this coordinate by the given scale factors. The x and y values are
 * scaled by {@code sx} and {@code opt_sy} respectively.  If {@code opt_sy}
 * is not given, then {@code sx} is used for both x and y.
 * @param {number} sx The scale factor to use for the x dimension.
 * @param {number=} opt_sy The scale factor to use for the y dimension.
 * @return {!goog.math.Coordinate} This coordinate after scaling.
 */
goog.math.Coordinate.prototype.scale = function(sx, opt_sy) {
  var sy = goog.isNumber(opt_sy) ? opt_sy : sx;
  this.x *= sx;
  this.y *= sy;
  return this;
};


/**
 * Rotates this coordinate clockwise about the origin (or, optionally, the given
 * center) by the given angle, in radians.
 * @param {number} radians The angle by which to rotate this coordinate
 *     clockwise about the given center, in radians.
 * @param {!goog.math.Coordinate=} opt_center The center of rotation. Defaults
 *     to (0, 0) if not given.
 */
goog.math.Coordinate.prototype.rotateRadians = function(radians, opt_center) {
  var center = opt_center || new goog.math.Coordinate(0, 0);

  var x = this.x;
  var y = this.y;
  var cos = Math.cos(radians);
  var sin = Math.sin(radians);

  this.x = (x - center.x) * cos - (y - center.y) * sin + center.x;
  this.y = (x - center.x) * sin + (y - center.y) * cos + center.y;
};


/**
 * Rotates this coordinate clockwise about the origin (or, optionally, the given
 * center) by the given angle, in degrees.
 * @param {number} degrees The angle by which to rotate this coordinate
 *     clockwise about the given center, in degrees.
 * @param {!goog.math.Coordinate=} opt_center The center of rotation. Defaults
 *     to (0, 0) if not given.
 */
goog.math.Coordinate.prototype.rotateDegrees = function(degrees, opt_center) {
  this.rotateRadians(goog.math.toRadians(degrees), opt_center);
};

// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview A utility class for representing two-dimensional sizes.
 * @author brenneman@google.com (Shawn Brenneman)
 */


goog.provide('goog.math.Size');



/**
 * Class for representing sizes consisting of a width and height. Undefined
 * width and height support is deprecated and results in compiler warning.
 * @param {number} width Width.
 * @param {number} height Height.
 * @struct
 * @constructor
 */
goog.math.Size = function(width, height) {
  /**
   * Width
   * @type {number}
   */
  this.width = width;

  /**
   * Height
   * @type {number}
   */
  this.height = height;
};


/**
 * Compares sizes for equality.
 * @param {goog.math.Size} a A Size.
 * @param {goog.math.Size} b A Size.
 * @return {boolean} True iff the sizes have equal widths and equal
 *     heights, or if both are null.
 */
goog.math.Size.equals = function(a, b) {
  if (a == b) {
    return true;
  }
  if (!a || !b) {
    return false;
  }
  return a.width == b.width && a.height == b.height;
};


/**
 * @return {!goog.math.Size} A new copy of the Size.
 */
goog.math.Size.prototype.clone = function() {
  return new goog.math.Size(this.width, this.height);
};


if (goog.DEBUG) {
  /**
   * Returns a nice string representing size.
   * @return {string} In the form (50 x 73).
   * @override
   */
  goog.math.Size.prototype.toString = function() {
    return '(' + this.width + ' x ' + this.height + ')';
  };
}


/**
 * @return {number} The longer of the two dimensions in the size.
 */
goog.math.Size.prototype.getLongest = function() {
  return Math.max(this.width, this.height);
};


/**
 * @return {number} The shorter of the two dimensions in the size.
 */
goog.math.Size.prototype.getShortest = function() {
  return Math.min(this.width, this.height);
};


/**
 * @return {number} The area of the size (width * height).
 */
goog.math.Size.prototype.area = function() {
  return this.width * this.height;
};


/**
 * @return {number} The perimeter of the size (width + height) * 2.
 */
goog.math.Size.prototype.perimeter = function() {
  return (this.width + this.height) * 2;
};


/**
 * @return {number} The ratio of the size's width to its height.
 */
goog.math.Size.prototype.aspectRatio = function() {
  return this.width / this.height;
};


/**
 * @return {boolean} True if the size has zero area, false if both dimensions
 *     are non-zero numbers.
 */
goog.math.Size.prototype.isEmpty = function() {
  return !this.area();
};


/**
 * Clamps the width and height parameters upward to integer values.
 * @return {!goog.math.Size} This size with ceil'd components.
 */
goog.math.Size.prototype.ceil = function() {
  this.width = Math.ceil(this.width);
  this.height = Math.ceil(this.height);
  return this;
};


/**
 * @param {!goog.math.Size} target The target size.
 * @return {boolean} True if this Size is the same size or smaller than the
 *     target size in both dimensions.
 */
goog.math.Size.prototype.fitsInside = function(target) {
  return this.width <= target.width && this.height <= target.height;
};


/**
 * Clamps the width and height parameters downward to integer values.
 * @return {!goog.math.Size} This size with floored components.
 */
goog.math.Size.prototype.floor = function() {
  this.width = Math.floor(this.width);
  this.height = Math.floor(this.height);
  return this;
};


/**
 * Rounds the width and height parameters to integer values.
 * @return {!goog.math.Size} This size with rounded components.
 */
goog.math.Size.prototype.round = function() {
  this.width = Math.round(this.width);
  this.height = Math.round(this.height);
  return this;
};


/**
 * Scales this size by the given scale factors. The width and height are scaled
 * by {@code sx} and {@code opt_sy} respectively.  If {@code opt_sy} is not
 * given, then {@code sx} is used for both the width and height.
 * @param {number} sx The scale factor to use for the width.
 * @param {number=} opt_sy The scale factor to use for the height.
 * @return {!goog.math.Size} This Size object after scaling.
 */
goog.math.Size.prototype.scale = function(sx, opt_sy) {
  var sy = goog.isNumber(opt_sy) ? opt_sy : sx;
  this.width *= sx;
  this.height *= sy;
  return this;
};


/**
 * Uniformly scales the size to perfectly cover the dimensions of a given size.
 * If the size is already larger than the target, it will be scaled down to the
 * minimum size at which it still covers the entire target. The original aspect
 * ratio will be preserved.
 *
 * This function assumes that both Sizes contain strictly positive dimensions.
 * @param {!goog.math.Size} target The target size.
 * @return {!goog.math.Size} This Size object, after optional scaling.
 */
goog.math.Size.prototype.scaleToCover = function(target) {
  var s = this.aspectRatio() <= target.aspectRatio() ?
      target.width / this.width :
      target.height / this.height;

  return this.scale(s);
};


/**
 * Uniformly scales the size to fit inside the dimensions of a given size. The
 * original aspect ratio will be preserved.
 *
 * This function assumes that both Sizes contain strictly positive dimensions.
 * @param {!goog.math.Size} target The target size.
 * @return {!goog.math.Size} This Size object, after optional scaling.
 */
goog.math.Size.prototype.scaleToFit = function(target) {
  var s = this.aspectRatio() > target.aspectRatio() ?
      target.width / this.width :
      target.height / this.height;

  return this.scale(s);
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Utilities for manipulating the browser's Document Object Model
 * Inspiration taken *heavily* from mochikit (http://mochikit.com/).
 *
 * You can use {@link goog.dom.DomHelper} to create new dom helpers that refer
 * to a different document object.  This is useful if you are working with
 * frames or multiple windows.
 *
 * @author arv@google.com (Erik Arvidsson)
 */


// TODO(arv): Rename/refactor getTextContent and getRawTextContent. The problem
// is that getTextContent should mimic the DOM3 textContent. We should add a
// getInnerText (or getText) which tries to return the visible text, innerText.


goog.provide('goog.dom');
goog.provide('goog.dom.Appendable');
goog.provide('goog.dom.DomHelper');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom.BrowserFeature');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.TagName');
goog.require('goog.dom.safe');
goog.require('goog.html.SafeHtml');
goog.require('goog.math.Coordinate');
goog.require('goog.math.Size');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.string.Unicode');
goog.require('goog.userAgent');


/**
 * @define {boolean} Whether we know at compile time that the browser is in
 * quirks mode.
 */
goog.define('goog.dom.ASSUME_QUIRKS_MODE', false);


/**
 * @define {boolean} Whether we know at compile time that the browser is in
 * standards compliance mode.
 */
goog.define('goog.dom.ASSUME_STANDARDS_MODE', false);


/**
 * Whether we know the compatibility mode at compile time.
 * @type {boolean}
 * @private
 */
goog.dom.COMPAT_MODE_KNOWN_ =
    goog.dom.ASSUME_QUIRKS_MODE || goog.dom.ASSUME_STANDARDS_MODE;


/**
 * Gets the DomHelper object for the document where the element resides.
 * @param {(Node|Window)=} opt_element If present, gets the DomHelper for this
 *     element.
 * @return {!goog.dom.DomHelper} The DomHelper.
 */
goog.dom.getDomHelper = function(opt_element) {
  return opt_element ?
      new goog.dom.DomHelper(goog.dom.getOwnerDocument(opt_element)) :
      (goog.dom.defaultDomHelper_ ||
          (goog.dom.defaultDomHelper_ = new goog.dom.DomHelper()));
};


/**
 * Cached default DOM helper.
 * @type {goog.dom.DomHelper}
 * @private
 */
goog.dom.defaultDomHelper_;


/**
 * Gets the document object being used by the dom library.
 * @return {!Document} Document object.
 */
goog.dom.getDocument = function() {
  return document;
};


/**
 * Gets an element from the current document by element id.
 *
 * If an Element is passed in, it is returned.
 *
 * @param {string|Element} element Element ID or a DOM node.
 * @return {Element} The element with the given ID, or the node passed in.
 */
goog.dom.getElement = function(element) {
  return goog.dom.getElementHelper_(document, element);
};


/**
 * Gets an element by id from the given document (if present).
 * If an element is given, it is returned.
 * @param {!Document} doc
 * @param {string|Element} element Element ID or a DOM node.
 * @return {Element} The resulting element.
 * @private
 */
goog.dom.getElementHelper_ = function(doc, element) {
  return goog.isString(element) ?
      doc.getElementById(element) :
      element;
};


/**
 * Gets an element by id, asserting that the element is found.
 *
 * This is used when an element is expected to exist, and should fail with
 * an assertion error if it does not (if assertions are enabled).
 *
 * @param {string} id Element ID.
 * @return {!Element} The element with the given ID, if it exists.
 */
goog.dom.getRequiredElement = function(id) {
  return goog.dom.getRequiredElementHelper_(document, id);
};


/**
 * Helper function for getRequiredElementHelper functions, both static and
 * on DomHelper.  Asserts the element with the given id exists.
 * @param {!Document} doc
 * @param {string} id
 * @return {!Element} The element with the given ID, if it exists.
 * @private
 */
goog.dom.getRequiredElementHelper_ = function(doc, id) {
  // To prevent users passing in Elements as is permitted in getElement().
  goog.asserts.assertString(id);
  var element = goog.dom.getElementHelper_(doc, id);
  element = goog.asserts.assertElement(element,
      'No element found with id: ' + id);
  return element;
};


/**
 * Alias for getElement.
 * @param {string|Element} element Element ID or a DOM node.
 * @return {Element} The element with the given ID, or the node passed in.
 * @deprecated Use {@link goog.dom.getElement} instead.
 */
goog.dom.$ = goog.dom.getElement;


/**
 * Looks up elements by both tag and class name, using browser native functions
 * ({@code querySelectorAll}, {@code getElementsByTagName} or
 * {@code getElementsByClassName}) where possible. This function
 * is a useful, if limited, way of collecting a list of DOM elements
 * with certain characteristics.  {@code goog.dom.query} offers a
 * more powerful and general solution which allows matching on CSS3
 * selector expressions, but at increased cost in code size. If all you
 * need is particular tags belonging to a single class, this function
 * is fast and sleek.
 *
 * Note that tag names are case sensitive in the SVG namespace, and this
 * function converts opt_tag to uppercase for comparisons. For queries in the
 * SVG namespace you should use querySelector or querySelectorAll instead.
 * https://bugzilla.mozilla.org/show_bug.cgi?id=963870
 * https://bugs.webkit.org/show_bug.cgi?id=83438
 *
 * @see {goog.dom.query}
 *
 * @param {?string=} opt_tag Element tag name.
 * @param {?string=} opt_class Optional class name.
 * @param {(Document|Element)=} opt_el Optional element to look in.
 * @return { {length: number} } Array-like list of elements (only a length
 *     property and numerical indices are guaranteed to exist).
 */
goog.dom.getElementsByTagNameAndClass = function(opt_tag, opt_class, opt_el) {
  return goog.dom.getElementsByTagNameAndClass_(document, opt_tag, opt_class,
                                                opt_el);
};


/**
 * Returns a static, array-like list of the elements with the provided
 * className.
 * @see {goog.dom.query}
 * @param {string} className the name of the class to look for.
 * @param {(Document|Element)=} opt_el Optional element to look in.
 * @return { {length: number} } The items found with the class name provided.
 */
goog.dom.getElementsByClass = function(className, opt_el) {
  var parent = opt_el || document;
  if (goog.dom.canUseQuerySelector_(parent)) {
    return parent.querySelectorAll('.' + className);
  }
  return goog.dom.getElementsByTagNameAndClass_(
      document, '*', className, opt_el);
};


/**
 * Returns the first element with the provided className.
 * @see {goog.dom.query}
 * @param {string} className the name of the class to look for.
 * @param {Element|Document=} opt_el Optional element to look in.
 * @return {Element} The first item with the class name provided.
 */
goog.dom.getElementByClass = function(className, opt_el) {
  var parent = opt_el || document;
  var retVal = null;
  if (parent.getElementsByClassName) {
    retVal = parent.getElementsByClassName(className)[0];
  } else if (goog.dom.canUseQuerySelector_(parent)) {
    retVal = parent.querySelector('.' + className);
  } else {
    retVal = goog.dom.getElementsByTagNameAndClass_(
        document, '*', className, opt_el)[0];
  }
  return retVal || null;
};


/**
 * Ensures an element with the given className exists, and then returns the
 * first element with the provided className.
 * @see {goog.dom.query}
 * @param {string} className the name of the class to look for.
 * @param {!Element|!Document=} opt_root Optional element or document to look
 *     in.
 * @return {!Element} The first item with the class name provided.
 * @throws {goog.asserts.AssertionError} Thrown if no element is found.
 */
goog.dom.getRequiredElementByClass = function(className, opt_root) {
  var retValue = goog.dom.getElementByClass(className, opt_root);
  return goog.asserts.assert(retValue,
      'No element found with className: ' + className);
};


/**
 * Prefer the standardized (http://www.w3.org/TR/selectors-api/), native and
 * fast W3C Selectors API.
 * @param {!(Element|Document)} parent The parent document object.
 * @return {boolean} whether or not we can use parent.querySelector* APIs.
 * @private
 */
goog.dom.canUseQuerySelector_ = function(parent) {
  return !!(parent.querySelectorAll && parent.querySelector);
};


/**
 * Helper for {@code getElementsByTagNameAndClass}.
 * @param {!Document} doc The document to get the elements in.
 * @param {?string=} opt_tag Element tag name.
 * @param {?string=} opt_class Optional class name.
 * @param {(Document|Element)=} opt_el Optional element to look in.
 * @return { {length: number} } Array-like list of elements (only a length
 *     property and numerical indices are guaranteed to exist).
 * @private
 */
goog.dom.getElementsByTagNameAndClass_ = function(doc, opt_tag, opt_class,
                                                  opt_el) {
  var parent = opt_el || doc;
  var tagName = (opt_tag && opt_tag != '*') ? opt_tag.toUpperCase() : '';

  if (goog.dom.canUseQuerySelector_(parent) &&
      (tagName || opt_class)) {
    var query = tagName + (opt_class ? '.' + opt_class : '');
    return parent.querySelectorAll(query);
  }

  // Use the native getElementsByClassName if available, under the assumption
  // that even when the tag name is specified, there will be fewer elements to
  // filter through when going by class than by tag name
  if (opt_class && parent.getElementsByClassName) {
    var els = parent.getElementsByClassName(opt_class);

    if (tagName) {
      var arrayLike = {};
      var len = 0;

      // Filter for specific tags if requested.
      for (var i = 0, el; el = els[i]; i++) {
        if (tagName == el.nodeName) {
          arrayLike[len++] = el;
        }
      }
      arrayLike.length = len;

      return arrayLike;
    } else {
      return els;
    }
  }

  var els = parent.getElementsByTagName(tagName || '*');

  if (opt_class) {
    var arrayLike = {};
    var len = 0;
    for (var i = 0, el; el = els[i]; i++) {
      var className = el.className;
      // Check if className has a split function since SVG className does not.
      if (typeof className.split == 'function' &&
          goog.array.contains(className.split(/\s+/), opt_class)) {
        arrayLike[len++] = el;
      }
    }
    arrayLike.length = len;
    return arrayLike;
  } else {
    return els;
  }
};


/**
 * Alias for {@code getElementsByTagNameAndClass}.
 * @param {?string=} opt_tag Element tag name.
 * @param {?string=} opt_class Optional class name.
 * @param {Element=} opt_el Optional element to look in.
 * @return { {length: number} } Array-like list of elements (only a length
 *     property and numerical indices are guaranteed to exist).
 * @deprecated Use {@link goog.dom.getElementsByTagNameAndClass} instead.
 */
goog.dom.$$ = goog.dom.getElementsByTagNameAndClass;


/**
 * Sets multiple properties on a node.
 * @param {Element} element DOM node to set properties on.
 * @param {Object} properties Hash of property:value pairs.
 */
goog.dom.setProperties = function(element, properties) {
  goog.object.forEach(properties, function(val, key) {
    if (key == 'style') {
      element.style.cssText = val;
    } else if (key == 'class') {
      element.className = val;
    } else if (key == 'for') {
      element.htmlFor = val;
    } else if (goog.dom.DIRECT_ATTRIBUTE_MAP_.hasOwnProperty(key)) {
      element.setAttribute(goog.dom.DIRECT_ATTRIBUTE_MAP_[key], val);
    } else if (goog.string.startsWith(key, 'aria-') ||
        goog.string.startsWith(key, 'data-')) {
      element.setAttribute(key, val);
    } else {
      element[key] = val;
    }
  });
};


/**
 * Map of attributes that should be set using
 * element.setAttribute(key, val) instead of element[key] = val.  Used
 * by goog.dom.setProperties.
 *
 * @private {!Object<string, string>}
 * @const
 */
goog.dom.DIRECT_ATTRIBUTE_MAP_ = {
  'cellpadding': 'cellPadding',
  'cellspacing': 'cellSpacing',
  'colspan': 'colSpan',
  'frameborder': 'frameBorder',
  'height': 'height',
  'maxlength': 'maxLength',
  'role': 'role',
  'rowspan': 'rowSpan',
  'type': 'type',
  'usemap': 'useMap',
  'valign': 'vAlign',
  'width': 'width'
};


/**
 * Gets the dimensions of the viewport.
 *
 * Gecko Standards mode:
 * docEl.clientWidth  Width of viewport excluding scrollbar.
 * win.innerWidth     Width of viewport including scrollbar.
 * body.clientWidth   Width of body element.
 *
 * docEl.clientHeight Height of viewport excluding scrollbar.
 * win.innerHeight    Height of viewport including scrollbar.
 * body.clientHeight  Height of document.
 *
 * Gecko Backwards compatible mode:
 * docEl.clientWidth  Width of viewport excluding scrollbar.
 * win.innerWidth     Width of viewport including scrollbar.
 * body.clientWidth   Width of viewport excluding scrollbar.
 *
 * docEl.clientHeight Height of document.
 * win.innerHeight    Height of viewport including scrollbar.
 * body.clientHeight  Height of viewport excluding scrollbar.
 *
 * IE6/7 Standards mode:
 * docEl.clientWidth  Width of viewport excluding scrollbar.
 * win.innerWidth     Undefined.
 * body.clientWidth   Width of body element.
 *
 * docEl.clientHeight Height of viewport excluding scrollbar.
 * win.innerHeight    Undefined.
 * body.clientHeight  Height of document element.
 *
 * IE5 + IE6/7 Backwards compatible mode:
 * docEl.clientWidth  0.
 * win.innerWidth     Undefined.
 * body.clientWidth   Width of viewport excluding scrollbar.
 *
 * docEl.clientHeight 0.
 * win.innerHeight    Undefined.
 * body.clientHeight  Height of viewport excluding scrollbar.
 *
 * Opera 9 Standards and backwards compatible mode:
 * docEl.clientWidth  Width of viewport excluding scrollbar.
 * win.innerWidth     Width of viewport including scrollbar.
 * body.clientWidth   Width of viewport excluding scrollbar.
 *
 * docEl.clientHeight Height of document.
 * win.innerHeight    Height of viewport including scrollbar.
 * body.clientHeight  Height of viewport excluding scrollbar.
 *
 * WebKit:
 * Safari 2
 * docEl.clientHeight Same as scrollHeight.
 * docEl.clientWidth  Same as innerWidth.
 * win.innerWidth     Width of viewport excluding scrollbar.
 * win.innerHeight    Height of the viewport including scrollbar.
 * frame.innerHeight  Height of the viewport exluding scrollbar.
 *
 * Safari 3 (tested in 522)
 *
 * docEl.clientWidth  Width of viewport excluding scrollbar.
 * docEl.clientHeight Height of viewport excluding scrollbar in strict mode.
 * body.clientHeight  Height of viewport excluding scrollbar in quirks mode.
 *
 * @param {Window=} opt_window Optional window element to test.
 * @return {!goog.math.Size} Object with values 'width' and 'height'.
 */
goog.dom.getViewportSize = function(opt_window) {
  // TODO(arv): This should not take an argument
  return goog.dom.getViewportSize_(opt_window || window);
};


/**
 * Helper for {@code getViewportSize}.
 * @param {Window} win The window to get the view port size for.
 * @return {!goog.math.Size} Object with values 'width' and 'height'.
 * @private
 */
goog.dom.getViewportSize_ = function(win) {
  var doc = win.document;
  var el = goog.dom.isCss1CompatMode_(doc) ? doc.documentElement : doc.body;
  return new goog.math.Size(el.clientWidth, el.clientHeight);
};


/**
 * Calculates the height of the document.
 *
 * @return {number} The height of the current document.
 */
goog.dom.getDocumentHeight = function() {
  return goog.dom.getDocumentHeight_(window);
};


/**
 * Calculates the height of the document of the given window.
 *
 * Function code copied from the opensocial gadget api:
 *   gadgets.window.adjustHeight(opt_height)
 *
 * @private
 * @param {!Window} win The window whose document height to retrieve.
 * @return {number} The height of the document of the given window.
 */
goog.dom.getDocumentHeight_ = function(win) {
  // NOTE(eae): This method will return the window size rather than the document
  // size in webkit quirks mode.
  var doc = win.document;
  var height = 0;

  if (doc) {
    // Calculating inner content height is hard and different between
    // browsers rendering in Strict vs. Quirks mode.  We use a combination of
    // three properties within document.body and document.documentElement:
    // - scrollHeight
    // - offsetHeight
    // - clientHeight
    // These values differ significantly between browsers and rendering modes.
    // But there are patterns.  It just takes a lot of time and persistence
    // to figure out.

    var body = doc.body;
    var docEl = /** @type {!HTMLElement} */ (doc.documentElement);
    if (!(docEl && body)) {
      return 0;
    }

    // Get the height of the viewport
    var vh = goog.dom.getViewportSize_(win).height;
    if (goog.dom.isCss1CompatMode_(doc) && docEl.scrollHeight) {
      // In Strict mode:
      // The inner content height is contained in either:
      //    document.documentElement.scrollHeight
      //    document.documentElement.offsetHeight
      // Based on studying the values output by different browsers,
      // use the value that's NOT equal to the viewport height found above.
      height = docEl.scrollHeight != vh ?
          docEl.scrollHeight : docEl.offsetHeight;
    } else {
      // In Quirks mode:
      // documentElement.clientHeight is equal to documentElement.offsetHeight
      // except in IE.  In most browsers, document.documentElement can be used
      // to calculate the inner content height.
      // However, in other browsers (e.g. IE), document.body must be used
      // instead.  How do we know which one to use?
      // If document.documentElement.clientHeight does NOT equal
      // document.documentElement.offsetHeight, then use document.body.
      var sh = docEl.scrollHeight;
      var oh = docEl.offsetHeight;
      if (docEl.clientHeight != oh) {
        sh = body.scrollHeight;
        oh = body.offsetHeight;
      }

      // Detect whether the inner content height is bigger or smaller
      // than the bounding box (viewport).  If bigger, take the larger
      // value.  If smaller, take the smaller value.
      if (sh > vh) {
        // Content is larger
        height = sh > oh ? sh : oh;
      } else {
        // Content is smaller
        height = sh < oh ? sh : oh;
      }
    }
  }

  return height;
};


/**
 * Gets the page scroll distance as a coordinate object.
 *
 * @param {Window=} opt_window Optional window element to test.
 * @return {!goog.math.Coordinate} Object with values 'x' and 'y'.
 * @deprecated Use {@link goog.dom.getDocumentScroll} instead.
 */
goog.dom.getPageScroll = function(opt_window) {
  var win = opt_window || goog.global || window;
  return goog.dom.getDomHelper(win.document).getDocumentScroll();
};


/**
 * Gets the document scroll distance as a coordinate object.
 *
 * @return {!goog.math.Coordinate} Object with values 'x' and 'y'.
 */
goog.dom.getDocumentScroll = function() {
  return goog.dom.getDocumentScroll_(document);
};


/**
 * Helper for {@code getDocumentScroll}.
 *
 * @param {!Document} doc The document to get the scroll for.
 * @return {!goog.math.Coordinate} Object with values 'x' and 'y'.
 * @private
 */
goog.dom.getDocumentScroll_ = function(doc) {
  var el = goog.dom.getDocumentScrollElement_(doc);
  var win = goog.dom.getWindow_(doc);
  if (goog.userAgent.IE && goog.userAgent.isVersionOrHigher('10') &&
      win.pageYOffset != el.scrollTop) {
    // The keyboard on IE10 touch devices shifts the page using the pageYOffset
    // without modifying scrollTop. For this case, we want the body scroll
    // offsets.
    return new goog.math.Coordinate(el.scrollLeft, el.scrollTop);
  }
  return new goog.math.Coordinate(win.pageXOffset || el.scrollLeft,
      win.pageYOffset || el.scrollTop);
};


/**
 * Gets the document scroll element.
 * @return {!Element} Scrolling element.
 */
goog.dom.getDocumentScrollElement = function() {
  return goog.dom.getDocumentScrollElement_(document);
};


/**
 * Helper for {@code getDocumentScrollElement}.
 * @param {!Document} doc The document to get the scroll element for.
 * @return {!Element} Scrolling element.
 * @private
 */
goog.dom.getDocumentScrollElement_ = function(doc) {
  // Old WebKit needs body.scrollLeft in both quirks mode and strict mode. We
  // also default to the documentElement if the document does not have a body
  // (e.g. a SVG document).
  // Uses http://dev.w3.org/csswg/cssom-view/#dom-document-scrollingelement to
  // avoid trying to guess about browser behavior from the UA string.
  if (doc.scrollingElement) {
    return doc.scrollingElement;
  }
  if (!goog.userAgent.WEBKIT && goog.dom.isCss1CompatMode_(doc)) {
    return doc.documentElement;
  }
  return doc.body || doc.documentElement;
};


/**
 * Gets the window object associated with the given document.
 *
 * @param {Document=} opt_doc  Document object to get window for.
 * @return {!Window} The window associated with the given document.
 */
goog.dom.getWindow = function(opt_doc) {
  // TODO(arv): This should not take an argument.
  return opt_doc ? goog.dom.getWindow_(opt_doc) : window;
};


/**
 * Helper for {@code getWindow}.
 *
 * @param {!Document} doc  Document object to get window for.
 * @return {!Window} The window associated with the given document.
 * @private
 */
goog.dom.getWindow_ = function(doc) {
  return doc.parentWindow || doc.defaultView;
};


/**
 * Returns a dom node with a set of attributes.  This function accepts varargs
 * for subsequent nodes to be added.  Subsequent nodes will be added to the
 * first node as childNodes.
 *
 * So:
 * <code>createDom('div', null, createDom('p'), createDom('p'));</code>
 * would return a div with two child paragraphs
 *
 * @param {string} tagName Tag to create.
 * @param {(Object|Array<string>|string)=} opt_attributes If object, then a map
 *     of name-value pairs for attributes. If a string, then this is the
 *     className of the new element. If an array, the elements will be joined
 *     together as the className of the new element.
 * @param {...(Object|string|Array|NodeList)} var_args Further DOM nodes or
 *     strings for text nodes. If one of the var_args is an array or NodeList,
 *     its elements will be added as childNodes instead.
 * @return {!Element} Reference to a DOM node.
 */
goog.dom.createDom = function(tagName, opt_attributes, var_args) {
  return goog.dom.createDom_(document, arguments);
};


/**
 * Helper for {@code createDom}.
 * @param {!Document} doc The document to create the DOM in.
 * @param {!Arguments} args Argument object passed from the callers. See
 *     {@code goog.dom.createDom} for details.
 * @return {!Element} Reference to a DOM node.
 * @private
 */
goog.dom.createDom_ = function(doc, args) {
  var tagName = args[0];
  var attributes = args[1];

  // Internet Explorer is dumb:
  // name: https://msdn.microsoft.com/en-us/library/ms534184(v=vs.85).aspx
  // type: https://msdn.microsoft.com/en-us/library/ms534700(v=vs.85).aspx
  // Also does not allow setting of 'type' attribute on 'input' or 'button'.
  if (!goog.dom.BrowserFeature.CAN_ADD_NAME_OR_TYPE_ATTRIBUTES && attributes &&
      (attributes.name || attributes.type)) {
    var tagNameArr = ['<', tagName];
    if (attributes.name) {
      tagNameArr.push(' name="', goog.string.htmlEscape(attributes.name), '"');
    }
    if (attributes.type) {
      tagNameArr.push(' type="', goog.string.htmlEscape(attributes.type), '"');

      // Clone attributes map to remove 'type' without mutating the input.
      var clone = {};
      goog.object.extend(clone, attributes);

      // JSCompiler can't see how goog.object.extend added this property,
      // because it was essentially added by reflection.
      // So it needs to be quoted.
      delete clone['type'];

      attributes = clone;
    }
    tagNameArr.push('>');
    tagName = tagNameArr.join('');
  }

  var element = doc.createElement(tagName);

  if (attributes) {
    if (goog.isString(attributes)) {
      element.className = attributes;
    } else if (goog.isArray(attributes)) {
      element.className = attributes.join(' ');
    } else {
      goog.dom.setProperties(element, attributes);
    }
  }

  if (args.length > 2) {
    goog.dom.append_(doc, element, args, 2);
  }

  return element;
};


/**
 * Appends a node with text or other nodes.
 * @param {!Document} doc The document to create new nodes in.
 * @param {!Node} parent The node to append nodes to.
 * @param {!Arguments} args The values to add. See {@code goog.dom.append}.
 * @param {number} startIndex The index of the array to start from.
 * @private
 */
goog.dom.append_ = function(doc, parent, args, startIndex) {
  function childHandler(child) {
    // TODO(user): More coercion, ala MochiKit?
    if (child) {
      parent.appendChild(goog.isString(child) ?
          doc.createTextNode(child) : child);
    }
  }

  for (var i = startIndex; i < args.length; i++) {
    var arg = args[i];
    // TODO(attila): Fix isArrayLike to return false for a text node.
    if (goog.isArrayLike(arg) && !goog.dom.isNodeLike(arg)) {
      // If the argument is a node list, not a real array, use a clone,
      // because forEach can't be used to mutate a NodeList.
      goog.array.forEach(goog.dom.isNodeList(arg) ?
          goog.array.toArray(arg) : arg,
          childHandler);
    } else {
      childHandler(arg);
    }
  }
};


/**
 * Alias for {@code createDom}.
 * @param {string} tagName Tag to create.
 * @param {(string|Object)=} opt_attributes If object, then a map of name-value
 *     pairs for attributes. If a string, then this is the className of the new
 *     element.
 * @param {...(Object|string|Array|NodeList)} var_args Further DOM nodes or
 *     strings for text nodes. If one of the var_args is an array, its
 *     children will be added as childNodes instead.
 * @return {!Element} Reference to a DOM node.
 * @deprecated Use {@link goog.dom.createDom} instead.
 */
goog.dom.$dom = goog.dom.createDom;


/**
 * Creates a new element.
 * @param {string} name Tag name.
 * @return {!Element} The new element.
 */
goog.dom.createElement = function(name) {
  return document.createElement(name);
};


/**
 * Creates a new text node.
 * @param {number|string} content Content.
 * @return {!Text} The new text node.
 */
goog.dom.createTextNode = function(content) {
  return document.createTextNode(String(content));
};


/**
 * Create a table.
 * @param {number} rows The number of rows in the table.  Must be >= 1.
 * @param {number} columns The number of columns in the table.  Must be >= 1.
 * @param {boolean=} opt_fillWithNbsp If true, fills table entries with
 *     {@code goog.string.Unicode.NBSP} characters.
 * @return {!Element} The created table.
 */
goog.dom.createTable = function(rows, columns, opt_fillWithNbsp) {
  // TODO(user): Return HTMLTableElement, also in prototype function.
  // Callers need to be updated to e.g. not assign numbers to table.cellSpacing.
  return goog.dom.createTable_(document, rows, columns, !!opt_fillWithNbsp);
};


/**
 * Create a table.
 * @param {!Document} doc Document object to use to create the table.
 * @param {number} rows The number of rows in the table.  Must be >= 1.
 * @param {number} columns The number of columns in the table.  Must be >= 1.
 * @param {boolean} fillWithNbsp If true, fills table entries with
 *     {@code goog.string.Unicode.NBSP} characters.
 * @return {!HTMLTableElement} The created table.
 * @private
 */
goog.dom.createTable_ = function(doc, rows, columns, fillWithNbsp) {
  var table = /** @type {!HTMLTableElement} */
      (doc.createElement(goog.dom.TagName.TABLE));
  var tbody = table.appendChild(doc.createElement(goog.dom.TagName.TBODY));
  for (var i = 0; i < rows; i++) {
    var tr = doc.createElement(goog.dom.TagName.TR);
    for (var j = 0; j < columns; j++) {
      var td = doc.createElement(goog.dom.TagName.TD);
      // IE <= 9 will create a text node if we set text content to the empty
      // string, so we avoid doing it unless necessary. This ensures that the
      // same DOM tree is returned on all browsers.
      if (fillWithNbsp) {
        goog.dom.setTextContent(td, goog.string.Unicode.NBSP);
      }
      tr.appendChild(td);
    }
    tbody.appendChild(tr);
  }
  return table;
};


/**
 * Converts HTML markup into a node.
 * @param {!goog.html.SafeHtml} html The HTML markup to convert.
 * @return {!Node} The resulting node.
 */
goog.dom.safeHtmlToNode = function(html) {
  return goog.dom.safeHtmlToNode_(document, html);
};


/**
 * Helper for {@code safeHtmlToNode}.
 * @param {!Document} doc The document.
 * @param {!goog.html.SafeHtml} html The HTML markup to convert.
 * @return {!Node} The resulting node.
 * @private
 */
goog.dom.safeHtmlToNode_ = function(doc, html) {
  var tempDiv = doc.createElement(goog.dom.TagName.DIV);
  if (goog.dom.BrowserFeature.INNER_HTML_NEEDS_SCOPED_ELEMENT) {
    goog.dom.safe.setInnerHtml(tempDiv,
        goog.html.SafeHtml.concat(goog.html.SafeHtml.create('br'), html));
    tempDiv.removeChild(tempDiv.firstChild);
  } else {
    goog.dom.safe.setInnerHtml(tempDiv, html);
  }
  return goog.dom.childrenToNode_(doc, tempDiv);
};


/**
 * Converts an HTML string into a document fragment. The string must be
 * sanitized in order to avoid cross-site scripting. For example
 * {@code goog.dom.htmlToDocumentFragment('&lt;img src=x onerror=alert(0)&gt;')}
 * triggers an alert in all browsers, even if the returned document fragment
 * is thrown away immediately.
 *
 * NOTE: This method doesn't work if your htmlString contains elements that
 * can't be contained in a <div>. For example, <tr>.
 *
 * @param {string} htmlString The HTML string to convert.
 * @return {!Node} The resulting document fragment.
 */
goog.dom.htmlToDocumentFragment = function(htmlString) {
  return goog.dom.htmlToDocumentFragment_(document, htmlString);
};


// TODO(jakubvrana): Merge with {@code safeHtmlToNode_}.
/**
 * Helper for {@code htmlToDocumentFragment}.
 *
 * @param {!Document} doc The document.
 * @param {string} htmlString The HTML string to convert.
 * @return {!Node} The resulting document fragment.
 * @private
 */
goog.dom.htmlToDocumentFragment_ = function(doc, htmlString) {
  var tempDiv = doc.createElement(goog.dom.TagName.DIV);
  if (goog.dom.BrowserFeature.INNER_HTML_NEEDS_SCOPED_ELEMENT) {
    tempDiv.innerHTML = '<br>' + htmlString;
    tempDiv.removeChild(tempDiv.firstChild);
  } else {
    tempDiv.innerHTML = htmlString;
  }
  return goog.dom.childrenToNode_(doc, tempDiv);
};


/**
 * Helper for {@code htmlToDocumentFragment_}.
 * @param {!Document} doc The document.
 * @param {!Node} tempDiv The input node.
 * @return {!Node} The resulting node.
 * @private
 */
goog.dom.childrenToNode_ = function(doc, tempDiv) {
  if (tempDiv.childNodes.length == 1) {
    return tempDiv.removeChild(tempDiv.firstChild);
  } else {
    var fragment = doc.createDocumentFragment();
    while (tempDiv.firstChild) {
      fragment.appendChild(tempDiv.firstChild);
    }
    return fragment;
  }
};


/**
 * Returns true if the browser is in "CSS1-compatible" (standards-compliant)
 * mode, false otherwise.
 * @return {boolean} True if in CSS1-compatible mode.
 */
goog.dom.isCss1CompatMode = function() {
  return goog.dom.isCss1CompatMode_(document);
};


/**
 * Returns true if the browser is in "CSS1-compatible" (standards-compliant)
 * mode, false otherwise.
 * @param {!Document} doc The document to check.
 * @return {boolean} True if in CSS1-compatible mode.
 * @private
 */
goog.dom.isCss1CompatMode_ = function(doc) {
  if (goog.dom.COMPAT_MODE_KNOWN_) {
    return goog.dom.ASSUME_STANDARDS_MODE;
  }

  return doc.compatMode == 'CSS1Compat';
};


/**
 * Determines if the given node can contain children, intended to be used for
 * HTML generation.
 *
 * IE natively supports node.canHaveChildren but has inconsistent behavior.
 * Prior to IE8 the base tag allows children and in IE9 all nodes return true
 * for canHaveChildren.
 *
 * In practice all non-IE browsers allow you to add children to any node, but
 * the behavior is inconsistent:
 *
 * <pre>
 *   var a = document.createElement(goog.dom.TagName.BR);
 *   a.appendChild(document.createTextNode('foo'));
 *   a.appendChild(document.createTextNode('bar'));
 *   console.log(a.childNodes.length);  // 2
 *   console.log(a.innerHTML);  // Chrome: "", IE9: "foobar", FF3.5: "foobar"
 * </pre>
 *
 * For more information, see:
 * http://dev.w3.org/html5/markup/syntax.html#syntax-elements
 *
 * TODO(user): Rename shouldAllowChildren() ?
 *
 * @param {Node} node The node to check.
 * @return {boolean} Whether the node can contain children.
 */
goog.dom.canHaveChildren = function(node) {
  if (node.nodeType != goog.dom.NodeType.ELEMENT) {
    return false;
  }
  switch (/** @type {!Element} */ (node).tagName) {
    case goog.dom.TagName.APPLET:
    case goog.dom.TagName.AREA:
    case goog.dom.TagName.BASE:
    case goog.dom.TagName.BR:
    case goog.dom.TagName.COL:
    case goog.dom.TagName.COMMAND:
    case goog.dom.TagName.EMBED:
    case goog.dom.TagName.FRAME:
    case goog.dom.TagName.HR:
    case goog.dom.TagName.IMG:
    case goog.dom.TagName.INPUT:
    case goog.dom.TagName.IFRAME:
    case goog.dom.TagName.ISINDEX:
    case goog.dom.TagName.KEYGEN:
    case goog.dom.TagName.LINK:
    case goog.dom.TagName.NOFRAMES:
    case goog.dom.TagName.NOSCRIPT:
    case goog.dom.TagName.META:
    case goog.dom.TagName.OBJECT:
    case goog.dom.TagName.PARAM:
    case goog.dom.TagName.SCRIPT:
    case goog.dom.TagName.SOURCE:
    case goog.dom.TagName.STYLE:
    case goog.dom.TagName.TRACK:
    case goog.dom.TagName.WBR:
      return false;
  }
  return true;
};


/**
 * Appends a child to a node.
 * @param {Node} parent Parent.
 * @param {Node} child Child.
 */
goog.dom.appendChild = function(parent, child) {
  parent.appendChild(child);
};


/**
 * Appends a node with text or other nodes.
 * @param {!Node} parent The node to append nodes to.
 * @param {...goog.dom.Appendable} var_args The things to append to the node.
 *     If this is a Node it is appended as is.
 *     If this is a string then a text node is appended.
 *     If this is an array like object then fields 0 to length - 1 are appended.
 */
goog.dom.append = function(parent, var_args) {
  goog.dom.append_(goog.dom.getOwnerDocument(parent), parent, arguments, 1);
};


/**
 * Removes all the child nodes on a DOM node.
 * @param {Node} node Node to remove children from.
 */
goog.dom.removeChildren = function(node) {
  // Note: Iterations over live collections can be slow, this is the fastest
  // we could find. The double parenthesis are used to prevent JsCompiler and
  // strict warnings.
  var child;
  while ((child = node.firstChild)) {
    node.removeChild(child);
  }
};


/**
 * Inserts a new node before an existing reference node (i.e. as the previous
 * sibling). If the reference node has no parent, then does nothing.
 * @param {Node} newNode Node to insert.
 * @param {Node} refNode Reference node to insert before.
 */
goog.dom.insertSiblingBefore = function(newNode, refNode) {
  if (refNode.parentNode) {
    refNode.parentNode.insertBefore(newNode, refNode);
  }
};


/**
 * Inserts a new node after an existing reference node (i.e. as the next
 * sibling). If the reference node has no parent, then does nothing.
 * @param {Node} newNode Node to insert.
 * @param {Node} refNode Reference node to insert after.
 */
goog.dom.insertSiblingAfter = function(newNode, refNode) {
  if (refNode.parentNode) {
    refNode.parentNode.insertBefore(newNode, refNode.nextSibling);
  }
};


/**
 * Insert a child at a given index. If index is larger than the number of child
 * nodes that the parent currently has, the node is inserted as the last child
 * node.
 * @param {Element} parent The element into which to insert the child.
 * @param {Node} child The element to insert.
 * @param {number} index The index at which to insert the new child node. Must
 *     not be negative.
 */
goog.dom.insertChildAt = function(parent, child, index) {
  // Note that if the second argument is null, insertBefore
  // will append the child at the end of the list of children.
  parent.insertBefore(child, parent.childNodes[index] || null);
};


/**
 * Removes a node from its parent.
 * @param {Node} node The node to remove.
 * @return {Node} The node removed if removed; else, null.
 */
goog.dom.removeNode = function(node) {
  return node && node.parentNode ? node.parentNode.removeChild(node) : null;
};


/**
 * Replaces a node in the DOM tree. Will do nothing if {@code oldNode} has no
 * parent.
 * @param {Node} newNode Node to insert.
 * @param {Node} oldNode Node to replace.
 */
goog.dom.replaceNode = function(newNode, oldNode) {
  var parent = oldNode.parentNode;
  if (parent) {
    parent.replaceChild(newNode, oldNode);
  }
};


/**
 * Flattens an element. That is, removes it and replace it with its children.
 * Does nothing if the element is not in the document.
 * @param {Element} element The element to flatten.
 * @return {Element|undefined} The original element, detached from the document
 *     tree, sans children; or undefined, if the element was not in the document
 *     to begin with.
 */
goog.dom.flattenElement = function(element) {
  var child, parent = element.parentNode;
  if (parent && parent.nodeType != goog.dom.NodeType.DOCUMENT_FRAGMENT) {
    // Use IE DOM method (supported by Opera too) if available
    if (element.removeNode) {
      return /** @type {Element} */ (element.removeNode(false));
    } else {
      // Move all children of the original node up one level.
      while ((child = element.firstChild)) {
        parent.insertBefore(child, element);
      }

      // Detach the original element.
      return /** @type {Element} */ (goog.dom.removeNode(element));
    }
  }
};


/**
 * Returns an array containing just the element children of the given element.
 * @param {Element} element The element whose element children we want.
 * @return {!(Array|NodeList)} An array or array-like list of just the element
 *     children of the given element.
 */
goog.dom.getChildren = function(element) {
  // We check if the children attribute is supported for child elements
  // since IE8 misuses the attribute by also including comments.
  if (goog.dom.BrowserFeature.CAN_USE_CHILDREN_ATTRIBUTE &&
      element.children != undefined) {
    return element.children;
  }
  // Fall back to manually filtering the element's child nodes.
  return goog.array.filter(element.childNodes, function(node) {
    return node.nodeType == goog.dom.NodeType.ELEMENT;
  });
};


/**
 * Returns the first child node that is an element.
 * @param {Node} node The node to get the first child element of.
 * @return {Element} The first child node of {@code node} that is an element.
 */
goog.dom.getFirstElementChild = function(node) {
  if (goog.isDef(node.firstElementChild)) {
    return /** @type {!Element} */(node).firstElementChild;
  }
  return goog.dom.getNextElementNode_(node.firstChild, true);
};


/**
 * Returns the last child node that is an element.
 * @param {Node} node The node to get the last child element of.
 * @return {Element} The last child node of {@code node} that is an element.
 */
goog.dom.getLastElementChild = function(node) {
  if (goog.isDef(node.lastElementChild)) {
    return /** @type {!Element} */(node).lastElementChild;
  }
  return goog.dom.getNextElementNode_(node.lastChild, false);
};


/**
 * Returns the first next sibling that is an element.
 * @param {Node} node The node to get the next sibling element of.
 * @return {Element} The next sibling of {@code node} that is an element.
 */
goog.dom.getNextElementSibling = function(node) {
  if (goog.isDef(node.nextElementSibling)) {
    return /** @type {!Element} */(node).nextElementSibling;
  }
  return goog.dom.getNextElementNode_(node.nextSibling, true);
};


/**
 * Returns the first previous sibling that is an element.
 * @param {Node} node The node to get the previous sibling element of.
 * @return {Element} The first previous sibling of {@code node} that is
 *     an element.
 */
goog.dom.getPreviousElementSibling = function(node) {
  if (goog.isDef(node.previousElementSibling)) {
    return /** @type {!Element} */(node).previousElementSibling;
  }
  return goog.dom.getNextElementNode_(node.previousSibling, false);
};


/**
 * Returns the first node that is an element in the specified direction,
 * starting with {@code node}.
 * @param {Node} node The node to get the next element from.
 * @param {boolean} forward Whether to look forwards or backwards.
 * @return {Element} The first element.
 * @private
 */
goog.dom.getNextElementNode_ = function(node, forward) {
  while (node && node.nodeType != goog.dom.NodeType.ELEMENT) {
    node = forward ? node.nextSibling : node.previousSibling;
  }

  return /** @type {Element} */ (node);
};


/**
 * Returns the next node in source order from the given node.
 * @param {Node} node The node.
 * @return {Node} The next node in the DOM tree, or null if this was the last
 *     node.
 */
goog.dom.getNextNode = function(node) {
  if (!node) {
    return null;
  }

  if (node.firstChild) {
    return node.firstChild;
  }

  while (node && !node.nextSibling) {
    node = node.parentNode;
  }

  return node ? node.nextSibling : null;
};


/**
 * Returns the previous node in source order from the given node.
 * @param {Node} node The node.
 * @return {Node} The previous node in the DOM tree, or null if this was the
 *     first node.
 */
goog.dom.getPreviousNode = function(node) {
  if (!node) {
    return null;
  }

  if (!node.previousSibling) {
    return node.parentNode;
  }

  node = node.previousSibling;
  while (node && node.lastChild) {
    node = node.lastChild;
  }

  return node;
};


/**
 * Whether the object looks like a DOM node.
 * @param {?} obj The object being tested for node likeness.
 * @return {boolean} Whether the object looks like a DOM node.
 */
goog.dom.isNodeLike = function(obj) {
  return goog.isObject(obj) && obj.nodeType > 0;
};


/**
 * Whether the object looks like an Element.
 * @param {?} obj The object being tested for Element likeness.
 * @return {boolean} Whether the object looks like an Element.
 */
goog.dom.isElement = function(obj) {
  return goog.isObject(obj) && obj.nodeType == goog.dom.NodeType.ELEMENT;
};


/**
 * Returns true if the specified value is a Window object. This includes the
 * global window for HTML pages, and iframe windows.
 * @param {?} obj Variable to test.
 * @return {boolean} Whether the variable is a window.
 */
goog.dom.isWindow = function(obj) {
  return goog.isObject(obj) && obj['window'] == obj;
};


/**
 * Returns an element's parent, if it's an Element.
 * @param {Element} element The DOM element.
 * @return {Element} The parent, or null if not an Element.
 */
goog.dom.getParentElement = function(element) {
  var parent;
  if (goog.dom.BrowserFeature.CAN_USE_PARENT_ELEMENT_PROPERTY) {
    var isIe9 = goog.userAgent.IE &&
        goog.userAgent.isVersionOrHigher('9') &&
        !goog.userAgent.isVersionOrHigher('10');
    // SVG elements in IE9 can't use the parentElement property.
    // goog.global['SVGElement'] is not defined in IE9 quirks mode.
    if (!(isIe9 && goog.global['SVGElement'] &&
        element instanceof goog.global['SVGElement'])) {
      parent = element.parentElement;
      if (parent) {
        return parent;
      }
    }
  }
  parent = element.parentNode;
  return goog.dom.isElement(parent) ? /** @type {!Element} */ (parent) : null;
};


/**
 * Whether a node contains another node.
 * @param {Node} parent The node that should contain the other node.
 * @param {Node} descendant The node to test presence of.
 * @return {boolean} Whether the parent node contains the descendent node.
 */
goog.dom.contains = function(parent, descendant) {
  // We use browser specific methods for this if available since it is faster
  // that way.

  // IE DOM
  if (parent.contains && descendant.nodeType == goog.dom.NodeType.ELEMENT) {
    return parent == descendant || parent.contains(descendant);
  }

  // W3C DOM Level 3
  if (typeof parent.compareDocumentPosition != 'undefined') {
    return parent == descendant ||
        Boolean(parent.compareDocumentPosition(descendant) & 16);
  }

  // W3C DOM Level 1
  while (descendant && parent != descendant) {
    descendant = descendant.parentNode;
  }
  return descendant == parent;
};


/**
 * Compares the document order of two nodes, returning 0 if they are the same
 * node, a negative number if node1 is before node2, and a positive number if
 * node2 is before node1.  Note that we compare the order the tags appear in the
 * document so in the tree <b><i>text</i></b> the B node is considered to be
 * before the I node.
 *
 * @param {Node} node1 The first node to compare.
 * @param {Node} node2 The second node to compare.
 * @return {number} 0 if the nodes are the same node, a negative number if node1
 *     is before node2, and a positive number if node2 is before node1.
 */
goog.dom.compareNodeOrder = function(node1, node2) {
  // Fall out quickly for equality.
  if (node1 == node2) {
    return 0;
  }

  // Use compareDocumentPosition where available
  if (node1.compareDocumentPosition) {
    // 4 is the bitmask for FOLLOWS.
    return node1.compareDocumentPosition(node2) & 2 ? 1 : -1;
  }

  // Special case for document nodes on IE 7 and 8.
  if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) {
    if (node1.nodeType == goog.dom.NodeType.DOCUMENT) {
      return -1;
    }
    if (node2.nodeType == goog.dom.NodeType.DOCUMENT) {
      return 1;
    }
  }

  // Process in IE using sourceIndex - we check to see if the first node has
  // a source index or if its parent has one.
  if ('sourceIndex' in node1 ||
      (node1.parentNode && 'sourceIndex' in node1.parentNode)) {
    var isElement1 = node1.nodeType == goog.dom.NodeType.ELEMENT;
    var isElement2 = node2.nodeType == goog.dom.NodeType.ELEMENT;

    if (isElement1 && isElement2) {
      return node1.sourceIndex - node2.sourceIndex;
    } else {
      var parent1 = node1.parentNode;
      var parent2 = node2.parentNode;

      if (parent1 == parent2) {
        return goog.dom.compareSiblingOrder_(node1, node2);
      }

      if (!isElement1 && goog.dom.contains(parent1, node2)) {
        return -1 * goog.dom.compareParentsDescendantNodeIe_(node1, node2);
      }


      if (!isElement2 && goog.dom.contains(parent2, node1)) {
        return goog.dom.compareParentsDescendantNodeIe_(node2, node1);
      }

      return (isElement1 ? node1.sourceIndex : parent1.sourceIndex) -
             (isElement2 ? node2.sourceIndex : parent2.sourceIndex);
    }
  }

  // For Safari, we compare ranges.
  var doc = goog.dom.getOwnerDocument(node1);

  var range1, range2;
  range1 = doc.createRange();
  range1.selectNode(node1);
  range1.collapse(true);

  range2 = doc.createRange();
  range2.selectNode(node2);
  range2.collapse(true);

  return range1.compareBoundaryPoints(goog.global['Range'].START_TO_END,
      range2);
};


/**
 * Utility function to compare the position of two nodes, when
 * {@code textNode}'s parent is an ancestor of {@code node}.  If this entry
 * condition is not met, this function will attempt to reference a null object.
 * @param {!Node} textNode The textNode to compare.
 * @param {Node} node The node to compare.
 * @return {number} -1 if node is before textNode, +1 otherwise.
 * @private
 */
goog.dom.compareParentsDescendantNodeIe_ = function(textNode, node) {
  var parent = textNode.parentNode;
  if (parent == node) {
    // If textNode is a child of node, then node comes first.
    return -1;
  }
  var sibling = node;
  while (sibling.parentNode != parent) {
    sibling = sibling.parentNode;
  }
  return goog.dom.compareSiblingOrder_(sibling, textNode);
};


/**
 * Utility function to compare the position of two nodes known to be non-equal
 * siblings.
 * @param {Node} node1 The first node to compare.
 * @param {!Node} node2 The second node to compare.
 * @return {number} -1 if node1 is before node2, +1 otherwise.
 * @private
 */
goog.dom.compareSiblingOrder_ = function(node1, node2) {
  var s = node2;
  while ((s = s.previousSibling)) {
    if (s == node1) {
      // We just found node1 before node2.
      return -1;
    }
  }

  // Since we didn't find it, node1 must be after node2.
  return 1;
};


/**
 * Find the deepest common ancestor of the given nodes.
 * @param {...Node} var_args The nodes to find a common ancestor of.
 * @return {Node} The common ancestor of the nodes, or null if there is none.
 *     null will only be returned if two or more of the nodes are from different
 *     documents.
 */
goog.dom.findCommonAncestor = function(var_args) {
  var i, count = arguments.length;
  if (!count) {
    return null;
  } else if (count == 1) {
    return arguments[0];
  }

  var paths = [];
  var minLength = Infinity;
  for (i = 0; i < count; i++) {
    // Compute the list of ancestors.
    var ancestors = [];
    var node = arguments[i];
    while (node) {
      ancestors.unshift(node);
      node = node.parentNode;
    }

    // Save the list for comparison.
    paths.push(ancestors);
    minLength = Math.min(minLength, ancestors.length);
  }
  var output = null;
  for (i = 0; i < minLength; i++) {
    var first = paths[0][i];
    for (var j = 1; j < count; j++) {
      if (first != paths[j][i]) {
        return output;
      }
    }
    output = first;
  }
  return output;
};


/**
 * Returns the owner document for a node.
 * @param {Node|Window} node The node to get the document for.
 * @return {!Document} The document owning the node.
 */
goog.dom.getOwnerDocument = function(node) {
  // TODO(nnaze): Update param signature to be non-nullable.
  goog.asserts.assert(node, 'Node cannot be null or undefined.');
  return /** @type {!Document} */ (
      node.nodeType == goog.dom.NodeType.DOCUMENT ? node :
      node.ownerDocument || node.document);
};


/**
 * Cross-browser function for getting the document element of a frame or iframe.
 * @param {Element} frame Frame element.
 * @return {!Document} The frame content document.
 */
goog.dom.getFrameContentDocument = function(frame) {
  var doc = frame.contentDocument || frame.contentWindow.document;
  return doc;
};


/**
 * Cross-browser function for getting the window of a frame or iframe.
 * @param {Element} frame Frame element.
 * @return {Window} The window associated with the given frame.
 */
goog.dom.getFrameContentWindow = function(frame) {
  return frame.contentWindow ||
      goog.dom.getWindow(goog.dom.getFrameContentDocument(frame));
};


/**
 * Sets the text content of a node, with cross-browser support.
 * @param {Node} node The node to change the text content of.
 * @param {string|number} text The value that should replace the node's content.
 */
goog.dom.setTextContent = function(node, text) {
  goog.asserts.assert(node != null,
      'goog.dom.setTextContent expects a non-null value for node');

  if ('textContent' in node) {
    node.textContent = text;
  } else if (node.nodeType == goog.dom.NodeType.TEXT) {
    node.data = text;
  } else if (node.firstChild &&
             node.firstChild.nodeType == goog.dom.NodeType.TEXT) {
    // If the first child is a text node we just change its data and remove the
    // rest of the children.
    while (node.lastChild != node.firstChild) {
      node.removeChild(node.lastChild);
    }
    node.firstChild.data = text;
  } else {
    goog.dom.removeChildren(node);
    var doc = goog.dom.getOwnerDocument(node);
    node.appendChild(doc.createTextNode(String(text)));
  }
};


/**
 * Gets the outerHTML of a node, which islike innerHTML, except that it
 * actually contains the HTML of the node itself.
 * @param {Element} element The element to get the HTML of.
 * @return {string} The outerHTML of the given element.
 */
goog.dom.getOuterHtml = function(element) {
  // IE, Opera and WebKit all have outerHTML.
  if ('outerHTML' in element) {
    return element.outerHTML;
  } else {
    var doc = goog.dom.getOwnerDocument(element);
    var div = doc.createElement(goog.dom.TagName.DIV);
    div.appendChild(element.cloneNode(true));
    return div.innerHTML;
  }
};


/**
 * Finds the first descendant node that matches the filter function, using
 * a depth first search. This function offers the most general purpose way
 * of finding a matching element. You may also wish to consider
 * {@code goog.dom.query} which can express many matching criteria using
 * CSS selector expressions. These expressions often result in a more
 * compact representation of the desired result.
 * @see goog.dom.query
 *
 * @param {Node} root The root of the tree to search.
 * @param {function(Node) : boolean} p The filter function.
 * @return {Node|undefined} The found node or undefined if none is found.
 */
goog.dom.findNode = function(root, p) {
  var rv = [];
  var found = goog.dom.findNodes_(root, p, rv, true);
  return found ? rv[0] : undefined;
};


/**
 * Finds all the descendant nodes that match the filter function, using a
 * a depth first search. This function offers the most general-purpose way
 * of finding a set of matching elements. You may also wish to consider
 * {@code goog.dom.query} which can express many matching criteria using
 * CSS selector expressions. These expressions often result in a more
 * compact representation of the desired result.

 * @param {Node} root The root of the tree to search.
 * @param {function(Node) : boolean} p The filter function.
 * @return {!Array<!Node>} The found nodes or an empty array if none are found.
 */
goog.dom.findNodes = function(root, p) {
  var rv = [];
  goog.dom.findNodes_(root, p, rv, false);
  return rv;
};


/**
 * Finds the first or all the descendant nodes that match the filter function,
 * using a depth first search.
 * @param {Node} root The root of the tree to search.
 * @param {function(Node) : boolean} p The filter function.
 * @param {!Array<!Node>} rv The found nodes are added to this array.
 * @param {boolean} findOne If true we exit after the first found node.
 * @return {boolean} Whether the search is complete or not. True in case findOne
 *     is true and the node is found. False otherwise.
 * @private
 */
goog.dom.findNodes_ = function(root, p, rv, findOne) {
  if (root != null) {
    var child = root.firstChild;
    while (child) {
      if (p(child)) {
        rv.push(child);
        if (findOne) {
          return true;
        }
      }
      if (goog.dom.findNodes_(child, p, rv, findOne)) {
        return true;
      }
      child = child.nextSibling;
    }
  }
  return false;
};


/**
 * Map of tags whose content to ignore when calculating text length.
 * @private {!Object<string, number>}
 * @const
 */
goog.dom.TAGS_TO_IGNORE_ = {
  'SCRIPT': 1,
  'STYLE': 1,
  'HEAD': 1,
  'IFRAME': 1,
  'OBJECT': 1
};


/**
 * Map of tags which have predefined values with regard to whitespace.
 * @private {!Object<string, string>}
 * @const
 */
goog.dom.PREDEFINED_TAG_VALUES_ = {'IMG': ' ', 'BR': '\n'};


/**
 * Returns true if the element has a tab index that allows it to receive
 * keyboard focus (tabIndex >= 0), false otherwise.  Note that some elements
 * natively support keyboard focus, even if they have no tab index.
 * @param {!Element} element Element to check.
 * @return {boolean} Whether the element has a tab index that allows keyboard
 *     focus.
 * @see http://fluidproject.org/blog/2008/01/09/getting-setting-and-removing-tabindex-values-with-javascript/
 */
goog.dom.isFocusableTabIndex = function(element) {
  return goog.dom.hasSpecifiedTabIndex_(element) &&
         goog.dom.isTabIndexFocusable_(element);
};


/**
 * Enables or disables keyboard focus support on the element via its tab index.
 * Only elements for which {@link goog.dom.isFocusableTabIndex} returns true
 * (or elements that natively support keyboard focus, like form elements) can
 * receive keyboard focus.  See http://go/tabindex for more info.
 * @param {Element} element Element whose tab index is to be changed.
 * @param {boolean} enable Whether to set or remove a tab index on the element
 *     that supports keyboard focus.
 */
goog.dom.setFocusableTabIndex = function(element, enable) {
  if (enable) {
    element.tabIndex = 0;
  } else {
    // Set tabIndex to -1 first, then remove it. This is a workaround for
    // Safari (confirmed in version 4 on Windows). When removing the attribute
    // without setting it to -1 first, the element remains keyboard focusable
    // despite not having a tabIndex attribute anymore.
    element.tabIndex = -1;
    element.removeAttribute('tabIndex'); // Must be camelCase!
  }
};


/**
 * Returns true if the element can be focused, i.e. it has a tab index that
 * allows it to receive keyboard focus (tabIndex >= 0), or it is an element
 * that natively supports keyboard focus.
 * @param {!Element} element Element to check.
 * @return {boolean} Whether the element allows keyboard focus.
 */
goog.dom.isFocusable = function(element) {
  var focusable;
  // Some elements can have unspecified tab index and still receive focus.
  if (goog.dom.nativelySupportsFocus_(element)) {
    // Make sure the element is not disabled ...
    focusable = !element.disabled &&
        // ... and if a tab index is specified, it allows focus.
        (!goog.dom.hasSpecifiedTabIndex_(element) ||
         goog.dom.isTabIndexFocusable_(element));
  } else {
    focusable = goog.dom.isFocusableTabIndex(element);
  }

  // IE requires elements to be visible in order to focus them.
  return focusable && goog.userAgent.IE ?
             goog.dom.hasNonZeroBoundingRect_(element) : focusable;
};


/**
 * Returns true if the element has a specified tab index.
 * @param {!Element} element Element to check.
 * @return {boolean} Whether the element has a specified tab index.
 * @private
 */
goog.dom.hasSpecifiedTabIndex_ = function(element) {
  // IE returns 0 for an unset tabIndex, so we must use getAttributeNode(),
  // which returns an object with a 'specified' property if tabIndex is
  // specified.  This works on other browsers, too.
  var attrNode = element.getAttributeNode('tabindex'); // Must be lowercase!
  return goog.isDefAndNotNull(attrNode) && attrNode.specified;
};


/**
 * Returns true if the element's tab index allows the element to be focused.
 * @param {!Element} element Element to check.
 * @return {boolean} Whether the element's tab index allows focus.
 * @private
 */
goog.dom.isTabIndexFocusable_ = function(element) {
  var index = element.tabIndex;
  // NOTE: IE9 puts tabIndex in 16-bit int, e.g. -2 is 65534.
  return goog.isNumber(index) && index >= 0 && index < 32768;
};


/**
 * Returns true if the element is focusable even when tabIndex is not set.
 * @param {!Element} element Element to check.
 * @return {boolean} Whether the element natively supports focus.
 * @private
 */
goog.dom.nativelySupportsFocus_ = function(element) {
  return element.tagName == goog.dom.TagName.A ||
         element.tagName == goog.dom.TagName.INPUT ||
         element.tagName == goog.dom.TagName.TEXTAREA ||
         element.tagName == goog.dom.TagName.SELECT ||
         element.tagName == goog.dom.TagName.BUTTON;
};


/**
 * Returns true if the element has a bounding rectangle that would be visible
 * (i.e. its width and height are greater than zero).
 * @param {!Element} element Element to check.
 * @return {boolean} Whether the element has a non-zero bounding rectangle.
 * @private
 */
goog.dom.hasNonZeroBoundingRect_ = function(element) {
  var rect = goog.isFunction(element['getBoundingClientRect']) ?
      element.getBoundingClientRect() :
      {'height': element.offsetHeight, 'width': element.offsetWidth};
  return goog.isDefAndNotNull(rect) && rect.height > 0 && rect.width > 0;
};


/**
 * Returns the text content of the current node, without markup and invisible
 * symbols. New lines are stripped and whitespace is collapsed,
 * such that each character would be visible.
 *
 * In browsers that support it, innerText is used.  Other browsers attempt to
 * simulate it via node traversal.  Line breaks are canonicalized in IE.
 *
 * @param {Node} node The node from which we are getting content.
 * @return {string} The text content.
 */
goog.dom.getTextContent = function(node) {
  var textContent;
  // Note(arv): IE9, Opera, and Safari 3 support innerText but they include
  // text nodes in script tags. So we revert to use a user agent test here.
  if (goog.dom.BrowserFeature.CAN_USE_INNER_TEXT && ('innerText' in node)) {
    textContent = goog.string.canonicalizeNewlines(node.innerText);
    // Unfortunately .innerText() returns text with &shy; symbols
    // We need to filter it out and then remove duplicate whitespaces
  } else {
    var buf = [];
    goog.dom.getTextContent_(node, buf, true);
    textContent = buf.join('');
  }

  // Strip &shy; entities. goog.format.insertWordBreaks inserts them in Opera.
  textContent = textContent.replace(/ \xAD /g, ' ').replace(/\xAD/g, '');
  // Strip &#8203; entities. goog.format.insertWordBreaks inserts them in IE8.
  textContent = textContent.replace(/\u200B/g, '');

  // Skip this replacement on old browsers with working innerText, which
  // automatically turns &nbsp; into ' ' and / +/ into ' ' when reading
  // innerText.
  if (!goog.dom.BrowserFeature.CAN_USE_INNER_TEXT) {
    textContent = textContent.replace(/ +/g, ' ');
  }
  if (textContent != ' ') {
    textContent = textContent.replace(/^\s*/, '');
  }

  return textContent;
};


/**
 * Returns the text content of the current node, without markup.
 *
 * Unlike {@code getTextContent} this method does not collapse whitespaces
 * or normalize lines breaks.
 *
 * @param {Node} node The node from which we are getting content.
 * @return {string} The raw text content.
 */
goog.dom.getRawTextContent = function(node) {
  var buf = [];
  goog.dom.getTextContent_(node, buf, false);

  return buf.join('');
};


/**
 * Recursive support function for text content retrieval.
 *
 * @param {Node} node The node from which we are getting content.
 * @param {Array<string>} buf string buffer.
 * @param {boolean} normalizeWhitespace Whether to normalize whitespace.
 * @private
 */
goog.dom.getTextContent_ = function(node, buf, normalizeWhitespace) {
  if (node.nodeName in goog.dom.TAGS_TO_IGNORE_) {
    // ignore certain tags
  } else if (node.nodeType == goog.dom.NodeType.TEXT) {
    if (normalizeWhitespace) {
      buf.push(String(node.nodeValue).replace(/(\r\n|\r|\n)/g, ''));
    } else {
      buf.push(node.nodeValue);
    }
  } else if (node.nodeName in goog.dom.PREDEFINED_TAG_VALUES_) {
    buf.push(goog.dom.PREDEFINED_TAG_VALUES_[node.nodeName]);
  } else {
    var child = node.firstChild;
    while (child) {
      goog.dom.getTextContent_(child, buf, normalizeWhitespace);
      child = child.nextSibling;
    }
  }
};


/**
 * Returns the text length of the text contained in a node, without markup. This
 * is equivalent to the selection length if the node was selected, or the number
 * of cursor movements to traverse the node. Images & BRs take one space.  New
 * lines are ignored.
 *
 * @param {Node} node The node whose text content length is being calculated.
 * @return {number} The length of {@code node}'s text content.
 */
goog.dom.getNodeTextLength = function(node) {
  return goog.dom.getTextContent(node).length;
};


/**
 * Returns the text offset of a node relative to one of its ancestors. The text
 * length is the same as the length calculated by goog.dom.getNodeTextLength.
 *
 * @param {Node} node The node whose offset is being calculated.
 * @param {Node=} opt_offsetParent The node relative to which the offset will
 *     be calculated. Defaults to the node's owner document's body.
 * @return {number} The text offset.
 */
goog.dom.getNodeTextOffset = function(node, opt_offsetParent) {
  var root = opt_offsetParent || goog.dom.getOwnerDocument(node).body;
  var buf = [];
  while (node && node != root) {
    var cur = node;
    while ((cur = cur.previousSibling)) {
      buf.unshift(goog.dom.getTextContent(cur));
    }
    node = node.parentNode;
  }
  // Trim left to deal with FF cases when there might be line breaks and empty
  // nodes at the front of the text
  return goog.string.trimLeft(buf.join('')).replace(/ +/g, ' ').length;
};


/**
 * Returns the node at a given offset in a parent node.  If an object is
 * provided for the optional third parameter, the node and the remainder of the
 * offset will stored as properties of this object.
 * @param {Node} parent The parent node.
 * @param {number} offset The offset into the parent node.
 * @param {Object=} opt_result Object to be used to store the return value. The
 *     return value will be stored in the form {node: Node, remainder: number}
 *     if this object is provided.
 * @return {Node} The node at the given offset.
 */
goog.dom.getNodeAtOffset = function(parent, offset, opt_result) {
  var stack = [parent], pos = 0, cur = null;
  while (stack.length > 0 && pos < offset) {
    cur = stack.pop();
    if (cur.nodeName in goog.dom.TAGS_TO_IGNORE_) {
      // ignore certain tags
    } else if (cur.nodeType == goog.dom.NodeType.TEXT) {
      var text = cur.nodeValue.replace(/(\r\n|\r|\n)/g, '').replace(/ +/g, ' ');
      pos += text.length;
    } else if (cur.nodeName in goog.dom.PREDEFINED_TAG_VALUES_) {
      pos += goog.dom.PREDEFINED_TAG_VALUES_[cur.nodeName].length;
    } else {
      for (var i = cur.childNodes.length - 1; i >= 0; i--) {
        stack.push(cur.childNodes[i]);
      }
    }
  }
  if (goog.isObject(opt_result)) {
    opt_result.remainder = cur ? cur.nodeValue.length + offset - pos - 1 : 0;
    opt_result.node = cur;
  }

  return cur;
};


/**
 * Returns true if the object is a {@code NodeList}.  To qualify as a NodeList,
 * the object must have a numeric length property and an item function (which
 * has type 'string' on IE for some reason).
 * @param {Object} val Object to test.
 * @return {boolean} Whether the object is a NodeList.
 */
goog.dom.isNodeList = function(val) {
  // TODO(attila): Now the isNodeList is part of goog.dom we can use
  // goog.userAgent to make this simpler.
  // A NodeList must have a length property of type 'number' on all platforms.
  if (val && typeof val.length == 'number') {
    // A NodeList is an object everywhere except Safari, where it's a function.
    if (goog.isObject(val)) {
      // A NodeList must have an item function (on non-IE platforms) or an item
      // property of type 'string' (on IE).
      return typeof val.item == 'function' || typeof val.item == 'string';
    } else if (goog.isFunction(val)) {
      // On Safari, a NodeList is a function with an item property that is also
      // a function.
      return typeof val.item == 'function';
    }
  }

  // Not a NodeList.
  return false;
};


/**
 * Walks up the DOM hierarchy returning the first ancestor that has the passed
 * tag name and/or class name. If the passed element matches the specified
 * criteria, the element itself is returned.
 * @param {Node} element The DOM node to start with.
 * @param {?(goog.dom.TagName|string)=} opt_tag The tag name to match (or
 *     null/undefined to match only based on class name).
 * @param {?string=} opt_class The class name to match (or null/undefined to
 *     match only based on tag name).
 * @param {number=} opt_maxSearchSteps Maximum number of levels to search up the
 *     dom.
 * @return {Element} The first ancestor that matches the passed criteria, or
 *     null if no match is found.
 */
goog.dom.getAncestorByTagNameAndClass = function(element, opt_tag, opt_class,
    opt_maxSearchSteps) {
  if (!opt_tag && !opt_class) {
    return null;
  }
  var tagName = opt_tag ? opt_tag.toUpperCase() : null;
  return /** @type {Element} */ (goog.dom.getAncestor(element,
      function(node) {
        return (!tagName || node.nodeName == tagName) &&
               (!opt_class || goog.isString(node.className) &&
                   goog.array.contains(node.className.split(/\s+/), opt_class));
      }, true, opt_maxSearchSteps));
};


/**
 * Walks up the DOM hierarchy returning the first ancestor that has the passed
 * class name. If the passed element matches the specified criteria, the
 * element itself is returned.
 * @param {Node} element The DOM node to start with.
 * @param {string} className The class name to match.
 * @param {number=} opt_maxSearchSteps Maximum number of levels to search up the
 *     dom.
 * @return {Element} The first ancestor that matches the passed criteria, or
 *     null if none match.
 */
goog.dom.getAncestorByClass = function(element, className, opt_maxSearchSteps) {
  return goog.dom.getAncestorByTagNameAndClass(element, null, className,
      opt_maxSearchSteps);
};


/**
 * Walks up the DOM hierarchy returning the first ancestor that passes the
 * matcher function.
 * @param {Node} element The DOM node to start with.
 * @param {function(Node) : boolean} matcher A function that returns true if the
 *     passed node matches the desired criteria.
 * @param {boolean=} opt_includeNode If true, the node itself is included in
 *     the search (the first call to the matcher will pass startElement as
 *     the node to test).
 * @param {number=} opt_maxSearchSteps Maximum number of levels to search up the
 *     dom.
 * @return {Node} DOM node that matched the matcher, or null if there was
 *     no match.
 */
goog.dom.getAncestor = function(
    element, matcher, opt_includeNode, opt_maxSearchSteps) {
  if (!opt_includeNode) {
    element = element.parentNode;
  }
  var ignoreSearchSteps = opt_maxSearchSteps == null;
  var steps = 0;
  while (element && (ignoreSearchSteps || steps <= opt_maxSearchSteps)) {
    goog.asserts.assert(element.name != 'parentNode');
    if (matcher(element)) {
      return element;
    }
    element = element.parentNode;
    steps++;
  }
  // Reached the root of the DOM without a match
  return null;
};


/**
 * Determines the active element in the given document.
 * @param {Document} doc The document to look in.
 * @return {Element} The active element.
 */
goog.dom.getActiveElement = function(doc) {
  try {
    return doc && doc.activeElement;
  } catch (e) {
    // NOTE(nicksantos): Sometimes, evaluating document.activeElement in IE
    // throws an exception. I'm not 100% sure why, but I suspect it chokes
    // on document.activeElement if the activeElement has been recently
    // removed from the DOM by a JS operation.
    //
    // We assume that an exception here simply means
    // "there is no active element."
  }

  return null;
};


/**
 * Gives the current devicePixelRatio.
 *
 * By default, this is the value of window.devicePixelRatio (which should be
 * preferred if present).
 *
 * If window.devicePixelRatio is not present, the ratio is calculated with
 * window.matchMedia, if present. Otherwise, gives 1.0.
 *
 * Some browsers (including Chrome) consider the browser zoom level in the pixel
 * ratio, so the value may change across multiple calls.
 *
 * @return {number} The number of actual pixels per virtual pixel.
 */
goog.dom.getPixelRatio = function() {
  var win = goog.dom.getWindow();
  if (goog.isDef(win.devicePixelRatio)) {
    return win.devicePixelRatio;
  } else if (win.matchMedia) {
    return goog.dom.matchesPixelRatio_(.75) ||
           goog.dom.matchesPixelRatio_(1.5) ||
           goog.dom.matchesPixelRatio_(2) ||
           goog.dom.matchesPixelRatio_(3) || 1;
  }
  return 1;
};


/**
 * Calculates a mediaQuery to check if the current device supports the
 * given actual to virtual pixel ratio.
 * @param {number} pixelRatio The ratio of actual pixels to virtual pixels.
 * @return {number} pixelRatio if applicable, otherwise 0.
 * @private
 */
goog.dom.matchesPixelRatio_ = function(pixelRatio) {
  var win = goog.dom.getWindow();
  var query = ('(-webkit-min-device-pixel-ratio: ' + pixelRatio + '),' +
               '(min--moz-device-pixel-ratio: ' + pixelRatio + '),' +
               '(min-resolution: ' + pixelRatio + 'dppx)');
  return win.matchMedia(query).matches ? pixelRatio : 0;
};



/**
 * Create an instance of a DOM helper with a new document object.
 * @param {Document=} opt_document Document object to associate with this
 *     DOM helper.
 * @constructor
 */
goog.dom.DomHelper = function(opt_document) {
  /**
   * Reference to the document object to use
   * @type {!Document}
   * @private
   */
  this.document_ = opt_document || goog.global.document || document;
};


/**
 * Gets the dom helper object for the document where the element resides.
 * @param {Node=} opt_node If present, gets the DomHelper for this node.
 * @return {!goog.dom.DomHelper} The DomHelper.
 */
goog.dom.DomHelper.prototype.getDomHelper = goog.dom.getDomHelper;


/**
 * Sets the document object.
 * @param {!Document} document Document object.
 */
goog.dom.DomHelper.prototype.setDocument = function(document) {
  this.document_ = document;
};


/**
 * Gets the document object being used by the dom library.
 * @return {!Document} Document object.
 */
goog.dom.DomHelper.prototype.getDocument = function() {
  return this.document_;
};


/**
 * Alias for {@code getElementById}. If a DOM node is passed in then we just
 * return that.
 * @param {string|Element} element Element ID or a DOM node.
 * @return {Element} The element with the given ID, or the node passed in.
 */
goog.dom.DomHelper.prototype.getElement = function(element) {
  return goog.dom.getElementHelper_(this.document_, element);
};


/**
 * Gets an element by id, asserting that the element is found.
 *
 * This is used when an element is expected to exist, and should fail with
 * an assertion error if it does not (if assertions are enabled).
 *
 * @param {string} id Element ID.
 * @return {!Element} The element with the given ID, if it exists.
 */
goog.dom.DomHelper.prototype.getRequiredElement = function(id) {
  return goog.dom.getRequiredElementHelper_(this.document_, id);
};


/**
 * Alias for {@code getElement}.
 * @param {string|Element} element Element ID or a DOM node.
 * @return {Element} The element with the given ID, or the node passed in.
 * @deprecated Use {@link goog.dom.DomHelper.prototype.getElement} instead.
 */
goog.dom.DomHelper.prototype.$ = goog.dom.DomHelper.prototype.getElement;


/**
 * Looks up elements by both tag and class name, using browser native functions
 * ({@code querySelectorAll}, {@code getElementsByTagName} or
 * {@code getElementsByClassName}) where possible. The returned array is a live
 * NodeList or a static list depending on the code path taken.
 *
 * @see goog.dom.query
 *
 * @param {?string=} opt_tag Element tag name or * for all tags.
 * @param {?string=} opt_class Optional class name.
 * @param {(Document|Element)=} opt_el Optional element to look in.
 * @return { {length: number} } Array-like list of elements (only a length
 *     property and numerical indices are guaranteed to exist).
 */
goog.dom.DomHelper.prototype.getElementsByTagNameAndClass = function(opt_tag,
                                                                     opt_class,
                                                                     opt_el) {
  return goog.dom.getElementsByTagNameAndClass_(this.document_, opt_tag,
                                                opt_class, opt_el);
};


/**
 * Returns an array of all the elements with the provided className.
 * @see {goog.dom.query}
 * @param {string} className the name of the class to look for.
 * @param {Element|Document=} opt_el Optional element to look in.
 * @return { {length: number} } The items found with the class name provided.
 */
goog.dom.DomHelper.prototype.getElementsByClass = function(className, opt_el) {
  var doc = opt_el || this.document_;
  return goog.dom.getElementsByClass(className, doc);
};


/**
 * Returns the first element we find matching the provided class name.
 * @see {goog.dom.query}
 * @param {string} className the name of the class to look for.
 * @param {(Element|Document)=} opt_el Optional element to look in.
 * @return {Element} The first item found with the class name provided.
 */
goog.dom.DomHelper.prototype.getElementByClass = function(className, opt_el) {
  var doc = opt_el || this.document_;
  return goog.dom.getElementByClass(className, doc);
};


/**
 * Ensures an element with the given className exists, and then returns the
 * first element with the provided className.
 * @see {goog.dom.query}
 * @param {string} className the name of the class to look for.
 * @param {(!Element|!Document)=} opt_root Optional element or document to look
 *     in.
 * @return {!Element} The first item found with the class name provided.
 * @throws {goog.asserts.AssertionError} Thrown if no element is found.
 */
goog.dom.DomHelper.prototype.getRequiredElementByClass = function(className,
                                                                  opt_root) {
  var root = opt_root || this.document_;
  return goog.dom.getRequiredElementByClass(className, root);
};


/**
 * Alias for {@code getElementsByTagNameAndClass}.
 * @deprecated Use DomHelper getElementsByTagNameAndClass.
 * @see goog.dom.query
 *
 * @param {?string=} opt_tag Element tag name.
 * @param {?string=} opt_class Optional class name.
 * @param {Element=} opt_el Optional element to look in.
 * @return { {length: number} } Array-like list of elements (only a length
 *     property and numerical indices are guaranteed to exist).
 */
goog.dom.DomHelper.prototype.$$ =
    goog.dom.DomHelper.prototype.getElementsByTagNameAndClass;


/**
 * Sets a number of properties on a node.
 * @param {Element} element DOM node to set properties on.
 * @param {Object} properties Hash of property:value pairs.
 */
goog.dom.DomHelper.prototype.setProperties = goog.dom.setProperties;


/**
 * Gets the dimensions of the viewport.
 * @param {Window=} opt_window Optional window element to test. Defaults to
 *     the window of the Dom Helper.
 * @return {!goog.math.Size} Object with values 'width' and 'height'.
 */
goog.dom.DomHelper.prototype.getViewportSize = function(opt_window) {
  // TODO(arv): This should not take an argument. That breaks the rule of a
  // a DomHelper representing a single frame/window/document.
  return goog.dom.getViewportSize(opt_window || this.getWindow());
};


/**
 * Calculates the height of the document.
 *
 * @return {number} The height of the document.
 */
goog.dom.DomHelper.prototype.getDocumentHeight = function() {
  return goog.dom.getDocumentHeight_(this.getWindow());
};


/**
 * Typedef for use with goog.dom.createDom and goog.dom.append.
 * @typedef {Object|string|Array|NodeList}
 */
goog.dom.Appendable;


/**
 * Returns a dom node with a set of attributes.  This function accepts varargs
 * for subsequent nodes to be added.  Subsequent nodes will be added to the
 * first node as childNodes.
 *
 * So:
 * <code>createDom('div', null, createDom('p'), createDom('p'));</code>
 * would return a div with two child paragraphs
 *
 * An easy way to move all child nodes of an existing element to a new parent
 * element is:
 * <code>createDom('div', null, oldElement.childNodes);</code>
 * which will remove all child nodes from the old element and add them as
 * child nodes of the new DIV.
 *
 * @param {string} tagName Tag to create.
 * @param {Object|string=} opt_attributes If object, then a map of name-value
 *     pairs for attributes. If a string, then this is the className of the new
 *     element.
 * @param {...goog.dom.Appendable} var_args Further DOM nodes or
 *     strings for text nodes. If one of the var_args is an array or
 *     NodeList, its elements will be added as childNodes instead.
 * @return {!Element} Reference to a DOM node.
 */
goog.dom.DomHelper.prototype.createDom = function(tagName,
                                                  opt_attributes,
                                                  var_args) {
  return goog.dom.createDom_(this.document_, arguments);
};


/**
 * Alias for {@code createDom}.
 * @param {string} tagName Tag to create.
 * @param {(Object|string)=} opt_attributes If object, then a map of name-value
 *     pairs for attributes. If a string, then this is the className of the new
 *     element.
 * @param {...goog.dom.Appendable} var_args Further DOM nodes or strings for
 *     text nodes.  If one of the var_args is an array, its children will be
 *     added as childNodes instead.
 * @return {!Element} Reference to a DOM node.
 * @deprecated Use {@link goog.dom.DomHelper.prototype.createDom} instead.
 */
goog.dom.DomHelper.prototype.$dom = goog.dom.DomHelper.prototype.createDom;


/**
 * Creates a new element.
 * @param {string} name Tag name.
 * @return {!Element} The new element.
 */
goog.dom.DomHelper.prototype.createElement = function(name) {
  return this.document_.createElement(name);
};


/**
 * Creates a new text node.
 * @param {number|string} content Content.
 * @return {!Text} The new text node.
 */
goog.dom.DomHelper.prototype.createTextNode = function(content) {
  return this.document_.createTextNode(String(content));
};


/**
 * Create a table.
 * @param {number} rows The number of rows in the table.  Must be >= 1.
 * @param {number} columns The number of columns in the table.  Must be >= 1.
 * @param {boolean=} opt_fillWithNbsp If true, fills table entries with
 *     {@code goog.string.Unicode.NBSP} characters.
 * @return {!HTMLElement} The created table.
 */
goog.dom.DomHelper.prototype.createTable = function(rows, columns,
    opt_fillWithNbsp) {
  return goog.dom.createTable_(this.document_, rows, columns,
      !!opt_fillWithNbsp);
};


/**
 * Converts an HTML into a node or a document fragment. A single Node is used if
 * {@code html} only generates a single node. If {@code html} generates multiple
 * nodes then these are put inside a {@code DocumentFragment}.
 * @param {!goog.html.SafeHtml} html The HTML markup to convert.
 * @return {!Node} The resulting node.
 */
goog.dom.DomHelper.prototype.safeHtmlToNode = function(html) {
  return goog.dom.safeHtmlToNode_(this.document_, html);
};


/**
 * Converts an HTML string into a node or a document fragment.  A single Node
 * is used if the {@code htmlString} only generates a single node.  If the
 * {@code htmlString} generates multiple nodes then these are put inside a
 * {@code DocumentFragment}.
 *
 * @param {string} htmlString The HTML string to convert.
 * @return {!Node} The resulting node.
 */
goog.dom.DomHelper.prototype.htmlToDocumentFragment = function(htmlString) {
  return goog.dom.htmlToDocumentFragment_(this.document_, htmlString);
};


/**
 * Returns true if the browser is in "CSS1-compatible" (standards-compliant)
 * mode, false otherwise.
 * @return {boolean} True if in CSS1-compatible mode.
 */
goog.dom.DomHelper.prototype.isCss1CompatMode = function() {
  return goog.dom.isCss1CompatMode_(this.document_);
};


/**
 * Gets the window object associated with the document.
 * @return {!Window} The window associated with the given document.
 */
goog.dom.DomHelper.prototype.getWindow = function() {
  return goog.dom.getWindow_(this.document_);
};


/**
 * Gets the document scroll element.
 * @return {!Element} Scrolling element.
 */
goog.dom.DomHelper.prototype.getDocumentScrollElement = function() {
  return goog.dom.getDocumentScrollElement_(this.document_);
};


/**
 * Gets the document scroll distance as a coordinate object.
 * @return {!goog.math.Coordinate} Object with properties 'x' and 'y'.
 */
goog.dom.DomHelper.prototype.getDocumentScroll = function() {
  return goog.dom.getDocumentScroll_(this.document_);
};


/**
 * Determines the active element in the given document.
 * @param {Document=} opt_doc The document to look in.
 * @return {Element} The active element.
 */
goog.dom.DomHelper.prototype.getActiveElement = function(opt_doc) {
  return goog.dom.getActiveElement(opt_doc || this.document_);
};


/**
 * Appends a child to a node.
 * @param {Node} parent Parent.
 * @param {Node} child Child.
 */
goog.dom.DomHelper.prototype.appendChild = goog.dom.appendChild;


/**
 * Appends a node with text or other nodes.
 * @param {!Node} parent The node to append nodes to.
 * @param {...goog.dom.Appendable} var_args The things to append to the node.
 *     If this is a Node it is appended as is.
 *     If this is a string then a text node is appended.
 *     If this is an array like object then fields 0 to length - 1 are appended.
 */
goog.dom.DomHelper.prototype.append = goog.dom.append;


/**
 * Determines if the given node can contain children, intended to be used for
 * HTML generation.
 *
 * @param {Node} node The node to check.
 * @return {boolean} Whether the node can contain children.
 */
goog.dom.DomHelper.prototype.canHaveChildren = goog.dom.canHaveChildren;


/**
 * Removes all the child nodes on a DOM node.
 * @param {Node} node Node to remove children from.
 */
goog.dom.DomHelper.prototype.removeChildren = goog.dom.removeChildren;


/**
 * Inserts a new node before an existing reference node (i.e., as the previous
 * sibling). If the reference node has no parent, then does nothing.
 * @param {Node} newNode Node to insert.
 * @param {Node} refNode Reference node to insert before.
 */
goog.dom.DomHelper.prototype.insertSiblingBefore = goog.dom.insertSiblingBefore;


/**
 * Inserts a new node after an existing reference node (i.e., as the next
 * sibling). If the reference node has no parent, then does nothing.
 * @param {Node} newNode Node to insert.
 * @param {Node} refNode Reference node to insert after.
 */
goog.dom.DomHelper.prototype.insertSiblingAfter = goog.dom.insertSiblingAfter;


/**
 * Insert a child at a given index. If index is larger than the number of child
 * nodes that the parent currently has, the node is inserted as the last child
 * node.
 * @param {Element} parent The element into which to insert the child.
 * @param {Node} child The element to insert.
 * @param {number} index The index at which to insert the new child node. Must
 *     not be negative.
 */
goog.dom.DomHelper.prototype.insertChildAt = goog.dom.insertChildAt;


/**
 * Removes a node from its parent.
 * @param {Node} node The node to remove.
 * @return {Node} The node removed if removed; else, null.
 */
goog.dom.DomHelper.prototype.removeNode = goog.dom.removeNode;


/**
 * Replaces a node in the DOM tree. Will do nothing if {@code oldNode} has no
 * parent.
 * @param {Node} newNode Node to insert.
 * @param {Node} oldNode Node to replace.
 */
goog.dom.DomHelper.prototype.replaceNode = goog.dom.replaceNode;


/**
 * Flattens an element. That is, removes it and replace it with its children.
 * @param {Element} element The element to flatten.
 * @return {Element|undefined} The original element, detached from the document
 *     tree, sans children, or undefined if the element was already not in the
 *     document.
 */
goog.dom.DomHelper.prototype.flattenElement = goog.dom.flattenElement;


/**
 * Returns an array containing just the element children of the given element.
 * @param {Element} element The element whose element children we want.
 * @return {!(Array|NodeList)} An array or array-like list of just the element
 *     children of the given element.
 */
goog.dom.DomHelper.prototype.getChildren = goog.dom.getChildren;


/**
 * Returns the first child node that is an element.
 * @param {Node} node The node to get the first child element of.
 * @return {Element} The first child node of {@code node} that is an element.
 */
goog.dom.DomHelper.prototype.getFirstElementChild =
    goog.dom.getFirstElementChild;


/**
 * Returns the last child node that is an element.
 * @param {Node} node The node to get the last child element of.
 * @return {Element} The last child node of {@code node} that is an element.
 */
goog.dom.DomHelper.prototype.getLastElementChild = goog.dom.getLastElementChild;


/**
 * Returns the first next sibling that is an element.
 * @param {Node} node The node to get the next sibling element of.
 * @return {Element} The next sibling of {@code node} that is an element.
 */
goog.dom.DomHelper.prototype.getNextElementSibling =
    goog.dom.getNextElementSibling;


/**
 * Returns the first previous sibling that is an element.
 * @param {Node} node The node to get the previous sibling element of.
 * @return {Element} The first previous sibling of {@code node} that is
 *     an element.
 */
goog.dom.DomHelper.prototype.getPreviousElementSibling =
    goog.dom.getPreviousElementSibling;


/**
 * Returns the next node in source order from the given node.
 * @param {Node} node The node.
 * @return {Node} The next node in the DOM tree, or null if this was the last
 *     node.
 */
goog.dom.DomHelper.prototype.getNextNode = goog.dom.getNextNode;


/**
 * Returns the previous node in source order from the given node.
 * @param {Node} node The node.
 * @return {Node} The previous node in the DOM tree, or null if this was the
 *     first node.
 */
goog.dom.DomHelper.prototype.getPreviousNode = goog.dom.getPreviousNode;


/**
 * Whether the object looks like a DOM node.
 * @param {?} obj The object being tested for node likeness.
 * @return {boolean} Whether the object looks like a DOM node.
 */
goog.dom.DomHelper.prototype.isNodeLike = goog.dom.isNodeLike;


/**
 * Whether the object looks like an Element.
 * @param {?} obj The object being tested for Element likeness.
 * @return {boolean} Whether the object looks like an Element.
 */
goog.dom.DomHelper.prototype.isElement = goog.dom.isElement;


/**
 * Returns true if the specified value is a Window object. This includes the
 * global window for HTML pages, and iframe windows.
 * @param {?} obj Variable to test.
 * @return {boolean} Whether the variable is a window.
 */
goog.dom.DomHelper.prototype.isWindow = goog.dom.isWindow;


/**
 * Returns an element's parent, if it's an Element.
 * @param {Element} element The DOM element.
 * @return {Element} The parent, or null if not an Element.
 */
goog.dom.DomHelper.prototype.getParentElement = goog.dom.getParentElement;


/**
 * Whether a node contains another node.
 * @param {Node} parent The node that should contain the other node.
 * @param {Node} descendant The node to test presence of.
 * @return {boolean} Whether the parent node contains the descendent node.
 */
goog.dom.DomHelper.prototype.contains = goog.dom.contains;


/**
 * Compares the document order of two nodes, returning 0 if they are the same
 * node, a negative number if node1 is before node2, and a positive number if
 * node2 is before node1.  Note that we compare the order the tags appear in the
 * document so in the tree <b><i>text</i></b> the B node is considered to be
 * before the I node.
 *
 * @param {Node} node1 The first node to compare.
 * @param {Node} node2 The second node to compare.
 * @return {number} 0 if the nodes are the same node, a negative number if node1
 *     is before node2, and a positive number if node2 is before node1.
 */
goog.dom.DomHelper.prototype.compareNodeOrder = goog.dom.compareNodeOrder;


/**
 * Find the deepest common ancestor of the given nodes.
 * @param {...Node} var_args The nodes to find a common ancestor of.
 * @return {Node} The common ancestor of the nodes, or null if there is none.
 *     null will only be returned if two or more of the nodes are from different
 *     documents.
 */
goog.dom.DomHelper.prototype.findCommonAncestor = goog.dom.findCommonAncestor;


/**
 * Returns the owner document for a node.
 * @param {Node} node The node to get the document for.
 * @return {!Document} The document owning the node.
 */
goog.dom.DomHelper.prototype.getOwnerDocument = goog.dom.getOwnerDocument;


/**
 * Cross browser function for getting the document element of an iframe.
 * @param {Element} iframe Iframe element.
 * @return {!Document} The frame content document.
 */
goog.dom.DomHelper.prototype.getFrameContentDocument =
    goog.dom.getFrameContentDocument;


/**
 * Cross browser function for getting the window of a frame or iframe.
 * @param {Element} frame Frame element.
 * @return {Window} The window associated with the given frame.
 */
goog.dom.DomHelper.prototype.getFrameContentWindow =
    goog.dom.getFrameContentWindow;


/**
 * Sets the text content of a node, with cross-browser support.
 * @param {Node} node The node to change the text content of.
 * @param {string|number} text The value that should replace the node's content.
 */
goog.dom.DomHelper.prototype.setTextContent = goog.dom.setTextContent;


/**
 * Gets the outerHTML of a node, which islike innerHTML, except that it
 * actually contains the HTML of the node itself.
 * @param {Element} element The element to get the HTML of.
 * @return {string} The outerHTML of the given element.
 */
goog.dom.DomHelper.prototype.getOuterHtml = goog.dom.getOuterHtml;


/**
 * Finds the first descendant node that matches the filter function. This does
 * a depth first search.
 * @param {Node} root The root of the tree to search.
 * @param {function(Node) : boolean} p The filter function.
 * @return {Node|undefined} The found node or undefined if none is found.
 */
goog.dom.DomHelper.prototype.findNode = goog.dom.findNode;


/**
 * Finds all the descendant nodes that matches the filter function. This does a
 * depth first search.
 * @param {Node} root The root of the tree to search.
 * @param {function(Node) : boolean} p The filter function.
 * @return {Array<Node>} The found nodes or an empty array if none are found.
 */
goog.dom.DomHelper.prototype.findNodes = goog.dom.findNodes;


/**
 * Returns true if the element has a tab index that allows it to receive
 * keyboard focus (tabIndex >= 0), false otherwise.  Note that some elements
 * natively support keyboard focus, even if they have no tab index.
 * @param {!Element} element Element to check.
 * @return {boolean} Whether the element has a tab index that allows keyboard
 *     focus.
 */
goog.dom.DomHelper.prototype.isFocusableTabIndex = goog.dom.isFocusableTabIndex;


/**
 * Enables or disables keyboard focus support on the element via its tab index.
 * Only elements for which {@link goog.dom.isFocusableTabIndex} returns true
 * (or elements that natively support keyboard focus, like form elements) can
 * receive keyboard focus.  See http://go/tabindex for more info.
 * @param {Element} element Element whose tab index is to be changed.
 * @param {boolean} enable Whether to set or remove a tab index on the element
 *     that supports keyboard focus.
 */
goog.dom.DomHelper.prototype.setFocusableTabIndex =
    goog.dom.setFocusableTabIndex;


/**
 * Returns true if the element can be focused, i.e. it has a tab index that
 * allows it to receive keyboard focus (tabIndex >= 0), or it is an element
 * that natively supports keyboard focus.
 * @param {!Element} element Element to check.
 * @return {boolean} Whether the element allows keyboard focus.
 */
goog.dom.DomHelper.prototype.isFocusable = goog.dom.isFocusable;


/**
 * Returns the text contents of the current node, without markup. New lines are
 * stripped and whitespace is collapsed, such that each character would be
 * visible.
 *
 * In browsers that support it, innerText is used.  Other browsers attempt to
 * simulate it via node traversal.  Line breaks are canonicalized in IE.
 *
 * @param {Node} node The node from which we are getting content.
 * @return {string} The text content.
 */
goog.dom.DomHelper.prototype.getTextContent = goog.dom.getTextContent;


/**
 * Returns the text length of the text contained in a node, without markup. This
 * is equivalent to the selection length if the node was selected, or the number
 * of cursor movements to traverse the node. Images & BRs take one space.  New
 * lines are ignored.
 *
 * @param {Node} node The node whose text content length is being calculated.
 * @return {number} The length of {@code node}'s text content.
 */
goog.dom.DomHelper.prototype.getNodeTextLength = goog.dom.getNodeTextLength;


/**
 * Returns the text offset of a node relative to one of its ancestors. The text
 * length is the same as the length calculated by
 * {@code goog.dom.getNodeTextLength}.
 *
 * @param {Node} node The node whose offset is being calculated.
 * @param {Node=} opt_offsetParent Defaults to the node's owner document's body.
 * @return {number} The text offset.
 */
goog.dom.DomHelper.prototype.getNodeTextOffset = goog.dom.getNodeTextOffset;


/**
 * Returns the node at a given offset in a parent node.  If an object is
 * provided for the optional third parameter, the node and the remainder of the
 * offset will stored as properties of this object.
 * @param {Node} parent The parent node.
 * @param {number} offset The offset into the parent node.
 * @param {Object=} opt_result Object to be used to store the return value. The
 *     return value will be stored in the form {node: Node, remainder: number}
 *     if this object is provided.
 * @return {Node} The node at the given offset.
 */
goog.dom.DomHelper.prototype.getNodeAtOffset = goog.dom.getNodeAtOffset;


/**
 * Returns true if the object is a {@code NodeList}.  To qualify as a NodeList,
 * the object must have a numeric length property and an item function (which
 * has type 'string' on IE for some reason).
 * @param {Object} val Object to test.
 * @return {boolean} Whether the object is a NodeList.
 */
goog.dom.DomHelper.prototype.isNodeList = goog.dom.isNodeList;


/**
 * Walks up the DOM hierarchy returning the first ancestor that has the passed
 * tag name and/or class name. If the passed element matches the specified
 * criteria, the element itself is returned.
 * @param {Node} element The DOM node to start with.
 * @param {?(goog.dom.TagName|string)=} opt_tag The tag name to match (or
 *     null/undefined to match only based on class name).
 * @param {?string=} opt_class The class name to match (or null/undefined to
 *     match only based on tag name).
 * @param {number=} opt_maxSearchSteps Maximum number of levels to search up the
 *     dom.
 * @return {Element} The first ancestor that matches the passed criteria, or
 *     null if no match is found.
 */
goog.dom.DomHelper.prototype.getAncestorByTagNameAndClass =
    goog.dom.getAncestorByTagNameAndClass;


/**
 * Walks up the DOM hierarchy returning the first ancestor that has the passed
 * class name. If the passed element matches the specified criteria, the
 * element itself is returned.
 * @param {Node} element The DOM node to start with.
 * @param {string} class The class name to match.
 * @param {number=} opt_maxSearchSteps Maximum number of levels to search up the
 *     dom.
 * @return {Element} The first ancestor that matches the passed criteria, or
 *     null if none match.
 */
goog.dom.DomHelper.prototype.getAncestorByClass =
    goog.dom.getAncestorByClass;


/**
 * Walks up the DOM hierarchy returning the first ancestor that passes the
 * matcher function.
 * @param {Node} element The DOM node to start with.
 * @param {function(Node) : boolean} matcher A function that returns true if the
 *     passed node matches the desired criteria.
 * @param {boolean=} opt_includeNode If true, the node itself is included in
 *     the search (the first call to the matcher will pass startElement as
 *     the node to test).
 * @param {number=} opt_maxSearchSteps Maximum number of levels to search up the
 *     dom.
 * @return {Node} DOM node that matched the matcher, or null if there was
 *     no match.
 */
goog.dom.DomHelper.prototype.getAncestor = goog.dom.getAncestor;

// Copyright 2012 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Utilities for detecting, adding and removing classes.  Prefer
 * this over goog.dom.classes for new code since it attempts to use classList
 * (DOMTokenList: http://dom.spec.whatwg.org/#domtokenlist) which is faster
 * and requires less code.
 *
 * Note: these utilities are meant to operate on HTMLElements
 * and may have unexpected behavior on elements with differing interfaces
 * (such as SVGElements).
 */


goog.provide('goog.dom.classlist');

goog.require('goog.array');


/**
 * Override this define at build-time if you know your target supports it.
 * @define {boolean} Whether to use the classList property (DOMTokenList).
 */
goog.define('goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST', false);


/**
 * Gets an array-like object of class names on an element.
 * @param {Element} element DOM node to get the classes of.
 * @return {!goog.array.ArrayLike} Class names on {@code element}.
 */
goog.dom.classlist.get = function(element) {
  if (goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST || element.classList) {
    return element.classList;
  }

  var className = element.className;
  // Some types of elements don't have a className in IE (e.g. iframes).
  // Furthermore, in Firefox, className is not a string when the element is
  // an SVG element.
  return goog.isString(className) && className.match(/\S+/g) || [];
};


/**
 * Sets the entire class name of an element.
 * @param {Element} element DOM node to set class of.
 * @param {string} className Class name(s) to apply to element.
 */
goog.dom.classlist.set = function(element, className) {
  element.className = className;
};


/**
 * Returns true if an element has a class.  This method may throw a DOM
 * exception for an invalid or empty class name if DOMTokenList is used.
 * @param {Element} element DOM node to test.
 * @param {string} className Class name to test for.
 * @return {boolean} Whether element has the class.
 */
goog.dom.classlist.contains = function(element, className) {
  if (goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST || element.classList) {
    return element.classList.contains(className);
  }
  return goog.array.contains(goog.dom.classlist.get(element), className);
};


/**
 * Adds a class to an element.  Does not add multiples of class names.  This
 * method may throw a DOM exception for an invalid or empty class name if
 * DOMTokenList is used.
 * @param {Element} element DOM node to add class to.
 * @param {string} className Class name to add.
 */
goog.dom.classlist.add = function(element, className) {
  if (goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST || element.classList) {
    element.classList.add(className);
    return;
  }

  if (!goog.dom.classlist.contains(element, className)) {
    // Ensure we add a space if this is not the first class name added.
    element.className += element.className.length > 0 ?
        (' ' + className) : className;
  }
};


/**
 * Convenience method to add a number of class names at once.
 * @param {Element} element The element to which to add classes.
 * @param {goog.array.ArrayLike<string>} classesToAdd An array-like object
 * containing a collection of class names to add to the element.
 * This method may throw a DOM exception if classesToAdd contains invalid
 * or empty class names.
 */
goog.dom.classlist.addAll = function(element, classesToAdd) {
  if (goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST || element.classList) {
    goog.array.forEach(classesToAdd, function(className) {
      goog.dom.classlist.add(element, className);
    });
    return;
  }

  var classMap = {};

  // Get all current class names into a map.
  goog.array.forEach(goog.dom.classlist.get(element),
      function(className) {
        classMap[className] = true;
      });

  // Add new class names to the map.
  goog.array.forEach(classesToAdd,
      function(className) {
        classMap[className] = true;
      });

  // Flatten the keys of the map into the className.
  element.className = '';
  for (var className in classMap) {
    element.className += element.className.length > 0 ?
        (' ' + className) : className;
  }
};


/**
 * Removes a class from an element.  This method may throw a DOM exception
 * for an invalid or empty class name if DOMTokenList is used.
 * @param {Element} element DOM node to remove class from.
 * @param {string} className Class name to remove.
 */
goog.dom.classlist.remove = function(element, className) {
  if (goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST || element.classList) {
    element.classList.remove(className);
    return;
  }

  if (goog.dom.classlist.contains(element, className)) {
    // Filter out the class name.
    element.className = goog.array.filter(
        goog.dom.classlist.get(element),
        function(c) {
          return c != className;
        }).join(' ');
  }
};


/**
 * Removes a set of classes from an element.  Prefer this call to
 * repeatedly calling {@code goog.dom.classlist.remove} if you want to remove
 * a large set of class names at once.
 * @param {Element} element The element from which to remove classes.
 * @param {goog.array.ArrayLike<string>} classesToRemove An array-like object
 * containing a collection of class names to remove from the element.
 * This method may throw a DOM exception if classesToRemove contains invalid
 * or empty class names.
 */
goog.dom.classlist.removeAll = function(element, classesToRemove) {
  if (goog.dom.classlist.ALWAYS_USE_DOM_TOKEN_LIST || element.classList) {
    goog.array.forEach(classesToRemove, function(className) {
      goog.dom.classlist.remove(element, className);
    });
    return;
  }
  // Filter out those classes in classesToRemove.
  element.className = goog.array.filter(
      goog.dom.classlist.get(element),
      function(className) {
        // If this class is not one we are trying to remove,
        // add it to the array of new class names.
        return !goog.array.contains(classesToRemove, className);
      }).join(' ');
};


/**
 * Adds or removes a class depending on the enabled argument.  This method
 * may throw a DOM exception for an invalid or empty class name if DOMTokenList
 * is used.
 * @param {Element} element DOM node to add or remove the class on.
 * @param {string} className Class name to add or remove.
 * @param {boolean} enabled Whether to add or remove the class (true adds,
 *     false removes).
 */
goog.dom.classlist.enable = function(element, className, enabled) {
  if (enabled) {
    goog.dom.classlist.add(element, className);
  } else {
    goog.dom.classlist.remove(element, className);
  }
};


/**
 * Adds or removes a set of classes depending on the enabled argument.  This
 * method may throw a DOM exception for an invalid or empty class name if
 * DOMTokenList is used.
 * @param {!Element} element DOM node to add or remove the class on.
 * @param {goog.array.ArrayLike<string>} classesToEnable An array-like object
 *     containing a collection of class names to add or remove from the element.
 * @param {boolean} enabled Whether to add or remove the classes (true adds,
 *     false removes).
 */
goog.dom.classlist.enableAll = function(element, classesToEnable, enabled) {
  var f = enabled ? goog.dom.classlist.addAll :
      goog.dom.classlist.removeAll;
  f(element, classesToEnable);
};


/**
 * Switches a class on an element from one to another without disturbing other
 * classes. If the fromClass isn't removed, the toClass won't be added.  This
 * method may throw a DOM exception if the class names are empty or invalid.
 * @param {Element} element DOM node to swap classes on.
 * @param {string} fromClass Class to remove.
 * @param {string} toClass Class to add.
 * @return {boolean} Whether classes were switched.
 */
goog.dom.classlist.swap = function(element, fromClass, toClass) {
  if (goog.dom.classlist.contains(element, fromClass)) {
    goog.dom.classlist.remove(element, fromClass);
    goog.dom.classlist.add(element, toClass);
    return true;
  }
  return false;
};


/**
 * Removes a class if an element has it, and adds it the element doesn't have
 * it.  Won't affect other classes on the node.  This method may throw a DOM
 * exception if the class name is empty or invalid.
 * @param {Element} element DOM node to toggle class on.
 * @param {string} className Class to toggle.
 * @return {boolean} True if class was added, false if it was removed
 *     (in other words, whether element has the class after this function has
 *     been called).
 */
goog.dom.classlist.toggle = function(element, className) {
  var add = !goog.dom.classlist.contains(element, className);
  goog.dom.classlist.enable(element, className, add);
  return add;
};


/**
 * Adds and removes a class of an element.  Unlike
 * {@link goog.dom.classlist.swap}, this method adds the classToAdd regardless
 * of whether the classToRemove was present and had been removed.  This method
 * may throw a DOM exception if the class names are empty or invalid.
 *
 * @param {Element} element DOM node to swap classes on.
 * @param {string} classToRemove Class to remove.
 * @param {string} classToAdd Class to add.
 */
goog.dom.classlist.addRemove = function(element, classToRemove, classToAdd) {
  goog.dom.classlist.remove(element, classToRemove);
  goog.dom.classlist.add(element, classToAdd);
};

// Copyright 2012 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Vendor prefix getters.
 */

goog.provide('goog.dom.vendor');

goog.require('goog.string');
goog.require('goog.userAgent');


/**
 * Returns the JS vendor prefix used in CSS properties. Different vendors
 * use different methods of changing the case of the property names.
 *
 * @return {?string} The JS vendor prefix or null if there is none.
 */
goog.dom.vendor.getVendorJsPrefix = function() {
  if (goog.userAgent.WEBKIT) {
    return 'Webkit';
  } else if (goog.userAgent.GECKO) {
    return 'Moz';
  } else if (goog.userAgent.IE) {
    return 'ms';
  } else if (goog.userAgent.OPERA) {
    return 'O';
  }

  return null;
};


/**
 * Returns the vendor prefix used in CSS properties.
 *
 * @return {?string} The vendor prefix or null if there is none.
 */
goog.dom.vendor.getVendorPrefix = function() {
  if (goog.userAgent.WEBKIT) {
    return '-webkit';
  } else if (goog.userAgent.GECKO) {
    return '-moz';
  } else if (goog.userAgent.IE) {
    return '-ms';
  } else if (goog.userAgent.OPERA) {
    return '-o';
  }

  return null;
};


/**
 * @param {string} propertyName A property name.
 * @param {!Object=} opt_object If provided, we verify if the property exists in
 *     the object.
 * @return {?string} A vendor prefixed property name, or null if it does not
 *     exist.
 */
goog.dom.vendor.getPrefixedPropertyName = function(propertyName, opt_object) {
  // We first check for a non-prefixed property, if available.
  if (opt_object && propertyName in opt_object) {
    return propertyName;
  }
  var prefix = goog.dom.vendor.getVendorJsPrefix();
  if (prefix) {
    prefix = prefix.toLowerCase();
    var prefixedPropertyName = prefix + goog.string.toTitleCase(propertyName);
    return (!goog.isDef(opt_object) || prefixedPropertyName in opt_object) ?
        prefixedPropertyName : null;
  }
  return null;
};


/**
 * @param {string} eventType An event type.
 * @return {string} A lower-cased vendor prefixed event type.
 */
goog.dom.vendor.getPrefixedEventType = function(eventType) {
  var prefix = goog.dom.vendor.getVendorJsPrefix() || '';
  return (prefix + eventType).toLowerCase();
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview A utility class for representing a numeric box.
 */


goog.provide('goog.math.Box');

goog.require('goog.math.Coordinate');



/**
 * Class for representing a box. A box is specified as a top, right, bottom,
 * and left. A box is useful for representing margins and padding.
 *
 * This class assumes 'screen coordinates': larger Y coordinates are further
 * from the top of the screen.
 *
 * @param {number} top Top.
 * @param {number} right Right.
 * @param {number} bottom Bottom.
 * @param {number} left Left.
 * @struct
 * @constructor
 */
goog.math.Box = function(top, right, bottom, left) {
  /**
   * Top
   * @type {number}
   */
  this.top = top;

  /**
   * Right
   * @type {number}
   */
  this.right = right;

  /**
   * Bottom
   * @type {number}
   */
  this.bottom = bottom;

  /**
   * Left
   * @type {number}
   */
  this.left = left;
};


/**
 * Creates a Box by bounding a collection of goog.math.Coordinate objects
 * @param {...goog.math.Coordinate} var_args Coordinates to be included inside
 *     the box.
 * @return {!goog.math.Box} A Box containing all the specified Coordinates.
 */
goog.math.Box.boundingBox = function(var_args) {
  var box = new goog.math.Box(arguments[0].y, arguments[0].x,
                              arguments[0].y, arguments[0].x);
  for (var i = 1; i < arguments.length; i++) {
    box.expandToIncludeCoordinate(arguments[i]);
  }
  return box;
};


/**
 * @return {number} width The width of this Box.
 */
goog.math.Box.prototype.getWidth = function() {
  return this.right - this.left;
};


/**
 * @return {number} height The height of this Box.
 */
goog.math.Box.prototype.getHeight = function() {
  return this.bottom - this.top;
};


/**
 * Creates a copy of the box with the same dimensions.
 * @return {!goog.math.Box} A clone of this Box.
 */
goog.math.Box.prototype.clone = function() {
  return new goog.math.Box(this.top, this.right, this.bottom, this.left);
};


if (goog.DEBUG) {
  /**
   * Returns a nice string representing the box.
   * @return {string} In the form (50t, 73r, 24b, 13l).
   * @override
   */
  goog.math.Box.prototype.toString = function() {
    return '(' + this.top + 't, ' + this.right + 'r, ' + this.bottom + 'b, ' +
           this.left + 'l)';
  };
}


/**
 * Returns whether the box contains a coordinate or another box.
 *
 * @param {goog.math.Coordinate|goog.math.Box} other A Coordinate or a Box.
 * @return {boolean} Whether the box contains the coordinate or other box.
 */
goog.math.Box.prototype.contains = function(other) {
  return goog.math.Box.contains(this, other);
};


/**
 * Expands box with the given margins.
 *
 * @param {number|goog.math.Box} top Top margin or box with all margins.
 * @param {number=} opt_right Right margin.
 * @param {number=} opt_bottom Bottom margin.
 * @param {number=} opt_left Left margin.
 * @return {!goog.math.Box} A reference to this Box.
 */
goog.math.Box.prototype.expand = function(top, opt_right, opt_bottom,
    opt_left) {
  if (goog.isObject(top)) {
    this.top -= top.top;
    this.right += top.right;
    this.bottom += top.bottom;
    this.left -= top.left;
  } else {
    this.top -= top;
    this.right += opt_right;
    this.bottom += opt_bottom;
    this.left -= opt_left;
  }

  return this;
};


/**
 * Expand this box to include another box.
 * NOTE(user): This is used in code that needs to be very fast, please don't
 * add functionality to this function at the expense of speed (variable
 * arguments, accepting multiple argument types, etc).
 * @param {goog.math.Box} box The box to include in this one.
 */
goog.math.Box.prototype.expandToInclude = function(box) {
  this.left = Math.min(this.left, box.left);
  this.top = Math.min(this.top, box.top);
  this.right = Math.max(this.right, box.right);
  this.bottom = Math.max(this.bottom, box.bottom);
};


/**
 * Expand this box to include the coordinate.
 * @param {!goog.math.Coordinate} coord The coordinate to be included
 *     inside the box.
 */
goog.math.Box.prototype.expandToIncludeCoordinate = function(coord) {
  this.top = Math.min(this.top, coord.y);
  this.right = Math.max(this.right, coord.x);
  this.bottom = Math.max(this.bottom, coord.y);
  this.left = Math.min(this.left, coord.x);
};


/**
 * Compares boxes for equality.
 * @param {goog.math.Box} a A Box.
 * @param {goog.math.Box} b A Box.
 * @return {boolean} True iff the boxes are equal, or if both are null.
 */
goog.math.Box.equals = function(a, b) {
  if (a == b) {
    return true;
  }
  if (!a || !b) {
    return false;
  }
  return a.top == b.top && a.right == b.right &&
         a.bottom == b.bottom && a.left == b.left;
};


/**
 * Returns whether a box contains a coordinate or another box.
 *
 * @param {goog.math.Box} box A Box.
 * @param {goog.math.Coordinate|goog.math.Box} other A Coordinate or a Box.
 * @return {boolean} Whether the box contains the coordinate or other box.
 */
goog.math.Box.contains = function(box, other) {
  if (!box || !other) {
    return false;
  }

  if (other instanceof goog.math.Box) {
    return other.left >= box.left && other.right <= box.right &&
        other.top >= box.top && other.bottom <= box.bottom;
  }

  // other is a Coordinate.
  return other.x >= box.left && other.x <= box.right &&
         other.y >= box.top && other.y <= box.bottom;
};


/**
 * Returns the relative x position of a coordinate compared to a box.  Returns
 * zero if the coordinate is inside the box.
 *
 * @param {goog.math.Box} box A Box.
 * @param {goog.math.Coordinate} coord A Coordinate.
 * @return {number} The x position of {@code coord} relative to the nearest
 *     side of {@code box}, or zero if {@code coord} is inside {@code box}.
 */
goog.math.Box.relativePositionX = function(box, coord) {
  if (coord.x < box.left) {
    return coord.x - box.left;
  } else if (coord.x > box.right) {
    return coord.x - box.right;
  }
  return 0;
};


/**
 * Returns the relative y position of a coordinate compared to a box.  Returns
 * zero if the coordinate is inside the box.
 *
 * @param {goog.math.Box} box A Box.
 * @param {goog.math.Coordinate} coord A Coordinate.
 * @return {number} The y position of {@code coord} relative to the nearest
 *     side of {@code box}, or zero if {@code coord} is inside {@code box}.
 */
goog.math.Box.relativePositionY = function(box, coord) {
  if (coord.y < box.top) {
    return coord.y - box.top;
  } else if (coord.y > box.bottom) {
    return coord.y - box.bottom;
  }
  return 0;
};


/**
 * Returns the distance between a coordinate and the nearest corner/side of a
 * box. Returns zero if the coordinate is inside the box.
 *
 * @param {goog.math.Box} box A Box.
 * @param {goog.math.Coordinate} coord A Coordinate.
 * @return {number} The distance between {@code coord} and the nearest
 *     corner/side of {@code box}, or zero if {@code coord} is inside
 *     {@code box}.
 */
goog.math.Box.distance = function(box, coord) {
  var x = goog.math.Box.relativePositionX(box, coord);
  var y = goog.math.Box.relativePositionY(box, coord);
  return Math.sqrt(x * x + y * y);
};


/**
 * Returns whether two boxes intersect.
 *
 * @param {goog.math.Box} a A Box.
 * @param {goog.math.Box} b A second Box.
 * @return {boolean} Whether the boxes intersect.
 */
goog.math.Box.intersects = function(a, b) {
  return (a.left <= b.right && b.left <= a.right &&
          a.top <= b.bottom && b.top <= a.bottom);
};


/**
 * Returns whether two boxes would intersect with additional padding.
 *
 * @param {goog.math.Box} a A Box.
 * @param {goog.math.Box} b A second Box.
 * @param {number} padding The additional padding.
 * @return {boolean} Whether the boxes intersect.
 */
goog.math.Box.intersectsWithPadding = function(a, b, padding) {
  return (a.left <= b.right + padding && b.left <= a.right + padding &&
          a.top <= b.bottom + padding && b.top <= a.bottom + padding);
};


/**
 * Rounds the fields to the next larger integer values.
 *
 * @return {!goog.math.Box} This box with ceil'd fields.
 */
goog.math.Box.prototype.ceil = function() {
  this.top = Math.ceil(this.top);
  this.right = Math.ceil(this.right);
  this.bottom = Math.ceil(this.bottom);
  this.left = Math.ceil(this.left);
  return this;
};


/**
 * Rounds the fields to the next smaller integer values.
 *
 * @return {!goog.math.Box} This box with floored fields.
 */
goog.math.Box.prototype.floor = function() {
  this.top = Math.floor(this.top);
  this.right = Math.floor(this.right);
  this.bottom = Math.floor(this.bottom);
  this.left = Math.floor(this.left);
  return this;
};


/**
 * Rounds the fields to nearest integer values.
 *
 * @return {!goog.math.Box} This box with rounded fields.
 */
goog.math.Box.prototype.round = function() {
  this.top = Math.round(this.top);
  this.right = Math.round(this.right);
  this.bottom = Math.round(this.bottom);
  this.left = Math.round(this.left);
  return this;
};


/**
 * Translates this box by the given offsets. If a {@code goog.math.Coordinate}
 * is given, then the left and right values are translated by the coordinate's
 * x value and the top and bottom values are translated by the coordinate's y
 * value.  Otherwise, {@code tx} and {@code opt_ty} are used to translate the x
 * and y dimension values.
 *
 * @param {number|goog.math.Coordinate} tx The value to translate the x
 *     dimension values by or the the coordinate to translate this box by.
 * @param {number=} opt_ty The value to translate y dimension values by.
 * @return {!goog.math.Box} This box after translating.
 */
goog.math.Box.prototype.translate = function(tx, opt_ty) {
  if (tx instanceof goog.math.Coordinate) {
    this.left += tx.x;
    this.right += tx.x;
    this.top += tx.y;
    this.bottom += tx.y;
  } else {
    this.left += tx;
    this.right += tx;
    if (goog.isNumber(opt_ty)) {
      this.top += opt_ty;
      this.bottom += opt_ty;
    }
  }
  return this;
};


/**
 * Scales this coordinate by the given scale factors. The x and y dimension
 * values are scaled by {@code sx} and {@code opt_sy} respectively.
 * If {@code opt_sy} is not given, then {@code sx} is used for both x and y.
 *
 * @param {number} sx The scale factor to use for the x dimension.
 * @param {number=} opt_sy The scale factor to use for the y dimension.
 * @return {!goog.math.Box} This box after scaling.
 */
goog.math.Box.prototype.scale = function(sx, opt_sy) {
  var sy = goog.isNumber(opt_sy) ? opt_sy : sx;
  this.left *= sx;
  this.right *= sx;
  this.top *= sy;
  this.bottom *= sy;
  return this;
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview A utility class for representing rectangles.
 */

goog.provide('goog.math.Rect');

goog.require('goog.math.Box');
goog.require('goog.math.Coordinate');
goog.require('goog.math.Size');



/**
 * Class for representing rectangular regions.
 * @param {number} x Left.
 * @param {number} y Top.
 * @param {number} w Width.
 * @param {number} h Height.
 * @struct
 * @constructor
 */
goog.math.Rect = function(x, y, w, h) {
  /** @type {number} */
  this.left = x;

  /** @type {number} */
  this.top = y;

  /** @type {number} */
  this.width = w;

  /** @type {number} */
  this.height = h;
};


/**
 * @return {!goog.math.Rect} A new copy of this Rectangle.
 */
goog.math.Rect.prototype.clone = function() {
  return new goog.math.Rect(this.left, this.top, this.width, this.height);
};


/**
 * Returns a new Box object with the same position and dimensions as this
 * rectangle.
 * @return {!goog.math.Box} A new Box representation of this Rectangle.
 */
goog.math.Rect.prototype.toBox = function() {
  var right = this.left + this.width;
  var bottom = this.top + this.height;
  return new goog.math.Box(this.top,
                           right,
                           bottom,
                           this.left);
};


/**
 * Creates a new Rect object with the position and size given.
 * @param {!goog.math.Coordinate} position The top-left coordinate of the Rect
 * @param {!goog.math.Size} size The size of the Rect
 * @return {!goog.math.Rect} A new Rect initialized with the given position and
 *     size.
 */
goog.math.Rect.createFromPositionAndSize = function(position, size) {
  return new goog.math.Rect(position.x, position.y, size.width, size.height);
};


/**
 * Creates a new Rect object with the same position and dimensions as a given
 * Box.  Note that this is only the inverse of toBox if left/top are defined.
 * @param {goog.math.Box} box A box.
 * @return {!goog.math.Rect} A new Rect initialized with the box's position
 *     and size.
 */
goog.math.Rect.createFromBox = function(box) {
  return new goog.math.Rect(box.left, box.top,
      box.right - box.left, box.bottom - box.top);
};


if (goog.DEBUG) {
  /**
   * Returns a nice string representing size and dimensions of rectangle.
   * @return {string} In the form (50, 73 - 75w x 25h).
   * @override
   */
  goog.math.Rect.prototype.toString = function() {
    return '(' + this.left + ', ' + this.top + ' - ' + this.width + 'w x ' +
           this.height + 'h)';
  };
}


/**
 * Compares rectangles for equality.
 * @param {goog.math.Rect} a A Rectangle.
 * @param {goog.math.Rect} b A Rectangle.
 * @return {boolean} True iff the rectangles have the same left, top, width,
 *     and height, or if both are null.
 */
goog.math.Rect.equals = function(a, b) {
  if (a == b) {
    return true;
  }
  if (!a || !b) {
    return false;
  }
  return a.left == b.left && a.width == b.width &&
         a.top == b.top && a.height == b.height;
};


/**
 * Computes the intersection of this rectangle and the rectangle parameter.  If
 * there is no intersection, returns false and leaves this rectangle as is.
 * @param {goog.math.Rect} rect A Rectangle.
 * @return {boolean} True iff this rectangle intersects with the parameter.
 */
goog.math.Rect.prototype.intersection = function(rect) {
  var x0 = Math.max(this.left, rect.left);
  var x1 = Math.min(this.left + this.width, rect.left + rect.width);

  if (x0 <= x1) {
    var y0 = Math.max(this.top, rect.top);
    var y1 = Math.min(this.top + this.height, rect.top + rect.height);

    if (y0 <= y1) {
      this.left = x0;
      this.top = y0;
      this.width = x1 - x0;
      this.height = y1 - y0;

      return true;
    }
  }
  return false;
};


/**
 * Returns the intersection of two rectangles. Two rectangles intersect if they
 * touch at all, for example, two zero width and height rectangles would
 * intersect if they had the same top and left.
 * @param {goog.math.Rect} a A Rectangle.
 * @param {goog.math.Rect} b A Rectangle.
 * @return {goog.math.Rect} A new intersection rect (even if width and height
 *     are 0), or null if there is no intersection.
 */
goog.math.Rect.intersection = function(a, b) {
  // There is no nice way to do intersection via a clone, because any such
  // clone might be unnecessary if this function returns null.  So, we duplicate
  // code from above.

  var x0 = Math.max(a.left, b.left);
  var x1 = Math.min(a.left + a.width, b.left + b.width);

  if (x0 <= x1) {
    var y0 = Math.max(a.top, b.top);
    var y1 = Math.min(a.top + a.height, b.top + b.height);

    if (y0 <= y1) {
      return new goog.math.Rect(x0, y0, x1 - x0, y1 - y0);
    }
  }
  return null;
};


/**
 * Returns whether two rectangles intersect. Two rectangles intersect if they
 * touch at all, for example, two zero width and height rectangles would
 * intersect if they had the same top and left.
 * @param {goog.math.Rect} a A Rectangle.
 * @param {goog.math.Rect} b A Rectangle.
 * @return {boolean} Whether a and b intersect.
 */
goog.math.Rect.intersects = function(a, b) {
  return (a.left <= b.left + b.width && b.left <= a.left + a.width &&
      a.top <= b.top + b.height && b.top <= a.top + a.height);
};


/**
 * Returns whether a rectangle intersects this rectangle.
 * @param {goog.math.Rect} rect A rectangle.
 * @return {boolean} Whether rect intersects this rectangle.
 */
goog.math.Rect.prototype.intersects = function(rect) {
  return goog.math.Rect.intersects(this, rect);
};


/**
 * Computes the difference regions between two rectangles. The return value is
 * an array of 0 to 4 rectangles defining the remaining regions of the first
 * rectangle after the second has been subtracted.
 * @param {goog.math.Rect} a A Rectangle.
 * @param {goog.math.Rect} b A Rectangle.
 * @return {!Array<!goog.math.Rect>} An array with 0 to 4 rectangles which
 *     together define the difference area of rectangle a minus rectangle b.
 */
goog.math.Rect.difference = function(a, b) {
  var intersection = goog.math.Rect.intersection(a, b);
  if (!intersection || !intersection.height || !intersection.width) {
    return [a.clone()];
  }

  var result = [];

  var top = a.top;
  var height = a.height;

  var ar = a.left + a.width;
  var ab = a.top + a.height;

  var br = b.left + b.width;
  var bb = b.top + b.height;

  // Subtract off any area on top where A extends past B
  if (b.top > a.top) {
    result.push(new goog.math.Rect(a.left, a.top, a.width, b.top - a.top));
    top = b.top;
    // If we're moving the top down, we also need to subtract the height diff.
    height -= b.top - a.top;
  }
  // Subtract off any area on bottom where A extends past B
  if (bb < ab) {
    result.push(new goog.math.Rect(a.left, bb, a.width, ab - bb));
    height = bb - top;
  }
  // Subtract any area on left where A extends past B
  if (b.left > a.left) {
    result.push(new goog.math.Rect(a.left, top, b.left - a.left, height));
  }
  // Subtract any area on right where A extends past B
  if (br < ar) {
    result.push(new goog.math.Rect(br, top, ar - br, height));
  }

  return result;
};


/**
 * Computes the difference regions between this rectangle and {@code rect}. The
 * return value is an array of 0 to 4 rectangles defining the remaining regions
 * of this rectangle after the other has been subtracted.
 * @param {goog.math.Rect} rect A Rectangle.
 * @return {!Array<!goog.math.Rect>} An array with 0 to 4 rectangles which
 *     together define the difference area of rectangle a minus rectangle b.
 */
goog.math.Rect.prototype.difference = function(rect) {
  return goog.math.Rect.difference(this, rect);
};


/**
 * Expand this rectangle to also include the area of the given rectangle.
 * @param {goog.math.Rect} rect The other rectangle.
 */
goog.math.Rect.prototype.boundingRect = function(rect) {
  // We compute right and bottom before we change left and top below.
  var right = Math.max(this.left + this.width, rect.left + rect.width);
  var bottom = Math.max(this.top + this.height, rect.top + rect.height);

  this.left = Math.min(this.left, rect.left);
  this.top = Math.min(this.top, rect.top);

  this.width = right - this.left;
  this.height = bottom - this.top;
};


/**
 * Returns a new rectangle which completely contains both input rectangles.
 * @param {goog.math.Rect} a A rectangle.
 * @param {goog.math.Rect} b A rectangle.
 * @return {goog.math.Rect} A new bounding rect, or null if either rect is
 *     null.
 */
goog.math.Rect.boundingRect = function(a, b) {
  if (!a || !b) {
    return null;
  }

  var clone = a.clone();
  clone.boundingRect(b);

  return clone;
};


/**
 * Tests whether this rectangle entirely contains another rectangle or
 * coordinate.
 *
 * @param {goog.math.Rect|goog.math.Coordinate} another The rectangle or
 *     coordinate to test for containment.
 * @return {boolean} Whether this rectangle contains given rectangle or
 *     coordinate.
 */
goog.math.Rect.prototype.contains = function(another) {
  if (another instanceof goog.math.Rect) {
    return this.left <= another.left &&
           this.left + this.width >= another.left + another.width &&
           this.top <= another.top &&
           this.top + this.height >= another.top + another.height;
  } else { // (another instanceof goog.math.Coordinate)
    return another.x >= this.left &&
           another.x <= this.left + this.width &&
           another.y >= this.top &&
           another.y <= this.top + this.height;
  }
};


/**
 * @param {!goog.math.Coordinate} point A coordinate.
 * @return {number} The squared distance between the point and the closest
 *     point inside the rectangle. Returns 0 if the point is inside the
 *     rectangle.
 */
goog.math.Rect.prototype.squaredDistance = function(point) {
  var dx = point.x < this.left ?
      this.left - point.x : Math.max(point.x - (this.left + this.width), 0);
  var dy = point.y < this.top ?
      this.top - point.y : Math.max(point.y - (this.top + this.height), 0);
  return dx * dx + dy * dy;
};


/**
 * @param {!goog.math.Coordinate} point A coordinate.
 * @return {number} The distance between the point and the closest point
 *     inside the rectangle. Returns 0 if the point is inside the rectangle.
 */
goog.math.Rect.prototype.distance = function(point) {
  return Math.sqrt(this.squaredDistance(point));
};


/**
 * @return {!goog.math.Size} The size of this rectangle.
 */
goog.math.Rect.prototype.getSize = function() {
  return new goog.math.Size(this.width, this.height);
};


/**
 * @return {!goog.math.Coordinate} A new coordinate for the top-left corner of
 *     the rectangle.
 */
goog.math.Rect.prototype.getTopLeft = function() {
  return new goog.math.Coordinate(this.left, this.top);
};


/**
 * @return {!goog.math.Coordinate} A new coordinate for the center of the
 *     rectangle.
 */
goog.math.Rect.prototype.getCenter = function() {
  return new goog.math.Coordinate(
      this.left + this.width / 2, this.top + this.height / 2);
};


/**
 * @return {!goog.math.Coordinate} A new coordinate for the bottom-right corner
 *     of the rectangle.
 */
goog.math.Rect.prototype.getBottomRight = function() {
  return new goog.math.Coordinate(
      this.left + this.width, this.top + this.height);
};


/**
 * Rounds the fields to the next larger integer values.
 * @return {!goog.math.Rect} This rectangle with ceil'd fields.
 */
goog.math.Rect.prototype.ceil = function() {
  this.left = Math.ceil(this.left);
  this.top = Math.ceil(this.top);
  this.width = Math.ceil(this.width);
  this.height = Math.ceil(this.height);
  return this;
};


/**
 * Rounds the fields to the next smaller integer values.
 * @return {!goog.math.Rect} This rectangle with floored fields.
 */
goog.math.Rect.prototype.floor = function() {
  this.left = Math.floor(this.left);
  this.top = Math.floor(this.top);
  this.width = Math.floor(this.width);
  this.height = Math.floor(this.height);
  return this;
};


/**
 * Rounds the fields to nearest integer values.
 * @return {!goog.math.Rect} This rectangle with rounded fields.
 */
goog.math.Rect.prototype.round = function() {
  this.left = Math.round(this.left);
  this.top = Math.round(this.top);
  this.width = Math.round(this.width);
  this.height = Math.round(this.height);
  return this;
};


/**
 * Translates this rectangle by the given offsets. If a
 * {@code goog.math.Coordinate} is given, then the left and top values are
 * translated by the coordinate's x and y values. Otherwise, top and left are
 * translated by {@code tx} and {@code opt_ty} respectively.
 * @param {number|goog.math.Coordinate} tx The value to translate left by or the
 *     the coordinate to translate this rect by.
 * @param {number=} opt_ty The value to translate top by.
 * @return {!goog.math.Rect} This rectangle after translating.
 */
goog.math.Rect.prototype.translate = function(tx, opt_ty) {
  if (tx instanceof goog.math.Coordinate) {
    this.left += tx.x;
    this.top += tx.y;
  } else {
    this.left += tx;
    if (goog.isNumber(opt_ty)) {
      this.top += opt_ty;
    }
  }
  return this;
};


/**
 * Scales this rectangle by the given scale factors. The left and width values
 * are scaled by {@code sx} and the top and height values are scaled by
 * {@code opt_sy}.  If {@code opt_sy} is not given, then all fields are scaled
 * by {@code sx}.
 * @param {number} sx The scale factor to use for the x dimension.
 * @param {number=} opt_sy The scale factor to use for the y dimension.
 * @return {!goog.math.Rect} This rectangle after scaling.
 */
goog.math.Rect.prototype.scale = function(sx, opt_sy) {
  var sy = goog.isNumber(opt_sy) ? opt_sy : sx;
  this.left *= sx;
  this.width *= sx;
  this.top *= sy;
  this.height *= sy;
  return this;
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Utilities for element styles.
 *
 * @author arv@google.com (Erik Arvidsson)
 * @author eae@google.com (Emil A Eklund)
 * @see ../demos/inline_block_quirks.html
 * @see ../demos/inline_block_standards.html
 * @see ../demos/style_viewport.html
 */

goog.provide('goog.style');


goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.TagName');
goog.require('goog.dom.vendor');
goog.require('goog.math.Box');
goog.require('goog.math.Coordinate');
goog.require('goog.math.Rect');
goog.require('goog.math.Size');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.userAgent');

goog.forwardDeclare('goog.events.BrowserEvent');
goog.forwardDeclare('goog.events.Event');


/**
 * Sets a style value on an element.
 *
 * This function is not indended to patch issues in the browser's style
 * handling, but to allow easy programmatic access to setting dash-separated
 * style properties.  An example is setting a batch of properties from a data
 * object without overwriting old styles.  When possible, use native APIs:
 * elem.style.propertyKey = 'value' or (if obliterating old styles is fine)
 * elem.style.cssText = 'property1: value1; property2: value2'.
 *
 * @param {Element} element The element to change.
 * @param {string|Object} style If a string, a style name. If an object, a hash
 *     of style names to style values.
 * @param {string|number|boolean=} opt_value If style was a string, then this
 *     should be the value.
 */
goog.style.setStyle = function(element, style, opt_value) {
  if (goog.isString(style)) {
    goog.style.setStyle_(element, opt_value, style);
  } else {
    for (var key in style) {
      goog.style.setStyle_(element, style[key], key);
    }
  }
};


/**
 * Sets a style value on an element, with parameters swapped to work with
 * {@code goog.object.forEach()}. Prepends a vendor-specific prefix when
 * necessary.
 * @param {Element} element The element to change.
 * @param {string|number|boolean|undefined} value Style value.
 * @param {string} style Style name.
 * @private
 */
goog.style.setStyle_ = function(element, value, style) {
  var propertyName = goog.style.getVendorJsStyleName_(element, style);

  if (propertyName) {
    element.style[propertyName] = value;
  }
};


/**
 * Style name cache that stores previous property name lookups.
 *
 * This is used by setStyle to speed up property lookups, entries look like:
 *   { StyleName: ActualPropertyName }
 *
 * @private {!Object<string, string>}
 */
goog.style.styleNameCache_ = {};


/**
 * Returns the style property name in camel-case. If it does not exist and a
 * vendor-specific version of the property does exist, then return the vendor-
 * specific property name instead.
 * @param {Element} element The element to change.
 * @param {string} style Style name.
 * @return {string} Vendor-specific style.
 * @private
 */
goog.style.getVendorJsStyleName_ = function(element, style) {
  var propertyName = goog.style.styleNameCache_[style];
  if (!propertyName) {
    var camelStyle = goog.string.toCamelCase(style);
    propertyName = camelStyle;

    if (element.style[camelStyle] === undefined) {
      var prefixedStyle = goog.dom.vendor.getVendorJsPrefix() +
          goog.string.toTitleCase(camelStyle);

      if (element.style[prefixedStyle] !== undefined) {
        propertyName = prefixedStyle;
      }
    }
    goog.style.styleNameCache_[style] = propertyName;
  }

  return propertyName;
};


/**
 * Returns the style property name in CSS notation. If it does not exist and a
 * vendor-specific version of the property does exist, then return the vendor-
 * specific property name instead.
 * @param {Element} element The element to change.
 * @param {string} style Style name.
 * @return {string} Vendor-specific style.
 * @private
 */
goog.style.getVendorStyleName_ = function(element, style) {
  var camelStyle = goog.string.toCamelCase(style);

  if (element.style[camelStyle] === undefined) {
    var prefixedStyle = goog.dom.vendor.getVendorJsPrefix() +
        goog.string.toTitleCase(camelStyle);

    if (element.style[prefixedStyle] !== undefined) {
      return goog.dom.vendor.getVendorPrefix() + '-' + style;
    }
  }

  return style;
};


/**
 * Retrieves an explicitly-set style value of a node. This returns '' if there
 * isn't a style attribute on the element or if this style property has not been
 * explicitly set in script.
 *
 * @param {Element} element Element to get style of.
 * @param {string} property Property to get, css-style (if you have a camel-case
 * property, use element.style[style]).
 * @return {string} Style value.
 */
goog.style.getStyle = function(element, property) {
  // element.style is '' for well-known properties which are unset.
  // For for browser specific styles as 'filter' is undefined
  // so we need to return '' explicitly to make it consistent across
  // browsers.
  var styleValue = element.style[goog.string.toCamelCase(property)];

  // Using typeof here because of a bug in Safari 5.1, where this value
  // was undefined, but === undefined returned false.
  if (typeof(styleValue) !== 'undefined') {
    return styleValue;
  }

  return element.style[goog.style.getVendorJsStyleName_(element, property)] ||
      '';
};


/**
 * Retrieves a computed style value of a node. It returns empty string if the
 * value cannot be computed (which will be the case in Internet Explorer) or
 * "none" if the property requested is an SVG one and it has not been
 * explicitly set (firefox and webkit).
 *
 * @param {Element} element Element to get style of.
 * @param {string} property Property to get (camel-case).
 * @return {string} Style value.
 */
goog.style.getComputedStyle = function(element, property) {
  var doc = goog.dom.getOwnerDocument(element);
  if (doc.defaultView && doc.defaultView.getComputedStyle) {
    var styles = doc.defaultView.getComputedStyle(element, null);
    if (styles) {
      // element.style[..] is undefined for browser specific styles
      // as 'filter'.
      return styles[property] || styles.getPropertyValue(property) || '';
    }
  }

  return '';
};


/**
 * Gets the cascaded style value of a node, or null if the value cannot be
 * computed (only Internet Explorer can do this).
 *
 * @param {Element} element Element to get style of.
 * @param {string} style Property to get (camel-case).
 * @return {string} Style value.
 */
goog.style.getCascadedStyle = function(element, style) {
  // TODO(nicksantos): This should be documented to return null. #fixTypes
  return element.currentStyle ? element.currentStyle[style] : null;
};


/**
 * Cross-browser pseudo get computed style. It returns the computed style where
 * available. If not available it tries the cascaded style value (IE
 * currentStyle) and in worst case the inline style value.  It shouldn't be
 * called directly, see http://wiki/Main/ComputedStyleVsCascadedStyle for
 * discussion.
 *
 * @param {Element} element Element to get style of.
 * @param {string} style Property to get (must be camelCase, not css-style.).
 * @return {string} Style value.
 * @private
 */
goog.style.getStyle_ = function(element, style) {
  return goog.style.getComputedStyle(element, style) ||
         goog.style.getCascadedStyle(element, style) ||
         (element.style && element.style[style]);
};


/**
 * Retrieves the computed value of the box-sizing CSS attribute.
 * Browser support: http://caniuse.com/css3-boxsizing.
 * @param {!Element} element The element whose box-sizing to get.
 * @return {?string} 'content-box', 'border-box' or 'padding-box'. null if
 *     box-sizing is not supported (IE7 and below).
 */
goog.style.getComputedBoxSizing = function(element) {
  return goog.style.getStyle_(element, 'boxSizing') ||
      goog.style.getStyle_(element, 'MozBoxSizing') ||
      goog.style.getStyle_(element, 'WebkitBoxSizing') || null;
};


/**
 * Retrieves the computed value of the position CSS attribute.
 * @param {Element} element The element to get the position of.
 * @return {string} Position value.
 */
goog.style.getComputedPosition = function(element) {
  return goog.style.getStyle_(element, 'position');
};


/**
 * Retrieves the computed background color string for a given element. The
 * string returned is suitable for assigning to another element's
 * background-color, but is not guaranteed to be in any particular string
 * format. Accessing the color in a numeric form may not be possible in all
 * browsers or with all input.
 *
 * If the background color for the element is defined as a hexadecimal value,
 * the resulting string can be parsed by goog.color.parse in all supported
 * browsers.
 *
 * Whether named colors like "red" or "lightblue" get translated into a
 * format which can be parsed is browser dependent. Calling this function on
 * transparent elements will return "transparent" in most browsers or
 * "rgba(0, 0, 0, 0)" in WebKit.
 * @param {Element} element The element to get the background color of.
 * @return {string} The computed string value of the background color.
 */
goog.style.getBackgroundColor = function(element) {
  return goog.style.getStyle_(element, 'backgroundColor');
};


/**
 * Retrieves the computed value of the overflow-x CSS attribute.
 * @param {Element} element The element to get the overflow-x of.
 * @return {string} The computed string value of the overflow-x attribute.
 */
goog.style.getComputedOverflowX = function(element) {
  return goog.style.getStyle_(element, 'overflowX');
};


/**
 * Retrieves the computed value of the overflow-y CSS attribute.
 * @param {Element} element The element to get the overflow-y of.
 * @return {string} The computed string value of the overflow-y attribute.
 */
goog.style.getComputedOverflowY = function(element) {
  return goog.style.getStyle_(element, 'overflowY');
};


/**
 * Retrieves the computed value of the z-index CSS attribute.
 * @param {Element} element The element to get the z-index of.
 * @return {string|number} The computed value of the z-index attribute.
 */
goog.style.getComputedZIndex = function(element) {
  return goog.style.getStyle_(element, 'zIndex');
};


/**
 * Retrieves the computed value of the text-align CSS attribute.
 * @param {Element} element The element to get the text-align of.
 * @return {string} The computed string value of the text-align attribute.
 */
goog.style.getComputedTextAlign = function(element) {
  return goog.style.getStyle_(element, 'textAlign');
};


/**
 * Retrieves the computed value of the cursor CSS attribute.
 * @param {Element} element The element to get the cursor of.
 * @return {string} The computed string value of the cursor attribute.
 */
goog.style.getComputedCursor = function(element) {
  return goog.style.getStyle_(element, 'cursor');
};


/**
 * Retrieves the computed value of the CSS transform attribute.
 * @param {Element} element The element to get the transform of.
 * @return {string} The computed string representation of the transform matrix.
 */
goog.style.getComputedTransform = function(element) {
  var property = goog.style.getVendorStyleName_(element, 'transform');
  return goog.style.getStyle_(element, property) ||
      goog.style.getStyle_(element, 'transform');
};


/**
 * Sets the top/left values of an element.  If no unit is specified in the
 * argument then it will add px. The second argument is required if the first
 * argument is a string or number and is ignored if the first argument
 * is a coordinate.
 * @param {Element} el Element to move.
 * @param {string|number|goog.math.Coordinate} arg1 Left position or coordinate.
 * @param {string|number=} opt_arg2 Top position.
 */
goog.style.setPosition = function(el, arg1, opt_arg2) {
  var x, y;

  if (arg1 instanceof goog.math.Coordinate) {
    x = arg1.x;
    y = arg1.y;
  } else {
    x = arg1;
    y = opt_arg2;
  }

  el.style.left = goog.style.getPixelStyleValue_(
      /** @type {number|string} */ (x), false);
  el.style.top = goog.style.getPixelStyleValue_(
      /** @type {number|string} */ (y), false);
};


/**
 * Gets the offsetLeft and offsetTop properties of an element and returns them
 * in a Coordinate object
 * @param {Element} element Element.
 * @return {!goog.math.Coordinate} The position.
 */
goog.style.getPosition = function(element) {
  return new goog.math.Coordinate(
      /** @type {!HTMLElement} */ (element).offsetLeft,
      /** @type {!HTMLElement} */ (element).offsetTop);
};


/**
 * Returns the viewport element for a particular document
 * @param {Node=} opt_node DOM node (Document is OK) to get the viewport element
 *     of.
 * @return {Element} document.documentElement or document.body.
 */
goog.style.getClientViewportElement = function(opt_node) {
  var doc;
  if (opt_node) {
    doc = goog.dom.getOwnerDocument(opt_node);
  } else {
    doc = goog.dom.getDocument();
  }

  // In old IE versions the document.body represented the viewport
  if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9) &&
      !goog.dom.getDomHelper(doc).isCss1CompatMode()) {
    return doc.body;
  }
  return doc.documentElement;
};


/**
 * Calculates the viewport coordinates relative to the page/document
 * containing the node. The viewport may be the browser viewport for
 * non-iframe document, or the iframe container for iframe'd document.
 * @param {!Document} doc The document to use as the reference point.
 * @return {!goog.math.Coordinate} The page offset of the viewport.
 */
goog.style.getViewportPageOffset = function(doc) {
  var body = doc.body;
  var documentElement = doc.documentElement;
  var scrollLeft = body.scrollLeft || documentElement.scrollLeft;
  var scrollTop = body.scrollTop || documentElement.scrollTop;
  return new goog.math.Coordinate(scrollLeft, scrollTop);
};


/**
 * Gets the client rectangle of the DOM element.
 *
 * getBoundingClientRect is part of a new CSS object model draft (with a
 * long-time presence in IE), replacing the error-prone parent offset
 * computation and the now-deprecated Gecko getBoxObjectFor.
 *
 * This utility patches common browser bugs in getBoundingClientRect. It
 * will fail if getBoundingClientRect is unsupported.
 *
 * If the element is not in the DOM, the result is undefined, and an error may
 * be thrown depending on user agent.
 *
 * @param {!Element} el The element whose bounding rectangle is being queried.
 * @return {Object} A native bounding rectangle with numerical left, top,
 *     right, and bottom.  Reported by Firefox to be of object type ClientRect.
 * @private
 */
goog.style.getBoundingClientRect_ = function(el) {
  var rect;
  try {
    rect = el.getBoundingClientRect();
  } catch (e) {
    // In IE < 9, calling getBoundingClientRect on an orphan element raises an
    // "Unspecified Error". All other browsers return zeros.
    return {'left': 0, 'top': 0, 'right': 0, 'bottom': 0};
  }

  // Patch the result in IE only, so that this function can be inlined if
  // compiled for non-IE.
  if (goog.userAgent.IE && el.ownerDocument.body) {

    // In IE, most of the time, 2 extra pixels are added to the top and left
    // due to the implicit 2-pixel inset border.  In IE6/7 quirks mode and
    // IE6 standards mode, this border can be overridden by setting the
    // document element's border to zero -- thus, we cannot rely on the
    // offset always being 2 pixels.

    // In quirks mode, the offset can be determined by querying the body's
    // clientLeft/clientTop, but in standards mode, it is found by querying
    // the document element's clientLeft/clientTop.  Since we already called
    // getBoundingClientRect we have already forced a reflow, so it is not
    // too expensive just to query them all.

    // See: http://msdn.microsoft.com/en-us/library/ms536433(VS.85).aspx
    var doc = el.ownerDocument;
    rect.left -= doc.documentElement.clientLeft + doc.body.clientLeft;
    rect.top -= doc.documentElement.clientTop + doc.body.clientTop;
  }
  return rect;
};


/**
 * Returns the first parent that could affect the position of a given element.
 * @param {Element} element The element to get the offset parent for.
 * @return {Element} The first offset parent or null if one cannot be found.
 */
goog.style.getOffsetParent = function(element) {
  // element.offsetParent does the right thing in IE7 and below.  In other
  // browsers it only includes elements with position absolute, relative or
  // fixed, not elements with overflow set to auto or scroll.
  if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(8)) {
    return element.offsetParent;
  }

  var doc = goog.dom.getOwnerDocument(element);
  var positionStyle = goog.style.getStyle_(element, 'position');
  var skipStatic = positionStyle == 'fixed' || positionStyle == 'absolute';
  for (var parent = element.parentNode; parent && parent != doc;
       parent = parent.parentNode) {
    // Skip shadowDOM roots.
    if (parent.nodeType == goog.dom.NodeType.DOCUMENT_FRAGMENT &&
        parent.host) {
      parent = parent.host;
    }
    positionStyle =
        goog.style.getStyle_(/** @type {!Element} */ (parent), 'position');
    skipStatic = skipStatic && positionStyle == 'static' &&
                 parent != doc.documentElement && parent != doc.body;
    if (!skipStatic && (parent.scrollWidth > parent.clientWidth ||
                        parent.scrollHeight > parent.clientHeight ||
                        positionStyle == 'fixed' ||
                        positionStyle == 'absolute' ||
                        positionStyle == 'relative')) {
      return /** @type {!Element} */ (parent);
    }
  }
  return null;
};


/**
 * Calculates and returns the visible rectangle for a given element. Returns a
 * box describing the visible portion of the nearest scrollable offset ancestor.
 * Coordinates are given relative to the document.
 *
 * @param {Element} element Element to get the visible rect for.
 * @return {goog.math.Box} Bounding elementBox describing the visible rect or
 *     null if scrollable ancestor isn't inside the visible viewport.
 */
goog.style.getVisibleRectForElement = function(element) {
  var visibleRect = new goog.math.Box(0, Infinity, Infinity, 0);
  var dom = goog.dom.getDomHelper(element);
  var body = dom.getDocument().body;
  var documentElement = dom.getDocument().documentElement;
  var scrollEl = dom.getDocumentScrollElement();

  // Determine the size of the visible rect by climbing the dom accounting for
  // all scrollable containers.
  for (var el = element; el = goog.style.getOffsetParent(el); ) {
    // clientWidth is zero for inline block elements in IE.
    // on WEBKIT, body element can have clientHeight = 0 and scrollHeight > 0
    if ((!goog.userAgent.IE || el.clientWidth != 0) &&
        (!goog.userAgent.WEBKIT || el.clientHeight != 0 || el != body) &&
        // body may have overflow set on it, yet we still get the entire
        // viewport. In some browsers, el.offsetParent may be
        // document.documentElement, so check for that too.
        (el != body && el != documentElement &&
            goog.style.getStyle_(el, 'overflow') != 'visible')) {
      var pos = goog.style.getPageOffset(el);
      var client = goog.style.getClientLeftTop(el);
      pos.x += client.x;
      pos.y += client.y;

      visibleRect.top = Math.max(visibleRect.top, pos.y);
      visibleRect.right = Math.min(visibleRect.right,
                                   pos.x + el.clientWidth);
      visibleRect.bottom = Math.min(visibleRect.bottom,
                                    pos.y + el.clientHeight);
      visibleRect.left = Math.max(visibleRect.left, pos.x);
    }
  }

  // Clip by window's viewport.
  var scrollX = scrollEl.scrollLeft, scrollY = scrollEl.scrollTop;
  visibleRect.left = Math.max(visibleRect.left, scrollX);
  visibleRect.top = Math.max(visibleRect.top, scrollY);
  var winSize = dom.getViewportSize();
  visibleRect.right = Math.min(visibleRect.right, scrollX + winSize.width);
  visibleRect.bottom = Math.min(visibleRect.bottom, scrollY + winSize.height);
  return visibleRect.top >= 0 && visibleRect.left >= 0 &&
         visibleRect.bottom > visibleRect.top &&
         visibleRect.right > visibleRect.left ?
         visibleRect : null;
};


/**
 * Calculate the scroll position of {@code container} with the minimum amount so
 * that the content and the borders of the given {@code element} become visible.
 * If the element is bigger than the container, its top left corner will be
 * aligned as close to the container's top left corner as possible.
 *
 * @param {Element} element The element to make visible.
 * @param {Element=} opt_container The container to scroll. If not set, then the
 *     document scroll element will be used.
 * @param {boolean=} opt_center Whether to center the element in the container.
 *     Defaults to false.
 * @return {!goog.math.Coordinate} The new scroll position of the container,
 *     in form of goog.math.Coordinate(scrollLeft, scrollTop).
 */
goog.style.getContainerOffsetToScrollInto =
    function(element, opt_container, opt_center) {
  var container = opt_container || goog.dom.getDocumentScrollElement();
  // Absolute position of the element's border's top left corner.
  var elementPos = goog.style.getPageOffset(element);
  // Absolute position of the container's border's top left corner.
  var containerPos = goog.style.getPageOffset(container);
  var containerBorder = goog.style.getBorderBox(container);
  if (container == goog.dom.getDocumentScrollElement()) {
    // The element position is calculated based on the page offset, and the
    // document scroll element holds the scroll position within the page. We can
    // use the scroll position to calculate the relative position from the
    // element.
    var relX = elementPos.x - container.scrollLeft;
    var relY = elementPos.y - container.scrollTop;
    if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(10)) {
      // In older versions of IE getPageOffset(element) does not include the
      // container border so it has to be added to accomodate.
      relX += containerBorder.left;
      relY += containerBorder.top;
    }
  } else {
    // Relative pos. of the element's border box to the container's content box.
    var relX = elementPos.x - containerPos.x - containerBorder.left;
    var relY = elementPos.y - containerPos.y - containerBorder.top;
  }
  // How much the element can move in the container, i.e. the difference between
  // the element's bottom-right-most and top-left-most position where it's
  // fully visible.
  var spaceX = container.clientWidth -
      /** @type {HTMLElement} */ (element).offsetWidth;
  var spaceY = container.clientHeight -
      /** @type {HTMLElement} */ (element).offsetHeight;

  var scrollLeft = container.scrollLeft;
  var scrollTop = container.scrollTop;
  if (opt_center) {
    // All browsers round non-integer scroll positions down.
    scrollLeft += relX - spaceX / 2;
    scrollTop += relY - spaceY / 2;
  } else {
    // This formula was designed to give the correct scroll values in the
    // following cases:
    // - element is higher than container (spaceY < 0) => scroll down by relY
    // - element is not higher that container (spaceY >= 0):
    //   - it is above container (relY < 0) => scroll up by abs(relY)
    //   - it is below container (relY > spaceY) => scroll down by relY - spaceY
    //   - it is in the container => don't scroll
    scrollLeft += Math.min(relX, Math.max(relX - spaceX, 0));
    scrollTop += Math.min(relY, Math.max(relY - spaceY, 0));
  }
  return new goog.math.Coordinate(scrollLeft, scrollTop);
};


/**
 * Changes the scroll position of {@code container} with the minimum amount so
 * that the content and the borders of the given {@code element} become visible.
 * If the element is bigger than the container, its top left corner will be
 * aligned as close to the container's top left corner as possible.
 *
 * @param {Element} element The element to make visible.
 * @param {Element=} opt_container The container to scroll. If not set, then the
 *     document scroll element will be used.
 * @param {boolean=} opt_center Whether to center the element in the container.
 *     Defaults to false.
 */
goog.style.scrollIntoContainerView =
    function(element, opt_container, opt_center) {
  var container = opt_container || goog.dom.getDocumentScrollElement();
  var offset =
      goog.style.getContainerOffsetToScrollInto(element, container, opt_center);
  container.scrollLeft = offset.x;
  container.scrollTop = offset.y;
};


/**
 * Returns clientLeft (width of the left border and, if the directionality is
 * right to left, the vertical scrollbar) and clientTop as a coordinate object.
 *
 * @param {Element} el Element to get clientLeft for.
 * @return {!goog.math.Coordinate} Client left and top.
 */
goog.style.getClientLeftTop = function(el) {
  return new goog.math.Coordinate(el.clientLeft, el.clientTop);
};


/**
 * Returns a Coordinate object relative to the top-left of the HTML document.
 * Implemented as a single function to save having to do two recursive loops in
 * opera and safari just to get both coordinates.  If you just want one value do
 * use goog.style.getPageOffsetLeft() and goog.style.getPageOffsetTop(), but
 * note if you call both those methods the tree will be analysed twice.
 *
 * @param {Element} el Element to get the page offset for.
 * @return {!goog.math.Coordinate} The page offset.
 */
goog.style.getPageOffset = function(el) {
  var doc = goog.dom.getOwnerDocument(el);
  // TODO(gboyer): Update the jsdoc in a way that doesn't break the universe.
  goog.asserts.assertObject(el, 'Parameter is required');

  // NOTE(arv): If element is hidden (display none or disconnected or any the
  // ancestors are hidden) we get (0,0) by default but we still do the
  // accumulation of scroll position.

  // TODO(arv): Should we check if the node is disconnected and in that case
  //            return (0,0)?

  var pos = new goog.math.Coordinate(0, 0);
  var viewportElement = goog.style.getClientViewportElement(doc);
  if (el == viewportElement) {
    // viewport is always at 0,0 as that defined the coordinate system for this
    // function - this avoids special case checks in the code below
    return pos;
  }

  var box = goog.style.getBoundingClientRect_(el);
  // Must add the scroll coordinates in to get the absolute page offset
  // of element since getBoundingClientRect returns relative coordinates to
  // the viewport.
  var scrollCoord = goog.dom.getDomHelper(doc).getDocumentScroll();
  pos.x = box.left + scrollCoord.x;
  pos.y = box.top + scrollCoord.y;

  return pos;
};


/**
 * Returns the left coordinate of an element relative to the HTML document
 * @param {Element} el Elements.
 * @return {number} The left coordinate.
 */
goog.style.getPageOffsetLeft = function(el) {
  return goog.style.getPageOffset(el).x;
};


/**
 * Returns the top coordinate of an element relative to the HTML document
 * @param {Element} el Elements.
 * @return {number} The top coordinate.
 */
goog.style.getPageOffsetTop = function(el) {
  return goog.style.getPageOffset(el).y;
};


/**
 * Returns a Coordinate object relative to the top-left of an HTML document
 * in an ancestor frame of this element. Used for measuring the position of
 * an element inside a frame relative to a containing frame.
 *
 * @param {Element} el Element to get the page offset for.
 * @param {Window} relativeWin The window to measure relative to. If relativeWin
 *     is not in the ancestor frame chain of the element, we measure relative to
 *     the top-most window.
 * @return {!goog.math.Coordinate} The page offset.
 */
goog.style.getFramedPageOffset = function(el, relativeWin) {
  var position = new goog.math.Coordinate(0, 0);

  // Iterate up the ancestor frame chain, keeping track of the current window
  // and the current element in that window.
  var currentWin = goog.dom.getWindow(goog.dom.getOwnerDocument(el));
  var currentEl = el;
  do {
    // if we're at the top window, we want to get the page offset.
    // if we're at an inner frame, we only want to get the window position
    // so that we can determine the actual page offset in the context of
    // the outer window.
    var offset = currentWin == relativeWin ?
        goog.style.getPageOffset(currentEl) :
        goog.style.getClientPositionForElement_(
            goog.asserts.assert(currentEl));

    position.x += offset.x;
    position.y += offset.y;
  } while (currentWin && currentWin != relativeWin &&
      currentWin != currentWin.parent &&
      (currentEl = currentWin.frameElement) &&
      (currentWin = currentWin.parent));

  return position;
};


/**
 * Translates the specified rect relative to origBase page, for newBase page.
 * If origBase and newBase are the same, this function does nothing.
 *
 * @param {goog.math.Rect} rect The source rectangle relative to origBase page,
 *     and it will have the translated result.
 * @param {goog.dom.DomHelper} origBase The DomHelper for the input rectangle.
 * @param {goog.dom.DomHelper} newBase The DomHelper for the resultant
 *     coordinate.  This must be a DOM for an ancestor frame of origBase
 *     or the same as origBase.
 */
goog.style.translateRectForAnotherFrame = function(rect, origBase, newBase) {
  if (origBase.getDocument() != newBase.getDocument()) {
    var body = origBase.getDocument().body;
    var pos = goog.style.getFramedPageOffset(body, newBase.getWindow());

    // Adjust Body's margin.
    pos = goog.math.Coordinate.difference(pos, goog.style.getPageOffset(body));

    if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9) &&
        !origBase.isCss1CompatMode()) {
      pos = goog.math.Coordinate.difference(pos, origBase.getDocumentScroll());
    }

    rect.left += pos.x;
    rect.top += pos.y;
  }
};


/**
 * Returns the position of an element relative to another element in the
 * document.  A relative to B
 * @param {Element|Event|goog.events.Event} a Element or mouse event whose
 *     position we're calculating.
 * @param {Element|Event|goog.events.Event} b Element or mouse event position
 *     is relative to.
 * @return {!goog.math.Coordinate} The relative position.
 */
goog.style.getRelativePosition = function(a, b) {
  var ap = goog.style.getClientPosition(a);
  var bp = goog.style.getClientPosition(b);
  return new goog.math.Coordinate(ap.x - bp.x, ap.y - bp.y);
};


/**
 * Returns the position of the event or the element's border box relative to
 * the client viewport.
 * @param {!Element} el Element whose position to get.
 * @return {!goog.math.Coordinate} The position.
 * @private
 */
goog.style.getClientPositionForElement_ = function(el) {
  var box = goog.style.getBoundingClientRect_(el);
  return new goog.math.Coordinate(box.left, box.top);
};


/**
 * Returns the position of the event or the element's border box relative to
 * the client viewport. If an event is passed, and if this event is a "touch"
 * event, then the position of the first changedTouches will be returned.
 * @param {Element|Event|goog.events.Event} el Element or a mouse / touch event.
 * @return {!goog.math.Coordinate} The position.
 */
goog.style.getClientPosition = function(el) {
  goog.asserts.assert(el);
  if (el.nodeType == goog.dom.NodeType.ELEMENT) {
    return goog.style.getClientPositionForElement_(
        /** @type {!Element} */ (el));
  } else {
    var targetEvent = el.changedTouches ? el.changedTouches[0] : el;
    return new goog.math.Coordinate(
        targetEvent.clientX,
        targetEvent.clientY);
  }
};


/**
 * Moves an element to the given coordinates relative to the client viewport.
 * @param {Element} el Absolutely positioned element to set page offset for.
 *     It must be in the document.
 * @param {number|goog.math.Coordinate} x Left position of the element's margin
 *     box or a coordinate object.
 * @param {number=} opt_y Top position of the element's margin box.
 */
goog.style.setPageOffset = function(el, x, opt_y) {
  // Get current pageoffset
  var cur = goog.style.getPageOffset(el);

  if (x instanceof goog.math.Coordinate) {
    opt_y = x.y;
    x = x.x;
  }

  // NOTE(arv): We cannot allow strings for x and y. We could but that would
  // require us to manually transform between different units

  // Work out deltas
  var dx = x - cur.x;
  var dy = opt_y - cur.y;

  // Set position to current left/top + delta
  goog.style.setPosition(el, /** @type {!HTMLElement} */ (el).offsetLeft + dx,
                         /** @type {!HTMLElement} */ (el).offsetTop + dy);
};


/**
 * Sets the width/height values of an element.  If an argument is numeric,
 * or a goog.math.Size is passed, it is assumed to be pixels and will add
 * 'px' after converting it to an integer in string form. (This just sets the
 * CSS width and height properties so it might set content-box or border-box
 * size depending on the box model the browser is using.)
 *
 * @param {Element} element Element to set the size of.
 * @param {string|number|goog.math.Size} w Width of the element, or a
 *     size object.
 * @param {string|number=} opt_h Height of the element. Required if w is not a
 *     size object.
 */
goog.style.setSize = function(element, w, opt_h) {
  var h;
  if (w instanceof goog.math.Size) {
    h = w.height;
    w = w.width;
  } else {
    if (opt_h == undefined) {
      throw Error('missing height argument');
    }
    h = opt_h;
  }

  goog.style.setWidth(element, /** @type {string|number} */ (w));
  goog.style.setHeight(element, h);
};


/**
 * Helper function to create a string to be set into a pixel-value style
 * property of an element. Can round to the nearest integer value.
 *
 * @param {string|number} value The style value to be used. If a number,
 *     'px' will be appended, otherwise the value will be applied directly.
 * @param {boolean} round Whether to round the nearest integer (if property
 *     is a number).
 * @return {string} The string value for the property.
 * @private
 */
goog.style.getPixelStyleValue_ = function(value, round) {
  if (typeof value == 'number') {
    value = (round ? Math.round(value) : value) + 'px';
  }

  return value;
};


/**
 * Set the height of an element.  Sets the element's style property.
 * @param {Element} element Element to set the height of.
 * @param {string|number} height The height value to set.  If a number, 'px'
 *     will be appended, otherwise the value will be applied directly.
 */
goog.style.setHeight = function(element, height) {
  element.style.height = goog.style.getPixelStyleValue_(height, true);
};


/**
 * Set the width of an element.  Sets the element's style property.
 * @param {Element} element Element to set the width of.
 * @param {string|number} width The width value to set.  If a number, 'px'
 *     will be appended, otherwise the value will be applied directly.
 */
goog.style.setWidth = function(element, width) {
  element.style.width = goog.style.getPixelStyleValue_(width, true);
};


/**
 * Gets the height and width of an element, even if its display is none.
 *
 * Specifically, this returns the height and width of the border box,
 * irrespective of the box model in effect.
 *
 * Note that this function does not take CSS transforms into account. Please see
 * {@code goog.style.getTransformedSize}.
 * @param {Element} element Element to get size of.
 * @return {!goog.math.Size} Object with width/height properties.
 */
goog.style.getSize = function(element) {
  return goog.style.evaluateWithTemporaryDisplay_(
      goog.style.getSizeWithDisplay_, /** @type {!Element} */ (element));
};


/**
 * Call {@code fn} on {@code element} such that {@code element}'s dimensions are
 * accurate when it's passed to {@code fn}.
 * @param {function(!Element): T} fn Function to call with {@code element} as
 *     an argument after temporarily changing {@code element}'s display such
 *     that its dimensions are accurate.
 * @param {!Element} element Element (which may have display none) to use as
 *     argument to {@code fn}.
 * @return {T} Value returned by calling {@code fn} with {@code element}.
 * @template T
 * @private
 */
goog.style.evaluateWithTemporaryDisplay_ = function(fn, element) {
  if (goog.style.getStyle_(element, 'display') != 'none') {
    return fn(element);
  }

  var style = element.style;
  var originalDisplay = style.display;
  var originalVisibility = style.visibility;
  var originalPosition = style.position;

  style.visibility = 'hidden';
  style.position = 'absolute';
  style.display = 'inline';

  var retVal = fn(element);

  style.display = originalDisplay;
  style.position = originalPosition;
  style.visibility = originalVisibility;

  return retVal;
};


/**
 * Gets the height and width of an element when the display is not none.
 * @param {Element} element Element to get size of.
 * @return {!goog.math.Size} Object with width/height properties.
 * @private
 */
goog.style.getSizeWithDisplay_ = function(element) {
  var offsetWidth = /** @type {!HTMLElement} */ (element).offsetWidth;
  var offsetHeight = /** @type {!HTMLElement} */ (element).offsetHeight;
  var webkitOffsetsZero =
      goog.userAgent.WEBKIT && !offsetWidth && !offsetHeight;
  if ((!goog.isDef(offsetWidth) || webkitOffsetsZero) &&
      element.getBoundingClientRect) {
    // Fall back to calling getBoundingClientRect when offsetWidth or
    // offsetHeight are not defined, or when they are zero in WebKit browsers.
    // This makes sure that we return for the correct size for SVG elements, but
    // will still return 0 on Webkit prior to 534.8, see
    // http://trac.webkit.org/changeset/67252.
    var clientRect = goog.style.getBoundingClientRect_(element);
    return new goog.math.Size(clientRect.right - clientRect.left,
        clientRect.bottom - clientRect.top);
  }
  return new goog.math.Size(offsetWidth, offsetHeight);
};


/**
 * Gets the height and width of an element, post transform, even if its display
 * is none.
 *
 * This is like {@code goog.style.getSize}, except:
 * <ol>
 * <li>Takes webkitTransforms such as rotate and scale into account.
 * <li>Will return null if {@code element} doesn't respond to
 *     {@code getBoundingClientRect}.
 * <li>Currently doesn't make sense on non-WebKit browsers which don't support
 *    webkitTransforms.
 * </ol>
 * @param {!Element} element Element to get size of.
 * @return {goog.math.Size} Object with width/height properties.
 */
goog.style.getTransformedSize = function(element) {
  if (!element.getBoundingClientRect) {
    return null;
  }

  var clientRect = goog.style.evaluateWithTemporaryDisplay_(
      goog.style.getBoundingClientRect_, element);
  return new goog.math.Size(clientRect.right - clientRect.left,
      clientRect.bottom - clientRect.top);
};


/**
 * Returns a bounding rectangle for a given element in page space.
 * @param {Element} element Element to get bounds of. Must not be display none.
 * @return {!goog.math.Rect} Bounding rectangle for the element.
 */
goog.style.getBounds = function(element) {
  var o = goog.style.getPageOffset(element);
  var s = goog.style.getSize(element);
  return new goog.math.Rect(o.x, o.y, s.width, s.height);
};


/**
 * Converts a CSS selector in the form style-property to styleProperty.
 * @param {*} selector CSS Selector.
 * @return {string} Camel case selector.
 * @deprecated Use goog.string.toCamelCase instead.
 */
goog.style.toCamelCase = function(selector) {
  return goog.string.toCamelCase(String(selector));
};


/**
 * Converts a CSS selector in the form styleProperty to style-property.
 * @param {string} selector Camel case selector.
 * @return {string} Selector cased.
 * @deprecated Use goog.string.toSelectorCase instead.
 */
goog.style.toSelectorCase = function(selector) {
  return goog.string.toSelectorCase(selector);
};


/**
 * Gets the opacity of a node (x-browser). This gets the inline style opacity
 * of the node, and does not take into account the cascaded or the computed
 * style for this node.
 * @param {Element} el Element whose opacity has to be found.
 * @return {number|string} Opacity between 0 and 1 or an empty string {@code ''}
 *     if the opacity is not set.
 */
goog.style.getOpacity = function(el) {
  var style = el.style;
  var result = '';
  if ('opacity' in style) {
    result = style.opacity;
  } else if ('MozOpacity' in style) {
    result = style.MozOpacity;
  } else if ('filter' in style) {
    var match = style.filter.match(/alpha\(opacity=([\d.]+)\)/);
    if (match) {
      result = String(match[1] / 100);
    }
  }
  return result == '' ? result : Number(result);
};


/**
 * Sets the opacity of a node (x-browser).
 * @param {Element} el Elements whose opacity has to be set.
 * @param {number|string} alpha Opacity between 0 and 1 or an empty string
 *     {@code ''} to clear the opacity.
 */
goog.style.setOpacity = function(el, alpha) {
  var style = el.style;
  if ('opacity' in style) {
    style.opacity = alpha;
  } else if ('MozOpacity' in style) {
    style.MozOpacity = alpha;
  } else if ('filter' in style) {
    // TODO(arv): Overwriting the filter might have undesired side effects.
    if (alpha === '') {
      style.filter = '';
    } else {
      style.filter = 'alpha(opacity=' + alpha * 100 + ')';
    }
  }
};


/**
 * Sets the background of an element to a transparent image in a browser-
 * independent manner.
 *
 * This function does not support repeating backgrounds or alternate background
 * positions to match the behavior of Internet Explorer. It also does not
 * support sizingMethods other than crop since they cannot be replicated in
 * browsers other than Internet Explorer.
 *
 * @param {Element} el The element to set background on.
 * @param {string} src The image source URL.
 */
goog.style.setTransparentBackgroundImage = function(el, src) {
  var style = el.style;
  // It is safe to use the style.filter in IE only. In Safari 'filter' is in
  // style object but access to style.filter causes it to throw an exception.
  // Note: IE8 supports images with an alpha channel.
  if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('8')) {
    // See TODO in setOpacity.
    style.filter = 'progid:DXImageTransform.Microsoft.AlphaImageLoader(' +
        'src="' + src + '", sizingMethod="crop")';
  } else {
    // Set style properties individually instead of using background shorthand
    // to prevent overwriting a pre-existing background color.
    style.backgroundImage = 'url(' + src + ')';
    style.backgroundPosition = 'top left';
    style.backgroundRepeat = 'no-repeat';
  }
};


/**
 * Clears the background image of an element in a browser independent manner.
 * @param {Element} el The element to clear background image for.
 */
goog.style.clearTransparentBackgroundImage = function(el) {
  var style = el.style;
  if ('filter' in style) {
    // See TODO in setOpacity.
    style.filter = '';
  } else {
    // Set style properties individually instead of using background shorthand
    // to prevent overwriting a pre-existing background color.
    style.backgroundImage = 'none';
  }
};


/**
 * Shows or hides an element from the page. Hiding the element is done by
 * setting the display property to "none", removing the element from the
 * rendering hierarchy so it takes up no space. To show the element, the default
 * inherited display property is restored (defined either in stylesheets or by
 * the browser's default style rules.)
 *
 * Caveat 1: if the inherited display property for the element is set to "none"
 * by the stylesheets, that is the property that will be restored by a call to
 * showElement(), effectively toggling the display between "none" and "none".
 *
 * Caveat 2: if the element display style is set inline (by setting either
 * element.style.display or a style attribute in the HTML), a call to
 * showElement will clear that setting and defer to the inherited style in the
 * stylesheet.
 * @param {Element} el Element to show or hide.
 * @param {*} display True to render the element in its default style,
 *     false to disable rendering the element.
 * @deprecated Use goog.style.setElementShown instead.
 */
goog.style.showElement = function(el, display) {
  goog.style.setElementShown(el, display);
};


/**
 * Shows or hides an element from the page. Hiding the element is done by
 * setting the display property to "none", removing the element from the
 * rendering hierarchy so it takes up no space. To show the element, the default
 * inherited display property is restored (defined either in stylesheets or by
 * the browser's default style rules).
 *
 * Caveat 1: if the inherited display property for the element is set to "none"
 * by the stylesheets, that is the property that will be restored by a call to
 * setElementShown(), effectively toggling the display between "none" and
 * "none".
 *
 * Caveat 2: if the element display style is set inline (by setting either
 * element.style.display or a style attribute in the HTML), a call to
 * setElementShown will clear that setting and defer to the inherited style in
 * the stylesheet.
 * @param {Element} el Element to show or hide.
 * @param {*} isShown True to render the element in its default style,
 *     false to disable rendering the element.
 */
goog.style.setElementShown = function(el, isShown) {
  el.style.display = isShown ? '' : 'none';
};


/**
 * Test whether the given element has been shown or hidden via a call to
 * {@link #setElementShown}.
 *
 * Note this is strictly a companion method for a call
 * to {@link #setElementShown} and the same caveats apply; in particular, this
 * method does not guarantee that the return value will be consistent with
 * whether or not the element is actually visible.
 *
 * @param {Element} el The element to test.
 * @return {boolean} Whether the element has been shown.
 * @see #setElementShown
 */
goog.style.isElementShown = function(el) {
  return el.style.display != 'none';
};


/**
 * Installs the styles string into the window that contains opt_element.  If
 * opt_element is null, the main window is used.
 * @param {string} stylesString The style string to install.
 * @param {Node=} opt_node Node whose parent document should have the
 *     styles installed.
 * @return {Element|StyleSheet} The style element created.
 */
goog.style.installStyles = function(stylesString, opt_node) {
  var dh = goog.dom.getDomHelper(opt_node);
  var styleSheet = null;

  // IE < 11 requires createStyleSheet. Note that doc.createStyleSheet will be
  // undefined as of IE 11.
  var doc = dh.getDocument();
  if (goog.userAgent.IE && doc.createStyleSheet) {
    styleSheet = doc.createStyleSheet();
    goog.style.setStyles(styleSheet, stylesString);
  } else {
    var head = dh.getElementsByTagNameAndClass(goog.dom.TagName.HEAD)[0];

    // In opera documents are not guaranteed to have a head element, thus we
    // have to make sure one exists before using it.
    if (!head) {
      var body = dh.getElementsByTagNameAndClass(goog.dom.TagName.BODY)[0];
      head = dh.createDom(goog.dom.TagName.HEAD);
      body.parentNode.insertBefore(head, body);
    }
    styleSheet = dh.createDom(goog.dom.TagName.STYLE);
    // NOTE(user): Setting styles after the style element has been appended
    // to the head results in a nasty Webkit bug in certain scenarios. Please
    // refer to https://bugs.webkit.org/show_bug.cgi?id=26307 for additional
    // details.
    goog.style.setStyles(styleSheet, stylesString);
    dh.appendChild(head, styleSheet);
  }
  return styleSheet;
};


/**
 * Removes the styles added by {@link #installStyles}.
 * @param {Element|StyleSheet} styleSheet The value returned by
 *     {@link #installStyles}.
 */
goog.style.uninstallStyles = function(styleSheet) {
  var node = styleSheet.ownerNode || styleSheet.owningElement ||
      /** @type {Element} */ (styleSheet);
  goog.dom.removeNode(node);
};


/**
 * Sets the content of a style element.  The style element can be any valid
 * style element.  This element will have its content completely replaced by
 * the new stylesString.
 * @param {Element|StyleSheet} element A stylesheet element as returned by
 *     installStyles.
 * @param {string} stylesString The new content of the stylesheet.
 */
goog.style.setStyles = function(element, stylesString) {
  if (goog.userAgent.IE && goog.isDef(element.cssText)) {
    // Adding the selectors individually caused the browser to hang if the
    // selector was invalid or there were CSS comments.  Setting the cssText of
    // the style node works fine and ignores CSS that IE doesn't understand.
    // However IE >= 11 doesn't support cssText any more, so we make sure that
    // cssText is a defined property and otherwise fall back to innerHTML.
    element.cssText = stylesString;
  } else {
    element.innerHTML = stylesString;
  }
};


/**
 * Sets 'white-space: pre-wrap' for a node (x-browser).
 *
 * There are as many ways of specifying pre-wrap as there are browsers.
 *
 * CSS3/IE8: white-space: pre-wrap;
 * Mozilla:  white-space: -moz-pre-wrap;
 * Opera:    white-space: -o-pre-wrap;
 * IE6/7:    white-space: pre; word-wrap: break-word;
 *
 * @param {Element} el Element to enable pre-wrap for.
 */
goog.style.setPreWrap = function(el) {
  var style = el.style;
  if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('8')) {
    style.whiteSpace = 'pre';
    style.wordWrap = 'break-word';
  } else if (goog.userAgent.GECKO) {
    style.whiteSpace = '-moz-pre-wrap';
  } else {
    style.whiteSpace = 'pre-wrap';
  }
};


/**
 * Sets 'display: inline-block' for an element (cross-browser).
 * @param {Element} el Element to which the inline-block display style is to be
 *    applied.
 * @see ../demos/inline_block_quirks.html
 * @see ../demos/inline_block_standards.html
 */
goog.style.setInlineBlock = function(el) {
  var style = el.style;
  // Without position:relative, weirdness ensues.  Just accept it and move on.
  style.position = 'relative';

  if (goog.userAgent.IE && !goog.userAgent.isVersionOrHigher('8')) {
    // IE8 supports inline-block so fall through to the else
    // Zoom:1 forces hasLayout, display:inline gives inline behavior.
    style.zoom = '1';
    style.display = 'inline';
  } else {
    // Opera, Webkit, and Safari seem to do OK with the standard inline-block
    // style.
    style.display = 'inline-block';
  }
};


/**
 * Returns true if the element is using right to left (rtl) direction.
 * @param {Element} el  The element to test.
 * @return {boolean} True for right to left, false for left to right.
 */
goog.style.isRightToLeft = function(el) {
  return 'rtl' == goog.style.getStyle_(el, 'direction');
};


/**
 * The CSS style property corresponding to an element being
 * unselectable on the current browser platform (null if none).
 * Opera and IE instead use a DOM attribute 'unselectable'.
 * @type {?string}
 * @private
 */
goog.style.unselectableStyle_ =
    goog.userAgent.GECKO ? 'MozUserSelect' :
    goog.userAgent.WEBKIT ? 'WebkitUserSelect' :
    null;


/**
 * Returns true if the element is set to be unselectable, false otherwise.
 * Note that on some platforms (e.g. Mozilla), even if an element isn't set
 * to be unselectable, it will behave as such if any of its ancestors is
 * unselectable.
 * @param {Element} el  Element to check.
 * @return {boolean}  Whether the element is set to be unselectable.
 */
goog.style.isUnselectable = function(el) {
  if (goog.style.unselectableStyle_) {
    return el.style[goog.style.unselectableStyle_].toLowerCase() == 'none';
  } else if (goog.userAgent.IE || goog.userAgent.OPERA) {
    return el.getAttribute('unselectable') == 'on';
  }
  return false;
};


/**
 * Makes the element and its descendants selectable or unselectable.  Note
 * that on some platforms (e.g. Mozilla), even if an element isn't set to
 * be unselectable, it will behave as such if any of its ancestors is
 * unselectable.
 * @param {Element} el  The element to alter.
 * @param {boolean} unselectable  Whether the element and its descendants
 *     should be made unselectable.
 * @param {boolean=} opt_noRecurse  Whether to only alter the element's own
 *     selectable state, and leave its descendants alone; defaults to false.
 */
goog.style.setUnselectable = function(el, unselectable, opt_noRecurse) {
  // TODO(attila): Do we need all of TR_DomUtil.makeUnselectable() in Closure?
  var descendants = !opt_noRecurse ? el.getElementsByTagName('*') : null;
  var name = goog.style.unselectableStyle_;
  if (name) {
    // Add/remove the appropriate CSS style to/from the element and its
    // descendants.
    var value = unselectable ? 'none' : '';
    // MathML elements do not have a style property. Verify before setting.
    if (el.style) {
      el.style[name] = value;
    }
    if (descendants) {
      for (var i = 0, descendant; descendant = descendants[i]; i++) {
        if (descendant.style) {
          descendant.style[name] = value;
        }
      }
    }
  } else if (goog.userAgent.IE || goog.userAgent.OPERA) {
    // Toggle the 'unselectable' attribute on the element and its descendants.
    var value = unselectable ? 'on' : '';
    el.setAttribute('unselectable', value);
    if (descendants) {
      for (var i = 0, descendant; descendant = descendants[i]; i++) {
        descendant.setAttribute('unselectable', value);
      }
    }
  }
};


/**
 * Gets the border box size for an element.
 * @param {Element} element  The element to get the size for.
 * @return {!goog.math.Size} The border box size.
 */
goog.style.getBorderBoxSize = function(element) {
  return new goog.math.Size(
      /** @type {!HTMLElement} */ (element).offsetWidth,
      /** @type {!HTMLElement} */ (element).offsetHeight);
};


/**
 * Sets the border box size of an element. This is potentially expensive in IE
 * if the document is CSS1Compat mode
 * @param {Element} element  The element to set the size on.
 * @param {goog.math.Size} size  The new size.
 */
goog.style.setBorderBoxSize = function(element, size) {
  var doc = goog.dom.getOwnerDocument(element);
  var isCss1CompatMode = goog.dom.getDomHelper(doc).isCss1CompatMode();

  if (goog.userAgent.IE &&
      !goog.userAgent.isVersionOrHigher('10') &&
      (!isCss1CompatMode || !goog.userAgent.isVersionOrHigher('8'))) {
    var style = element.style;
    if (isCss1CompatMode) {
      var paddingBox = goog.style.getPaddingBox(element);
      var borderBox = goog.style.getBorderBox(element);
      style.pixelWidth = size.width - borderBox.left - paddingBox.left -
                         paddingBox.right - borderBox.right;
      style.pixelHeight = size.height - borderBox.top - paddingBox.top -
                          paddingBox.bottom - borderBox.bottom;
    } else {
      style.pixelWidth = size.width;
      style.pixelHeight = size.height;
    }
  } else {
    goog.style.setBoxSizingSize_(element, size, 'border-box');
  }
};


/**
 * Gets the content box size for an element.  This is potentially expensive in
 * all browsers.
 * @param {Element} element  The element to get the size for.
 * @return {!goog.math.Size} The content box size.
 */
goog.style.getContentBoxSize = function(element) {
  var doc = goog.dom.getOwnerDocument(element);
  var ieCurrentStyle = goog.userAgent.IE && element.currentStyle;
  if (ieCurrentStyle &&
      goog.dom.getDomHelper(doc).isCss1CompatMode() &&
      ieCurrentStyle.width != 'auto' && ieCurrentStyle.height != 'auto' &&
      !ieCurrentStyle.boxSizing) {
    // If IE in CSS1Compat mode than just use the width and height.
    // If we have a boxSizing then fall back on measuring the borders etc.
    var width = goog.style.getIePixelValue_(element, ieCurrentStyle.width,
                                            'width', 'pixelWidth');
    var height = goog.style.getIePixelValue_(element, ieCurrentStyle.height,
                                             'height', 'pixelHeight');
    return new goog.math.Size(width, height);
  } else {
    var borderBoxSize = goog.style.getBorderBoxSize(element);
    var paddingBox = goog.style.getPaddingBox(element);
    var borderBox = goog.style.getBorderBox(element);
    return new goog.math.Size(borderBoxSize.width -
                              borderBox.left - paddingBox.left -
                              paddingBox.right - borderBox.right,
                              borderBoxSize.height -
                              borderBox.top - paddingBox.top -
                              paddingBox.bottom - borderBox.bottom);
  }
};


/**
 * Sets the content box size of an element. This is potentially expensive in IE
 * if the document is BackCompat mode.
 * @param {Element} element  The element to set the size on.
 * @param {goog.math.Size} size  The new size.
 */
goog.style.setContentBoxSize = function(element, size) {
  var doc = goog.dom.getOwnerDocument(element);
  var isCss1CompatMode = goog.dom.getDomHelper(doc).isCss1CompatMode();
  if (goog.userAgent.IE &&
      !goog.userAgent.isVersionOrHigher('10') &&
      (!isCss1CompatMode || !goog.userAgent.isVersionOrHigher('8'))) {
    var style = element.style;
    if (isCss1CompatMode) {
      style.pixelWidth = size.width;
      style.pixelHeight = size.height;
    } else {
      var paddingBox = goog.style.getPaddingBox(element);
      var borderBox = goog.style.getBorderBox(element);
      style.pixelWidth = size.width + borderBox.left + paddingBox.left +
                         paddingBox.right + borderBox.right;
      style.pixelHeight = size.height + borderBox.top + paddingBox.top +
                          paddingBox.bottom + borderBox.bottom;
    }
  } else {
    goog.style.setBoxSizingSize_(element, size, 'content-box');
  }
};


/**
 * Helper function that sets the box sizing as well as the width and height
 * @param {Element} element  The element to set the size on.
 * @param {goog.math.Size} size  The new size to set.
 * @param {string} boxSizing  The box-sizing value.
 * @private
 */
goog.style.setBoxSizingSize_ = function(element, size, boxSizing) {
  var style = element.style;
  if (goog.userAgent.GECKO) {
    style.MozBoxSizing = boxSizing;
  } else if (goog.userAgent.WEBKIT) {
    style.WebkitBoxSizing = boxSizing;
  } else {
    // Includes IE8 and Opera 9.50+
    style.boxSizing = boxSizing;
  }

  // Setting this to a negative value will throw an exception on IE
  // (and doesn't do anything different than setting it to 0).
  style.width = Math.max(size.width, 0) + 'px';
  style.height = Math.max(size.height, 0) + 'px';
};


/**
 * IE specific function that converts a non pixel unit to pixels.
 * @param {Element} element  The element to convert the value for.
 * @param {string} value  The current value as a string. The value must not be
 *     ''.
 * @param {string} name  The CSS property name to use for the converstion. This
 *     should be 'left', 'top', 'width' or 'height'.
 * @param {string} pixelName  The CSS pixel property name to use to get the
 *     value in pixels.
 * @return {number} The value in pixels.
 * @private
 */
goog.style.getIePixelValue_ = function(element, value, name, pixelName) {
  // Try if we already have a pixel value. IE does not do half pixels so we
  // only check if it matches a number followed by 'px'.
  if (/^\d+px?$/.test(value)) {
    return parseInt(value, 10);
  } else {
    var oldStyleValue = element.style[name];
    var oldRuntimeValue = element.runtimeStyle[name];
    // set runtime style to prevent changes
    element.runtimeStyle[name] = element.currentStyle[name];
    element.style[name] = value;
    var pixelValue = element.style[pixelName];
    // restore
    element.style[name] = oldStyleValue;
    element.runtimeStyle[name] = oldRuntimeValue;
    return pixelValue;
  }
};


/**
 * Helper function for getting the pixel padding or margin for IE.
 * @param {Element} element  The element to get the padding for.
 * @param {string} propName  The property name.
 * @return {number} The pixel padding.
 * @private
 */
goog.style.getIePixelDistance_ = function(element, propName) {
  var value = goog.style.getCascadedStyle(element, propName);
  return value ?
      goog.style.getIePixelValue_(element, value, 'left', 'pixelLeft') : 0;
};


/**
 * Gets the computed paddings or margins (on all sides) in pixels.
 * @param {Element} element  The element to get the padding for.
 * @param {string} stylePrefix  Pass 'padding' to retrieve the padding box,
 *     or 'margin' to retrieve the margin box.
 * @return {!goog.math.Box} The computed paddings or margins.
 * @private
 */
goog.style.getBox_ = function(element, stylePrefix) {
  if (goog.userAgent.IE) {
    var left = goog.style.getIePixelDistance_(element, stylePrefix + 'Left');
    var right = goog.style.getIePixelDistance_(element, stylePrefix + 'Right');
    var top = goog.style.getIePixelDistance_(element, stylePrefix + 'Top');
    var bottom = goog.style.getIePixelDistance_(
        element, stylePrefix + 'Bottom');
    return new goog.math.Box(top, right, bottom, left);
  } else {
    // On non-IE browsers, getComputedStyle is always non-null.
    var left = goog.style.getComputedStyle(element, stylePrefix + 'Left');
    var right = goog.style.getComputedStyle(element, stylePrefix + 'Right');
    var top = goog.style.getComputedStyle(element, stylePrefix + 'Top');
    var bottom = goog.style.getComputedStyle(element, stylePrefix + 'Bottom');

    // NOTE(arv): Gecko can return floating point numbers for the computed
    // style values.
    return new goog.math.Box(parseFloat(top),
                             parseFloat(right),
                             parseFloat(bottom),
                             parseFloat(left));
  }
};


/**
 * Gets the computed paddings (on all sides) in pixels.
 * @param {Element} element  The element to get the padding for.
 * @return {!goog.math.Box} The computed paddings.
 */
goog.style.getPaddingBox = function(element) {
  return goog.style.getBox_(element, 'padding');
};


/**
 * Gets the computed margins (on all sides) in pixels.
 * @param {Element} element  The element to get the margins for.
 * @return {!goog.math.Box} The computed margins.
 */
goog.style.getMarginBox = function(element) {
  return goog.style.getBox_(element, 'margin');
};


/**
 * A map used to map the border width keywords to a pixel width.
 * @type {Object}
 * @private
 */
goog.style.ieBorderWidthKeywords_ = {
  'thin': 2,
  'medium': 4,
  'thick': 6
};


/**
 * Helper function for IE to get the pixel border.
 * @param {Element} element  The element to get the pixel border for.
 * @param {string} prop  The part of the property name.
 * @return {number} The value in pixels.
 * @private
 */
goog.style.getIePixelBorder_ = function(element, prop) {
  if (goog.style.getCascadedStyle(element, prop + 'Style') == 'none') {
    return 0;
  }
  var width = goog.style.getCascadedStyle(element, prop + 'Width');
  if (width in goog.style.ieBorderWidthKeywords_) {
    return goog.style.ieBorderWidthKeywords_[width];
  }
  return goog.style.getIePixelValue_(element, width, 'left', 'pixelLeft');
};


/**
 * Gets the computed border widths (on all sides) in pixels
 * @param {Element} element  The element to get the border widths for.
 * @return {!goog.math.Box} The computed border widths.
 */
goog.style.getBorderBox = function(element) {
  if (goog.userAgent.IE && !goog.userAgent.isDocumentModeOrHigher(9)) {
    var left = goog.style.getIePixelBorder_(element, 'borderLeft');
    var right = goog.style.getIePixelBorder_(element, 'borderRight');
    var top = goog.style.getIePixelBorder_(element, 'borderTop');
    var bottom = goog.style.getIePixelBorder_(element, 'borderBottom');
    return new goog.math.Box(top, right, bottom, left);
  } else {
    // On non-IE browsers, getComputedStyle is always non-null.
    var left = goog.style.getComputedStyle(element, 'borderLeftWidth');
    var right = goog.style.getComputedStyle(element, 'borderRightWidth');
    var top = goog.style.getComputedStyle(element, 'borderTopWidth');
    var bottom = goog.style.getComputedStyle(element, 'borderBottomWidth');

    return new goog.math.Box(parseFloat(top),
                             parseFloat(right),
                             parseFloat(bottom),
                             parseFloat(left));
  }
};


/**
 * Returns the font face applied to a given node. Opera and IE should return
 * the font actually displayed. Firefox returns the author's most-preferred
 * font (whether the browser is capable of displaying it or not.)
 * @param {Element} el  The element whose font family is returned.
 * @return {string} The font family applied to el.
 */
goog.style.getFontFamily = function(el) {
  var doc = goog.dom.getOwnerDocument(el);
  var font = '';
  // The moveToElementText method from the TextRange only works if the element
  // is attached to the owner document.
  if (doc.body.createTextRange && goog.dom.contains(doc, el)) {
    var range = doc.body.createTextRange();
    range.moveToElementText(el);
    /** @preserveTry */
    try {
      font = range.queryCommandValue('FontName');
    } catch (e) {
      // This is a workaround for a awkward exception.
      // On some IE, there is an exception coming from it.
      // The error description from this exception is:
      // This window has already been registered as a drop target
      // This is bogus description, likely due to a bug in ie.
      font = '';
    }
  }
  if (!font) {
    // Note if for some reason IE can't derive FontName with a TextRange, we
    // fallback to using currentStyle
    font = goog.style.getStyle_(el, 'fontFamily');
  }

  // Firefox returns the applied font-family string (author's list of
  // preferred fonts.) We want to return the most-preferred font, in lieu of
  // the *actually* applied font.
  var fontsArray = font.split(',');
  if (fontsArray.length > 1) font = fontsArray[0];

  // Sanitize for x-browser consistency:
  // Strip quotes because browsers aren't consistent with how they're
  // applied; Opera always encloses, Firefox sometimes, and IE never.
  return goog.string.stripQuotes(font, '"\'');
};


/**
 * Regular expression used for getLengthUnits.
 * @type {RegExp}
 * @private
 */
goog.style.lengthUnitRegex_ = /[^\d]+$/;


/**
 * Returns the units used for a CSS length measurement.
 * @param {string} value  A CSS length quantity.
 * @return {?string} The units of measurement.
 */
goog.style.getLengthUnits = function(value) {
  var units = value.match(goog.style.lengthUnitRegex_);
  return units && units[0] || null;
};


/**
 * Map of absolute CSS length units
 * @type {Object}
 * @private
 */
goog.style.ABSOLUTE_CSS_LENGTH_UNITS_ = {
  'cm' : 1,
  'in' : 1,
  'mm' : 1,
  'pc' : 1,
  'pt' : 1
};


/**
 * Map of relative CSS length units that can be accurately converted to px
 * font-size values using getIePixelValue_. Only units that are defined in
 * relation to a font size are convertible (%, small, etc. are not).
 * @type {Object}
 * @private
 */
goog.style.CONVERTIBLE_RELATIVE_CSS_UNITS_ = {
  'em' : 1,
  'ex' : 1
};


/**
 * Returns the font size, in pixels, of text in an element.
 * @param {Element} el  The element whose font size is returned.
 * @return {number} The font size (in pixels).
 */
goog.style.getFontSize = function(el) {
  var fontSize = goog.style.getStyle_(el, 'fontSize');
  var sizeUnits = goog.style.getLengthUnits(fontSize);
  if (fontSize && 'px' == sizeUnits) {
    // NOTE(user): This could be parseFloat instead, but IE doesn't return
    // decimal fractions in getStyle_ and Firefox reports the fractions, but
    // ignores them when rendering. Interestingly enough, when we force the
    // issue and size something to e.g., 50% of 25px, the browsers round in
    // opposite directions with Firefox reporting 12px and IE 13px. I punt.
    return parseInt(fontSize, 10);
  }

  // In IE, we can convert absolute length units to a px value using
  // goog.style.getIePixelValue_. Units defined in relation to a font size
  // (em, ex) are applied relative to the element's parentNode and can also
  // be converted.
  if (goog.userAgent.IE) {
    if (sizeUnits in goog.style.ABSOLUTE_CSS_LENGTH_UNITS_) {
      return goog.style.getIePixelValue_(el,
                                         fontSize,
                                         'left',
                                         'pixelLeft');
    } else if (el.parentNode &&
               el.parentNode.nodeType == goog.dom.NodeType.ELEMENT &&
               sizeUnits in goog.style.CONVERTIBLE_RELATIVE_CSS_UNITS_) {
      // Check the parent size - if it is the same it means the relative size
      // value is inherited and we therefore don't want to count it twice.  If
      // it is different, this element either has explicit style or has a CSS
      // rule applying to it.
      var parentElement = /** @type {!Element} */ (el.parentNode);
      var parentSize = goog.style.getStyle_(parentElement, 'fontSize');
      return goog.style.getIePixelValue_(parentElement,
                                         fontSize == parentSize ?
                                             '1em' : fontSize,
                                         'left',
                                         'pixelLeft');
    }
  }

  // Sometimes we can't cleanly find the font size (some units relative to a
  // node's parent's font size are difficult: %, smaller et al), so we create
  // an invisible, absolutely-positioned span sized to be the height of an 'M'
  // rendered in its parent's (i.e., our target element's) font size. This is
  // the definition of CSS's font size attribute.
  var sizeElement = goog.dom.createDom(
      goog.dom.TagName.SPAN,
      {'style': 'visibility:hidden;position:absolute;' +
            'line-height:0;padding:0;margin:0;border:0;height:1em;'});
  goog.dom.appendChild(el, sizeElement);
  fontSize = sizeElement.offsetHeight;
  goog.dom.removeNode(sizeElement);

  return fontSize;
};


/**
 * Parses a style attribute value.  Converts CSS property names to camel case.
 * @param {string} value The style attribute value.
 * @return {!Object} Map of CSS properties to string values.
 */
goog.style.parseStyleAttribute = function(value) {
  var result = {};
  goog.array.forEach(value.split(/\s*;\s*/), function(pair) {
    var keyValue = pair.match(/\s*([\w-]+)\s*\:(.+)/);
    if (keyValue) {
      var styleName = keyValue[1];
      var styleValue = goog.string.trim(keyValue[2]);
      result[goog.string.toCamelCase(styleName.toLowerCase())] = styleValue;
    }
  });
  return result;
};


/**
 * Reverse of parseStyleAttribute; that is, takes a style object and returns the
 * corresponding attribute value.  Converts camel case property names to proper
 * CSS selector names.
 * @param {Object} obj Map of CSS properties to values.
 * @return {string} The style attribute value.
 */
goog.style.toStyleAttribute = function(obj) {
  var buffer = [];
  goog.object.forEach(obj, function(value, key) {
    buffer.push(goog.string.toSelectorCase(key), ':', value, ';');
  });
  return buffer.join('');
};


/**
 * Sets CSS float property on an element.
 * @param {Element} el The element to set float property on.
 * @param {string} value The value of float CSS property to set on this element.
 */
goog.style.setFloat = function(el, value) {
  el.style[goog.userAgent.IE ? 'styleFloat' : 'cssFloat'] = value;
};


/**
 * Gets value of explicitly-set float CSS property on an element.
 * @param {Element} el The element to get float property of.
 * @return {string} The value of explicitly-set float CSS property on this
 *     element.
 */
goog.style.getFloat = function(el) {
  return el.style[goog.userAgent.IE ? 'styleFloat' : 'cssFloat'] || '';
};


/**
 * Returns the scroll bar width (represents the width of both horizontal
 * and vertical scroll).
 *
 * @param {string=} opt_className An optional class name (or names) to apply
 *     to the invisible div created to measure the scrollbar. This is necessary
 *     if some scrollbars are styled differently than others.
 * @return {number} The scroll bar width in px.
 */
goog.style.getScrollbarWidth = function(opt_className) {
  // Add two hidden divs.  The child div is larger than the parent and
  // forces scrollbars to appear on it.
  // Using overflow:scroll does not work consistently with scrollbars that
  // are styled with ::-webkit-scrollbar.
  var outerDiv = goog.dom.createElement(goog.dom.TagName.DIV);
  if (opt_className) {
    outerDiv.className = opt_className;
  }
  outerDiv.style.cssText = 'overflow:auto;' +
      'position:absolute;top:0;width:100px;height:100px';
  var innerDiv = goog.dom.createElement(goog.dom.TagName.DIV);
  goog.style.setSize(innerDiv, '200px', '200px');
  outerDiv.appendChild(innerDiv);
  goog.dom.appendChild(goog.dom.getDocument().body, outerDiv);
  var width = outerDiv.offsetWidth - outerDiv.clientWidth;
  goog.dom.removeNode(outerDiv);
  return width;
};


/**
 * Regular expression to extract x and y translation components from a CSS
 * transform Matrix representation.
 *
 * @type {!RegExp}
 * @const
 * @private
 */
goog.style.MATRIX_TRANSLATION_REGEX_ =
    new RegExp('matrix\\([0-9\\.\\-]+, [0-9\\.\\-]+, ' +
               '[0-9\\.\\-]+, [0-9\\.\\-]+, ' +
               '([0-9\\.\\-]+)p?x?, ([0-9\\.\\-]+)p?x?\\)');


/**
 * Returns the x,y translation component of any CSS transforms applied to the
 * element, in pixels.
 *
 * @param {!Element} element The element to get the translation of.
 * @return {!goog.math.Coordinate} The CSS translation of the element in px.
 */
goog.style.getCssTranslation = function(element) {
  var transform = goog.style.getComputedTransform(element);
  if (!transform) {
    return new goog.math.Coordinate(0, 0);
  }
  var matches = transform.match(goog.style.MATRIX_TRANSLATION_REGEX_);
  if (!matches) {
    return new goog.math.Coordinate(0, 0);
  }
  return new goog.math.Coordinate(parseFloat(matches[1]),
                                  parseFloat(matches[2]));
};

goog.provide('ol.MapEvent');
goog.provide('ol.MapEventType');

goog.require('goog.events.Event');


/**
 * @enum {string}
 */
ol.MapEventType = {

  /**
   * Triggered after a map frame is rendered.
   * @event ol.MapEvent#postrender
   * @api
   */
  POSTRENDER: 'postrender',

  /**
   * Triggered after the map is moved.
   * @event ol.MapEvent#moveend
   * @api stable
   */
  MOVEEND: 'moveend'

};


/**
 * @classdesc
 * Events emitted as map events are instances of this type.
 * See {@link ol.Map} for which events trigger a map event.
 *
 * @constructor
 * @extends {goog.events.Event}
 * @implements {oli.MapEvent}
 * @param {string} type Event type.
 * @param {ol.Map} map Map.
 * @param {?olx.FrameState=} opt_frameState Frame state.
 */
ol.MapEvent = function(type, map, opt_frameState) {

  goog.base(this, type);

  /**
   * The map where the event occurred.
   * @type {ol.Map}
   * @api stable
   */
  this.map = map;

  /**
   * The frame state at the time of the event.
   * @type {?olx.FrameState}
   * @api
   */
  this.frameState = opt_frameState !== undefined ? opt_frameState : null;

};
goog.inherits(ol.MapEvent, goog.events.Event);

goog.provide('ol.control.Control');

goog.require('goog.dom');
goog.require('goog.events');
goog.require('ol');
goog.require('ol.MapEventType');
goog.require('ol.Object');


/**
 * @classdesc
 * A control is a visible widget with a DOM element in a fixed position on the
 * screen. They can involve user input (buttons), or be informational only;
 * the position is determined using CSS. By default these are placed in the
 * container with CSS class name `ol-overlaycontainer-stopevent`, but can use
 * any outside DOM element.
 *
 * This is the base class for controls. You can use it for simple custom
 * controls by creating the element with listeners, creating an instance:
 * ```js
 * var myControl = new ol.control.Control({element: myElement});
 * ```
 * and then adding this to the map.
 *
 * The main advantage of having this as a control rather than a simple separate
 * DOM element is that preventing propagation is handled for you. Controls
 * will also be `ol.Object`s in a `ol.Collection`, so you can use their
 * methods.
 *
 * You can also extend this base for your own control class. See
 * examples/custom-controls for an example of how to do this.
 *
 * @constructor
 * @extends {ol.Object}
 * @implements {oli.control.Control}
 * @param {olx.control.ControlOptions} options Control options.
 * @api stable
 */
ol.control.Control = function(options) {

  goog.base(this);

  /**
   * @protected
   * @type {Element}
   */
  this.element = options.element ? options.element : null;

  /**
   * @private
   * @type {Element}
   */
  this.target_ = null;

  /**
   * @private
   * @type {ol.Map}
   */
  this.map_ = null;

  /**
   * @protected
   * @type {!Array.<?number>}
   */
  this.listenerKeys = [];

  /**
   * @type {function(ol.MapEvent)}
   */
  this.render = options.render ? options.render : ol.nullFunction;

  if (options.target) {
    this.setTarget(options.target);
  }

};
goog.inherits(ol.control.Control, ol.Object);


/**
 * @inheritDoc
 */
ol.control.Control.prototype.disposeInternal = function() {
  goog.dom.removeNode(this.element);
  goog.base(this, 'disposeInternal');
};


/**
 * Get the map associated with this control.
 * @return {ol.Map} Map.
 * @api stable
 */
ol.control.Control.prototype.getMap = function() {
  return this.map_;
};


/**
 * Remove the control from its current map and attach it to the new map.
 * Subclasses may set up event handlers to get notified about changes to
 * the map here.
 * @param {ol.Map} map Map.
 * @api stable
 */
ol.control.Control.prototype.setMap = function(map) {
  if (this.map_) {
    goog.dom.removeNode(this.element);
  }
  if (this.listenerKeys.length > 0) {
    this.listenerKeys.forEach(goog.events.unlistenByKey);
    this.listenerKeys.length = 0;
  }
  this.map_ = map;
  if (this.map_) {
    var target = this.target_ ?
        this.target_ : map.getOverlayContainerStopEvent();
    target.appendChild(this.element);
    if (this.render !== ol.nullFunction) {
      this.listenerKeys.push(goog.events.listen(map,
          ol.MapEventType.POSTRENDER, this.render, false, this));
    }
    map.render();
  }
};


/**
 * This function is used to set a target element for the control. It has no
 * effect if it is called after the control has been added to the map (i.e.
 * after `setMap` is called on the control). If no `target` is set in the
 * options passed to the control constructor and if `setTarget` is not called
 * then the control is added to the map's overlay container.
 * @param {Element|string} target Target.
 * @api
 */
ol.control.Control.prototype.setTarget = function(target) {
  this.target_ = goog.dom.getElement(target);
};

goog.provide('ol.css');


/**
 * The CSS class for hidden feature.
 *
 * @const
 * @type {string}
 */
ol.css.CLASS_HIDDEN = 'ol-hidden';


/**
 * The CSS class that we'll give the DOM elements to have them unselectable.
 *
 * @const
 * @type {string}
 */
ol.css.CLASS_UNSELECTABLE = 'ol-unselectable';


/**
 * The CSS class for unsupported feature.
 *
 * @const
 * @type {string}
 */
ol.css.CLASS_UNSUPPORTED = 'ol-unsupported';


/**
 * The CSS class for controls.
 *
 * @const
 * @type {string}
 */
ol.css.CLASS_CONTROL = 'ol-control';

goog.provide('ol.structs.LRUCache');

goog.require('goog.asserts');
goog.require('goog.object');


/**
 * Implements a Least-Recently-Used cache where the keys do not conflict with
 * Object's properties (e.g. 'hasOwnProperty' is not allowed as a key). Expiring
 * items from the cache is the responsibility of the user.
 * @constructor
 * @struct
 * @template T
 */
ol.structs.LRUCache = function() {

  /**
   * @private
   * @type {number}
   */
  this.count_ = 0;

  /**
   * @private
   * @type {Object.<string, ol.structs.LRUCacheEntry>}
   */
  this.entries_ = {};

  /**
   * @private
   * @type {?ol.structs.LRUCacheEntry}
   */
  this.oldest_ = null;

  /**
   * @private
   * @type {?ol.structs.LRUCacheEntry}
   */
  this.newest_ = null;

};


/**
 * FIXME empty description for jsdoc
 */
ol.structs.LRUCache.prototype.assertValid = function() {
  if (this.count_ === 0) {
    goog.asserts.assert(goog.object.isEmpty(this.entries_),
        'entries must be an empty object (count = 0)');
    goog.asserts.assert(!this.oldest_,
        'oldest must be null (count = 0)');
    goog.asserts.assert(!this.newest_,
        'newest must be null (count = 0)');
  } else {
    goog.asserts.assert(goog.object.getCount(this.entries_) == this.count_,
        'number of entries matches count');
    goog.asserts.assert(this.oldest_,
        'we have an oldest entry');
    goog.asserts.assert(!this.oldest_.older,
        'no entry is older than oldest');
    goog.asserts.assert(this.newest_,
        'we have a newest entry');
    goog.asserts.assert(!this.newest_.newer,
        'no entry is newer than newest');
    var i, entry;
    var older = null;
    i = 0;
    for (entry = this.oldest_; entry; entry = entry.newer) {
      goog.asserts.assert(entry.older === older,
          'entry.older links to correct older');
      older = entry;
      ++i;
    }
    goog.asserts.assert(i == this.count_, 'iterated correct amount of times');
    var newer = null;
    i = 0;
    for (entry = this.newest_; entry; entry = entry.older) {
      goog.asserts.assert(entry.newer === newer,
          'entry.newer links to correct newer');
      newer = entry;
      ++i;
    }
    goog.asserts.assert(i == this.count_, 'iterated correct amount of times');
  }
};


/**
 * FIXME empty description for jsdoc
 */
ol.structs.LRUCache.prototype.clear = function() {
  this.count_ = 0;
  this.entries_ = {};
  this.oldest_ = null;
  this.newest_ = null;
};


/**
 * @param {string} key Key.
 * @return {boolean} Contains key.
 */
ol.structs.LRUCache.prototype.containsKey = function(key) {
  return this.entries_.hasOwnProperty(key);
};


/**
 * @param {function(this: S, T, string, ol.structs.LRUCache): ?} f The function
 *     to call for every entry from the oldest to the newer. This function takes
 *     3 arguments (the entry value, the entry key and the LRUCache object).
 *     The return value is ignored.
 * @param {S=} opt_this The object to use as `this` in `f`.
 * @template S
 */
ol.structs.LRUCache.prototype.forEach = function(f, opt_this) {
  var entry = this.oldest_;
  while (entry) {
    f.call(opt_this, entry.value_, entry.key_, this);
    entry = entry.newer;
  }
};


/**
 * @param {string} key Key.
 * @return {T} Value.
 */
ol.structs.LRUCache.prototype.get = function(key) {
  var entry = this.entries_[key];
  goog.asserts.assert(entry !== undefined, 'an entry exists for key %s', key);
  if (entry === this.newest_) {
    return entry.value_;
  } else if (entry === this.oldest_) {
    this.oldest_ = this.oldest_.newer;
    this.oldest_.older = null;
  } else {
    entry.newer.older = entry.older;
    entry.older.newer = entry.newer;
  }
  entry.newer = null;
  entry.older = this.newest_;
  this.newest_.newer = entry;
  this.newest_ = entry;
  return entry.value_;
};


/**
 * @return {number} Count.
 */
ol.structs.LRUCache.prototype.getCount = function() {
  return this.count_;
};


/**
 * @return {Array.<string>} Keys.
 */
ol.structs.LRUCache.prototype.getKeys = function() {
  var keys = new Array(this.count_);
  var i = 0;
  var entry;
  for (entry = this.newest_; entry; entry = entry.older) {
    keys[i++] = entry.key_;
  }
  goog.asserts.assert(i == this.count_, 'iterated correct number of times');
  return keys;
};


/**
 * @return {Array.<T>} Values.
 */
ol.structs.LRUCache.prototype.getValues = function() {
  var values = new Array(this.count_);
  var i = 0;
  var entry;
  for (entry = this.newest_; entry; entry = entry.older) {
    values[i++] = entry.value_;
  }
  goog.asserts.assert(i == this.count_, 'iterated correct number of times');
  return values;
};


/**
 * @return {T} Last value.
 */
ol.structs.LRUCache.prototype.peekLast = function() {
  goog.asserts.assert(this.oldest_, 'oldest must not be null');
  return this.oldest_.value_;
};


/**
 * @return {string} Last key.
 */
ol.structs.LRUCache.prototype.peekLastKey = function() {
  goog.asserts.assert(this.oldest_, 'oldest must not be null');
  return this.oldest_.key_;
};


/**
 * @return {T} value Value.
 */
ol.structs.LRUCache.prototype.pop = function() {
  goog.asserts.assert(this.oldest_, 'oldest must not be null');
  goog.asserts.assert(this.newest_, 'newest must not be null');
  var entry = this.oldest_;
  goog.asserts.assert(entry.key_ in this.entries_,
      'oldest is indexed in entries');
  delete this.entries_[entry.key_];
  if (entry.newer) {
    entry.newer.older = null;
  }
  this.oldest_ = entry.newer;
  if (!this.oldest_) {
    this.newest_ = null;
  }
  --this.count_;
  return entry.value_;
};


/**
 * @param {string} key Key.
 * @param {T} value Value.
 */
ol.structs.LRUCache.prototype.replace = function(key, value) {
  this.get(key);  // update `newest_`
  this.entries_[key].value_ = value;
};


/**
 * @param {string} key Key.
 * @param {T} value Value.
 */
ol.structs.LRUCache.prototype.set = function(key, value) {
  goog.asserts.assert(!(key in {}),
      'key is not a standard property of objects (e.g. "__proto__")');
  goog.asserts.assert(!(key in this.entries_),
      'key is not used already');
  var entry = {
    key_: key,
    newer: null,
    older: this.newest_,
    value_: value
  };
  if (!this.newest_) {
    this.oldest_ = entry;
  } else {
    this.newest_.newer = entry;
  }
  this.newest_ = entry;
  this.entries_[key] = entry;
  ++this.count_;
};


/**
 * @typedef {{key_: string,
 *            newer: ol.structs.LRUCacheEntry,
 *            older: ol.structs.LRUCacheEntry,
 *            value_: *}}
 */
ol.structs.LRUCacheEntry;

goog.provide('ol.TileCache');

goog.require('ol');
goog.require('ol.TileRange');
goog.require('ol.structs.LRUCache');
goog.require('ol.tilecoord');


/**
 * @constructor
 * @extends {ol.structs.LRUCache.<ol.Tile>}
 * @param {number=} opt_highWaterMark High water mark.
 * @struct
 */
ol.TileCache = function(opt_highWaterMark) {

  goog.base(this);

  /**
   * @private
   * @type {number}
   */
  this.highWaterMark_ = opt_highWaterMark !== undefined ?
      opt_highWaterMark : ol.DEFAULT_TILE_CACHE_HIGH_WATER_MARK;

};
goog.inherits(ol.TileCache, ol.structs.LRUCache);


/**
 * @return {boolean} Can expire cache.
 */
ol.TileCache.prototype.canExpireCache = function() {
  return this.getCount() > this.highWaterMark_;
};


/**
 * @param {Object.<string, ol.TileRange>} usedTiles Used tiles.
 */
ol.TileCache.prototype.expireCache = function(usedTiles) {
  var tile, zKey;
  while (this.canExpireCache()) {
    tile = this.peekLast();
    zKey = tile.tileCoord[0].toString();
    if (zKey in usedTiles && usedTiles[zKey].contains(tile.tileCoord)) {
      break;
    } else {
      this.pop().dispose();
    }
  }
};


/**
 * Remove a tile range from the cache, e.g. to invalidate tiles.
 * @param {ol.TileRange} tileRange The tile range to prune.
 */
ol.TileCache.prototype.pruneTileRange = function(tileRange) {
  var i = this.getCount(),
      key;
  while (i--) {
    key = this.peekLastKey();
    if (tileRange.contains(ol.tilecoord.createFromString(key))) {
      this.pop().dispose();
    } else {
      this.get(key);
    }
  }
};

goog.provide('ol.Tile');
goog.provide('ol.TileState');

goog.require('goog.events');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('ol.TileCoord');


/**
 * @enum {number}
 */
ol.TileState = {
  IDLE: 0,
  LOADING: 1,
  LOADED: 2,
  ERROR: 3,
  EMPTY: 4
};


/**
 * @classdesc
 * Base class for tiles.
 *
 * @constructor
 * @extends {goog.events.EventTarget}
 * @param {ol.TileCoord} tileCoord Tile coordinate.
 * @param {ol.TileState} state State.
 */
ol.Tile = function(tileCoord, state) {

  goog.base(this);

  /**
   * @type {ol.TileCoord}
   */
  this.tileCoord = tileCoord;

  /**
   * @protected
   * @type {ol.TileState}
   */
  this.state = state;

  /**
   * An "interim" tile for this tile. The interim tile may be used while this
   * one is loading, for "smooth" transitions when changing params/dimensions
   * on the source.
   * @type {ol.Tile}
   */
  this.interimTile = null;

  /**
   * A key assigned to the tile. This is used by the tile source to determine
   * if this tile can effectively be used, or if a new tile should be created
   * and this one be used as an interim tile for this new tile.
   * @type {string}
   */
  this.key = '';

};
goog.inherits(ol.Tile, goog.events.EventTarget);


/**
 * @protected
 */
ol.Tile.prototype.changed = function() {
  this.dispatchEvent(goog.events.EventType.CHANGE);
};


/**
 * Get the HTML image element for this tile (may be a Canvas, Image, or Video).
 * @function
 * @param {Object=} opt_context Object.
 * @return {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} Image.
 */
ol.Tile.prototype.getImage = goog.abstractMethod;


/**
 * @return {string} Key.
 */
ol.Tile.prototype.getKey = function() {
  return goog.getUid(this).toString();
};


/**
 * Get the tile coordinate for this tile.
 * @return {ol.TileCoord} The tile coordinate.
 * @api
 */
ol.Tile.prototype.getTileCoord = function() {
  return this.tileCoord;
};


/**
 * @return {ol.TileState} State.
 */
ol.Tile.prototype.getState = function() {
  return this.state;
};


/**
 * FIXME empty description for jsdoc
 */
ol.Tile.prototype.load = goog.abstractMethod;

goog.provide('ol.source.Source');
goog.provide('ol.source.State');

goog.require('ol');
goog.require('ol.Attribution');
goog.require('ol.Object');
goog.require('ol.proj');


/**
 * State of the source, one of 'undefined', 'loading', 'ready' or 'error'.
 * @enum {string}
 * @api
 */
ol.source.State = {
  UNDEFINED: 'undefined',
  LOADING: 'loading',
  READY: 'ready',
  ERROR: 'error'
};


/**
 * @typedef {{attributions: (Array.<ol.Attribution>|undefined),
 *            logo: (string|olx.LogoOptions|undefined),
 *            projection: ol.proj.ProjectionLike,
 *            state: (ol.source.State|undefined),
 *            wrapX: (boolean|undefined)}}
 */
ol.source.SourceOptions;


/**
 * @classdesc
 * Abstract base class; normally only used for creating subclasses and not
 * instantiated in apps.
 * Base class for {@link ol.layer.Layer} sources.
 *
 * A generic `change` event is triggered when the state of the source changes.
 *
 * @constructor
 * @extends {ol.Object}
 * @param {ol.source.SourceOptions} options Source options.
 * @api stable
 */
ol.source.Source = function(options) {

  goog.base(this);

  /**
   * @private
   * @type {ol.proj.Projection}
   */
  this.projection_ = ol.proj.get(options.projection);

  /**
   * @private
   * @type {Array.<ol.Attribution>}
   */
  this.attributions_ = options.attributions !== undefined ?
      options.attributions : null;

  /**
   * @private
   * @type {string|olx.LogoOptions|undefined}
   */
  this.logo_ = options.logo;

  /**
   * @private
   * @type {ol.source.State}
   */
  this.state_ = options.state !== undefined ?
      options.state : ol.source.State.READY;

  /**
   * @private
   * @type {boolean}
   */
  this.wrapX_ = options.wrapX !== undefined ? options.wrapX : false;

};
goog.inherits(ol.source.Source, ol.Object);


/**
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {number} resolution Resolution.
 * @param {number} rotation Rotation.
 * @param {Object.<string, boolean>} skippedFeatureUids Skipped feature uids.
 * @param {function((ol.Feature|ol.render.Feature)): T} callback Feature
 *     callback.
 * @return {T|undefined} Callback result.
 * @template T
 */
ol.source.Source.prototype.forEachFeatureAtCoordinate = ol.nullFunction;


/**
 * Get the attributions of the source.
 * @return {Array.<ol.Attribution>} Attributions.
 * @api stable
 */
ol.source.Source.prototype.getAttributions = function() {
  return this.attributions_;
};


/**
 * Get the logo of the source.
 * @return {string|olx.LogoOptions|undefined} Logo.
 * @api stable
 */
ol.source.Source.prototype.getLogo = function() {
  return this.logo_;
};


/**
 * Get the projection of the source.
 * @return {ol.proj.Projection} Projection.
 * @api
 */
ol.source.Source.prototype.getProjection = function() {
  return this.projection_;
};


/**
 * @return {Array.<number>|undefined} Resolutions.
 */
ol.source.Source.prototype.getResolutions = goog.abstractMethod;


/**
 * Get the state of the source, see {@link ol.source.State} for possible states.
 * @return {ol.source.State} State.
 * @api
 */
ol.source.Source.prototype.getState = function() {
  return this.state_;
};


/**
 * @return {boolean|undefined} Wrap X.
 */
ol.source.Source.prototype.getWrapX = function() {
  return this.wrapX_;
};


/**
 * Set the attributions of the source.
 * @param {Array.<ol.Attribution>} attributions Attributions.
 * @api
 */
ol.source.Source.prototype.setAttributions = function(attributions) {
  this.attributions_ = attributions;
  this.changed();
};


/**
 * Set the logo of the source.
 * @param {string|olx.LogoOptions|undefined} logo Logo.
 */
ol.source.Source.prototype.setLogo = function(logo) {
  this.logo_ = logo;
};


/**
 * Set the state of the source.
 * @param {ol.source.State} state State.
 * @protected
 */
ol.source.Source.prototype.setState = function(state) {
  this.state_ = state;
  this.changed();
};


/**
 * Set the projection of the source.
 * @param {ol.proj.Projection} projection Projection.
 */
ol.source.Source.prototype.setProjection = function(projection) {
  this.projection_ = projection;
};

goog.provide('ol.tilegrid.TileGrid');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.object');
goog.require('ol');
goog.require('ol.Coordinate');
goog.require('ol.TileCoord');
goog.require('ol.TileRange');
goog.require('ol.array');
goog.require('ol.extent');
goog.require('ol.extent.Corner');
goog.require('ol.math');
goog.require('ol.proj');
goog.require('ol.proj.METERS_PER_UNIT');
goog.require('ol.proj.Projection');
goog.require('ol.proj.Units');
goog.require('ol.size');
goog.require('ol.tilecoord');


/**
 * @classdesc
 * Base class for setting the grid pattern for sources accessing tiled-image
 * servers.
 *
 * @constructor
 * @param {olx.tilegrid.TileGridOptions} options Tile grid options.
 * @struct
 * @api stable
 */
ol.tilegrid.TileGrid = function(options) {

  /**
   * @protected
   * @type {number}
   */
  this.minZoom = options.minZoom !== undefined ? options.minZoom : 0;

  /**
   * @private
   * @type {!Array.<number>}
   */
  this.resolutions_ = options.resolutions;
  goog.asserts.assert(goog.array.isSorted(this.resolutions_, function(a, b) {
    return b - a;
  }, true), 'resolutions must be sorted in descending order');

  /**
   * @protected
   * @type {number}
   */
  this.maxZoom = this.resolutions_.length - 1;

  /**
   * @private
   * @type {ol.Coordinate}
   */
  this.origin_ = options.origin !== undefined ? options.origin : null;

  /**
   * @private
   * @type {Array.<ol.Coordinate>}
   */
  this.origins_ = null;
  if (options.origins !== undefined) {
    this.origins_ = options.origins;
    goog.asserts.assert(this.origins_.length == this.resolutions_.length,
        'number of origins and resolutions must be equal');
  }

  var extent = options.extent;

  if (extent !== undefined &&
      !this.origin_ && !this.origins_) {
    this.origin_ = ol.extent.getTopLeft(extent);
  }

  goog.asserts.assert(
      (!this.origin_ && this.origins_) ||
      (this.origin_ && !this.origins_),
      'either origin or origins must be configured, never both');

  /**
   * @private
   * @type {Array.<number|ol.Size>}
   */
  this.tileSizes_ = null;
  if (options.tileSizes !== undefined) {
    this.tileSizes_ = options.tileSizes;
    goog.asserts.assert(this.tileSizes_.length == this.resolutions_.length,
        'number of tileSizes and resolutions must be equal');
  }

  /**
   * @private
   * @type {number|ol.Size}
   */
  this.tileSize_ = options.tileSize !== undefined ?
      options.tileSize :
      !this.tileSizes_ ? ol.DEFAULT_TILE_SIZE : null;
  goog.asserts.assert(
      (!this.tileSize_ && this.tileSizes_) ||
      (this.tileSize_ && !this.tileSizes_),
      'either tileSize or tileSizes must be configured, never both');

  /**
   * @private
   * @type {ol.Extent}
   */
  this.extent_ = extent !== undefined ? extent : null;


  /**
   * @private
   * @type {Array.<ol.TileRange>}
   */
  this.fullTileRanges_ = null;

  if (options.sizes !== undefined) {
    goog.asserts.assert(options.sizes.length == this.resolutions_.length,
        'number of sizes and resolutions must be equal');
    this.fullTileRanges_ = options.sizes.map(function(size, z) {
      goog.asserts.assert(size[0] !== 0, 'width must not be 0');
      goog.asserts.assert(size[1] !== 0, 'height must not be 0');
      var tileRange = new ol.TileRange(
          Math.min(0, size[0]), Math.max(size[0] - 1, -1),
          Math.min(0, size[1]), Math.max(size[1] - 1, -1));
      if (this.minZoom <= z && z <= this.maxZoom && extent !== undefined) {
        goog.asserts.assert(tileRange.containsTileRange(
            this.getTileRangeForExtentAndZ(extent, z)),
            'extent tile range must not exceed tilegrid width and height');
      }
      return tileRange;
    }, this);
  } else if (extent) {
    this.calculateTileRanges_(extent);
  }

  /**
   * @private
   * @type {ol.Size}
   */
  this.tmpSize_ = [0, 0];

};


/**
 * @private
 * @type {ol.TileCoord}
 */
ol.tilegrid.TileGrid.tmpTileCoord_ = [0, 0, 0];


/**
 * @param {ol.TileCoord} tileCoord Tile coordinate.
 * @param {function(this: T, number, ol.TileRange): boolean} callback Callback.
 * @param {T=} opt_this The object to use as `this` in `callback`.
 * @param {ol.TileRange=} opt_tileRange Temporary ol.TileRange object.
 * @param {ol.Extent=} opt_extent Temporary ol.Extent object.
 * @return {boolean} Callback succeeded.
 * @template T
 */
ol.tilegrid.TileGrid.prototype.forEachTileCoordParentTileRange = function(tileCoord, callback, opt_this, opt_tileRange, opt_extent) {
  var tileCoordExtent = this.getTileCoordExtent(tileCoord, opt_extent);
  var z = tileCoord[0] - 1;
  while (z >= this.minZoom) {
    if (callback.call(opt_this, z,
        this.getTileRangeForExtentAndZ(tileCoordExtent, z, opt_tileRange))) {
      return true;
    }
    --z;
  }
  return false;
};


/**
 * Get the extent for this tile grid, if it was configured.
 * @return {ol.Extent} Extent.
 */
ol.tilegrid.TileGrid.prototype.getExtent = function() {
  return this.extent_;
};


/**
 * Get the maximum zoom level for the grid.
 * @return {number} Max zoom.
 * @api
 */
ol.tilegrid.TileGrid.prototype.getMaxZoom = function() {
  return this.maxZoom;
};


/**
 * Get the minimum zoom level for the grid.
 * @return {number} Min zoom.
 * @api
 */
ol.tilegrid.TileGrid.prototype.getMinZoom = function() {
  return this.minZoom;
};


/**
 * Get the origin for the grid at the given zoom level.
 * @param {number} z Z.
 * @return {ol.Coordinate} Origin.
 * @api stable
 */
ol.tilegrid.TileGrid.prototype.getOrigin = function(z) {
  if (this.origin_) {
    return this.origin_;
  } else {
    goog.asserts.assert(this.origins_,
        'origins cannot be null if origin is null');
    goog.asserts.assert(this.minZoom <= z && z <= this.maxZoom,
        'given z is not in allowed range (%s <= %s <= %s)',
        this.minZoom, z, this.maxZoom);
    return this.origins_[z];
  }
};


/**
 * Get the resolution for the given zoom level.
 * @param {number} z Z.
 * @return {number} Resolution.
 * @api stable
 */
ol.tilegrid.TileGrid.prototype.getResolution = function(z) {
  goog.asserts.assert(this.minZoom <= z && z <= this.maxZoom,
      'given z is not in allowed range (%s <= %s <= %s)',
      this.minZoom, z, this.maxZoom);
  return this.resolutions_[z];
};


/**
 * Get the list of resolutions for the tile grid.
 * @return {Array.<number>} Resolutions.
 * @api stable
 */
ol.tilegrid.TileGrid.prototype.getResolutions = function() {
  return this.resolutions_;
};


/**
 * @param {ol.TileCoord} tileCoord Tile coordinate.
 * @param {ol.TileRange=} opt_tileRange Temporary ol.TileRange object.
 * @param {ol.Extent=} opt_extent Temporary ol.Extent object.
 * @return {ol.TileRange} Tile range.
 */
ol.tilegrid.TileGrid.prototype.getTileCoordChildTileRange = function(tileCoord, opt_tileRange, opt_extent) {
  if (tileCoord[0] < this.maxZoom) {
    var tileCoordExtent = this.getTileCoordExtent(tileCoord, opt_extent);
    return this.getTileRangeForExtentAndZ(
        tileCoordExtent, tileCoord[0] + 1, opt_tileRange);
  } else {
    return null;
  }
};


/**
 * @param {number} z Z.
 * @param {ol.TileRange} tileRange Tile range.
 * @param {ol.Extent=} opt_extent Temporary ol.Extent object.
 * @return {ol.Extent} Extent.
 */
ol.tilegrid.TileGrid.prototype.getTileRangeExtent = function(z, tileRange, opt_extent) {
  var origin = this.getOrigin(z);
  var resolution = this.getResolution(z);
  var tileSize = ol.size.toSize(this.getTileSize(z), this.tmpSize_);
  var minX = origin[0] + tileRange.minX * tileSize[0] * resolution;
  var maxX = origin[0] + (tileRange.maxX + 1) * tileSize[0] * resolution;
  var minY = origin[1] + tileRange.minY * tileSize[1] * resolution;
  var maxY = origin[1] + (tileRange.maxY + 1) * tileSize[1] * resolution;
  return ol.extent.createOrUpdate(minX, minY, maxX, maxY, opt_extent);
};


/**
 * @param {ol.Extent} extent Extent.
 * @param {number} resolution Resolution.
 * @param {ol.TileRange=} opt_tileRange Temporary tile range object.
 * @return {ol.TileRange} Tile range.
 */
ol.tilegrid.TileGrid.prototype.getTileRangeForExtentAndResolution = function(extent, resolution, opt_tileRange) {
  var tileCoord = ol.tilegrid.TileGrid.tmpTileCoord_;
  this.getTileCoordForXYAndResolution_(
      extent[0], extent[1], resolution, false, tileCoord);
  var minX = tileCoord[1];
  var minY = tileCoord[2];
  this.getTileCoordForXYAndResolution_(
      extent[2], extent[3], resolution, true, tileCoord);
  return ol.TileRange.createOrUpdate(
      minX, tileCoord[1], minY, tileCoord[2], opt_tileRange);
};


/**
 * @param {ol.Extent} extent Extent.
 * @param {number} z Z.
 * @param {ol.TileRange=} opt_tileRange Temporary tile range object.
 * @return {ol.TileRange} Tile range.
 */
ol.tilegrid.TileGrid.prototype.getTileRangeForExtentAndZ = function(extent, z, opt_tileRange) {
  var resolution = this.getResolution(z);
  return this.getTileRangeForExtentAndResolution(
      extent, resolution, opt_tileRange);
};


/**
 * @param {ol.TileCoord} tileCoord Tile coordinate.
 * @return {ol.Coordinate} Tile center.
 */
ol.tilegrid.TileGrid.prototype.getTileCoordCenter = function(tileCoord) {
  var origin = this.getOrigin(tileCoord[0]);
  var resolution = this.getResolution(tileCoord[0]);
  var tileSize = ol.size.toSize(this.getTileSize(tileCoord[0]), this.tmpSize_);
  return [
    origin[0] + (tileCoord[1] + 0.5) * tileSize[0] * resolution,
    origin[1] + (tileCoord[2] + 0.5) * tileSize[1] * resolution
  ];
};


/**
 * Get the extent of a tile coordinate.
 *
 * @param {ol.TileCoord} tileCoord Tile coordinate.
 * @param {ol.Extent=} opt_extent Temporary extent object.
 * @return {ol.Extent} Extent.
 * @api
 */
ol.tilegrid.TileGrid.prototype.getTileCoordExtent = function(tileCoord, opt_extent) {
  var origin = this.getOrigin(tileCoord[0]);
  var resolution = this.getResolution(tileCoord[0]);
  var tileSize = ol.size.toSize(this.getTileSize(tileCoord[0]), this.tmpSize_);
  var minX = origin[0] + tileCoord[1] * tileSize[0] * resolution;
  var minY = origin[1] + tileCoord[2] * tileSize[1] * resolution;
  var maxX = minX + tileSize[0] * resolution;
  var maxY = minY + tileSize[1] * resolution;
  return ol.extent.createOrUpdate(minX, minY, maxX, maxY, opt_extent);
};


/**
 * Get the tile coordinate for the given map coordinate and resolution.  This
 * method considers that coordinates that intersect tile boundaries should be
 * assigned the higher tile coordinate.
 *
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {number} resolution Resolution.
 * @param {ol.TileCoord=} opt_tileCoord Destination ol.TileCoord object.
 * @return {ol.TileCoord} Tile coordinate.
 * @api
 */
ol.tilegrid.TileGrid.prototype.getTileCoordForCoordAndResolution = function(coordinate, resolution, opt_tileCoord) {
  return this.getTileCoordForXYAndResolution_(
      coordinate[0], coordinate[1], resolution, false, opt_tileCoord);
};


/**
 * @param {number} x X.
 * @param {number} y Y.
 * @param {number} resolution Resolution.
 * @param {boolean} reverseIntersectionPolicy Instead of letting edge
 *     intersections go to the higher tile coordinate, let edge intersections
 *     go to the lower tile coordinate.
 * @param {ol.TileCoord=} opt_tileCoord Temporary ol.TileCoord object.
 * @return {ol.TileCoord} Tile coordinate.
 * @private
 */
ol.tilegrid.TileGrid.prototype.getTileCoordForXYAndResolution_ = function(
    x, y, resolution, reverseIntersectionPolicy, opt_tileCoord) {
  var z = this.getZForResolution(resolution);
  var scale = resolution / this.getResolution(z);
  var origin = this.getOrigin(z);
  var tileSize = ol.size.toSize(this.getTileSize(z), this.tmpSize_);

  var adjustX = reverseIntersectionPolicy ? 0.5 : 0;
  var adjustY = reverseIntersectionPolicy ? 0 : 0.5;
  var xFromOrigin = Math.floor((x - origin[0]) / resolution + adjustX);
  var yFromOrigin = Math.floor((y - origin[1]) / resolution + adjustY);
  var tileCoordX = scale * xFromOrigin / tileSize[0];
  var tileCoordY = scale * yFromOrigin / tileSize[1];

  if (reverseIntersectionPolicy) {
    tileCoordX = Math.ceil(tileCoordX) - 1;
    tileCoordY = Math.ceil(tileCoordY) - 1;
  } else {
    tileCoordX = Math.floor(tileCoordX);
    tileCoordY = Math.floor(tileCoordY);
  }

  return ol.tilecoord.createOrUpdate(z, tileCoordX, tileCoordY, opt_tileCoord);
};


/**
 * Get a tile coordinate given a map coordinate and zoom level.
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {number} z Zoom level.
 * @param {ol.TileCoord=} opt_tileCoord Destination ol.TileCoord object.
 * @return {ol.TileCoord} Tile coordinate.
 * @api
 */
ol.tilegrid.TileGrid.prototype.getTileCoordForCoordAndZ = function(coordinate, z, opt_tileCoord) {
  var resolution = this.getResolution(z);
  return this.getTileCoordForXYAndResolution_(
      coordinate[0], coordinate[1], resolution, false, opt_tileCoord);
};


/**
 * @param {ol.TileCoord} tileCoord Tile coordinate.
 * @return {number} Tile resolution.
 */
ol.tilegrid.TileGrid.prototype.getTileCoordResolution = function(tileCoord) {
  goog.asserts.assert(
      this.minZoom <= tileCoord[0] && tileCoord[0] <= this.maxZoom,
      'z of given tilecoord is not in allowed range (%s <= %s <= %s',
      this.minZoom, tileCoord[0], this.maxZoom);
  return this.resolutions_[tileCoord[0]];
};


/**
 * Get the tile size for a zoom level. The type of the return value matches the
 * `tileSize` or `tileSizes` that the tile grid was configured with. To always
 * get an `ol.Size`, run the result through `ol.size.toSize()`.
 * @param {number} z Z.
 * @return {number|ol.Size} Tile size.
 * @api stable
 */
ol.tilegrid.TileGrid.prototype.getTileSize = function(z) {
  if (this.tileSize_) {
    return this.tileSize_;
  } else {
    goog.asserts.assert(this.tileSizes_,
        'tileSizes cannot be null if tileSize is null');
    goog.asserts.assert(this.minZoom <= z && z <= this.maxZoom,
        'z is not in allowed range (%s <= %s <= %s',
        this.minZoom, z, this.maxZoom);
    return this.tileSizes_[z];
  }
};


/**
 * @param {number} z Zoom level.
 * @return {ol.TileRange} Extent tile range for the specified zoom level.
 */
ol.tilegrid.TileGrid.prototype.getFullTileRange = function(z) {
  if (!this.fullTileRanges_) {
    return null;
  } else {
    goog.asserts.assert(this.minZoom <= z && z <= this.maxZoom,
        'z is not in allowed range (%s <= %s <= %s',
        this.minZoom, z, this.maxZoom);
    return this.fullTileRanges_[z];
  }
};


/**
 * @param {number} resolution Resolution.
 * @return {number} Z.
 */
ol.tilegrid.TileGrid.prototype.getZForResolution = function(resolution) {
  var z = ol.array.linearFindNearest(this.resolutions_, resolution, 0);
  return ol.math.clamp(z, this.minZoom, this.maxZoom);
};


/**
 * @param {!ol.Extent} extent Extent for this tile grid.
 * @private
 */
ol.tilegrid.TileGrid.prototype.calculateTileRanges_ = function(extent) {
  var length = this.resolutions_.length;
  var fullTileRanges = new Array(length);
  for (var z = this.minZoom; z < length; ++z) {
    fullTileRanges[z] = this.getTileRangeForExtentAndZ(extent, z);
  }
  this.fullTileRanges_ = fullTileRanges;
};


/**
 * @param {ol.proj.Projection} projection Projection.
 * @return {ol.tilegrid.TileGrid} Default tile grid for the passed projection.
 */
ol.tilegrid.getForProjection = function(projection) {
  var tileGrid = projection.getDefaultTileGrid();
  if (!tileGrid) {
    tileGrid = ol.tilegrid.createForProjection(projection);
    projection.setDefaultTileGrid(tileGrid);
  }
  return tileGrid;
};


/**
 * @param {ol.Extent} extent Extent.
 * @param {number=} opt_maxZoom Maximum zoom level (default is
 *     ol.DEFAULT_MAX_ZOOM).
 * @param {number|ol.Size=} opt_tileSize Tile size (default uses
 *     ol.DEFAULT_TILE_SIZE).
 * @param {ol.extent.Corner=} opt_corner Extent corner (default is
 *     ol.extent.Corner.TOP_LEFT).
 * @return {ol.tilegrid.TileGrid} TileGrid instance.
 */
ol.tilegrid.createForExtent = function(extent, opt_maxZoom, opt_tileSize, opt_corner) {
  var corner = opt_corner !== undefined ?
      opt_corner : ol.extent.Corner.TOP_LEFT;

  var resolutions = ol.tilegrid.resolutionsFromExtent(
      extent, opt_maxZoom, opt_tileSize);

  return new ol.tilegrid.TileGrid({
    extent: extent,
    origin: ol.extent.getCorner(extent, corner),
    resolutions: resolutions,
    tileSize: opt_tileSize
  });
};


/**
 * Creates a tile grid with a standard XYZ tiling scheme.
 * @param {olx.tilegrid.XYZOptions=} opt_options Tile grid options.
 * @return {ol.tilegrid.TileGrid} Tile grid instance.
 * @api
 */
ol.tilegrid.createXYZ = function(opt_options) {
  var options = /** @type {olx.tilegrid.TileGridOptions} */ ({});
  goog.object.extend(options, opt_options !== undefined ?
      opt_options : /** @type {olx.tilegrid.XYZOptions} */ ({}));
  if (options.extent === undefined) {
    options.extent = ol.proj.get('EPSG:3857').getExtent();
  }
  options.resolutions = ol.tilegrid.resolutionsFromExtent(
      options.extent, options.maxZoom, options.tileSize);
  delete options.maxZoom;

  return new ol.tilegrid.TileGrid(options);
};


/**
 * Create a resolutions array from an extent.  A zoom factor of 2 is assumed.
 * @param {ol.Extent} extent Extent.
 * @param {number=} opt_maxZoom Maximum zoom level (default is
 *     ol.DEFAULT_MAX_ZOOM).
 * @param {number|ol.Size=} opt_tileSize Tile size (default uses
 *     ol.DEFAULT_TILE_SIZE).
 * @return {!Array.<number>} Resolutions array.
 */
ol.tilegrid.resolutionsFromExtent = function(extent, opt_maxZoom, opt_tileSize) {
  var maxZoom = opt_maxZoom !== undefined ?
      opt_maxZoom : ol.DEFAULT_MAX_ZOOM;

  var height = ol.extent.getHeight(extent);
  var width = ol.extent.getWidth(extent);

  var tileSize = ol.size.toSize(opt_tileSize !== undefined ?
      opt_tileSize : ol.DEFAULT_TILE_SIZE);
  var maxResolution = Math.max(
      width / tileSize[0], height / tileSize[1]);

  var length = maxZoom + 1;
  var resolutions = new Array(length);
  for (var z = 0; z < length; ++z) {
    resolutions[z] = maxResolution / Math.pow(2, z);
  }
  return resolutions;
};


/**
 * @param {ol.proj.ProjectionLike} projection Projection.
 * @param {number=} opt_maxZoom Maximum zoom level (default is
 *     ol.DEFAULT_MAX_ZOOM).
 * @param {ol.Size=} opt_tileSize Tile size (default uses ol.DEFAULT_TILE_SIZE).
 * @param {ol.extent.Corner=} opt_corner Extent corner (default is
 *     ol.extent.Corner.BOTTOM_LEFT).
 * @return {ol.tilegrid.TileGrid} TileGrid instance.
 */
ol.tilegrid.createForProjection = function(projection, opt_maxZoom, opt_tileSize, opt_corner) {
  var extent = ol.tilegrid.extentFromProjection(projection);
  return ol.tilegrid.createForExtent(
      extent, opt_maxZoom, opt_tileSize, opt_corner);
};


/**
 * Generate a tile grid extent from a projection.  If the projection has an
 * extent, it is used.  If not, a global extent is assumed.
 * @param {ol.proj.ProjectionLike} projection Projection.
 * @return {ol.Extent} Extent.
 */
ol.tilegrid.extentFromProjection = function(projection) {
  projection = ol.proj.get(projection);
  var extent = projection.getExtent();
  if (!extent) {
    var half = 180 * ol.proj.METERS_PER_UNIT[ol.proj.Units.DEGREES] /
        projection.getMetersPerUnit();
    extent = ol.extent.createOrUpdate(-half, -half, half, half);
  }
  return extent;
};

goog.provide('ol.source.Tile');
goog.provide('ol.source.TileEvent');
goog.provide('ol.source.TileOptions');

goog.require('goog.asserts');
goog.require('goog.events.Event');
goog.require('ol');
goog.require('ol.Attribution');
goog.require('ol.Extent');
goog.require('ol.TileCache');
goog.require('ol.TileRange');
goog.require('ol.TileState');
goog.require('ol.proj');
goog.require('ol.size');
goog.require('ol.source.Source');
goog.require('ol.tilecoord');
goog.require('ol.tilegrid.TileGrid');


/**
 * @typedef {{attributions: (Array.<ol.Attribution>|undefined),
 *            cacheSize: (number|undefined),
 *            extent: (ol.Extent|undefined),
 *            logo: (string|olx.LogoOptions|undefined),
 *            opaque: (boolean|undefined),
 *            tilePixelRatio: (number|undefined),
 *            projection: ol.proj.ProjectionLike,
 *            state: (ol.source.State|undefined),
 *            tileGrid: (ol.tilegrid.TileGrid|undefined),
 *            wrapX: (boolean|undefined)}}
 */
ol.source.TileOptions;


/**
 * @classdesc
 * Abstract base class; normally only used for creating subclasses and not
 * instantiated in apps.
 * Base class for sources providing images divided into a tile grid.
 *
 * @constructor
 * @extends {ol.source.Source}
 * @param {ol.source.TileOptions} options Tile source options.
 * @api
 */
ol.source.Tile = function(options) {

  goog.base(this, {
    attributions: options.attributions,
    extent: options.extent,
    logo: options.logo,
    projection: options.projection,
    state: options.state,
    wrapX: options.wrapX
  });

  /**
   * @private
   * @type {boolean}
   */
  this.opaque_ = options.opaque !== undefined ? options.opaque : false;

  /**
   * @private
   * @type {number}
   */
  this.tilePixelRatio_ = options.tilePixelRatio !== undefined ?
      options.tilePixelRatio : 1;

  /**
   * @protected
   * @type {ol.tilegrid.TileGrid}
   */
  this.tileGrid = options.tileGrid !== undefined ? options.tileGrid : null;

  /**
   * @protected
   * @type {ol.TileCache}
   */
  this.tileCache = new ol.TileCache(options.cacheSize);

  /**
   * @protected
   * @type {ol.Size}
   */
  this.tmpSize = [0, 0];

};
goog.inherits(ol.source.Tile, ol.source.Source);


/**
 * @return {boolean} Can expire cache.
 */
ol.source.Tile.prototype.canExpireCache = function() {
  return this.tileCache.canExpireCache();
};


/**
 * @param {ol.proj.Projection} projection Projection.
 * @param {Object.<string, ol.TileRange>} usedTiles Used tiles.
 */
ol.source.Tile.prototype.expireCache = function(projection, usedTiles) {
  var tileCache = this.getTileCacheForProjection(projection);
  if (tileCache) {
    tileCache.expireCache(usedTiles);
  }
};


/**
 * @param {ol.proj.Projection} projection Projection.
 * @param {number} z Zoom level.
 * @param {ol.TileRange} tileRange Tile range.
 * @param {function(ol.Tile):(boolean|undefined)} callback Called with each
 *     loaded tile.  If the callback returns `false`, the tile will not be
 *     considered loaded.
 * @return {boolean} The tile range is fully covered with loaded tiles.
 */
ol.source.Tile.prototype.forEachLoadedTile = function(projection, z, tileRange, callback) {
  var tileCache = this.getTileCacheForProjection(projection);
  if (!tileCache) {
    return false;
  }

  var covered = true;
  var tile, tileCoordKey, loaded;
  for (var x = tileRange.minX; x <= tileRange.maxX; ++x) {
    for (var y = tileRange.minY; y <= tileRange.maxY; ++y) {
      tileCoordKey = this.getKeyZXY(z, x, y);
      loaded = false;
      if (tileCache.containsKey(tileCoordKey)) {
        tile = /** @type {!ol.Tile} */ (tileCache.get(tileCoordKey));
        loaded = tile.getState() === ol.TileState.LOADED;
        if (loaded) {
          loaded = (callback(tile) !== false);
        }
      }
      if (!loaded) {
        covered = false;
      }
    }
  }
  return covered;
};


/**
 * @return {number} Gutter.
 */
ol.source.Tile.prototype.getGutter = function() {
  return 0;
};


/**
 * Return the "parameters" key, a string composed of the source's
 * parameters/dimensions.
 * @return {string} The parameters key.
 * @protected
 */
ol.source.Tile.prototype.getKeyParams = function() {
  return '';
};


/**
 * @param {number} z Z.
 * @param {number} x X.
 * @param {number} y Y.
 * @return {string} Key.
 * @protected
 */
ol.source.Tile.prototype.getKeyZXY = ol.tilecoord.getKeyZXY;


/**
 * @param {ol.proj.Projection} projection Projection.
 * @return {boolean} Opaque.
 */
ol.source.Tile.prototype.getOpaque = function(projection) {
  return this.opaque_;
};


/**
 * @inheritDoc
 */
ol.source.Tile.prototype.getResolutions = function() {
  return this.tileGrid.getResolutions();
};


/**
 * @param {number} z Tile coordinate z.
 * @param {number} x Tile coordinate x.
 * @param {number} y Tile coordinate y.
 * @param {number} pixelRatio Pixel ratio.
 * @param {ol.proj.Projection} projection Projection.
 * @return {!ol.Tile} Tile.
 */
ol.source.Tile.prototype.getTile = goog.abstractMethod;


/**
 * Return the tile grid of the tile source.
 * @return {ol.tilegrid.TileGrid} Tile grid.
 * @api stable
 */
ol.source.Tile.prototype.getTileGrid = function() {
  return this.tileGrid;
};


/**
 * @param {ol.proj.Projection} projection Projection.
 * @return {ol.tilegrid.TileGrid} Tile grid.
 */
ol.source.Tile.prototype.getTileGridForProjection = function(projection) {
  if (!this.tileGrid) {
    return ol.tilegrid.getForProjection(projection);
  } else {
    return this.tileGrid;
  }
};


/**
 * @param {ol.proj.Projection} projection Projection.
 * @return {ol.TileCache} Tile cache.
 * @protected
 */
ol.source.Tile.prototype.getTileCacheForProjection = function(projection) {
  var thisProj = this.getProjection();
  if (thisProj && !ol.proj.equivalent(thisProj, projection)) {
    return null;
  } else {
    return this.tileCache;
  }
};


/**
 * @param {number} pixelRatio Pixel ratio.
 * @return {number} Tile pixel ratio.
 */
ol.source.Tile.prototype.getTilePixelRatio = function(pixelRatio) {
  return this.tilePixelRatio_;
};


/**
 * @param {number} z Z.
 * @param {number} pixelRatio Pixel ratio.
 * @param {ol.proj.Projection} projection Projection.
 * @return {ol.Size} Tile size.
 */
ol.source.Tile.prototype.getTilePixelSize = function(z, pixelRatio, projection) {
  var tileGrid = this.getTileGridForProjection(projection);
  var tilePixelRatio = this.getTilePixelRatio(pixelRatio);
  var tileSize = ol.size.toSize(tileGrid.getTileSize(z), this.tmpSize);
  if (tilePixelRatio == 1) {
    return tileSize;
  } else {
    return ol.size.scale(tileSize, tilePixelRatio, this.tmpSize);
  }
};


/**
 * Returns a tile coordinate wrapped around the x-axis. When the tile coordinate
 * is outside the resolution and extent range of the tile grid, `null` will be
 * returned.
 * @param {ol.TileCoord} tileCoord Tile coordinate.
 * @param {ol.proj.Projection=} opt_projection Projection.
 * @return {ol.TileCoord} Tile coordinate to be passed to the tileUrlFunction or
 *     null if no tile URL should be created for the passed `tileCoord`.
 */
ol.source.Tile.prototype.getTileCoordForTileUrlFunction = function(tileCoord, opt_projection) {
  var projection = opt_projection !== undefined ?
      opt_projection : this.getProjection();
  var tileGrid = this.getTileGridForProjection(projection);
  goog.asserts.assert(tileGrid, 'tile grid needed');
  if (this.getWrapX() && projection.isGlobal()) {
    tileCoord = ol.tilecoord.wrapX(tileCoord, tileGrid, projection);
  }
  return ol.tilecoord.withinExtentAndZ(tileCoord, tileGrid) ? tileCoord : null;
};


/**
 * Marks a tile coord as being used, without triggering a load.
 * @param {number} z Tile coordinate z.
 * @param {number} x Tile coordinate x.
 * @param {number} y Tile coordinate y.
 * @param {ol.proj.Projection} projection Projection.
 */
ol.source.Tile.prototype.useTile = ol.nullFunction;


/**
 * @classdesc
 * Events emitted by {@link ol.source.Tile} instances are instances of this
 * type.
 *
 * @constructor
 * @extends {goog.events.Event}
 * @implements {oli.source.TileEvent}
 * @param {string} type Type.
 * @param {ol.Tile} tile The tile.
 */
ol.source.TileEvent = function(type, tile) {

  goog.base(this, type);

  /**
   * The tile related to the event.
   * @type {ol.Tile}
   * @api
   */
  this.tile = tile;

};
goog.inherits(ol.source.TileEvent, goog.events.Event);


/**
 * @enum {string}
 */
ol.source.TileEventType = {

  /**
   * Triggered when a tile starts loading.
   * @event ol.source.TileEvent#tileloadstart
   * @api stable
   */
  TILELOADSTART: 'tileloadstart',

  /**
   * Triggered when a tile finishes loading.
   * @event ol.source.TileEvent#tileloadend
   * @api stable
   */
  TILELOADEND: 'tileloadend',

  /**
   * Triggered if tile loading results in an error.
   * @event ol.source.TileEvent#tileloaderror
   * @api stable
   */
  TILELOADERROR: 'tileloaderror'

};

// FIXME handle date line wrap

goog.provide('ol.control.Attribution');

goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.classlist');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.object');
goog.require('goog.style');
goog.require('ol');
goog.require('ol.Attribution');
goog.require('ol.control.Control');
goog.require('ol.css');
goog.require('ol.source.Tile');


/**
 * @classdesc
 * Control to show all the attributions associated with the layer sources
 * in the map. This control is one of the default controls included in maps.
 * By default it will show in the bottom right portion of the map, but this can
 * be changed by using a css selector for `.ol-attribution`.
 *
 * @constructor
 * @extends {ol.control.Control}
 * @param {olx.control.AttributionOptions=} opt_options Attribution options.
 * @api stable
 */
ol.control.Attribution = function(opt_options) {

  var options = opt_options ? opt_options : {};

  /**
   * @private
   * @type {Element}
   */
  this.ulElement_ = document.createElement('UL');

  /**
   * @private
   * @type {Element}
   */
  this.logoLi_ = document.createElement('LI');

  this.ulElement_.appendChild(this.logoLi_);
  goog.style.setElementShown(this.logoLi_, false);

  /**
   * @private
   * @type {boolean}
   */
  this.collapsed_ = options.collapsed !== undefined ? options.collapsed : true;

  /**
   * @private
   * @type {boolean}
   */
  this.collapsible_ = options.collapsible !== undefined ?
      options.collapsible : true;

  if (!this.collapsible_) {
    this.collapsed_ = false;
  }

  var className = options.className ? options.className : 'ol-attribution';

  var tipLabel = options.tipLabel ? options.tipLabel : 'Attributions';

  var collapseLabel = options.collapseLabel ? options.collapseLabel : '\u00BB';

  /**
   * @private
   * @type {Node}
   */
  this.collapseLabel_ = goog.isString(collapseLabel) ?
      goog.dom.createDom('SPAN', {}, collapseLabel) :
      collapseLabel;

  var label = options.label ? options.label : 'i';

  /**
   * @private
   * @type {Node}
   */
  this.label_ = goog.isString(label) ?
      goog.dom.createDom('SPAN', {}, label) :
      label;

  var activeLabel = (this.collapsible_ && !this.collapsed_) ?
      this.collapseLabel_ : this.label_;
  var button = goog.dom.createDom('BUTTON', {
    'type': 'button',
    'title': tipLabel
  }, activeLabel);

  goog.events.listen(button, goog.events.EventType.CLICK,
      this.handleClick_, false, this);

  var cssClasses = className + ' ' + ol.css.CLASS_UNSELECTABLE + ' ' +
      ol.css.CLASS_CONTROL +
      (this.collapsed_ && this.collapsible_ ? ' ol-collapsed' : '') +
      (this.collapsible_ ? '' : ' ol-uncollapsible');
  var element = goog.dom.createDom('DIV',
      cssClasses, this.ulElement_, button);

  var render = options.render ? options.render : ol.control.Attribution.render;

  goog.base(this, {
    element: element,
    render: render,
    target: options.target
  });

  /**
   * @private
   * @type {boolean}
   */
  this.renderedVisible_ = true;

  /**
   * @private
   * @type {Object.<string, Element>}
   */
  this.attributionElements_ = {};

  /**
   * @private
   * @type {Object.<string, boolean>}
   */
  this.attributionElementRenderedVisible_ = {};

  /**
   * @private
   * @type {Object.<string, Element>}
   */
  this.logoElements_ = {};

};
goog.inherits(ol.control.Attribution, ol.control.Control);


/**
 * @param {?olx.FrameState} frameState Frame state.
 * @return {Array.<Object.<string, ol.Attribution>>} Attributions.
 */
ol.control.Attribution.prototype.getSourceAttributions = function(frameState) {
  var i, ii, j, jj, tileRanges, source, sourceAttribution,
      sourceAttributionKey, sourceAttributions, sourceKey;
  var intersectsTileRange;
  var layerStatesArray = frameState.layerStatesArray;
  /** @type {Object.<string, ol.Attribution>} */
  var attributions = goog.object.clone(frameState.attributions);
  /** @type {Object.<string, ol.Attribution>} */
  var hiddenAttributions = {};
  var projection = frameState.viewState.projection;
  goog.asserts.assert(projection, 'projection of viewState required');
  for (i = 0, ii = layerStatesArray.length; i < ii; i++) {
    source = layerStatesArray[i].layer.getSource();
    if (!source) {
      continue;
    }
    sourceKey = goog.getUid(source).toString();
    sourceAttributions = source.getAttributions();
    if (!sourceAttributions) {
      continue;
    }
    for (j = 0, jj = sourceAttributions.length; j < jj; j++) {
      sourceAttribution = sourceAttributions[j];
      sourceAttributionKey = goog.getUid(sourceAttribution).toString();
      if (sourceAttributionKey in attributions) {
        continue;
      }
      tileRanges = frameState.usedTiles[sourceKey];
      if (tileRanges) {
        goog.asserts.assertInstanceof(source, ol.source.Tile,
            'source should be an ol.source.Tile');
        var tileGrid = source.getTileGridForProjection(projection);
        goog.asserts.assert(tileGrid, 'tileGrid required for projection');
        intersectsTileRange = sourceAttribution.intersectsAnyTileRange(
            tileRanges, tileGrid, projection);
      } else {
        intersectsTileRange = false;
      }
      if (intersectsTileRange) {
        if (sourceAttributionKey in hiddenAttributions) {
          delete hiddenAttributions[sourceAttributionKey];
        }
        attributions[sourceAttributionKey] = sourceAttribution;
      } else {
        hiddenAttributions[sourceAttributionKey] = sourceAttribution;
      }
    }
  }
  return [attributions, hiddenAttributions];
};


/**
 * Update the attribution element.
 * @param {ol.MapEvent} mapEvent Map event.
 * @this {ol.control.Attribution}
 * @api
 */
ol.control.Attribution.render = function(mapEvent) {
  this.updateElement_(mapEvent.frameState);
};


/**
 * @private
 * @param {?olx.FrameState} frameState Frame state.
 */
ol.control.Attribution.prototype.updateElement_ = function(frameState) {

  if (!frameState) {
    if (this.renderedVisible_) {
      goog.style.setElementShown(this.element, false);
      this.renderedVisible_ = false;
    }
    return;
  }

  var attributions = this.getSourceAttributions(frameState);
  /** @type {Object.<string, ol.Attribution>} */
  var visibleAttributions = attributions[0];
  /** @type {Object.<string, ol.Attribution>} */
  var hiddenAttributions = attributions[1];

  var attributionElement, attributionKey;
  for (attributionKey in this.attributionElements_) {
    if (attributionKey in visibleAttributions) {
      if (!this.attributionElementRenderedVisible_[attributionKey]) {
        goog.style.setElementShown(
            this.attributionElements_[attributionKey], true);
        this.attributionElementRenderedVisible_[attributionKey] = true;
      }
      delete visibleAttributions[attributionKey];
    } else if (attributionKey in hiddenAttributions) {
      if (this.attributionElementRenderedVisible_[attributionKey]) {
        goog.style.setElementShown(
            this.attributionElements_[attributionKey], false);
        delete this.attributionElementRenderedVisible_[attributionKey];
      }
      delete hiddenAttributions[attributionKey];
    } else {
      goog.dom.removeNode(this.attributionElements_[attributionKey]);
      delete this.attributionElements_[attributionKey];
      delete this.attributionElementRenderedVisible_[attributionKey];
    }
  }
  for (attributionKey in visibleAttributions) {
    attributionElement = document.createElement('LI');
    attributionElement.innerHTML =
        visibleAttributions[attributionKey].getHTML();
    this.ulElement_.appendChild(attributionElement);
    this.attributionElements_[attributionKey] = attributionElement;
    this.attributionElementRenderedVisible_[attributionKey] = true;
  }
  for (attributionKey in hiddenAttributions) {
    attributionElement = document.createElement('LI');
    attributionElement.innerHTML =
        hiddenAttributions[attributionKey].getHTML();
    goog.style.setElementShown(attributionElement, false);
    this.ulElement_.appendChild(attributionElement);
    this.attributionElements_[attributionKey] = attributionElement;
  }

  var renderVisible =
      !goog.object.isEmpty(this.attributionElementRenderedVisible_) ||
      !goog.object.isEmpty(frameState.logos);
  if (this.renderedVisible_ != renderVisible) {
    goog.style.setElementShown(this.element, renderVisible);
    this.renderedVisible_ = renderVisible;
  }
  if (renderVisible &&
      goog.object.isEmpty(this.attributionElementRenderedVisible_)) {
    goog.dom.classlist.add(this.element, 'ol-logo-only');
  } else {
    goog.dom.classlist.remove(this.element, 'ol-logo-only');
  }

  this.insertLogos_(frameState);

};


/**
 * @param {?olx.FrameState} frameState Frame state.
 * @private
 */
ol.control.Attribution.prototype.insertLogos_ = function(frameState) {

  var logo;
  var logos = frameState.logos;
  var logoElements = this.logoElements_;

  for (logo in logoElements) {
    if (!(logo in logos)) {
      goog.dom.removeNode(logoElements[logo]);
      delete logoElements[logo];
    }
  }

  var image, logoElement, logoKey;
  for (logoKey in logos) {
    if (!(logoKey in logoElements)) {
      image = new Image();
      image.src = logoKey;
      var logoValue = logos[logoKey];
      if (logoValue === '') {
        logoElement = image;
      } else {
        logoElement = goog.dom.createDom('A', {
          'href': logoValue
        });
        logoElement.appendChild(image);
      }
      this.logoLi_.appendChild(logoElement);
      logoElements[logoKey] = logoElement;
    }
  }

  goog.style.setElementShown(this.logoLi_, !goog.object.isEmpty(logos));

};


/**
 * @param {goog.events.BrowserEvent} event The event to handle
 * @private
 */
ol.control.Attribution.prototype.handleClick_ = function(event) {
  event.preventDefault();
  this.handleToggle_();
};


/**
 * @private
 */
ol.control.Attribution.prototype.handleToggle_ = function() {
  goog.dom.classlist.toggle(this.element, 'ol-collapsed');
  if (this.collapsed_) {
    goog.dom.replaceNode(this.collapseLabel_, this.label_);
  } else {
    goog.dom.replaceNode(this.label_, this.collapseLabel_);
  }
  this.collapsed_ = !this.collapsed_;
};


/**
 * Return `true` if the attribution is collapsible, `false` otherwise.
 * @return {boolean} True if the widget is collapsible.
 * @api stable
 */
ol.control.Attribution.prototype.getCollapsible = function() {
  return this.collapsible_;
};


/**
 * Set whether the attribution should be collapsible.
 * @param {boolean} collapsible True if the widget is collapsible.
 * @api stable
 */
ol.control.Attribution.prototype.setCollapsible = function(collapsible) {
  if (this.collapsible_ === collapsible) {
    return;
  }
  this.collapsible_ = collapsible;
  goog.dom.classlist.toggle(this.element, 'ol-uncollapsible');
  if (!collapsible && this.collapsed_) {
    this.handleToggle_();
  }
};


/**
 * Collapse or expand the attribution according to the passed parameter. Will
 * not do anything if the attribution isn't collapsible or if the current
 * collapsed state is already the one requested.
 * @param {boolean} collapsed True if the widget is collapsed.
 * @api stable
 */
ol.control.Attribution.prototype.setCollapsed = function(collapsed) {
  if (!this.collapsible_ || this.collapsed_ === collapsed) {
    return;
  }
  this.handleToggle_();
};


/**
 * Return `true` when the attribution is currently collapsed or `false`
 * otherwise.
 * @return {boolean} True if the widget is collapsed.
 * @api stable
 */
ol.control.Attribution.prototype.getCollapsed = function() {
  return this.collapsed_;
};

goog.provide('ol.control.Rotate');

goog.require('goog.dom');
goog.require('goog.dom.classlist');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('ol');
goog.require('ol.animation');
goog.require('ol.control.Control');
goog.require('ol.css');
goog.require('ol.easing');


/**
 * @classdesc
 * A button control to reset rotation to 0.
 * To style this control use css selector `.ol-rotate`. A `.ol-hidden` css
 * selector is added to the button when the rotation is 0.
 *
 * @constructor
 * @extends {ol.control.Control}
 * @param {olx.control.RotateOptions=} opt_options Rotate options.
 * @api stable
 */
ol.control.Rotate = function(opt_options) {

  var options = opt_options ? opt_options : {};

  var className = options.className ?
      options.className : 'ol-rotate';

  var label = options.label ? options.label : '\u21E7';

  /**
   * @type {Element}
   * @private
   */
  this.label_ = null;

  if (goog.isString(label)) {
    this.label_ = goog.dom.createDom('SPAN',
        'ol-compass', label);
  } else {
    this.label_ = label;
    goog.dom.classlist.add(this.label_, 'ol-compass');
  }

  var tipLabel = options.tipLabel ? options.tipLabel : 'Reset rotation';

  var button = goog.dom.createDom('BUTTON', {
    'class': className + '-reset',
    'type' : 'button',
    'title': tipLabel
  }, this.label_);

  goog.events.listen(button, goog.events.EventType.CLICK,
      ol.control.Rotate.prototype.handleClick_, false, this);

  var cssClasses = className + ' ' + ol.css.CLASS_UNSELECTABLE + ' ' +
      ol.css.CLASS_CONTROL;
  var element = goog.dom.createDom('DIV', cssClasses, button);

  var render = options.render ? options.render : ol.control.Rotate.render;

  this.callResetNorth_ = options.resetNorth ? options.resetNorth : undefined;

  goog.base(this, {
    element: element,
    render: render,
    target: options.target
  });

  /**
   * @type {number}
   * @private
   */
  this.duration_ = options.duration !== undefined ? options.duration : 250;

  /**
   * @type {boolean}
   * @private
   */
  this.autoHide_ = options.autoHide !== undefined ? options.autoHide : true;

  /**
   * @private
   * @type {number|undefined}
   */
  this.rotation_ = undefined;

  if (this.autoHide_) {
    goog.dom.classlist.add(this.element, ol.css.CLASS_HIDDEN);
  }

};
goog.inherits(ol.control.Rotate, ol.control.Control);


/**
 * @param {goog.events.BrowserEvent} event The event to handle
 * @private
 */
ol.control.Rotate.prototype.handleClick_ = function(event) {
  event.preventDefault();
  if (this.callResetNorth_ !== undefined) {
    this.callResetNorth_();
  } else {
    this.resetNorth_();
  }
};


/**
 * @private
 */
ol.control.Rotate.prototype.resetNorth_ = function() {
  var map = this.getMap();
  var view = map.getView();
  if (!view) {
    // the map does not have a view, so we can't act
    // upon it
    return;
  }
  var currentRotation = view.getRotation();
  if (currentRotation !== undefined) {
    if (this.duration_ > 0) {
      currentRotation = currentRotation % (2 * Math.PI);
      if (currentRotation < -Math.PI) {
        currentRotation += 2 * Math.PI;
      }
      if (currentRotation > Math.PI) {
        currentRotation -= 2 * Math.PI;
      }
      map.beforeRender(ol.animation.rotate({
        rotation: currentRotation,
        duration: this.duration_,
        easing: ol.easing.easeOut
      }));
    }
    view.setRotation(0);
  }
};


/**
 * Update the rotate control element.
 * @param {ol.MapEvent} mapEvent Map event.
 * @this {ol.control.Rotate}
 * @api
 */
ol.control.Rotate.render = function(mapEvent) {
  var frameState = mapEvent.frameState;
  if (!frameState) {
    return;
  }
  var rotation = frameState.viewState.rotation;
  if (rotation != this.rotation_) {
    var transform = 'rotate(' + rotation + 'rad)';
    if (this.autoHide_) {
      goog.dom.classlist.enable(
          this.element, ol.css.CLASS_HIDDEN, rotation === 0);
    }
    this.label_.style.msTransform = transform;
    this.label_.style.webkitTransform = transform;
    this.label_.style.transform = transform;
  }
  this.rotation_ = rotation;
};

goog.provide('ol.control.Zoom');

goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('ol.animation');
goog.require('ol.control.Control');
goog.require('ol.css');
goog.require('ol.easing');


/**
 * @classdesc
 * A control with 2 buttons, one for zoom in and one for zoom out.
 * This control is one of the default controls of a map. To style this control
 * use css selectors `.ol-zoom-in` and `.ol-zoom-out`.
 *
 * @constructor
 * @extends {ol.control.Control}
 * @param {olx.control.ZoomOptions=} opt_options Zoom options.
 * @api stable
 */
ol.control.Zoom = function(opt_options) {

  var options = opt_options ? opt_options : {};

  var className = options.className ? options.className : 'ol-zoom';

  var delta = options.delta ? options.delta : 1;

  var zoomInLabel = options.zoomInLabel ? options.zoomInLabel : '+';
  var zoomOutLabel = options.zoomOutLabel ? options.zoomOutLabel : '\u2212';

  var zoomInTipLabel = options.zoomInTipLabel ?
      options.zoomInTipLabel : 'Zoom in';
  var zoomOutTipLabel = options.zoomOutTipLabel ?
      options.zoomOutTipLabel : 'Zoom out';

  var inElement = goog.dom.createDom('BUTTON', {
    'class': className + '-in',
    'type' : 'button',
    'title': zoomInTipLabel
  }, zoomInLabel);

  goog.events.listen(inElement,
      goog.events.EventType.CLICK, goog.partial(
          ol.control.Zoom.prototype.handleClick_, delta), false, this);

  var outElement = goog.dom.createDom('BUTTON', {
    'class': className + '-out',
    'type' : 'button',
    'title': zoomOutTipLabel
  }, zoomOutLabel);

  goog.events.listen(outElement,
      goog.events.EventType.CLICK, goog.partial(
          ol.control.Zoom.prototype.handleClick_, -delta), false, this);

  var cssClasses = className + ' ' + ol.css.CLASS_UNSELECTABLE + ' ' +
      ol.css.CLASS_CONTROL;
  var element = goog.dom.createDom('DIV', cssClasses, inElement,
      outElement);

  goog.base(this, {
    element: element,
    target: options.target
  });

  /**
   * @type {number}
   * @private
   */
  this.duration_ = options.duration !== undefined ? options.duration : 250;

};
goog.inherits(ol.control.Zoom, ol.control.Control);


/**
 * @param {number} delta Zoom delta.
 * @param {goog.events.BrowserEvent} event The event to handle
 * @private
 */
ol.control.Zoom.prototype.handleClick_ = function(delta, event) {
  event.preventDefault();
  this.zoomByDelta_(delta);
};


/**
 * @param {number} delta Zoom delta.
 * @private
 */
ol.control.Zoom.prototype.zoomByDelta_ = function(delta) {
  var map = this.getMap();
  var view = map.getView();
  if (!view) {
    // the map does not have a view, so we can't act
    // upon it
    return;
  }
  var currentResolution = view.getResolution();
  if (currentResolution) {
    if (this.duration_ > 0) {
      map.beforeRender(ol.animation.zoom({
        resolution: currentResolution,
        duration: this.duration_,
        easing: ol.easing.easeOut
      }));
    }
    var newResolution = view.constrainResolution(currentResolution, delta);
    view.setResolution(newResolution);
  }
};

goog.provide('ol.control');

goog.require('ol');
goog.require('ol.Collection');
goog.require('ol.control.Attribution');
goog.require('ol.control.Rotate');
goog.require('ol.control.Zoom');


/**
 * Set of controls included in maps by default. Unless configured otherwise,
 * this returns a collection containing an instance of each of the following
 * controls:
 * * {@link ol.control.Zoom}
 * * {@link ol.control.Rotate}
 * * {@link ol.control.Attribution}
 *
 * @param {olx.control.DefaultsOptions=} opt_options Defaults options.
 * @return {ol.Collection.<ol.control.Control>} Controls.
 * @api stable
 */
ol.control.defaults = function(opt_options) {

  var options = opt_options ? opt_options : {};

  var controls = new ol.Collection();

  var zoomControl = options.zoom !== undefined ? options.zoom : true;
  if (zoomControl) {
    controls.push(new ol.control.Zoom(options.zoomOptions));
  }

  var rotateControl = options.rotate !== undefined ? options.rotate : true;
  if (rotateControl) {
    controls.push(new ol.control.Rotate(options.rotateOptions));
  }

  var attributionControl = options.attribution !== undefined ?
      options.attribution : true;
  if (attributionControl) {
    controls.push(new ol.control.Attribution(options.attributionOptions));
  }

  return controls;

};

// Copyright 2012 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Functions for managing full screen status of the DOM.
 *
 */

goog.provide('goog.dom.fullscreen');
goog.provide('goog.dom.fullscreen.EventType');

goog.require('goog.dom');
goog.require('goog.userAgent');


/**
 * Event types for full screen.
 * @enum {string}
 */
goog.dom.fullscreen.EventType = {
  /** Dispatched by the Document when the fullscreen status changes. */
  CHANGE: (function() {
    if (goog.userAgent.WEBKIT) {
      return 'webkitfullscreenchange';
    }
    if (goog.userAgent.GECKO) {
      return 'mozfullscreenchange';
    }
    if (goog.userAgent.IE) {
      return 'MSFullscreenChange';
    }
    // Opera 12-14, and W3C standard (Draft):
    // https://dvcs.w3.org/hg/fullscreen/raw-file/tip/Overview.html
    return 'fullscreenchange';
  })()
};


/**
 * Determines if full screen is supported.
 * @param {!goog.dom.DomHelper=} opt_domHelper The DomHelper for the DOM being
 *     queried. If not provided, use the current DOM.
 * @return {boolean} True iff full screen is supported.
 */
goog.dom.fullscreen.isSupported = function(opt_domHelper) {
  var doc = goog.dom.fullscreen.getDocument_(opt_domHelper);
  var body = doc.body;
  return !!(body.webkitRequestFullscreen ||
      (body.mozRequestFullScreen && doc.mozFullScreenEnabled) ||
      (body.msRequestFullscreen && doc.msFullscreenEnabled) ||
      (body.requestFullscreen && doc.fullscreenEnabled));
};


/**
 * Requests putting the element in full screen.
 * @param {!Element} element The element to put full screen.
 */
goog.dom.fullscreen.requestFullScreen = function(element) {
  if (element.webkitRequestFullscreen) {
    element.webkitRequestFullscreen();
  } else if (element.mozRequestFullScreen) {
    element.mozRequestFullScreen();
  } else if (element.msRequestFullscreen) {
    element.msRequestFullscreen();
  } else if (element.requestFullscreen) {
    element.requestFullscreen();
  }
};


/**
 * Requests putting the element in full screen with full keyboard access.
 * @param {!Element} element The element to put full screen.
 */
goog.dom.fullscreen.requestFullScreenWithKeys = function(
    element) {
  if (element.mozRequestFullScreenWithKeys) {
    element.mozRequestFullScreenWithKeys();
  } else if (element.webkitRequestFullscreen) {
    element.webkitRequestFullscreen();
  } else {
    goog.dom.fullscreen.requestFullScreen(element);
  }
};


/**
 * Exits full screen.
 * @param {!goog.dom.DomHelper=} opt_domHelper The DomHelper for the DOM being
 *     queried. If not provided, use the current DOM.
 */
goog.dom.fullscreen.exitFullScreen = function(opt_domHelper) {
  var doc = goog.dom.fullscreen.getDocument_(opt_domHelper);
  if (doc.webkitCancelFullScreen) {
    doc.webkitCancelFullScreen();
  } else if (doc.mozCancelFullScreen) {
    doc.mozCancelFullScreen();
  } else if (doc.msExitFullscreen) {
    doc.msExitFullscreen();
  } else if (doc.exitFullscreen) {
    doc.exitFullscreen();
  }
};


/**
 * Determines if the document is full screen.
 * @param {!goog.dom.DomHelper=} opt_domHelper The DomHelper for the DOM being
 *     queried. If not provided, use the current DOM.
 * @return {boolean} Whether the document is full screen.
 */
goog.dom.fullscreen.isFullScreen = function(opt_domHelper) {
  var doc = goog.dom.fullscreen.getDocument_(opt_domHelper);
  // IE 11 doesn't have similar boolean property, so check whether
  // document.msFullscreenElement is null instead.
  return !!(doc.webkitIsFullScreen || doc.mozFullScreen ||
      doc.msFullscreenElement || doc.fullscreenElement);
};


/**
 * Get the root element in full screen mode.
 * @param {!goog.dom.DomHelper=} opt_domHelper The DomHelper for the DOM being
 *     queried. If not provided, use the current DOM.
 * @return {?Element} The root element in full screen mode.
 */
goog.dom.fullscreen.getFullScreenElement = function(opt_domHelper) {
  var doc = goog.dom.fullscreen.getDocument_(opt_domHelper);
  var element_list = [
    doc.webkitFullscreenElement,
    doc.mozFullScreenElement,
    doc.msFullscreenElement,
    doc.fullscreenElement
  ];
  for (var i = 0; i < element_list.length; i++) {
    if (element_list[i] != null) {
      return element_list[i];
    }
  }
  return null;
};


/**
 * Gets the document object of the dom.
 * @param {!goog.dom.DomHelper=} opt_domHelper The DomHelper for the DOM being
 *     queried. If not provided, use the current DOM.
 * @return {!Document} The dom document.
 * @private
 */
goog.dom.fullscreen.getDocument_ = function(opt_domHelper) {
  return opt_domHelper ?
      opt_domHelper.getDocument() :
      goog.dom.getDomHelper().getDocument();
};

goog.provide('ol.control.FullScreen');

goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.classlist');
goog.require('goog.dom.fullscreen');
goog.require('goog.dom.fullscreen.EventType');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('ol');
goog.require('ol.control.Control');
goog.require('ol.css');


/**
 * @classdesc
 * Provides a button that when clicked fills up the full screen with the map.
 * When in full screen mode, a close button is shown to exit full screen mode.
 * The [Fullscreen API](http://www.w3.org/TR/fullscreen/) is used to
 * toggle the map in full screen mode.
 *
 *
 * @constructor
 * @extends {ol.control.Control}
 * @param {olx.control.FullScreenOptions=} opt_options Options.
 * @api stable
 */
ol.control.FullScreen = function(opt_options) {

  var options = opt_options ? opt_options : {};

  /**
   * @private
   * @type {string}
   */
  this.cssClassName_ = options.className ? options.className : 'ol-full-screen';

  var label = options.label ? options.label : '\u2922';

  /**
   * @private
   * @type {Node}
   */
  this.labelNode_ = goog.isString(label) ?
      document.createTextNode(label) : label;

  var labelActive = options.labelActive ? options.labelActive : '\u00d7';

  /**
   * @private
   * @type {Node}
   */
  this.labelActiveNode_ = goog.isString(labelActive) ?
      document.createTextNode(labelActive) : labelActive;

  var tipLabel = options.tipLabel ? options.tipLabel : 'Toggle full-screen';
  var button = goog.dom.createDom('BUTTON', {
    'class': this.cssClassName_ + '-' + goog.dom.fullscreen.isFullScreen(),
    'type': 'button',
    'title': tipLabel
  }, this.labelNode_);

  goog.events.listen(button, goog.events.EventType.CLICK,
      this.handleClick_, false, this);

  goog.events.listen(goog.global.document,
      goog.dom.fullscreen.EventType.CHANGE,
      this.handleFullScreenChange_, false, this);

  var cssClasses = this.cssClassName_ + ' ' + ol.css.CLASS_UNSELECTABLE +
      ' ' + ol.css.CLASS_CONTROL + ' ' +
      (!goog.dom.fullscreen.isSupported() ? ol.css.CLASS_UNSUPPORTED : '');
  var element = goog.dom.createDom('DIV', cssClasses, button);

  goog.base(this, {
    element: element,
    target: options.target
  });

  /**
   * @private
   * @type {boolean}
   */
  this.keys_ = options.keys !== undefined ? options.keys : false;

};
goog.inherits(ol.control.FullScreen, ol.control.Control);


/**
 * @param {goog.events.BrowserEvent} event The event to handle
 * @private
 */
ol.control.FullScreen.prototype.handleClick_ = function(event) {
  event.preventDefault();
  this.handleFullScreen_();
};


/**
 * @private
 */
ol.control.FullScreen.prototype.handleFullScreen_ = function() {
  if (!goog.dom.fullscreen.isSupported()) {
    return;
  }
  var map = this.getMap();
  if (!map) {
    return;
  }
  if (goog.dom.fullscreen.isFullScreen()) {
    goog.dom.fullscreen.exitFullScreen();
  } else {
    var element = map.getTargetElement();
    goog.asserts.assert(element, 'element should be defined');
    if (this.keys_) {
      goog.dom.fullscreen.requestFullScreenWithKeys(element);
    } else {
      goog.dom.fullscreen.requestFullScreen(element);
    }
  }
};


/**
 * @private
 */
ol.control.FullScreen.prototype.handleFullScreenChange_ = function() {
  var opened = this.cssClassName_ + '-true';
  var closed = this.cssClassName_ + '-false';
  var button = goog.dom.getFirstElementChild(this.element);
  var map = this.getMap();
  if (goog.dom.fullscreen.isFullScreen()) {
    goog.dom.classlist.swap(button, closed, opened);
    goog.dom.replaceNode(this.labelActiveNode_, this.labelNode_);
  } else {
    goog.dom.classlist.swap(button, opened, closed);
    goog.dom.replaceNode(this.labelNode_, this.labelActiveNode_);
  }
  if (map) {
    map.updateSize();
  }
};

goog.provide('ol.Pixel');


/**
 * An array with two elements, representing a pixel. The first element is the
 * x-coordinate, the second the y-coordinate of the pixel.
 * @typedef {Array.<number>}
 * @api stable
 */
ol.Pixel;

// FIXME should listen on appropriate pane, once it is defined

goog.provide('ol.control.MousePosition');

goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('ol.CoordinateFormatType');
goog.require('ol.Object');
goog.require('ol.Pixel');
goog.require('ol.TransformFunction');
goog.require('ol.control.Control');
goog.require('ol.proj');
goog.require('ol.proj.Projection');


/**
 * @enum {string}
 */
ol.control.MousePositionProperty = {
  PROJECTION: 'projection',
  COORDINATE_FORMAT: 'coordinateFormat'
};


/**
 * @classdesc
 * A control to show the 2D coordinates of the mouse cursor. By default, these
 * are in the view projection, but can be in any supported projection.
 * By default the control is shown in the top right corner of the map, but this
 * can be changed by using the css selector `.ol-mouse-position`.
 *
 * @constructor
 * @extends {ol.control.Control}
 * @param {olx.control.MousePositionOptions=} opt_options Mouse position
 *     options.
 * @api stable
 */
ol.control.MousePosition = function(opt_options) {

  var options = opt_options ? opt_options : {};

  var className = options.className ? options.className : 'ol-mouse-position';

  var element = goog.dom.createDom('DIV', className);

  var render = options.render ?
      options.render : ol.control.MousePosition.render;

  goog.base(this, {
    element: element,
    render: render,
    target: options.target
  });

  goog.events.listen(this,
      ol.Object.getChangeEventType(ol.control.MousePositionProperty.PROJECTION),
      this.handleProjectionChanged_, false, this);

  if (options.coordinateFormat) {
    this.setCoordinateFormat(options.coordinateFormat);
  }
  if (options.projection) {
    this.setProjection(ol.proj.get(options.projection));
  }

  /**
   * @private
   * @type {string}
   */
  this.undefinedHTML_ = options.undefinedHTML ? options.undefinedHTML : '';

  /**
   * @private
   * @type {string}
   */
  this.renderedHTML_ = element.innerHTML;

  /**
   * @private
   * @type {ol.proj.Projection}
   */
  this.mapProjection_ = null;

  /**
   * @private
   * @type {?ol.TransformFunction}
   */
  this.transform_ = null;

  /**
   * @private
   * @type {ol.Pixel}
   */
  this.lastMouseMovePixel_ = null;

};
goog.inherits(ol.control.MousePosition, ol.control.Control);


/**
 * Update the mouseposition element.
 * @param {ol.MapEvent} mapEvent Map event.
 * @this {ol.control.MousePosition}
 * @api
 */
ol.control.MousePosition.render = function(mapEvent) {
  var frameState = mapEvent.frameState;
  if (!frameState) {
    this.mapProjection_ = null;
  } else {
    if (this.mapProjection_ != frameState.viewState.projection) {
      this.mapProjection_ = frameState.viewState.projection;
      this.transform_ = null;
    }
  }
  this.updateHTML_(this.lastMouseMovePixel_);
};


/**
 * @private
 */
ol.control.MousePosition.prototype.handleProjectionChanged_ = function() {
  this.transform_ = null;
};


/**
 * Return the coordinate format type used to render the current position or
 * undefined.
 * @return {ol.CoordinateFormatType|undefined} The format to render the current
 *     position in.
 * @observable
 * @api stable
 */
ol.control.MousePosition.prototype.getCoordinateFormat = function() {
  return /** @type {ol.CoordinateFormatType|undefined} */ (
      this.get(ol.control.MousePositionProperty.COORDINATE_FORMAT));
};


/**
 * Return the projection that is used to report the mouse position.
 * @return {ol.proj.Projection|undefined} The projection to report mouse
 *     position in.
 * @observable
 * @api stable
 */
ol.control.MousePosition.prototype.getProjection = function() {
  return /** @type {ol.proj.Projection|undefined} */ (
      this.get(ol.control.MousePositionProperty.PROJECTION));
};


/**
 * @param {goog.events.BrowserEvent} browserEvent Browser event.
 * @protected
 */
ol.control.MousePosition.prototype.handleMouseMove = function(browserEvent) {
  var map = this.getMap();
  this.lastMouseMovePixel_ = map.getEventPixel(browserEvent.getBrowserEvent());
  this.updateHTML_(this.lastMouseMovePixel_);
};


/**
 * @param {goog.events.BrowserEvent} browserEvent Browser event.
 * @protected
 */
ol.control.MousePosition.prototype.handleMouseOut = function(browserEvent) {
  this.updateHTML_(null);
  this.lastMouseMovePixel_ = null;
};


/**
 * @inheritDoc
 * @api stable
 */
ol.control.MousePosition.prototype.setMap = function(map) {
  goog.base(this, 'setMap', map);
  if (map) {
    var viewport = map.getViewport();
    this.listenerKeys.push(
        goog.events.listen(viewport, goog.events.EventType.MOUSEMOVE,
            this.handleMouseMove, false, this),
        goog.events.listen(viewport, goog.events.EventType.MOUSEOUT,
            this.handleMouseOut, false, this)
    );
  }
};


/**
 * Set the coordinate format type used to render the current position.
 * @param {ol.CoordinateFormatType} format The format to render the current
 *     position in.
 * @observable
 * @api stable
 */
ol.control.MousePosition.prototype.setCoordinateFormat = function(format) {
  this.set(ol.control.MousePositionProperty.COORDINATE_FORMAT, format);
};


/**
 * Set the projection that is used to report the mouse position.
 * @param {ol.proj.Projection} projection The projection to report mouse
 *     position in.
 * @observable
 * @api stable
 */
ol.control.MousePosition.prototype.setProjection = function(projection) {
  this.set(ol.control.MousePositionProperty.PROJECTION, projection);
};


/**
 * @param {?ol.Pixel} pixel Pixel.
 * @private
 */
ol.control.MousePosition.prototype.updateHTML_ = function(pixel) {
  var html = this.undefinedHTML_;
  if (pixel && this.mapProjection_) {
    if (!this.transform_) {
      var projection = this.getProjection();
      if (projection) {
        this.transform_ = ol.proj.getTransformFromProjections(
            this.mapProjection_, projection);
      } else {
        this.transform_ = ol.proj.identityTransform;
      }
    }
    var map = this.getMap();
    var coordinate = map.getCoordinateFromPixel(pixel);
    if (coordinate) {
      this.transform_(coordinate, coordinate);
      var coordinateFormat = this.getCoordinateFormat();
      if (coordinateFormat) {
        html = coordinateFormat(coordinate);
      } else {
        html = coordinate.toString();
      }
    }
  }
  if (!this.renderedHTML_ || html != this.renderedHTML_) {
    this.element.innerHTML = html;
    this.renderedHTML_ = html;
  }
};

// Copyright 2012 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview A delayed callback that pegs to the next animation frame
 * instead of a user-configurable timeout.
 *
 * @author nicksantos@google.com (Nick Santos)
 */

goog.provide('goog.async.AnimationDelay');

goog.require('goog.Disposable');
goog.require('goog.events');
goog.require('goog.functions');



// TODO(nicksantos): Should we factor out the common code between this and
// goog.async.Delay? I'm not sure if there's enough code for this to really
// make sense. Subclassing seems like the wrong approach for a variety of
// reasons. Maybe there should be a common interface?



/**
 * A delayed callback that pegs to the next animation frame
 * instead of a user configurable timeout. By design, this should have
 * the same interface as goog.async.Delay.
 *
 * Uses requestAnimationFrame and friends when available, but falls
 * back to a timeout of goog.async.AnimationDelay.TIMEOUT.
 *
 * For more on requestAnimationFrame and how you can use it to create smoother
 * animations, see:
 * @see http://paulirish.com/2011/requestanimationframe-for-smart-animating/
 *
 * @param {function(number)} listener Function to call when the delay completes.
 *     Will be passed the timestamp when it's called, in unix ms.
 * @param {Window=} opt_window The window object to execute the delay in.
 *     Defaults to the global object.
 * @param {Object=} opt_handler The object scope to invoke the function in.
 * @constructor
 * @struct
 * @extends {goog.Disposable}
 * @final
 */
goog.async.AnimationDelay = function(listener, opt_window, opt_handler) {
  goog.async.AnimationDelay.base(this, 'constructor');

  /**
   * Identifier of the active delay timeout, or event listener,
   * or null when inactive.
   * @private {goog.events.Key|number}
   */
  this.id_ = null;

  /**
   * If we're using dom listeners.
   * @private {?boolean}
   */
  this.usingListeners_ = false;

  /**
   * The function that will be invoked after a delay.
   * @private {function(number)}
   */
  this.listener_ = listener;

  /**
   * The object context to invoke the callback in.
   * @private {Object|undefined}
   */
  this.handler_ = opt_handler;

  /**
   * @private {Window}
   */
  this.win_ = opt_window || window;

  /**
   * Cached callback function invoked when the delay finishes.
   * @private {function()}
   */
  this.callback_ = goog.bind(this.doAction_, this);
};
goog.inherits(goog.async.AnimationDelay, goog.Disposable);


/**
 * Default wait timeout for animations (in milliseconds).  Only used for timed
 * animation, which uses a timer (setTimeout) to schedule animation.
 *
 * @type {number}
 * @const
 */
goog.async.AnimationDelay.TIMEOUT = 20;


/**
 * Name of event received from the requestAnimationFrame in Firefox.
 *
 * @type {string}
 * @const
 * @private
 */
goog.async.AnimationDelay.MOZ_BEFORE_PAINT_EVENT_ = 'MozBeforePaint';


/**
 * Starts the delay timer. The provided listener function will be called
 * before the next animation frame.
 */
goog.async.AnimationDelay.prototype.start = function() {
  this.stop();
  this.usingListeners_ = false;

  var raf = this.getRaf_();
  var cancelRaf = this.getCancelRaf_();
  if (raf && !cancelRaf && this.win_.mozRequestAnimationFrame) {
    // Because Firefox (Gecko) runs animation in separate threads, it also saves
    // time by running the requestAnimationFrame callbacks in that same thread.
    // Sadly this breaks the assumption of implicit thread-safety in JS, and can
    // thus create thread-based inconsistencies on counters etc.
    //
    // Calling cycleAnimations_ using the MozBeforePaint event instead of as
    // callback fixes this.
    //
    // Trigger this condition only if the mozRequestAnimationFrame is available,
    // but not the W3C requestAnimationFrame function (as in draft) or the
    // equivalent cancel functions.
    this.id_ = goog.events.listen(
        this.win_,
        goog.async.AnimationDelay.MOZ_BEFORE_PAINT_EVENT_,
        this.callback_);
    this.win_.mozRequestAnimationFrame(null);
    this.usingListeners_ = true;
  } else if (raf && cancelRaf) {
    this.id_ = raf.call(this.win_, this.callback_);
  } else {
    this.id_ = this.win_.setTimeout(
        // Prior to Firefox 13, Gecko passed a non-standard parameter
        // to the callback that we want to ignore.
        goog.functions.lock(this.callback_),
        goog.async.AnimationDelay.TIMEOUT);
  }
};


/**
 * Stops the delay timer if it is active. No action is taken if the timer is not
 * in use.
 */
goog.async.AnimationDelay.prototype.stop = function() {
  if (this.isActive()) {
    var raf = this.getRaf_();
    var cancelRaf = this.getCancelRaf_();
    if (raf && !cancelRaf && this.win_.mozRequestAnimationFrame) {
      goog.events.unlistenByKey(this.id_);
    } else if (raf && cancelRaf) {
      cancelRaf.call(this.win_, /** @type {number} */ (this.id_));
    } else {
      this.win_.clearTimeout(/** @type {number} */ (this.id_));
    }
  }
  this.id_ = null;
};


/**
 * Fires delay's action even if timer has already gone off or has not been
 * started yet; guarantees action firing. Stops the delay timer.
 */
goog.async.AnimationDelay.prototype.fire = function() {
  this.stop();
  this.doAction_();
};


/**
 * Fires delay's action only if timer is currently active. Stops the delay
 * timer.
 */
goog.async.AnimationDelay.prototype.fireIfActive = function() {
  if (this.isActive()) {
    this.fire();
  }
};


/**
 * @return {boolean} True if the delay is currently active, false otherwise.
 */
goog.async.AnimationDelay.prototype.isActive = function() {
  return this.id_ != null;
};


/**
 * Invokes the callback function after the delay successfully completes.
 * @private
 */
goog.async.AnimationDelay.prototype.doAction_ = function() {
  if (this.usingListeners_ && this.id_) {
    goog.events.unlistenByKey(this.id_);
  }
  this.id_ = null;

  // We are not using the timestamp returned by requestAnimationFrame
  // because it may be either a Date.now-style time or a
  // high-resolution time (depending on browser implementation). Using
  // goog.now() will ensure that the timestamp used is consistent and
  // compatible with goog.fx.Animation.
  this.listener_.call(this.handler_, goog.now());
};


/** @override */
goog.async.AnimationDelay.prototype.disposeInternal = function() {
  this.stop();
  goog.async.AnimationDelay.base(this, 'disposeInternal');
};


/**
 * @return {?function(function(number)): number} The requestAnimationFrame
 *     function, or null if not available on this browser.
 * @private
 */
goog.async.AnimationDelay.prototype.getRaf_ = function() {
  var win = this.win_;
  return win.requestAnimationFrame ||
      win.webkitRequestAnimationFrame ||
      win.mozRequestAnimationFrame ||
      win.oRequestAnimationFrame ||
      win.msRequestAnimationFrame ||
      null;
};


/**
 * @return {?function(number): number} The cancelAnimationFrame function,
 *     or null if not available on this browser.
 * @private
 */
goog.async.AnimationDelay.prototype.getCancelRaf_ = function() {
  var win = this.win_;
  return win.cancelAnimationFrame ||
      win.cancelRequestAnimationFrame ||
      win.webkitCancelRequestAnimationFrame ||
      win.mozCancelRequestAnimationFrame ||
      win.oCancelRequestAnimationFrame ||
      win.msCancelRequestAnimationFrame ||
      null;
};

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Provides a function to schedule running a function as soon
 * as possible after the current JS execution stops and yields to the event
 * loop.
 *
 */

goog.provide('goog.async.nextTick');
goog.provide('goog.async.throwException');

goog.require('goog.debug.entryPointRegistry');
goog.require('goog.dom.TagName');
goog.require('goog.functions');
goog.require('goog.labs.userAgent.browser');
goog.require('goog.labs.userAgent.engine');


/**
 * Throw an item without interrupting the current execution context.  For
 * example, if processing a group of items in a loop, sometimes it is useful
 * to report an error while still allowing the rest of the batch to be
 * processed.
 * @param {*} exception
 */
goog.async.throwException = function(exception) {
  // Each throw needs to be in its own context.
  goog.global.setTimeout(function() { throw exception; }, 0);
};


/**
 * Fires the provided callbacks as soon as possible after the current JS
 * execution context. setTimeout(…, 0) takes at least 4ms when called from
 * within another setTimeout(…, 0) for legacy reasons.
 *
 * This will not schedule the callback as a microtask (i.e. a task that can
 * preempt user input or networking callbacks). It is meant to emulate what
 * setTimeout(_, 0) would do if it were not throttled. If you desire microtask
 * behavior, use {@see goog.Promise} instead.
 *
 * @param {function(this:SCOPE)} callback Callback function to fire as soon as
 *     possible.
 * @param {SCOPE=} opt_context Object in whose scope to call the listener.
 * @param {boolean=} opt_useSetImmediate Avoid the IE workaround that
 *     ensures correctness at the cost of speed. See comments for details.
 * @template SCOPE
 */
goog.async.nextTick = function(callback, opt_context, opt_useSetImmediate) {
  var cb = callback;
  if (opt_context) {
    cb = goog.bind(callback, opt_context);
  }
  cb = goog.async.nextTick.wrapCallback_(cb);
  // window.setImmediate was introduced and currently only supported by IE10+,
  // but due to a bug in the implementation it is not guaranteed that
  // setImmediate is faster than setTimeout nor that setImmediate N is before
  // setImmediate N+1. That is why we do not use the native version if
  // available. We do, however, call setImmediate if it is a normal function
  // because that indicates that it has been replaced by goog.testing.MockClock
  // which we do want to support.
  // See
  // http://connect.microsoft.com/IE/feedback/details/801823/setimmediate-and-messagechannel-are-broken-in-ie10
  //
  // Note we do allow callers to also request setImmediate if they are willing
  // to accept the possible tradeoffs of incorrectness in exchange for speed.
  // The IE fallback of readystate change is much slower.
  if (goog.isFunction(goog.global.setImmediate) &&
      // Opt in.
      (opt_useSetImmediate ||
      // or it isn't a browser or the environment is weird
      !goog.global.Window || !goog.global.Window.prototype ||
      // or something redefined setImmediate in which case we (YOLO) decide
      // to use it (This is so that we use the mockClock setImmediate. sigh).
      goog.global.Window.prototype.setImmediate != goog.global.setImmediate)) {
    goog.global.setImmediate(cb);
    return;
  }

  // Look for and cache the custom fallback version of setImmediate.
  if (!goog.async.nextTick.setImmediate_) {
    goog.async.nextTick.setImmediate_ =
        goog.async.nextTick.getSetImmediateEmulator_();
  }
  goog.async.nextTick.setImmediate_(cb);
};


/**
 * Cache for the setImmediate implementation.
 * @type {function(function())}
 * @private
 */
goog.async.nextTick.setImmediate_;


/**
 * Determines the best possible implementation to run a function as soon as
 * the JS event loop is idle.
 * @return {function(function())} The "setImmediate" implementation.
 * @private
 */
goog.async.nextTick.getSetImmediateEmulator_ = function() {
  // Create a private message channel and use it to postMessage empty messages
  // to ourselves.
  var Channel = goog.global['MessageChannel'];
  // If MessageChannel is not available and we are in a browser, implement
  // an iframe based polyfill in browsers that have postMessage and
  // document.addEventListener. The latter excludes IE8 because it has a
  // synchronous postMessage implementation.
  if (typeof Channel === 'undefined' && typeof window !== 'undefined' &&
      window.postMessage && window.addEventListener &&
      // Presto (The old pre-blink Opera engine) has problems with iframes
      // and contentWindow.
      !goog.labs.userAgent.engine.isPresto()) {
    /** @constructor */
    Channel = function() {
      // Make an empty, invisible iframe.
      var iframe = document.createElement(goog.dom.TagName.IFRAME);
      iframe.style.display = 'none';
      iframe.src = '';
      document.documentElement.appendChild(iframe);
      var win = iframe.contentWindow;
      var doc = win.document;
      doc.open();
      doc.write('');
      doc.close();
      // Do not post anything sensitive over this channel, as the workaround for
      // pages with file: origin could allow that information to be modified or
      // intercepted.
      var message = 'callImmediate' + Math.random();
      // The same origin policy rejects attempts to postMessage from file: urls
      // unless the origin is '*'.
      // TODO(b/16335441): Use '*' origin for data: and other similar protocols.
      var origin = win.location.protocol == 'file:' ?
          '*' : win.location.protocol + '//' + win.location.host;
      var onmessage = goog.bind(function(e) {
        // Validate origin and message to make sure that this message was
        // intended for us. If the origin is set to '*' (see above) only the
        // message needs to match since, for example, '*' != 'file://'. Allowing
        // the wildcard is ok, as we are not concerned with security here.
        if ((origin != '*' && e.origin != origin) || e.data != message) {
          return;
        }
        this['port1'].onmessage();
      }, this);
      win.addEventListener('message', onmessage, false);
      this['port1'] = {};
      this['port2'] = {
        postMessage: function() {
          win.postMessage(message, origin);
        }
      };
    };
  }
  if (typeof Channel !== 'undefined' &&
      (!goog.labs.userAgent.browser.isIE())) {
    // Exclude all of IE due to
    // http://codeforhire.com/2013/09/21/setimmediate-and-messagechannel-broken-on-internet-explorer-10/
    // which allows starving postMessage with a busy setTimeout loop.
    // This currently affects IE10 and IE11 which would otherwise be able
    // to use the postMessage based fallbacks.
    var channel = new Channel();
    // Use a fifo linked list to call callbacks in the right order.
    var head = {};
    var tail = head;
    channel['port1'].onmessage = function() {
      if (goog.isDef(head.next)) {
        head = head.next;
        var cb = head.cb;
        head.cb = null;
        cb();
      }
    };
    return function(cb) {
      tail.next = {
        cb: cb
      };
      tail = tail.next;
      channel['port2'].postMessage(0);
    };
  }
  // Implementation for IE6+: Script elements fire an asynchronous
  // onreadystatechange event when inserted into the DOM.
  if (typeof document !== 'undefined' && 'onreadystatechange' in
      document.createElement(goog.dom.TagName.SCRIPT)) {
    return function(cb) {
      var script = document.createElement(goog.dom.TagName.SCRIPT);
      script.onreadystatechange = function() {
        // Clean up and call the callback.
        script.onreadystatechange = null;
        script.parentNode.removeChild(script);
        script = null;
        cb();
        cb = null;
      };
      document.documentElement.appendChild(script);
    };
  }
  // Fall back to setTimeout with 0. In browsers this creates a delay of 5ms
  // or more.
  return function(cb) {
    goog.global.setTimeout(cb, 0);
  };
};


/**
 * Helper function that is overrided to protect callbacks with entry point
 * monitor if the application monitors entry points.
 * @param {function()} callback Callback function to fire as soon as possible.
 * @return {function()} The wrapped callback.
 * @private
 */
goog.async.nextTick.wrapCallback_ = goog.functions.identity;


// Register the callback function as an entry point, so that it can be
// monitored for exception handling, etc. This has to be done in this file
// since it requires special code to handle all browsers.
goog.debug.entryPointRegistry.register(
    /**
     * @param {function(!Function): !Function} transformer The transforming
     *     function.
     */
    function(transformer) {
      goog.async.nextTick.wrapCallback_ = transformer;
    });

// Copyright 2014 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview The SafeScript type and its builders.
 *
 * TODO(xtof): Link to document stating type contract.
 */

goog.provide('goog.html.SafeScript');

goog.require('goog.asserts');
goog.require('goog.string.Const');
goog.require('goog.string.TypedString');



/**
 * A string-like object which represents JavaScript code and that carries the
 * security type contract that its value, as a string, will not cause execution
 * of unconstrained attacker controlled code (XSS) when evaluated as JavaScript
 * in a browser.
 *
 * Instances of this type must be created via the factory method
 * {@code goog.html.SafeScript.fromConstant} and not by invoking its
 * constructor. The constructor intentionally takes no parameters and the type
 * is immutable; hence only a default instance corresponding to the empty string
 * can be obtained via constructor invocation.
 *
 * A SafeScript's string representation can safely be interpolated as the
 * content of a script element within HTML. The SafeScript string should not be
 * escaped before interpolation.
 *
 * Note that the SafeScript might contain text that is attacker-controlled but
 * that text should have been interpolated with appropriate escaping,
 * sanitization and/or validation into the right location in the script, such
 * that it is highly constrained in its effect (for example, it had to match a
 * set of whitelisted words).
 *
 * A SafeScript can be constructed via security-reviewed unchecked
 * conversions. In this case producers of SafeScript must ensure themselves that
 * the SafeScript does not contain unsafe script. Note in particular that
 * {@code &lt;} is dangerous, even when inside JavaScript strings, and so should
 * always be forbidden or JavaScript escaped in user controlled input. For
 * example, if {@code &lt;/script&gt;&lt;script&gt;evil&lt;/script&gt;"} were
 * interpolated inside a JavaScript string, it would break out of the context
 * of the original script element and {@code evil} would execute. Also note
 * that within an HTML script (raw text) element, HTML character references,
 * such as "&lt;" are not allowed. See
 * http://www.w3.org/TR/html5/scripting-1.html#restrictions-for-contents-of-script-elements.
 *
 * @see goog.html.SafeScript#fromConstant
 * @constructor
 * @final
 * @struct
 * @implements {goog.string.TypedString}
 */
goog.html.SafeScript = function() {
  /**
   * The contained value of this SafeScript.  The field has a purposely
   * ugly name to make (non-compiled) code that attempts to directly access this
   * field stand out.
   * @private {string}
   */
  this.privateDoNotAccessOrElseSafeScriptWrappedValue_ = '';

  /**
   * A type marker used to implement additional run-time type checking.
   * @see goog.html.SafeScript#unwrap
   * @const
   * @private
   */
  this.SAFE_SCRIPT_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ =
      goog.html.SafeScript.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_;
};


/**
 * @override
 * @const
 */
goog.html.SafeScript.prototype.implementsGoogStringTypedString = true;


/**
 * Type marker for the SafeScript type, used to implement additional
 * run-time type checking.
 * @const {!Object}
 * @private
 */
goog.html.SafeScript.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ = {};


/**
 * Creates a SafeScript object from a compile-time constant string.
 *
 * @param {!goog.string.Const} script A compile-time-constant string from which
 *     to create a SafeScript.
 * @return {!goog.html.SafeScript} A SafeScript object initialized to
 *     {@code script}.
 */
goog.html.SafeScript.fromConstant = function(script) {
  var scriptString = goog.string.Const.unwrap(script);
  if (scriptString.length === 0) {
    return goog.html.SafeScript.EMPTY;
  }
  return goog.html.SafeScript.createSafeScriptSecurityPrivateDoNotAccessOrElse(
      scriptString);
};


/**
 * Returns this SafeScript's value as a string.
 *
 * IMPORTANT: In code where it is security relevant that an object's type is
 * indeed {@code SafeScript}, use {@code goog.html.SafeScript.unwrap} instead of
 * this method. If in doubt, assume that it's security relevant. In particular,
 * note that goog.html functions which return a goog.html type do not guarantee
 * the returned instance is of the right type. For example:
 *
 * <pre>
 * var fakeSafeHtml = new String('fake');
 * fakeSafeHtml.__proto__ = goog.html.SafeHtml.prototype;
 * var newSafeHtml = goog.html.SafeHtml.htmlEscape(fakeSafeHtml);
 * // newSafeHtml is just an alias for fakeSafeHtml, it's passed through by
 * // goog.html.SafeHtml.htmlEscape() as fakeSafeHtml
 * // instanceof goog.html.SafeHtml.
 * </pre>
 *
 * @see goog.html.SafeScript#unwrap
 * @override
 */
goog.html.SafeScript.prototype.getTypedStringValue = function() {
  return this.privateDoNotAccessOrElseSafeScriptWrappedValue_;
};


if (goog.DEBUG) {
  /**
   * Returns a debug string-representation of this value.
   *
   * To obtain the actual string value wrapped in a SafeScript, use
   * {@code goog.html.SafeScript.unwrap}.
   *
   * @see goog.html.SafeScript#unwrap
   * @override
   */
  goog.html.SafeScript.prototype.toString = function() {
    return 'SafeScript{' +
        this.privateDoNotAccessOrElseSafeScriptWrappedValue_ + '}';
  };
}


/**
 * Performs a runtime check that the provided object is indeed a
 * SafeScript object, and returns its value.
 *
 * @param {!goog.html.SafeScript} safeScript The object to extract from.
 * @return {string} The safeScript object's contained string, unless
 *     the run-time type check fails. In that case, {@code unwrap} returns an
 *     innocuous string, or, if assertions are enabled, throws
 *     {@code goog.asserts.AssertionError}.
 */
goog.html.SafeScript.unwrap = function(safeScript) {
  // Perform additional Run-time type-checking to ensure that
  // safeScript is indeed an instance of the expected type.  This
  // provides some additional protection against security bugs due to
  // application code that disables type checks.
  // Specifically, the following checks are performed:
  // 1. The object is an instance of the expected type.
  // 2. The object is not an instance of a subclass.
  // 3. The object carries a type marker for the expected type. "Faking" an
  // object requires a reference to the type marker, which has names intended
  // to stand out in code reviews.
  if (safeScript instanceof goog.html.SafeScript &&
      safeScript.constructor === goog.html.SafeScript &&
      safeScript.SAFE_SCRIPT_TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_ ===
          goog.html.SafeScript.TYPE_MARKER_GOOG_HTML_SECURITY_PRIVATE_) {
    return safeScript.privateDoNotAccessOrElseSafeScriptWrappedValue_;
  } else {
    goog.asserts.fail(
        'expected object of type SafeScript, got \'' + safeScript + '\'');
    return 'type_error:SafeScript';
  }
};


/**
 * Package-internal utility method to create SafeScript instances.
 *
 * @param {string} script The string to initialize the SafeScript object with.
 * @return {!goog.html.SafeScript} The initialized SafeScript object.
 * @package
 */
goog.html.SafeScript.createSafeScriptSecurityPrivateDoNotAccessOrElse =
    function(script) {
  return new goog.html.SafeScript().initSecurityPrivateDoNotAccessOrElse_(
      script);
};


/**
 * Called from createSafeScriptSecurityPrivateDoNotAccessOrElse(). This
 * method exists only so that the compiler can dead code eliminate static
 * fields (like EMPTY) when they're not accessed.
 * @param {string} script
 * @return {!goog.html.SafeScript}
 * @private
 */
goog.html.SafeScript.prototype.initSecurityPrivateDoNotAccessOrElse_ = function(
    script) {
  this.privateDoNotAccessOrElseSafeScriptWrappedValue_ = script;
  return this;
};


/**
 * A SafeScript instance corresponding to the empty string.
 * @const {!goog.html.SafeScript}
 */
goog.html.SafeScript.EMPTY =
    goog.html.SafeScript.createSafeScriptSecurityPrivateDoNotAccessOrElse('');

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Unchecked conversions to create values of goog.html types from
 * plain strings.  Use of these functions could potentially result in instances
 * of goog.html types that violate their type contracts, and hence result in
 * security vulnerabilties.
 *
 * Therefore, all uses of the methods herein must be carefully security
 * reviewed.  Avoid use of the methods in this file whenever possible; instead
 * prefer to create instances of goog.html types using inherently safe builders
 * or template systems.
 *
 *
 *
 * @visibility {//closure/goog/html:approved_for_unchecked_conversion}
 * @visibility {//closure/goog/bin/sizetests:__pkg__}
 */


goog.provide('goog.html.uncheckedconversions');

goog.require('goog.asserts');
goog.require('goog.html.SafeHtml');
goog.require('goog.html.SafeScript');
goog.require('goog.html.SafeStyle');
goog.require('goog.html.SafeStyleSheet');
goog.require('goog.html.SafeUrl');
goog.require('goog.html.TrustedResourceUrl');
goog.require('goog.string');
goog.require('goog.string.Const');


/**
 * Performs an "unchecked conversion" to SafeHtml from a plain string that is
 * known to satisfy the SafeHtml type contract.
 *
 * IMPORTANT: Uses of this method must be carefully security-reviewed to ensure
 * that the value of {@code html} satisfies the SafeHtml type contract in all
 * possible program states.
 *
 *
 * @param {!goog.string.Const} justification A constant string explaining why
 *     this use of this method is safe. May include a security review ticket
 *     number.
 * @param {string} html A string that is claimed to adhere to the SafeHtml
 *     contract.
 * @param {?goog.i18n.bidi.Dir=} opt_dir The optional directionality of the
 *     SafeHtml to be constructed. A null or undefined value signifies an
 *     unknown directionality.
 * @return {!goog.html.SafeHtml} The value of html, wrapped in a SafeHtml
 *     object.
 * @suppress {visibility} For access to SafeHtml.create...  Note that this
 *     use is appropriate since this method is intended to be "package private"
 *     withing goog.html.  DO NOT call SafeHtml.create... from outside this
 *     package; use appropriate wrappers instead.
 */
goog.html.uncheckedconversions.safeHtmlFromStringKnownToSatisfyTypeContract =
    function(justification, html, opt_dir) {
  // unwrap() called inside an assert so that justification can be optimized
  // away in production code.
  goog.asserts.assertString(goog.string.Const.unwrap(justification),
                            'must provide justification');
  goog.asserts.assert(
      !goog.string.isEmptyOrWhitespace(goog.string.Const.unwrap(justification)),
      'must provide non-empty justification');
  return goog.html.SafeHtml.createSafeHtmlSecurityPrivateDoNotAccessOrElse(
      html, opt_dir || null);
};


/**
 * Performs an "unchecked conversion" to SafeScript from a plain string that is
 * known to satisfy the SafeScript type contract.
 *
 * IMPORTANT: Uses of this method must be carefully security-reviewed to ensure
 * that the value of {@code script} satisfies the SafeScript type contract in
 * all possible program states.
 *
 *
 * @param {!goog.string.Const} justification A constant string explaining why
 *     this use of this method is safe. May include a security review ticket
 *     number.
 * @param {string} script The string to wrap as a SafeScript.
 * @return {!goog.html.SafeScript} The value of {@code script}, wrapped in a
 *     SafeScript object.
 */
goog.html.uncheckedconversions.safeScriptFromStringKnownToSatisfyTypeContract =
    function(justification, script) {
  // unwrap() called inside an assert so that justification can be optimized
  // away in production code.
  goog.asserts.assertString(goog.string.Const.unwrap(justification),
                            'must provide justification');
  goog.asserts.assert(
      !goog.string.isEmpty(goog.string.Const.unwrap(justification)),
      'must provide non-empty justification');
  return goog.html.SafeScript.createSafeScriptSecurityPrivateDoNotAccessOrElse(
      script);
};


/**
 * Performs an "unchecked conversion" to SafeStyle from a plain string that is
 * known to satisfy the SafeStyle type contract.
 *
 * IMPORTANT: Uses of this method must be carefully security-reviewed to ensure
 * that the value of {@code style} satisfies the SafeUrl type contract in all
 * possible program states.
 *
 *
 * @param {!goog.string.Const} justification A constant string explaining why
 *     this use of this method is safe. May include a security review ticket
 *     number.
 * @param {string} style The string to wrap as a SafeStyle.
 * @return {!goog.html.SafeStyle} The value of {@code style}, wrapped in a
 *     SafeStyle object.
 */
goog.html.uncheckedconversions.safeStyleFromStringKnownToSatisfyTypeContract =
    function(justification, style) {
  // unwrap() called inside an assert so that justification can be optimized
  // away in production code.
  goog.asserts.assertString(goog.string.Const.unwrap(justification),
                            'must provide justification');
  goog.asserts.assert(
      !goog.string.isEmptyOrWhitespace(goog.string.Const.unwrap(justification)),
      'must provide non-empty justification');
  return goog.html.SafeStyle.createSafeStyleSecurityPrivateDoNotAccessOrElse(
      style);
};


/**
 * Performs an "unchecked conversion" to SafeStyleSheet from a plain string
 * that is known to satisfy the SafeStyleSheet type contract.
 *
 * IMPORTANT: Uses of this method must be carefully security-reviewed to ensure
 * that the value of {@code styleSheet} satisfies the SafeUrl type contract in
 * all possible program states.
 *
 *
 * @param {!goog.string.Const} justification A constant string explaining why
 *     this use of this method is safe. May include a security review ticket
 *     number.
 * @param {string} styleSheet The string to wrap as a SafeStyleSheet.
 * @return {!goog.html.SafeStyleSheet} The value of {@code styleSheet}, wrapped
 *     in a SafeStyleSheet object.
 */
goog.html.uncheckedconversions.
    safeStyleSheetFromStringKnownToSatisfyTypeContract =
    function(justification, styleSheet) {
  // unwrap() called inside an assert so that justification can be optimized
  // away in production code.
  goog.asserts.assertString(goog.string.Const.unwrap(justification),
                            'must provide justification');
  goog.asserts.assert(
      !goog.string.isEmptyOrWhitespace(goog.string.Const.unwrap(justification)),
      'must provide non-empty justification');
  return goog.html.SafeStyleSheet.
      createSafeStyleSheetSecurityPrivateDoNotAccessOrElse(styleSheet);
};


/**
 * Performs an "unchecked conversion" to SafeUrl from a plain string that is
 * known to satisfy the SafeUrl type contract.
 *
 * IMPORTANT: Uses of this method must be carefully security-reviewed to ensure
 * that the value of {@code url} satisfies the SafeUrl type contract in all
 * possible program states.
 *
 *
 * @param {!goog.string.Const} justification A constant string explaining why
 *     this use of this method is safe. May include a security review ticket
 *     number.
 * @param {string} url The string to wrap as a SafeUrl.
 * @return {!goog.html.SafeUrl} The value of {@code url}, wrapped in a SafeUrl
 *     object.
 */
goog.html.uncheckedconversions.safeUrlFromStringKnownToSatisfyTypeContract =
    function(justification, url) {
  // unwrap() called inside an assert so that justification can be optimized
  // away in production code.
  goog.asserts.assertString(goog.string.Const.unwrap(justification),
                            'must provide justification');
  goog.asserts.assert(
      !goog.string.isEmptyOrWhitespace(goog.string.Const.unwrap(justification)),
      'must provide non-empty justification');
  return goog.html.SafeUrl.createSafeUrlSecurityPrivateDoNotAccessOrElse(url);
};


/**
 * Performs an "unchecked conversion" to TrustedResourceUrl from a plain string
 * that is known to satisfy the TrustedResourceUrl type contract.
 *
 * IMPORTANT: Uses of this method must be carefully security-reviewed to ensure
 * that the value of {@code url} satisfies the TrustedResourceUrl type contract
 * in all possible program states.
 *
 *
 * @param {!goog.string.Const} justification A constant string explaining why
 *     this use of this method is safe. May include a security review ticket
 *     number.
 * @param {string} url The string to wrap as a TrustedResourceUrl.
 * @return {!goog.html.TrustedResourceUrl} The value of {@code url}, wrapped in
 *     a TrustedResourceUrl object.
 */
goog.html.uncheckedconversions.
    trustedResourceUrlFromStringKnownToSatisfyTypeContract =
    function(justification, url) {
  // unwrap() called inside an assert so that justification can be optimized
  // away in production code.
  goog.asserts.assertString(goog.string.Const.unwrap(justification),
                            'must provide justification');
  goog.asserts.assert(
      !goog.string.isEmptyOrWhitespace(goog.string.Const.unwrap(justification)),
      'must provide non-empty justification');
  return goog.html.TrustedResourceUrl.
      createTrustedResourceUrlSecurityPrivateDoNotAccessOrElse(url);
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Generics method for collection-like classes and objects.
 *
 * @author arv@google.com (Erik Arvidsson)
 *
 * This file contains functions to work with collections. It supports using
 * Map, Set, Array and Object and other classes that implement collection-like
 * methods.
 */


goog.provide('goog.structs');

goog.require('goog.array');
goog.require('goog.object');


// We treat an object as a dictionary if it has getKeys or it is an object that
// isn't arrayLike.


/**
 * Returns the number of values in the collection-like object.
 * @param {Object} col The collection-like object.
 * @return {number} The number of values in the collection-like object.
 */
goog.structs.getCount = function(col) {
  if (typeof col.getCount == 'function') {
    return col.getCount();
  }
  if (goog.isArrayLike(col) || goog.isString(col)) {
    return col.length;
  }
  return goog.object.getCount(col);
};


/**
 * Returns the values of the collection-like object.
 * @param {Object} col The collection-like object.
 * @return {!Array<?>} The values in the collection-like object.
 */
goog.structs.getValues = function(col) {
  if (typeof col.getValues == 'function') {
    return col.getValues();
  }
  if (goog.isString(col)) {
    return col.split('');
  }
  if (goog.isArrayLike(col)) {
    var rv = [];
    var l = col.length;
    for (var i = 0; i < l; i++) {
      rv.push(col[i]);
    }
    return rv;
  }
  return goog.object.getValues(col);
};


/**
 * Returns the keys of the collection. Some collections have no notion of
 * keys/indexes and this function will return undefined in those cases.
 * @param {Object} col The collection-like object.
 * @return {!Array|undefined} The keys in the collection.
 */
goog.structs.getKeys = function(col) {
  if (typeof col.getKeys == 'function') {
    return col.getKeys();
  }
  // if we have getValues but no getKeys we know this is a key-less collection
  if (typeof col.getValues == 'function') {
    return undefined;
  }
  if (goog.isArrayLike(col) || goog.isString(col)) {
    var rv = [];
    var l = col.length;
    for (var i = 0; i < l; i++) {
      rv.push(i);
    }
    return rv;
  }

  return goog.object.getKeys(col);
};


/**
 * Whether the collection contains the given value. This is O(n) and uses
 * equals (==) to test the existence.
 * @param {Object} col The collection-like object.
 * @param {*} val The value to check for.
 * @return {boolean} True if the map contains the value.
 */
goog.structs.contains = function(col, val) {
  if (typeof col.contains == 'function') {
    return col.contains(val);
  }
  if (typeof col.containsValue == 'function') {
    return col.containsValue(val);
  }
  if (goog.isArrayLike(col) || goog.isString(col)) {
    return goog.array.contains(/** @type {!Array<?>} */ (col), val);
  }
  return goog.object.containsValue(col, val);
};


/**
 * Whether the collection is empty.
 * @param {Object} col The collection-like object.
 * @return {boolean} True if empty.
 */
goog.structs.isEmpty = function(col) {
  if (typeof col.isEmpty == 'function') {
    return col.isEmpty();
  }

  // We do not use goog.string.isEmptyOrWhitespace because here we treat the string as
  // collection and as such even whitespace matters

  if (goog.isArrayLike(col) || goog.isString(col)) {
    return goog.array.isEmpty(/** @type {!Array<?>} */ (col));
  }
  return goog.object.isEmpty(col);
};


/**
 * Removes all the elements from the collection.
 * @param {Object} col The collection-like object.
 */
goog.structs.clear = function(col) {
  // NOTE(arv): This should not contain strings because strings are immutable
  if (typeof col.clear == 'function') {
    col.clear();
  } else if (goog.isArrayLike(col)) {
    goog.array.clear(/** @type {goog.array.ArrayLike} */ (col));
  } else {
    goog.object.clear(col);
  }
};


/**
 * Calls a function for each value in a collection. The function takes
 * three arguments; the value, the key and the collection.
 *
 * NOTE: This will be deprecated soon! Please use a more specific method if
 * possible, e.g. goog.array.forEach, goog.object.forEach, etc.
 *
 * @param {S} col The collection-like object.
 * @param {function(this:T,?,?,S):?} f The function to call for every value.
 *     This function takes
 *     3 arguments (the value, the key or undefined if the collection has no
 *     notion of keys, and the collection) and the return value is irrelevant.
 * @param {T=} opt_obj The object to be used as the value of 'this'
 *     within {@code f}.
 * @template T,S
 */
goog.structs.forEach = function(col, f, opt_obj) {
  if (typeof col.forEach == 'function') {
    col.forEach(f, opt_obj);
  } else if (goog.isArrayLike(col) || goog.isString(col)) {
    goog.array.forEach(/** @type {!Array<?>} */ (col), f, opt_obj);
  } else {
    var keys = goog.structs.getKeys(col);
    var values = goog.structs.getValues(col);
    var l = values.length;
    for (var i = 0; i < l; i++) {
      f.call(opt_obj, values[i], keys && keys[i], col);
    }
  }
};


/**
 * Calls a function for every value in the collection. When a call returns true,
 * adds the value to a new collection (Array is returned by default).
 *
 * @param {S} col The collection-like object.
 * @param {function(this:T,?,?,S):boolean} f The function to call for every
 *     value. This function takes
 *     3 arguments (the value, the key or undefined if the collection has no
 *     notion of keys, and the collection) and should return a Boolean. If the
 *     return value is true the value is added to the result collection. If it
 *     is false the value is not included.
 * @param {T=} opt_obj The object to be used as the value of 'this'
 *     within {@code f}.
 * @return {!Object|!Array<?>} A new collection where the passed values are
 *     present. If col is a key-less collection an array is returned.  If col
 *     has keys and values a plain old JS object is returned.
 * @template T,S
 */
goog.structs.filter = function(col, f, opt_obj) {
  if (typeof col.filter == 'function') {
    return col.filter(f, opt_obj);
  }
  if (goog.isArrayLike(col) || goog.isString(col)) {
    return goog.array.filter(/** @type {!Array<?>} */ (col), f, opt_obj);
  }

  var rv;
  var keys = goog.structs.getKeys(col);
  var values = goog.structs.getValues(col);
  var l = values.length;
  if (keys) {
    rv = {};
    for (var i = 0; i < l; i++) {
      if (f.call(opt_obj, values[i], keys[i], col)) {
        rv[keys[i]] = values[i];
      }
    }
  } else {
    // We should not use goog.array.filter here since we want to make sure that
    // the index is undefined as well as make sure that col is passed to the
    // function.
    rv = [];
    for (var i = 0; i < l; i++) {
      if (f.call(opt_obj, values[i], undefined, col)) {
        rv.push(values[i]);
      }
    }
  }
  return rv;
};


/**
 * Calls a function for every value in the collection and adds the result into a
 * new collection (defaults to creating a new Array).
 *
 * @param {S} col The collection-like object.
 * @param {function(this:T,?,?,S):V} f The function to call for every value.
 *     This function takes 3 arguments (the value, the key or undefined if the
 *     collection has no notion of keys, and the collection) and should return
 *     something. The result will be used as the value in the new collection.
 * @param {T=} opt_obj  The object to be used as the value of 'this'
 *     within {@code f}.
 * @return {!Object<V>|!Array<V>} A new collection with the new values.  If
 *     col is a key-less collection an array is returned.  If col has keys and
 *     values a plain old JS object is returned.
 * @template T,S,V
 */
goog.structs.map = function(col, f, opt_obj) {
  if (typeof col.map == 'function') {
    return col.map(f, opt_obj);
  }
  if (goog.isArrayLike(col) || goog.isString(col)) {
    return goog.array.map(/** @type {!Array<?>} */ (col), f, opt_obj);
  }

  var rv;
  var keys = goog.structs.getKeys(col);
  var values = goog.structs.getValues(col);
  var l = values.length;
  if (keys) {
    rv = {};
    for (var i = 0; i < l; i++) {
      rv[keys[i]] = f.call(opt_obj, values[i], keys[i], col);
    }
  } else {
    // We should not use goog.array.map here since we want to make sure that
    // the index is undefined as well as make sure that col is passed to the
    // function.
    rv = [];
    for (var i = 0; i < l; i++) {
      rv[i] = f.call(opt_obj, values[i], undefined, col);
    }
  }
  return rv;
};


/**
 * Calls f for each value in a collection. If any call returns true this returns
 * true (without checking the rest). If all returns false this returns false.
 *
 * @param {S} col The collection-like object.
 * @param {function(this:T,?,?,S):boolean} f The function to call for every
 *     value. This function takes 3 arguments (the value, the key or undefined
 *     if the collection has no notion of keys, and the collection) and should
 *     return a boolean.
 * @param {T=} opt_obj  The object to be used as the value of 'this'
 *     within {@code f}.
 * @return {boolean} True if any value passes the test.
 * @template T,S
 */
goog.structs.some = function(col, f, opt_obj) {
  if (typeof col.some == 'function') {
    return col.some(f, opt_obj);
  }
  if (goog.isArrayLike(col) || goog.isString(col)) {
    return goog.array.some(/** @type {!Array<?>} */ (col), f, opt_obj);
  }
  var keys = goog.structs.getKeys(col);
  var values = goog.structs.getValues(col);
  var l = values.length;
  for (var i = 0; i < l; i++) {
    if (f.call(opt_obj, values[i], keys && keys[i], col)) {
      return true;
    }
  }
  return false;
};


/**
 * Calls f for each value in a collection. If all calls return true this return
 * true this returns true. If any returns false this returns false at this point
 *  and does not continue to check the remaining values.
 *
 * @param {S} col The collection-like object.
 * @param {function(this:T,?,?,S):boolean} f The function to call for every
 *     value. This function takes 3 arguments (the value, the key or
 *     undefined if the collection has no notion of keys, and the collection)
 *     and should return a boolean.
 * @param {T=} opt_obj  The object to be used as the value of 'this'
 *     within {@code f}.
 * @return {boolean} True if all key-value pairs pass the test.
 * @template T,S
 */
goog.structs.every = function(col, f, opt_obj) {
  if (typeof col.every == 'function') {
    return col.every(f, opt_obj);
  }
  if (goog.isArrayLike(col) || goog.isString(col)) {
    return goog.array.every(/** @type {!Array<?>} */ (col), f, opt_obj);
  }
  var keys = goog.structs.getKeys(col);
  var values = goog.structs.getValues(col);
  var l = values.length;
  for (var i = 0; i < l; i++) {
    if (!f.call(opt_obj, values[i], keys && keys[i], col)) {
      return false;
    }
  }
  return true;
};

// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Defines the collection interface.
 *
 * @author nnaze@google.com (Nathan Naze)
 */

goog.provide('goog.structs.Collection');



/**
 * An interface for a collection of values.
 * @interface
 * @template T
 */
goog.structs.Collection = function() {};


/**
 * @param {T} value Value to add to the collection.
 */
goog.structs.Collection.prototype.add;


/**
 * @param {T} value Value to remove from the collection.
 */
goog.structs.Collection.prototype.remove;


/**
 * @param {T} value Value to find in the collection.
 * @return {boolean} Whether the collection contains the specified value.
 */
goog.structs.Collection.prototype.contains;


/**
 * @return {number} The number of values stored in the collection.
 */
goog.structs.Collection.prototype.getCount;


// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Python style iteration utilities.
 * @author arv@google.com (Erik Arvidsson)
 */


goog.provide('goog.iter');
goog.provide('goog.iter.Iterable');
goog.provide('goog.iter.Iterator');
goog.provide('goog.iter.StopIteration');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.functions');
goog.require('goog.math');


/**
 * @typedef {goog.iter.Iterator|{length:number}|{__iterator__}}
 */
goog.iter.Iterable;


/**
 * Singleton Error object that is used to terminate iterations.
 * @const {!Error}
 */
goog.iter.StopIteration = ('StopIteration' in goog.global) ?
    // For script engines that support legacy iterators.
    goog.global['StopIteration'] :
    { message: 'StopIteration', stack: ''};



/**
 * Class/interface for iterators.  An iterator needs to implement a {@code next}
 * method and it needs to throw a {@code goog.iter.StopIteration} when the
 * iteration passes beyond the end.  Iterators have no {@code hasNext} method.
 * It is recommended to always use the helper functions to iterate over the
 * iterator or in case you are only targeting JavaScript 1.7 for in loops.
 * @constructor
 * @template VALUE
 */
goog.iter.Iterator = function() {};


/**
 * Returns the next value of the iteration.  This will throw the object
 * {@see goog.iter#StopIteration} when the iteration passes the end.
 * @return {VALUE} Any object or value.
 */
goog.iter.Iterator.prototype.next = function() {
  throw goog.iter.StopIteration;
};


/**
 * Returns the {@code Iterator} object itself.  This is used to implement
 * the iterator protocol in JavaScript 1.7
 * @param {boolean=} opt_keys  Whether to return the keys or values. Default is
 *     to only return the values.  This is being used by the for-in loop (true)
 *     and the for-each-in loop (false).  Even though the param gives a hint
 *     about what the iterator will return there is no guarantee that it will
 *     return the keys when true is passed.
 * @return {!goog.iter.Iterator<VALUE>} The object itself.
 */
goog.iter.Iterator.prototype.__iterator__ = function(opt_keys) {
  return this;
};


/**
 * Returns an iterator that knows how to iterate over the values in the object.
 * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable  If the
 *     object is an iterator it will be returned as is.  If the object has an
 *     {@code __iterator__} method that will be called to get the value
 *     iterator.  If the object is an array-like object we create an iterator
 *     for that.
 * @return {!goog.iter.Iterator<VALUE>} An iterator that knows how to iterate
 *     over the values in {@code iterable}.
 * @template VALUE
 */
goog.iter.toIterator = function(iterable) {
  if (iterable instanceof goog.iter.Iterator) {
    return iterable;
  }
  if (typeof iterable.__iterator__ == 'function') {
    return iterable.__iterator__(false);
  }
  if (goog.isArrayLike(iterable)) {
    var i = 0;
    var newIter = new goog.iter.Iterator;
    newIter.next = function() {
      while (true) {
        if (i >= iterable.length) {
          throw goog.iter.StopIteration;
        }
        // Don't include deleted elements.
        if (!(i in iterable)) {
          i++;
          continue;
        }
        return iterable[i++];
      }
    };
    return newIter;
  }


  // TODO(arv): Should we fall back on goog.structs.getValues()?
  throw Error('Not implemented');
};


/**
 * Calls a function for each element in the iterator with the element of the
 * iterator passed as argument.
 *
 * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable  The iterator
 *     to iterate over. If the iterable is an object {@code toIterator} will be
 *     called on it.
 * @param {function(this:THIS,VALUE,?,!goog.iter.Iterator<VALUE>)} f
 *     The function to call for every element.  This function takes 3 arguments
 *     (the element, undefined, and the iterator) and the return value is
 *     irrelevant.  The reason for passing undefined as the second argument is
 *     so that the same function can be used in {@see goog.array#forEach} as
 *     well as others.  The third parameter is of type "number" for
 *     arraylike objects, undefined, otherwise.
 * @param {THIS=} opt_obj  The object to be used as the value of 'this' within
 *     {@code f}.
 * @template THIS, VALUE
 */
goog.iter.forEach = function(iterable, f, opt_obj) {
  if (goog.isArrayLike(iterable)) {
    /** @preserveTry */
    try {
      // NOTES: this passes the index number to the second parameter
      // of the callback contrary to the documentation above.
      goog.array.forEach(/** @type {goog.array.ArrayLike} */(iterable), f,
                         opt_obj);
    } catch (ex) {
      if (ex !== goog.iter.StopIteration) {
        throw ex;
      }
    }
  } else {
    iterable = goog.iter.toIterator(iterable);
    /** @preserveTry */
    try {
      while (true) {
        f.call(opt_obj, iterable.next(), undefined, iterable);
      }
    } catch (ex) {
      if (ex !== goog.iter.StopIteration) {
        throw ex;
      }
    }
  }
};


/**
 * Calls a function for every element in the iterator, and if the function
 * returns true adds the element to a new iterator.
 *
 * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
 *     to iterate over.
 * @param {
 *     function(this:THIS,VALUE,undefined,!goog.iter.Iterator<VALUE>):boolean} f
 *     The function to call for every element. This function takes 3 arguments
 *     (the element, undefined, and the iterator) and should return a boolean.
 *     If the return value is true the element will be included in the returned
 *     iterator.  If it is false the element is not included.
 * @param {THIS=} opt_obj The object to be used as the value of 'this' within
 *     {@code f}.
 * @return {!goog.iter.Iterator<VALUE>} A new iterator in which only elements
 *     that passed the test are present.
 * @template THIS, VALUE
 */
goog.iter.filter = function(iterable, f, opt_obj) {
  var iterator = goog.iter.toIterator(iterable);
  var newIter = new goog.iter.Iterator;
  newIter.next = function() {
    while (true) {
      var val = iterator.next();
      if (f.call(opt_obj, val, undefined, iterator)) {
        return val;
      }
    }
  };
  return newIter;
};


/**
 * Calls a function for every element in the iterator, and if the function
 * returns false adds the element to a new iterator.
 *
 * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
 *     to iterate over.
 * @param {
 *     function(this:THIS,VALUE,undefined,!goog.iter.Iterator<VALUE>):boolean} f
 *     The function to call for every element. This function takes 3 arguments
 *     (the element, undefined, and the iterator) and should return a boolean.
 *     If the return value is false the element will be included in the returned
 *     iterator.  If it is true the element is not included.
 * @param {THIS=} opt_obj The object to be used as the value of 'this' within
 *     {@code f}.
 * @return {!goog.iter.Iterator<VALUE>} A new iterator in which only elements
 *     that did not pass the test are present.
 * @template THIS, VALUE
 */
goog.iter.filterFalse = function(iterable, f, opt_obj) {
  return goog.iter.filter(iterable, goog.functions.not(f), opt_obj);
};


/**
 * Creates a new iterator that returns the values in a range.  This function
 * can take 1, 2 or 3 arguments:
 * <pre>
 * range(5) same as range(0, 5, 1)
 * range(2, 5) same as range(2, 5, 1)
 * </pre>
 *
 * @param {number} startOrStop  The stop value if only one argument is provided.
 *     The start value if 2 or more arguments are provided.  If only one
 *     argument is used the start value is 0.
 * @param {number=} opt_stop  The stop value.  If left out then the first
 *     argument is used as the stop value.
 * @param {number=} opt_step  The number to increment with between each call to
 *     next.  This can be negative.
 * @return {!goog.iter.Iterator<number>} A new iterator that returns the values
 *     in the range.
 */
goog.iter.range = function(startOrStop, opt_stop, opt_step) {
  var start = 0;
  var stop = startOrStop;
  var step = opt_step || 1;
  if (arguments.length > 1) {
    start = startOrStop;
    stop = opt_stop;
  }
  if (step == 0) {
    throw Error('Range step argument must not be zero');
  }

  var newIter = new goog.iter.Iterator;
  newIter.next = function() {
    if (step > 0 && start >= stop || step < 0 && start <= stop) {
      throw goog.iter.StopIteration;
    }
    var rv = start;
    start += step;
    return rv;
  };
  return newIter;
};


/**
 * Joins the values in a iterator with a delimiter.
 * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
 *     to get the values from.
 * @param {string} deliminator  The text to put between the values.
 * @return {string} The joined value string.
 * @template VALUE
 */
goog.iter.join = function(iterable, deliminator) {
  return goog.iter.toArray(iterable).join(deliminator);
};


/**
 * For every element in the iterator call a function and return a new iterator
 * with that value.
 *
 * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
 *     iterator to iterate over.
 * @param {
 *     function(this:THIS,VALUE,undefined,!goog.iter.Iterator<VALUE>):RESULT} f
 *     The function to call for every element.  This function takes 3 arguments
 *     (the element, undefined, and the iterator) and should return a new value.
 * @param {THIS=} opt_obj The object to be used as the value of 'this' within
 *     {@code f}.
 * @return {!goog.iter.Iterator<RESULT>} A new iterator that returns the
 *     results of applying the function to each element in the original
 *     iterator.
 * @template THIS, VALUE, RESULT
 */
goog.iter.map = function(iterable, f, opt_obj) {
  var iterator = goog.iter.toIterator(iterable);
  var newIter = new goog.iter.Iterator;
  newIter.next = function() {
    var val = iterator.next();
    return f.call(opt_obj, val, undefined, iterator);
  };
  return newIter;
};


/**
 * Passes every element of an iterator into a function and accumulates the
 * result.
 *
 * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
 *     to iterate over.
 * @param {function(this:THIS,VALUE,VALUE):VALUE} f The function to call for
 *     every element. This function takes 2 arguments (the function's previous
 *     result or the initial value, and the value of the current element).
 *     function(previousValue, currentElement) : newValue.
 * @param {VALUE} val The initial value to pass into the function on the first
 *     call.
 * @param {THIS=} opt_obj  The object to be used as the value of 'this' within
 *     f.
 * @return {VALUE} Result of evaluating f repeatedly across the values of
 *     the iterator.
 * @template THIS, VALUE
 */
goog.iter.reduce = function(iterable, f, val, opt_obj) {
  var rval = val;
  goog.iter.forEach(iterable, function(val) {
    rval = f.call(opt_obj, rval, val);
  });
  return rval;
};


/**
 * Goes through the values in the iterator. Calls f for each of these, and if
 * any of them returns true, this returns true (without checking the rest). If
 * all return false this will return false.
 *
 * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
 *     object.
 * @param {
 *     function(this:THIS,VALUE,undefined,!goog.iter.Iterator<VALUE>):boolean} f
 *     The function to call for every value. This function takes 3 arguments
 *     (the value, undefined, and the iterator) and should return a boolean.
 * @param {THIS=} opt_obj The object to be used as the value of 'this' within
 *     {@code f}.
 * @return {boolean} true if any value passes the test.
 * @template THIS, VALUE
 */
goog.iter.some = function(iterable, f, opt_obj) {
  iterable = goog.iter.toIterator(iterable);
  /** @preserveTry */
  try {
    while (true) {
      if (f.call(opt_obj, iterable.next(), undefined, iterable)) {
        return true;
      }
    }
  } catch (ex) {
    if (ex !== goog.iter.StopIteration) {
      throw ex;
    }
  }
  return false;
};


/**
 * Goes through the values in the iterator. Calls f for each of these and if any
 * of them returns false this returns false (without checking the rest). If all
 * return true this will return true.
 *
 * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
 *     object.
 * @param {
 *     function(this:THIS,VALUE,undefined,!goog.iter.Iterator<VALUE>):boolean} f
 *     The function to call for every value. This function takes 3 arguments
 *     (the value, undefined, and the iterator) and should return a boolean.
 * @param {THIS=} opt_obj The object to be used as the value of 'this' within
 *     {@code f}.
 * @return {boolean} true if every value passes the test.
 * @template THIS, VALUE
 */
goog.iter.every = function(iterable, f, opt_obj) {
  iterable = goog.iter.toIterator(iterable);
  /** @preserveTry */
  try {
    while (true) {
      if (!f.call(opt_obj, iterable.next(), undefined, iterable)) {
        return false;
      }
    }
  } catch (ex) {
    if (ex !== goog.iter.StopIteration) {
      throw ex;
    }
  }
  return true;
};


/**
 * Takes zero or more iterables and returns one iterator that will iterate over
 * them in the order chained.
 * @param {...!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} var_args Any
 *     number of iterable objects.
 * @return {!goog.iter.Iterator<VALUE>} Returns a new iterator that will
 *     iterate over all the given iterables' contents.
 * @template VALUE
 */
goog.iter.chain = function(var_args) {
  return goog.iter.chainFromIterable(arguments);
};


/**
 * Takes a single iterable containing zero or more iterables and returns one
 * iterator that will iterate over each one in the order given.
 * @see http://docs.python.org/2/library/itertools.html#itertools.chain.from_iterable
 * @param {goog.iter.Iterable} iterable The iterable of iterables to chain.
 * @return {!goog.iter.Iterator<VALUE>} Returns a new iterator that will
 *     iterate over all the contents of the iterables contained within
 *     {@code iterable}.
 * @template VALUE
 */
goog.iter.chainFromIterable = function(iterable) {
  var iterator = goog.iter.toIterator(iterable);
  var iter = new goog.iter.Iterator();
  var current = null;

  iter.next = function() {
    while (true) {
      if (current == null) {
        var it = iterator.next();
        current = goog.iter.toIterator(it);
      }
      try {
        return current.next();
      } catch (ex) {
        if (ex !== goog.iter.StopIteration) {
          throw ex;
        }
        current = null;
      }
    }
  };

  return iter;
};


/**
 * Builds a new iterator that iterates over the original, but skips elements as
 * long as a supplied function returns true.
 * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
 *     object.
 * @param {
 *     function(this:THIS,VALUE,undefined,!goog.iter.Iterator<VALUE>):boolean} f
 *     The function to call for every value. This function takes 3 arguments
 *     (the value, undefined, and the iterator) and should return a boolean.
 * @param {THIS=} opt_obj The object to be used as the value of 'this' within
 *     {@code f}.
 * @return {!goog.iter.Iterator<VALUE>} A new iterator that drops elements from
 *     the original iterator as long as {@code f} is true.
 * @template THIS, VALUE
 */
goog.iter.dropWhile = function(iterable, f, opt_obj) {
  var iterator = goog.iter.toIterator(iterable);
  var newIter = new goog.iter.Iterator;
  var dropping = true;
  newIter.next = function() {
    while (true) {
      var val = iterator.next();
      if (dropping && f.call(opt_obj, val, undefined, iterator)) {
        continue;
      } else {
        dropping = false;
      }
      return val;
    }
  };
  return newIter;
};


/**
 * Builds a new iterator that iterates over the original, but only as long as a
 * supplied function returns true.
 * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
 *     object.
 * @param {
 *     function(this:THIS,VALUE,undefined,!goog.iter.Iterator<VALUE>):boolean} f
 *     The function to call for every value. This function takes 3 arguments
 *     (the value, undefined, and the iterator) and should return a boolean.
 * @param {THIS=} opt_obj This is used as the 'this' object in f when called.
 * @return {!goog.iter.Iterator<VALUE>} A new iterator that keeps elements in
 *     the original iterator as long as the function is true.
 * @template THIS, VALUE
 */
goog.iter.takeWhile = function(iterable, f, opt_obj) {
  var iterator = goog.iter.toIterator(iterable);
  var iter = new goog.iter.Iterator();
  iter.next = function() {
    var val = iterator.next();
    if (f.call(opt_obj, val, undefined, iterator)) {
      return val;
    }
    throw goog.iter.StopIteration;
  };
  return iter;
};


/**
 * Converts the iterator to an array
 * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterator
 *     to convert to an array.
 * @return {!Array<VALUE>} An array of the elements the iterator iterates over.
 * @template VALUE
 */
goog.iter.toArray = function(iterable) {
  // Fast path for array-like.
  if (goog.isArrayLike(iterable)) {
    return goog.array.toArray(/** @type {!goog.array.ArrayLike} */(iterable));
  }
  iterable = goog.iter.toIterator(iterable);
  var array = [];
  goog.iter.forEach(iterable, function(val) {
    array.push(val);
  });
  return array;
};


/**
 * Iterates over two iterables and returns true if they contain the same
 * sequence of elements and have the same length.
 * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable1 The first
 *     iterable object.
 * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable2 The second
 *     iterable object.
 * @param {function(VALUE,VALUE):boolean=} opt_equalsFn Optional comparison
 *     function.
 *     Should take two arguments to compare, and return true if the arguments
 *     are equal. Defaults to {@link goog.array.defaultCompareEquality} which
 *     compares the elements using the built-in '===' operator.
 * @return {boolean} true if the iterables contain the same sequence of elements
 *     and have the same length.
 * @template VALUE
 */
goog.iter.equals = function(iterable1, iterable2, opt_equalsFn) {
  var fillValue = {};
  var pairs = goog.iter.zipLongest(fillValue, iterable1, iterable2);
  var equalsFn = opt_equalsFn || goog.array.defaultCompareEquality;
  return goog.iter.every(pairs, function(pair) {
    return equalsFn(pair[0], pair[1]);
  });
};


/**
 * Advances the iterator to the next position, returning the given default value
 * instead of throwing an exception if the iterator has no more entries.
 * @param {goog.iter.Iterator<VALUE>|goog.iter.Iterable} iterable The iterable
 *     object.
 * @param {VALUE} defaultValue The value to return if the iterator is empty.
 * @return {VALUE} The next item in the iteration, or defaultValue if the
 *     iterator was empty.
 * @template VALUE
 */
goog.iter.nextOrValue = function(iterable, defaultValue) {
  try {
    return goog.iter.toIterator(iterable).next();
  } catch (e) {
    if (e != goog.iter.StopIteration) {
      throw e;
    }
    return defaultValue;
  }
};


/**
 * Cartesian product of zero or more sets.  Gives an iterator that gives every
 * combination of one element chosen from each set.  For example,
 * ([1, 2], [3, 4]) gives ([1, 3], [1, 4], [2, 3], [2, 4]).
 * @see http://docs.python.org/library/itertools.html#itertools.product
 * @param {...!goog.array.ArrayLike<VALUE>} var_args Zero or more sets, as
 *     arrays.
 * @return {!goog.iter.Iterator<!Array<VALUE>>} An iterator that gives each
 *     n-tuple (as an array).
 * @template VALUE
 */
goog.iter.product = function(var_args) {
  var someArrayEmpty = goog.array.some(arguments, function(arr) {
    return !arr.length;
  });

  // An empty set in a cartesian product gives an empty set.
  if (someArrayEmpty || !arguments.length) {
    return new goog.iter.Iterator();
  }

  var iter = new goog.iter.Iterator();
  var arrays = arguments;

  // The first indices are [0, 0, ...]
  var indicies = goog.array.repeat(0, arrays.length);

  iter.next = function() {

    if (indicies) {
      var retVal = goog.array.map(indicies, function(valueIndex, arrayIndex) {
        return arrays[arrayIndex][valueIndex];
      });

      // Generate the next-largest indices for the next call.
      // Increase the rightmost index. If it goes over, increase the next
      // rightmost (like carry-over addition).
      for (var i = indicies.length - 1; i >= 0; i--) {
        // Assertion prevents compiler warning below.
        goog.asserts.assert(indicies);
        if (indicies[i] < arrays[i].length - 1) {
          indicies[i]++;
          break;
        }

        // We're at the last indices (the last element of every array), so
        // the iteration is over on the next call.
        if (i == 0) {
          indicies = null;
          break;
        }
        // Reset the index in this column and loop back to increment the
        // next one.
        indicies[i] = 0;
      }
      return retVal;
    }

    throw goog.iter.StopIteration;
  };

  return iter;
};


/**
 * Create an iterator to cycle over the iterable's elements indefinitely.
 * For example, ([1, 2, 3]) would return : 1, 2, 3, 1, 2, 3, ...
 * @see: http://docs.python.org/library/itertools.html#itertools.cycle.
 * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
 *     iterable object.
 * @return {!goog.iter.Iterator<VALUE>} An iterator that iterates indefinitely
 *     over the values in {@code iterable}.
 * @template VALUE
 */
goog.iter.cycle = function(iterable) {
  var baseIterator = goog.iter.toIterator(iterable);

  // We maintain a cache to store the iterable elements as we iterate
  // over them. The cache is used to return elements once we have
  // iterated over the iterable once.
  var cache = [];
  var cacheIndex = 0;

  var iter = new goog.iter.Iterator();

  // This flag is set after the iterable is iterated over once
  var useCache = false;

  iter.next = function() {
    var returnElement = null;

    // Pull elements off the original iterator if not using cache
    if (!useCache) {
      try {
        // Return the element from the iterable
        returnElement = baseIterator.next();
        cache.push(returnElement);
        return returnElement;
      } catch (e) {
        // If an exception other than StopIteration is thrown
        // or if there are no elements to iterate over (the iterable was empty)
        // throw an exception
        if (e != goog.iter.StopIteration || goog.array.isEmpty(cache)) {
          throw e;
        }
        // set useCache to true after we know that a 'StopIteration' exception
        // was thrown and the cache is not empty (to handle the 'empty iterable'
        // use case)
        useCache = true;
      }
    }

    returnElement = cache[cacheIndex];
    cacheIndex = (cacheIndex + 1) % cache.length;

    return returnElement;
  };

  return iter;
};


/**
 * Creates an iterator that counts indefinitely from a starting value.
 * @see http://docs.python.org/2/library/itertools.html#itertools.count
 * @param {number=} opt_start The starting value. Default is 0.
 * @param {number=} opt_step The number to increment with between each call to
 *     next. Negative and floating point numbers are allowed. Default is 1.
 * @return {!goog.iter.Iterator<number>} A new iterator that returns the values
 *     in the series.
 */
goog.iter.count = function(opt_start, opt_step) {
  var counter = opt_start || 0;
  var step = goog.isDef(opt_step) ? opt_step : 1;
  var iter = new goog.iter.Iterator();

  iter.next = function() {
    var returnValue = counter;
    counter += step;
    return returnValue;
  };

  return iter;
};


/**
 * Creates an iterator that returns the same object or value repeatedly.
 * @param {VALUE} value Any object or value to repeat.
 * @return {!goog.iter.Iterator<VALUE>} A new iterator that returns the
 *     repeated value.
 * @template VALUE
 */
goog.iter.repeat = function(value) {
  var iter = new goog.iter.Iterator();

  iter.next = goog.functions.constant(value);

  return iter;
};


/**
 * Creates an iterator that returns running totals from the numbers in
 * {@code iterable}. For example, the array {@code [1, 2, 3, 4, 5]} yields
 * {@code 1 -> 3 -> 6 -> 10 -> 15}.
 * @see http://docs.python.org/3.2/library/itertools.html#itertools.accumulate
 * @param {!goog.iter.Iterable<number>} iterable The iterable of numbers to
 *     accumulate.
 * @return {!goog.iter.Iterator<number>} A new iterator that returns the
 *     numbers in the series.
 */
goog.iter.accumulate = function(iterable) {
  var iterator = goog.iter.toIterator(iterable);
  var total = 0;
  var iter = new goog.iter.Iterator();

  iter.next = function() {
    total += iterator.next();
    return total;
  };

  return iter;
};


/**
 * Creates an iterator that returns arrays containing the ith elements from the
 * provided iterables. The returned arrays will be the same size as the number
 * of iterables given in {@code var_args}. Once the shortest iterable is
 * exhausted, subsequent calls to {@code next()} will throw
 * {@code goog.iter.StopIteration}.
 * @see http://docs.python.org/2/library/itertools.html#itertools.izip
 * @param {...!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} var_args Any
 *     number of iterable objects.
 * @return {!goog.iter.Iterator<!Array<VALUE>>} A new iterator that returns
 *     arrays of elements from the provided iterables.
 * @template VALUE
 */
goog.iter.zip = function(var_args) {
  var args = arguments;
  var iter = new goog.iter.Iterator();

  if (args.length > 0) {
    var iterators = goog.array.map(args, goog.iter.toIterator);
    iter.next = function() {
      var arr = goog.array.map(iterators, function(it) {
        return it.next();
      });
      return arr;
    };
  }

  return iter;
};


/**
 * Creates an iterator that returns arrays containing the ith elements from the
 * provided iterables. The returned arrays will be the same size as the number
 * of iterables given in {@code var_args}. Shorter iterables will be extended
 * with {@code fillValue}. Once the longest iterable is exhausted, subsequent
 * calls to {@code next()} will throw {@code goog.iter.StopIteration}.
 * @see http://docs.python.org/2/library/itertools.html#itertools.izip_longest
 * @param {VALUE} fillValue The object or value used to fill shorter iterables.
 * @param {...!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} var_args Any
 *     number of iterable objects.
 * @return {!goog.iter.Iterator<!Array<VALUE>>} A new iterator that returns
 *     arrays of elements from the provided iterables.
 * @template VALUE
 */
goog.iter.zipLongest = function(fillValue, var_args) {
  var args = goog.array.slice(arguments, 1);
  var iter = new goog.iter.Iterator();

  if (args.length > 0) {
    var iterators = goog.array.map(args, goog.iter.toIterator);

    iter.next = function() {
      var iteratorsHaveValues = false;  // false when all iterators are empty.
      var arr = goog.array.map(iterators, function(it) {
        var returnValue;
        try {
          returnValue = it.next();
          // Iterator had a value, so we've not exhausted the iterators.
          // Set flag accordingly.
          iteratorsHaveValues = true;
        } catch (ex) {
          if (ex !== goog.iter.StopIteration) {
            throw ex;
          }
          returnValue = fillValue;
        }
        return returnValue;
      });

      if (!iteratorsHaveValues) {
        throw goog.iter.StopIteration;
      }
      return arr;
    };
  }

  return iter;
};


/**
 * Creates an iterator that filters {@code iterable} based on a series of
 * {@code selectors}. On each call to {@code next()}, one item is taken from
 * both the {@code iterable} and {@code selectors} iterators. If the item from
 * {@code selectors} evaluates to true, the item from {@code iterable} is given.
 * Otherwise, it is skipped. Once either {@code iterable} or {@code selectors}
 * is exhausted, subsequent calls to {@code next()} will throw
 * {@code goog.iter.StopIteration}.
 * @see http://docs.python.org/2/library/itertools.html#itertools.compress
 * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
 *     iterable to filter.
 * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} selectors An
 *     iterable of items to be evaluated in a boolean context to determine if
 *     the corresponding element in {@code iterable} should be included in the
 *     result.
 * @return {!goog.iter.Iterator<VALUE>} A new iterator that returns the
 *     filtered values.
 * @template VALUE
 */
goog.iter.compress = function(iterable, selectors) {
  var selectorIterator = goog.iter.toIterator(selectors);

  return goog.iter.filter(iterable, function() {
    return !!selectorIterator.next();
  });
};



/**
 * Implements the {@code goog.iter.groupBy} iterator.
 * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
 *     iterable to group.
 * @param {function(...VALUE): KEY=} opt_keyFunc  Optional function for
 *     determining the key value for each group in the {@code iterable}. Default
 *     is the identity function.
 * @constructor
 * @extends {goog.iter.Iterator<!Array<?>>}
 * @template KEY, VALUE
 * @private
 */
goog.iter.GroupByIterator_ = function(iterable, opt_keyFunc) {

  /**
   * The iterable to group, coerced to an iterator.
   * @type {!goog.iter.Iterator}
   */
  this.iterator = goog.iter.toIterator(iterable);

  /**
   * A function for determining the key value for each element in the iterable.
   * If no function is provided, the identity function is used and returns the
   * element unchanged.
   * @type {function(...VALUE): KEY}
   */
  this.keyFunc = opt_keyFunc || goog.functions.identity;

  /**
   * The target key for determining the start of a group.
   * @type {KEY}
   */
  this.targetKey;

  /**
   * The current key visited during iteration.
   * @type {KEY}
   */
  this.currentKey;

  /**
   * The current value being added to the group.
   * @type {VALUE}
   */
  this.currentValue;
};
goog.inherits(goog.iter.GroupByIterator_, goog.iter.Iterator);


/** @override */
goog.iter.GroupByIterator_.prototype.next = function() {
  while (this.currentKey == this.targetKey) {
    this.currentValue = this.iterator.next();  // Exits on StopIteration
    this.currentKey = this.keyFunc(this.currentValue);
  }
  this.targetKey = this.currentKey;
  return [this.currentKey, this.groupItems_(this.targetKey)];
};


/**
 * Performs the grouping of objects using the given key.
 * @param {KEY} targetKey  The target key object for the group.
 * @return {!Array<VALUE>} An array of grouped objects.
 * @private
 */
goog.iter.GroupByIterator_.prototype.groupItems_ = function(targetKey) {
  var arr = [];
  while (this.currentKey == targetKey) {
    arr.push(this.currentValue);
    try {
      this.currentValue = this.iterator.next();
    } catch (ex) {
      if (ex !== goog.iter.StopIteration) {
        throw ex;
      }
      break;
    }
    this.currentKey = this.keyFunc(this.currentValue);
  }
  return arr;
};


/**
 * Creates an iterator that returns arrays containing elements from the
 * {@code iterable} grouped by a key value. For iterables with repeated
 * elements (i.e. sorted according to a particular key function), this function
 * has a {@code uniq}-like effect. For example, grouping the array:
 * {@code [A, B, B, C, C, A]} produces
 * {@code [A, [A]], [B, [B, B]], [C, [C, C]], [A, [A]]}.
 * @see http://docs.python.org/2/library/itertools.html#itertools.groupby
 * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
 *     iterable to group.
 * @param {function(...VALUE): KEY=} opt_keyFunc  Optional function for
 *     determining the key value for each group in the {@code iterable}. Default
 *     is the identity function.
 * @return {!goog.iter.Iterator<!Array<?>>} A new iterator that returns
 *     arrays of consecutive key and groups.
 * @template KEY, VALUE
 */
goog.iter.groupBy = function(iterable, opt_keyFunc) {
  return new goog.iter.GroupByIterator_(iterable, opt_keyFunc);
};


/**
 * Gives an iterator that gives the result of calling the given function
 * <code>f</code> with the arguments taken from the next element from
 * <code>iterable</code> (the elements are expected to also be iterables).
 *
 * Similar to {@see goog.iter#map} but allows the function to accept multiple
 * arguments from the iterable.
 *
 * @param {!goog.iter.Iterable<!goog.iter.Iterable>} iterable The iterable of
 *     iterables to iterate over.
 * @param {function(this:THIS,...*):RESULT} f The function to call for every
 *     element.  This function takes N+2 arguments, where N represents the
 *     number of items from the next element of the iterable. The two
 *     additional arguments passed to the function are undefined and the
 *     iterator itself. The function should return a new value.
 * @param {THIS=} opt_obj The object to be used as the value of 'this' within
 *     {@code f}.
 * @return {!goog.iter.Iterator<RESULT>} A new iterator that returns the
 *     results of applying the function to each element in the original
 *     iterator.
 * @template THIS, RESULT
 */
goog.iter.starMap = function(iterable, f, opt_obj) {
  var iterator = goog.iter.toIterator(iterable);
  var iter = new goog.iter.Iterator();

  iter.next = function() {
    var args = goog.iter.toArray(iterator.next());
    return f.apply(opt_obj, goog.array.concat(args, undefined, iterator));
  };

  return iter;
};


/**
 * Returns an array of iterators each of which can iterate over the values in
 * {@code iterable} without advancing the others.
 * @see http://docs.python.org/2/library/itertools.html#itertools.tee
 * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
 *     iterable to tee.
 * @param {number=} opt_num  The number of iterators to create. Default is 2.
 * @return {!Array<goog.iter.Iterator<VALUE>>} An array of iterators.
 * @template VALUE
 */
goog.iter.tee = function(iterable, opt_num) {
  var iterator = goog.iter.toIterator(iterable);
  var num = goog.isNumber(opt_num) ? opt_num : 2;
  var buffers = goog.array.map(goog.array.range(num), function() {
    return [];
  });

  var addNextIteratorValueToBuffers = function() {
    var val = iterator.next();
    goog.array.forEach(buffers, function(buffer) {
      buffer.push(val);
    });
  };

  var createIterator = function(buffer) {
    // Each tee'd iterator has an associated buffer (initially empty). When a
    // tee'd iterator's buffer is empty, it calls
    // addNextIteratorValueToBuffers(), adding the next value to all tee'd
    // iterators' buffers, and then returns that value. This allows each
    // iterator to be advanced independently.
    var iter = new goog.iter.Iterator();

    iter.next = function() {
      if (goog.array.isEmpty(buffer)) {
        addNextIteratorValueToBuffers();
      }
      goog.asserts.assert(!goog.array.isEmpty(buffer));
      return buffer.shift();
    };

    return iter;
  };

  return goog.array.map(buffers, createIterator);
};


/**
 * Creates an iterator that returns arrays containing a count and an element
 * obtained from the given {@code iterable}.
 * @see http://docs.python.org/2/library/functions.html#enumerate
 * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
 *     iterable to enumerate.
 * @param {number=} opt_start  Optional starting value. Default is 0.
 * @return {!goog.iter.Iterator<!Array<?>>} A new iterator containing
 *     count/item pairs.
 * @template VALUE
 */
goog.iter.enumerate = function(iterable, opt_start) {
  return goog.iter.zip(goog.iter.count(opt_start), iterable);
};


/**
 * Creates an iterator that returns the first {@code limitSize} elements from an
 * iterable. If this number is greater than the number of elements in the
 * iterable, all the elements are returned.
 * @see http://goo.gl/V0sihp Inspired by the limit iterator in Guava.
 * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
 *     iterable to limit.
 * @param {number} limitSize  The maximum number of elements to return.
 * @return {!goog.iter.Iterator<VALUE>} A new iterator containing
 *     {@code limitSize} elements.
 * @template VALUE
 */
goog.iter.limit = function(iterable, limitSize) {
  goog.asserts.assert(goog.math.isInt(limitSize) && limitSize >= 0);

  var iterator = goog.iter.toIterator(iterable);

  var iter = new goog.iter.Iterator();
  var remaining = limitSize;

  iter.next = function() {
    if (remaining-- > 0) {
      return iterator.next();
    }
    throw goog.iter.StopIteration;
  };

  return iter;
};


/**
 * Creates an iterator that is advanced {@code count} steps ahead. Consumed
 * values are silently discarded. If {@code count} is greater than the number
 * of elements in {@code iterable}, an empty iterator is returned. Subsequent
 * calls to {@code next()} will throw {@code goog.iter.StopIteration}.
 * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
 *     iterable to consume.
 * @param {number} count  The number of elements to consume from the iterator.
 * @return {!goog.iter.Iterator<VALUE>} An iterator advanced zero or more steps
 *     ahead.
 * @template VALUE
 */
goog.iter.consume = function(iterable, count) {
  goog.asserts.assert(goog.math.isInt(count) && count >= 0);

  var iterator = goog.iter.toIterator(iterable);

  while (count-- > 0) {
    goog.iter.nextOrValue(iterator, null);
  }

  return iterator;
};


/**
 * Creates an iterator that returns a range of elements from an iterable.
 * Similar to {@see goog.array#slice} but does not support negative indexes.
 * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
 *     iterable to slice.
 * @param {number} start  The index of the first element to return.
 * @param {number=} opt_end  The index after the last element to return. If
 *     defined, must be greater than or equal to {@code start}.
 * @return {!goog.iter.Iterator<VALUE>} A new iterator containing a slice of
 *     the original.
 * @template VALUE
 */
goog.iter.slice = function(iterable, start, opt_end) {
  goog.asserts.assert(goog.math.isInt(start) && start >= 0);

  var iterator = goog.iter.consume(iterable, start);

  if (goog.isNumber(opt_end)) {
    goog.asserts.assert(goog.math.isInt(opt_end) && opt_end >= start);
    iterator = goog.iter.limit(iterator, opt_end - start /* limitSize */);
  }

  return iterator;
};


/**
 * Checks an array for duplicate elements.
 * @param {Array<VALUE>|goog.array.ArrayLike} arr The array to check for
 *     duplicates.
 * @return {boolean} True, if the array contains duplicates, false otherwise.
 * @private
 * @template VALUE
 */
// TODO(user): Consider moving this into goog.array as a public function.
goog.iter.hasDuplicates_ = function(arr) {
  var deduped = [];
  goog.array.removeDuplicates(arr, deduped);
  return arr.length != deduped.length;
};


/**
 * Creates an iterator that returns permutations of elements in
 * {@code iterable}.
 *
 * Permutations are obtained by taking the Cartesian product of
 * {@code opt_length} iterables and filtering out those with repeated
 * elements. For example, the permutations of {@code [1,2,3]} are
 * {@code [[1,2,3], [1,3,2], [2,1,3], [2,3,1], [3,1,2], [3,2,1]]}.
 * @see http://docs.python.org/2/library/itertools.html#itertools.permutations
 * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
 *     iterable from which to generate permutations.
 * @param {number=} opt_length Length of each permutation. If omitted, defaults
 *     to the length of {@code iterable}.
 * @return {!goog.iter.Iterator<!Array<VALUE>>} A new iterator containing the
 *     permutations of {@code iterable}.
 * @template VALUE
 */
goog.iter.permutations = function(iterable, opt_length) {
  var elements = goog.iter.toArray(iterable);
  var length = goog.isNumber(opt_length) ? opt_length : elements.length;

  var sets = goog.array.repeat(elements, length);
  var product = goog.iter.product.apply(undefined, sets);

  return goog.iter.filter(product, function(arr) {
    return !goog.iter.hasDuplicates_(arr);
  });
};


/**
 * Creates an iterator that returns combinations of elements from
 * {@code iterable}.
 *
 * Combinations are obtained by taking the {@see goog.iter#permutations} of
 * {@code iterable} and filtering those whose elements appear in the order they
 * are encountered in {@code iterable}. For example, the 3-length combinations
 * of {@code [0,1,2,3]} are {@code [[0,1,2], [0,1,3], [0,2,3], [1,2,3]]}.
 * @see http://docs.python.org/2/library/itertools.html#itertools.combinations
 * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
 *     iterable from which to generate combinations.
 * @param {number} length The length of each combination.
 * @return {!goog.iter.Iterator<!Array<VALUE>>} A new iterator containing
 *     combinations from the {@code iterable}.
 * @template VALUE
 */
goog.iter.combinations = function(iterable, length) {
  var elements = goog.iter.toArray(iterable);
  var indexes = goog.iter.range(elements.length);
  var indexIterator = goog.iter.permutations(indexes, length);
  // sortedIndexIterator will now give arrays of with the given length that
  // indicate what indexes into "elements" should be returned on each iteration.
  var sortedIndexIterator = goog.iter.filter(indexIterator, function(arr) {
    return goog.array.isSorted(arr);
  });

  var iter = new goog.iter.Iterator();

  function getIndexFromElements(index) {
    return elements[index];
  }

  iter.next = function() {
    return goog.array.map(sortedIndexIterator.next(), getIndexFromElements);
  };

  return iter;
};


/**
 * Creates an iterator that returns combinations of elements from
 * {@code iterable}, with repeated elements possible.
 *
 * Combinations are obtained by taking the Cartesian product of {@code length}
 * iterables and filtering those whose elements appear in the order they are
 * encountered in {@code iterable}. For example, the 2-length combinations of
 * {@code [1,2,3]} are {@code [[1,1], [1,2], [1,3], [2,2], [2,3], [3,3]]}.
 * @see http://docs.python.org/2/library/itertools.html#itertools.combinations_with_replacement
 * @see http://en.wikipedia.org/wiki/Combination#Number_of_combinations_with_repetition
 * @param {!goog.iter.Iterator<VALUE>|!goog.iter.Iterable} iterable The
 *     iterable to combine.
 * @param {number} length The length of each combination.
 * @return {!goog.iter.Iterator<!Array<VALUE>>} A new iterator containing
 *     combinations from the {@code iterable}.
 * @template VALUE
 */
goog.iter.combinationsWithReplacement = function(iterable, length) {
  var elements = goog.iter.toArray(iterable);
  var indexes = goog.array.range(elements.length);
  var sets = goog.array.repeat(indexes, length);
  var indexIterator = goog.iter.product.apply(undefined, sets);
  // sortedIndexIterator will now give arrays of with the given length that
  // indicate what indexes into "elements" should be returned on each iteration.
  var sortedIndexIterator = goog.iter.filter(indexIterator, function(arr) {
    return goog.array.isSorted(arr);
  });

  var iter = new goog.iter.Iterator();

  function getIndexFromElements(index) {
    return elements[index];
  }

  iter.next = function() {
    return goog.array.map(
        /** @type {!Array<number>} */
        (sortedIndexIterator.next()), getIndexFromElements);
  };

  return iter;
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Datastructure: Hash Map.
 *
 * @author arv@google.com (Erik Arvidsson)
 *
 * This file contains an implementation of a Map structure. It implements a lot
 * of the methods used in goog.structs so those functions work on hashes. This
 * is best suited for complex key types. For simple keys such as numbers and
 * strings consider using the lighter-weight utilities in goog.object.
 */


goog.provide('goog.structs.Map');

goog.require('goog.iter.Iterator');
goog.require('goog.iter.StopIteration');
goog.require('goog.object');



/**
 * Class for Hash Map datastructure.
 * @param {*=} opt_map Map or Object to initialize the map with.
 * @param {...*} var_args If 2 or more arguments are present then they
 *     will be used as key-value pairs.
 * @constructor
 * @template K, V
 */
goog.structs.Map = function(opt_map, var_args) {

  /**
   * Underlying JS object used to implement the map.
   * @private {!Object}
   */
  this.map_ = {};

  /**
   * An array of keys. This is necessary for two reasons:
   *   1. Iterating the keys using for (var key in this.map_) allocates an
   *      object for every key in IE which is really bad for IE6 GC perf.
   *   2. Without a side data structure, we would need to escape all the keys
   *      as that would be the only way we could tell during iteration if the
   *      key was an internal key or a property of the object.
   *
   * This array can contain deleted keys so it's necessary to check the map
   * as well to see if the key is still in the map (this doesn't require a
   * memory allocation in IE).
   * @private {!Array<string>}
   */
  this.keys_ = [];

  /**
   * The number of key value pairs in the map.
   * @private {number}
   */
  this.count_ = 0;

  /**
   * Version used to detect changes while iterating.
   * @private {number}
   */
  this.version_ = 0;

  var argLength = arguments.length;

  if (argLength > 1) {
    if (argLength % 2) {
      throw Error('Uneven number of arguments');
    }
    for (var i = 0; i < argLength; i += 2) {
      this.set(arguments[i], arguments[i + 1]);
    }
  } else if (opt_map) {
    this.addAll(/** @type {Object} */ (opt_map));
  }
};


/**
 * @return {number} The number of key-value pairs in the map.
 */
goog.structs.Map.prototype.getCount = function() {
  return this.count_;
};


/**
 * Returns the values of the map.
 * @return {!Array<V>} The values in the map.
 */
goog.structs.Map.prototype.getValues = function() {
  this.cleanupKeysArray_();

  var rv = [];
  for (var i = 0; i < this.keys_.length; i++) {
    var key = this.keys_[i];
    rv.push(this.map_[key]);
  }
  return rv;
};


/**
 * Returns the keys of the map.
 * @return {!Array<string>} Array of string values.
 */
goog.structs.Map.prototype.getKeys = function() {
  this.cleanupKeysArray_();
  return /** @type {!Array<string>} */ (this.keys_.concat());
};


/**
 * Whether the map contains the given key.
 * @param {*} key The key to check for.
 * @return {boolean} Whether the map contains the key.
 */
goog.structs.Map.prototype.containsKey = function(key) {
  return goog.structs.Map.hasKey_(this.map_, key);
};


/**
 * Whether the map contains the given value. This is O(n).
 * @param {V} val The value to check for.
 * @return {boolean} Whether the map contains the value.
 */
goog.structs.Map.prototype.containsValue = function(val) {
  for (var i = 0; i < this.keys_.length; i++) {
    var key = this.keys_[i];
    if (goog.structs.Map.hasKey_(this.map_, key) && this.map_[key] == val) {
      return true;
    }
  }
  return false;
};


/**
 * Whether this map is equal to the argument map.
 * @param {goog.structs.Map} otherMap The map against which to test equality.
 * @param {function(V, V): boolean=} opt_equalityFn Optional equality function
 *     to test equality of values. If not specified, this will test whether
 *     the values contained in each map are identical objects.
 * @return {boolean} Whether the maps are equal.
 */
goog.structs.Map.prototype.equals = function(otherMap, opt_equalityFn) {
  if (this === otherMap) {
    return true;
  }

  if (this.count_ != otherMap.getCount()) {
    return false;
  }

  var equalityFn = opt_equalityFn || goog.structs.Map.defaultEquals;

  this.cleanupKeysArray_();
  for (var key, i = 0; key = this.keys_[i]; i++) {
    if (!equalityFn(this.get(key), otherMap.get(key))) {
      return false;
    }
  }

  return true;
};


/**
 * Default equality test for values.
 * @param {*} a The first value.
 * @param {*} b The second value.
 * @return {boolean} Whether a and b reference the same object.
 */
goog.structs.Map.defaultEquals = function(a, b) {
  return a === b;
};


/**
 * @return {boolean} Whether the map is empty.
 */
goog.structs.Map.prototype.isEmpty = function() {
  return this.count_ == 0;
};


/**
 * Removes all key-value pairs from the map.
 */
goog.structs.Map.prototype.clear = function() {
  this.map_ = {};
  this.keys_.length = 0;
  this.count_ = 0;
  this.version_ = 0;
};


/**
 * Removes a key-value pair based on the key. This is O(logN) amortized due to
 * updating the keys array whenever the count becomes half the size of the keys
 * in the keys array.
 * @param {*} key  The key to remove.
 * @return {boolean} Whether object was removed.
 */
goog.structs.Map.prototype.remove = function(key) {
  if (goog.structs.Map.hasKey_(this.map_, key)) {
    delete this.map_[key];
    this.count_--;
    this.version_++;

    // clean up the keys array if the threshhold is hit
    if (this.keys_.length > 2 * this.count_) {
      this.cleanupKeysArray_();
    }

    return true;
  }
  return false;
};


/**
 * Cleans up the temp keys array by removing entries that are no longer in the
 * map.
 * @private
 */
goog.structs.Map.prototype.cleanupKeysArray_ = function() {
  if (this.count_ != this.keys_.length) {
    // First remove keys that are no longer in the map.
    var srcIndex = 0;
    var destIndex = 0;
    while (srcIndex < this.keys_.length) {
      var key = this.keys_[srcIndex];
      if (goog.structs.Map.hasKey_(this.map_, key)) {
        this.keys_[destIndex++] = key;
      }
      srcIndex++;
    }
    this.keys_.length = destIndex;
  }

  if (this.count_ != this.keys_.length) {
    // If the count still isn't correct, that means we have duplicates. This can
    // happen when the same key is added and removed multiple times. Now we have
    // to allocate one extra Object to remove the duplicates. This could have
    // been done in the first pass, but in the common case, we can avoid
    // allocating an extra object by only doing this when necessary.
    var seen = {};
    var srcIndex = 0;
    var destIndex = 0;
    while (srcIndex < this.keys_.length) {
      var key = this.keys_[srcIndex];
      if (!(goog.structs.Map.hasKey_(seen, key))) {
        this.keys_[destIndex++] = key;
        seen[key] = 1;
      }
      srcIndex++;
    }
    this.keys_.length = destIndex;
  }
};


/**
 * Returns the value for the given key.  If the key is not found and the default
 * value is not given this will return {@code undefined}.
 * @param {*} key The key to get the value for.
 * @param {DEFAULT=} opt_val The value to return if no item is found for the
 *     given key, defaults to undefined.
 * @return {V|DEFAULT} The value for the given key.
 * @template DEFAULT
 */
goog.structs.Map.prototype.get = function(key, opt_val) {
  if (goog.structs.Map.hasKey_(this.map_, key)) {
    return this.map_[key];
  }
  return opt_val;
};


/**
 * Adds a key-value pair to the map.
 * @param {*} key The key.
 * @param {V} value The value to add.
 * @return {*} Some subclasses return a value.
 */
goog.structs.Map.prototype.set = function(key, value) {
  if (!(goog.structs.Map.hasKey_(this.map_, key))) {
    this.count_++;
    this.keys_.push(key);
    // Only change the version if we add a new key.
    this.version_++;
  }
  this.map_[key] = value;
};


/**
 * Adds multiple key-value pairs from another goog.structs.Map or Object.
 * @param {Object} map  Object containing the data to add.
 */
goog.structs.Map.prototype.addAll = function(map) {
  var keys, values;
  if (map instanceof goog.structs.Map) {
    keys = map.getKeys();
    values = map.getValues();
  } else {
    keys = goog.object.getKeys(map);
    values = goog.object.getValues(map);
  }
  // we could use goog.array.forEach here but I don't want to introduce that
  // dependency just for this.
  for (var i = 0; i < keys.length; i++) {
    this.set(keys[i], values[i]);
  }
};


/**
 * Calls the given function on each entry in the map.
 * @param {function(this:T, V, K, goog.structs.Map<K,V>)} f
 * @param {T=} opt_obj The value of "this" inside f.
 * @template T
 */
goog.structs.Map.prototype.forEach = function(f, opt_obj) {
  var keys = this.getKeys();
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    var value = this.get(key);
    f.call(opt_obj, value, key, this);
  }
};


/**
 * Clones a map and returns a new map.
 * @return {!goog.structs.Map} A new map with the same key-value pairs.
 */
goog.structs.Map.prototype.clone = function() {
  return new goog.structs.Map(this);
};


/**
 * Returns a new map in which all the keys and values are interchanged
 * (keys become values and values become keys). If multiple keys map to the
 * same value, the chosen transposed value is implementation-dependent.
 *
 * It acts very similarly to {goog.object.transpose(Object)}.
 *
 * @return {!goog.structs.Map} The transposed map.
 */
goog.structs.Map.prototype.transpose = function() {
  var transposed = new goog.structs.Map();
  for (var i = 0; i < this.keys_.length; i++) {
    var key = this.keys_[i];
    var value = this.map_[key];
    transposed.set(value, key);
  }

  return transposed;
};


/**
 * @return {!Object} Object representation of the map.
 */
goog.structs.Map.prototype.toObject = function() {
  this.cleanupKeysArray_();
  var obj = {};
  for (var i = 0; i < this.keys_.length; i++) {
    var key = this.keys_[i];
    obj[key] = this.map_[key];
  }
  return obj;
};


/**
 * Returns an iterator that iterates over the keys in the map.  Removal of keys
 * while iterating might have undesired side effects.
 * @return {!goog.iter.Iterator} An iterator over the keys in the map.
 */
goog.structs.Map.prototype.getKeyIterator = function() {
  return this.__iterator__(true);
};


/**
 * Returns an iterator that iterates over the values in the map.  Removal of
 * keys while iterating might have undesired side effects.
 * @return {!goog.iter.Iterator} An iterator over the values in the map.
 */
goog.structs.Map.prototype.getValueIterator = function() {
  return this.__iterator__(false);
};


/**
 * Returns an iterator that iterates over the values or the keys in the map.
 * This throws an exception if the map was mutated since the iterator was
 * created.
 * @param {boolean=} opt_keys True to iterate over the keys. False to iterate
 *     over the values.  The default value is false.
 * @return {!goog.iter.Iterator} An iterator over the values or keys in the map.
 */
goog.structs.Map.prototype.__iterator__ = function(opt_keys) {
  // Clean up keys to minimize the risk of iterating over dead keys.
  this.cleanupKeysArray_();

  var i = 0;
  var version = this.version_;
  var selfObj = this;

  var newIter = new goog.iter.Iterator;
  newIter.next = function() {
    if (version != selfObj.version_) {
      throw Error('The map has changed since the iterator was created');
    }
    if (i >= selfObj.keys_.length) {
      throw goog.iter.StopIteration;
    }
    var key = selfObj.keys_[i++];
    return opt_keys ? key : selfObj.map_[key];
  };
  return newIter;
};


/**
 * Safe way to test for hasOwnProperty.  It even allows testing for
 * 'hasOwnProperty'.
 * @param {Object} obj The object to test for presence of the given key.
 * @param {*} key The key to check for.
 * @return {boolean} Whether the object has the key.
 * @private
 */
goog.structs.Map.hasKey_ = function(obj, key) {
  return Object.prototype.hasOwnProperty.call(obj, key);
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Datastructure: Set.
 *
 * @author arv@google.com (Erik Arvidsson)
 *
 * This class implements a set data structure. Adding and removing is O(1). It
 * supports both object and primitive values. Be careful because you can add
 * both 1 and new Number(1), because these are not the same. You can even add
 * multiple new Number(1) because these are not equal.
 */


goog.provide('goog.structs.Set');

goog.require('goog.structs');
goog.require('goog.structs.Collection');
goog.require('goog.structs.Map');



/**
 * A set that can contain both primitives and objects.  Adding and removing
 * elements is O(1).  Primitives are treated as identical if they have the same
 * type and convert to the same string.  Objects are treated as identical only
 * if they are references to the same object.  WARNING: A goog.structs.Set can
 * contain both 1 and (new Number(1)), because they are not the same.  WARNING:
 * Adding (new Number(1)) twice will yield two distinct elements, because they
 * are two different objects.  WARNING: Any object that is added to a
 * goog.structs.Set will be modified!  Because goog.getUid() is used to
 * identify objects, every object in the set will be mutated.
 * @param {Array<T>|Object<?,T>=} opt_values Initial values to start with.
 * @constructor
 * @implements {goog.structs.Collection<T>}
 * @final
 * @template T
 */
goog.structs.Set = function(opt_values) {
  this.map_ = new goog.structs.Map;
  if (opt_values) {
    this.addAll(opt_values);
  }
};


/**
 * Obtains a unique key for an element of the set.  Primitives will yield the
 * same key if they have the same type and convert to the same string.  Object
 * references will yield the same key only if they refer to the same object.
 * @param {*} val Object or primitive value to get a key for.
 * @return {string} A unique key for this value/object.
 * @private
 */
goog.structs.Set.getKey_ = function(val) {
  var type = typeof val;
  if (type == 'object' && val || type == 'function') {
    return 'o' + goog.getUid(/** @type {Object} */ (val));
  } else {
    return type.substr(0, 1) + val;
  }
};


/**
 * @return {number} The number of elements in the set.
 * @override
 */
goog.structs.Set.prototype.getCount = function() {
  return this.map_.getCount();
};


/**
 * Add a primitive or an object to the set.
 * @param {T} element The primitive or object to add.
 * @override
 */
goog.structs.Set.prototype.add = function(element) {
  this.map_.set(goog.structs.Set.getKey_(element), element);
};


/**
 * Adds all the values in the given collection to this set.
 * @param {Array<T>|goog.structs.Collection<T>|Object<?,T>} col A collection
 *     containing the elements to add.
 */
goog.structs.Set.prototype.addAll = function(col) {
  var values = goog.structs.getValues(col);
  var l = values.length;
  for (var i = 0; i < l; i++) {
    this.add(values[i]);
  }
};


/**
 * Removes all values in the given collection from this set.
 * @param {Array<T>|goog.structs.Collection<T>|Object<?,T>} col A collection
 *     containing the elements to remove.
 */
goog.structs.Set.prototype.removeAll = function(col) {
  var values = goog.structs.getValues(col);
  var l = values.length;
  for (var i = 0; i < l; i++) {
    this.remove(values[i]);
  }
};


/**
 * Removes the given element from this set.
 * @param {T} element The primitive or object to remove.
 * @return {boolean} Whether the element was found and removed.
 * @override
 */
goog.structs.Set.prototype.remove = function(element) {
  return this.map_.remove(goog.structs.Set.getKey_(element));
};


/**
 * Removes all elements from this set.
 */
goog.structs.Set.prototype.clear = function() {
  this.map_.clear();
};


/**
 * Tests whether this set is empty.
 * @return {boolean} True if there are no elements in this set.
 */
goog.structs.Set.prototype.isEmpty = function() {
  return this.map_.isEmpty();
};


/**
 * Tests whether this set contains the given element.
 * @param {T} element The primitive or object to test for.
 * @return {boolean} True if this set contains the given element.
 * @override
 */
goog.structs.Set.prototype.contains = function(element) {
  return this.map_.containsKey(goog.structs.Set.getKey_(element));
};


/**
 * Tests whether this set contains all the values in a given collection.
 * Repeated elements in the collection are ignored, e.g.  (new
 * goog.structs.Set([1, 2])).containsAll([1, 1]) is True.
 * @param {goog.structs.Collection<T>|Object} col A collection-like object.
 * @return {boolean} True if the set contains all elements.
 */
goog.structs.Set.prototype.containsAll = function(col) {
  return goog.structs.every(col, this.contains, this);
};


/**
 * Finds all values that are present in both this set and the given collection.
 * @param {Array<S>|Object<?,S>} col A collection.
 * @return {!goog.structs.Set<T|S>} A new set containing all the values
 *     (primitives or objects) present in both this set and the given
 *     collection.
 * @template S
 */
goog.structs.Set.prototype.intersection = function(col) {
  var result = new goog.structs.Set();

  var values = goog.structs.getValues(col);
  for (var i = 0; i < values.length; i++) {
    var value = values[i];
    if (this.contains(value)) {
      result.add(value);
    }
  }

  return result;
};


/**
 * Finds all values that are present in this set and not in the given
 * collection.
 * @param {Array<T>|goog.structs.Collection<T>|Object<?,T>} col A collection.
 * @return {!goog.structs.Set} A new set containing all the values
 *     (primitives or objects) present in this set but not in the given
 *     collection.
 */
goog.structs.Set.prototype.difference = function(col) {
  var result = this.clone();
  result.removeAll(col);
  return result;
};


/**
 * Returns an array containing all the elements in this set.
 * @return {!Array<T>} An array containing all the elements in this set.
 */
goog.structs.Set.prototype.getValues = function() {
  return this.map_.getValues();
};


/**
 * Creates a shallow clone of this set.
 * @return {!goog.structs.Set<T>} A new set containing all the same elements as
 *     this set.
 */
goog.structs.Set.prototype.clone = function() {
  return new goog.structs.Set(this);
};


/**
 * Tests whether the given collection consists of the same elements as this set,
 * regardless of order, without repetition.  Primitives are treated as equal if
 * they have the same type and convert to the same string; objects are treated
 * as equal if they are references to the same object.  This operation is O(n).
 * @param {goog.structs.Collection<T>|Object} col A collection.
 * @return {boolean} True if the given collection consists of the same elements
 *     as this set, regardless of order, without repetition.
 */
goog.structs.Set.prototype.equals = function(col) {
  return this.getCount() == goog.structs.getCount(col) && this.isSubsetOf(col);
};


/**
 * Tests whether the given collection contains all the elements in this set.
 * Primitives are treated as equal if they have the same type and convert to the
 * same string; objects are treated as equal if they are references to the same
 * object.  This operation is O(n).
 * @param {goog.structs.Collection<T>|Object} col A collection.
 * @return {boolean} True if this set is a subset of the given collection.
 */
goog.structs.Set.prototype.isSubsetOf = function(col) {
  var colCount = goog.structs.getCount(col);
  if (this.getCount() > colCount) {
    return false;
  }
  // TODO(user) Find the minimal collection size where the conversion makes
  // the contains() method faster.
  if (!(col instanceof goog.structs.Set) && colCount > 5) {
    // Convert to a goog.structs.Set so that goog.structs.contains runs in
    // O(1) time instead of O(n) time.
    col = new goog.structs.Set(col);
  }
  return goog.structs.every(this, function(value) {
    return goog.structs.contains(col, value);
  });
};


/**
 * Returns an iterator that iterates over the elements in this set.
 * @param {boolean=} opt_keys This argument is ignored.
 * @return {!goog.iter.Iterator} An iterator over the elements in this set.
 */
goog.structs.Set.prototype.__iterator__ = function(opt_keys) {
  return this.map_.__iterator__(false);
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Logging and debugging utilities.
 *
 * @see ../demos/debug.html
 */

goog.provide('goog.debug');

goog.require('goog.array');
goog.require('goog.html.SafeHtml');
goog.require('goog.html.SafeUrl');
goog.require('goog.html.uncheckedconversions');
goog.require('goog.string.Const');
goog.require('goog.structs.Set');
goog.require('goog.userAgent');


/** @define {boolean} Whether logging should be enabled. */
goog.define('goog.debug.LOGGING_ENABLED', goog.DEBUG);


/**
 * Catches onerror events fired by windows and similar objects.
 * @param {function(Object)} logFunc The function to call with the error
 *    information.
 * @param {boolean=} opt_cancel Whether to stop the error from reaching the
 *    browser.
 * @param {Object=} opt_target Object that fires onerror events.
 */
goog.debug.catchErrors = function(logFunc, opt_cancel, opt_target) {
  var target = opt_target || goog.global;
  var oldErrorHandler = target.onerror;
  var retVal = !!opt_cancel;

  // Chrome interprets onerror return value backwards (http://crbug.com/92062)
  // until it was fixed in webkit revision r94061 (Webkit 535.3). This
  // workaround still needs to be skipped in Safari after the webkit change
  // gets pushed out in Safari.
  // See https://bugs.webkit.org/show_bug.cgi?id=67119
  if (goog.userAgent.WEBKIT &&
      !goog.userAgent.isVersionOrHigher('535.3')) {
    retVal = !retVal;
  }

  /**
   * New onerror handler for this target. This onerror handler follows the spec
   * according to
   * http://www.whatwg.org/specs/web-apps/current-work/#runtime-script-errors
   * The spec was changed in August 2013 to support receiving column information
   * and an error object for all scripts on the same origin or cross origin
   * scripts with the proper headers. See
   * https://mikewest.org/2013/08/debugging-runtime-errors-with-window-onerror
   *
   * @param {string} message The error message. For cross-origin errors, this
   *     will be scrubbed to just "Script error.". For new browsers that have
   *     updated to follow the latest spec, errors that come from origins that
   *     have proper cross origin headers will not be scrubbed.
   * @param {string} url The URL of the script that caused the error. The URL
   *     will be scrubbed to "" for cross origin scripts unless the script has
   *     proper cross origin headers and the browser has updated to the latest
   *     spec.
   * @param {number} line The line number in the script that the error
   *     occurred on.
   * @param {number=} opt_col The optional column number that the error
   *     occurred on. Only browsers that have updated to the latest spec will
   *     include this.
   * @param {Error=} opt_error The optional actual error object for this
   *     error that should include the stack. Only browsers that have updated
   *     to the latest spec will inlude this parameter.
   * @return {boolean} Whether to prevent the error from reaching the browser.
   */
  target.onerror = function(message, url, line, opt_col, opt_error) {
    if (oldErrorHandler) {
      oldErrorHandler(message, url, line, opt_col, opt_error);
    }
    logFunc({
      message: message,
      fileName: url,
      line: line,
      col: opt_col,
      error: opt_error
    });
    return retVal;
  };
};


/**
 * Creates a string representing an object and all its properties.
 * @param {Object|null|undefined} obj Object to expose.
 * @param {boolean=} opt_showFn Show the functions as well as the properties,
 *     default is false.
 * @return {string} The string representation of {@code obj}.
 */
goog.debug.expose = function(obj, opt_showFn) {
  if (typeof obj == 'undefined') {
    return 'undefined';
  }
  if (obj == null) {
    return 'NULL';
  }
  var str = [];

  for (var x in obj) {
    if (!opt_showFn && goog.isFunction(obj[x])) {
      continue;
    }
    var s = x + ' = ';
    /** @preserveTry */
    try {
      s += obj[x];
    } catch (e) {
      s += '*** ' + e + ' ***';
    }
    str.push(s);
  }
  return str.join('\n');
};


/**
 * Creates a string representing a given primitive or object, and for an
 * object, all its properties and nested objects.  WARNING: If an object is
 * given, it and all its nested objects will be modified.  To detect reference
 * cycles, this method identifies objects using goog.getUid() which mutates the
 * object.
 * @param {*} obj Object to expose.
 * @param {boolean=} opt_showFn Also show properties that are functions (by
 *     default, functions are omitted).
 * @return {string} A string representation of {@code obj}.
 */
goog.debug.deepExpose = function(obj, opt_showFn) {
  var str = [];

  var helper = function(obj, space, parentSeen) {
    var nestspace = space + '  ';
    var seen = new goog.structs.Set(parentSeen);

    var indentMultiline = function(str) {
      return str.replace(/\n/g, '\n' + space);
    };

    /** @preserveTry */
    try {
      if (!goog.isDef(obj)) {
        str.push('undefined');
      } else if (goog.isNull(obj)) {
        str.push('NULL');
      } else if (goog.isString(obj)) {
        str.push('"' + indentMultiline(obj) + '"');
      } else if (goog.isFunction(obj)) {
        str.push(indentMultiline(String(obj)));
      } else if (goog.isObject(obj)) {
        if (seen.contains(obj)) {
          str.push('*** reference loop detected ***');
        } else {
          seen.add(obj);
          str.push('{');
          for (var x in obj) {
            if (!opt_showFn && goog.isFunction(obj[x])) {
              continue;
            }
            str.push('\n');
            str.push(nestspace);
            str.push(x + ' = ');
            helper(obj[x], nestspace, seen);
          }
          str.push('\n' + space + '}');
        }
      } else {
        str.push(obj);
      }
    } catch (e) {
      str.push('*** ' + e + ' ***');
    }
  };

  helper(obj, '', new goog.structs.Set());
  return str.join('');
};


/**
 * Recursively outputs a nested array as a string.
 * @param {Array<?>} arr The array.
 * @return {string} String representing nested array.
 */
goog.debug.exposeArray = function(arr) {
  var str = [];
  for (var i = 0; i < arr.length; i++) {
    if (goog.isArray(arr[i])) {
      str.push(goog.debug.exposeArray(arr[i]));
    } else {
      str.push(arr[i]);
    }
  }
  return '[ ' + str.join(', ') + ' ]';
};


/**
 * Exposes an exception that has been caught by a try...catch and outputs the
 * error as HTML with a stack trace.
 * @param {Object} err Error object or string.
 * @param {Function=} opt_fn Optional function to start stack trace from.
 * @return {string} Details of exception, as HTML.
 */
goog.debug.exposeException = function(err, opt_fn) {
  var html = goog.debug.exposeExceptionAsHtml(err, opt_fn);
  return goog.html.SafeHtml.unwrap(html);
};


/**
 * Exposes an exception that has been caught by a try...catch and outputs the
 * error with a stack trace.
 * @param {Object} err Error object or string.
 * @param {Function=} opt_fn Optional function to start stack trace from.
 * @return {!goog.html.SafeHtml} Details of exception.
 */
goog.debug.exposeExceptionAsHtml = function(err, opt_fn) {
  /** @preserveTry */
  try {
    var e = goog.debug.normalizeErrorObject(err);
    // Create the error message
    var viewSourceUrl = goog.debug.createViewSourceUrl_(e.fileName);
    var error = goog.html.SafeHtml.concat(
        goog.html.SafeHtml.htmlEscapePreservingNewlinesAndSpaces(
            'Message: ' + e.message + '\nUrl: '),
        goog.html.SafeHtml.create('a',
            {href: viewSourceUrl, target: '_new'}, e.fileName),
        goog.html.SafeHtml.htmlEscapePreservingNewlinesAndSpaces(
            '\nLine: ' + e.lineNumber + '\n\nBrowser stack:\n' +
            e.stack + '-> ' + '[end]\n\nJS stack traversal:\n' +
            goog.debug.getStacktrace(opt_fn) + '-> '));
    return error;
  } catch (e2) {
    return goog.html.SafeHtml.htmlEscapePreservingNewlinesAndSpaces(
        'Exception trying to expose exception! You win, we lose. ' + e2);
  }
};


/**
 * @param {?string=} opt_fileName
 * @return {!goog.html.SafeUrl} SafeUrl with view-source scheme, pointing at
 *     fileName.
 * @private
 */
goog.debug.createViewSourceUrl_ = function(opt_fileName) {
  if (!goog.isDefAndNotNull(opt_fileName)) {
    opt_fileName = '';
  }
  if (!/^https?:\/\//i.test(opt_fileName)) {
    return goog.html.SafeUrl.fromConstant(
        goog.string.Const.from('sanitizedviewsrc'));
  }
  var sanitizedFileName = goog.html.SafeUrl.sanitize(opt_fileName);
  return goog.html.uncheckedconversions.
      safeUrlFromStringKnownToSatisfyTypeContract(
          goog.string.Const.from('view-source scheme plus HTTP/HTTPS URL'),
          'view-source:' + goog.html.SafeUrl.unwrap(sanitizedFileName));
};


/**
 * Normalizes the error/exception object between browsers.
 * @param {Object} err Raw error object.
 * @return {!Object} Normalized error object.
 */
goog.debug.normalizeErrorObject = function(err) {
  var href = goog.getObjectByName('window.location.href');
  if (goog.isString(err)) {
    return {
      'message': err,
      'name': 'Unknown error',
      'lineNumber': 'Not available',
      'fileName': href,
      'stack': 'Not available'
    };
  }

  var lineNumber, fileName;
  var threwError = false;

  try {
    lineNumber = err.lineNumber || err.line || 'Not available';
  } catch (e) {
    // Firefox 2 sometimes throws an error when accessing 'lineNumber':
    // Message: Permission denied to get property UnnamedClass.lineNumber
    lineNumber = 'Not available';
    threwError = true;
  }

  try {
    fileName = err.fileName || err.filename || err.sourceURL ||
        // $googDebugFname may be set before a call to eval to set the filename
        // that the eval is supposed to present.
        goog.global['$googDebugFname'] || href;
  } catch (e) {
    // Firefox 2 may also throw an error when accessing 'filename'.
    fileName = 'Not available';
    threwError = true;
  }

  // The IE Error object contains only the name and the message.
  // The Safari Error object uses the line and sourceURL fields.
  if (threwError || !err.lineNumber || !err.fileName || !err.stack ||
      !err.message || !err.name) {
    return {
      'message': err.message || 'Not available',
      'name': err.name || 'UnknownError',
      'lineNumber': lineNumber,
      'fileName': fileName,
      'stack': err.stack || 'Not available'
    };
  }

  // Standards error object
  return err;
};


/**
 * Converts an object to an Error if it's a String,
 * adds a stacktrace if there isn't one,
 * and optionally adds an extra message.
 * @param {Error|string} err  the original thrown object or string.
 * @param {string=} opt_message  optional additional message to add to the
 *     error.
 * @return {!Error} If err is a string, it is used to create a new Error,
 *     which is enhanced and returned.  Otherwise err itself is enhanced
 *     and returned.
 */
goog.debug.enhanceError = function(err, opt_message) {
  var error;
  if (typeof err == 'string') {
    error = Error(err);
    if (Error.captureStackTrace) {
      // Trim this function off the call stack, if we can.
      Error.captureStackTrace(error, goog.debug.enhanceError);
    }
  } else {
    error = err;
  }

  if (!error.stack) {
    error.stack = goog.debug.getStacktrace(goog.debug.enhanceError);
  }
  if (opt_message) {
    // find the first unoccupied 'messageX' property
    var x = 0;
    while (error['message' + x]) {
      ++x;
    }
    error['message' + x] = String(opt_message);
  }
  return error;
};


/**
 * Gets the current stack trace. Simple and iterative - doesn't worry about
 * catching circular references or getting the args.
 * @param {number=} opt_depth Optional maximum depth to trace back to.
 * @return {string} A string with the function names of all functions in the
 *     stack, separated by \n.
 * @suppress {es5Strict}
 */
goog.debug.getStacktraceSimple = function(opt_depth) {
  if (goog.STRICT_MODE_COMPATIBLE) {
    var stack = goog.debug.getNativeStackTrace_(goog.debug.getStacktraceSimple);
    if (stack) {
      return stack;
    }
    // NOTE: browsers that have strict mode support also have native "stack"
    // properties.  Fall-through for legacy browser support.
  }

  var sb = [];
  var fn = arguments.callee.caller;
  var depth = 0;

  while (fn && (!opt_depth || depth < opt_depth)) {
    sb.push(goog.debug.getFunctionName(fn));
    sb.push('()\n');
    /** @preserveTry */
    try {
      fn = fn.caller;
    } catch (e) {
      sb.push('[exception trying to get caller]\n');
      break;
    }
    depth++;
    if (depth >= goog.debug.MAX_STACK_DEPTH) {
      sb.push('[...long stack...]');
      break;
    }
  }
  if (opt_depth && depth >= opt_depth) {
    sb.push('[...reached max depth limit...]');
  } else {
    sb.push('[end]');
  }

  return sb.join('');
};


/**
 * Max length of stack to try and output
 * @type {number}
 */
goog.debug.MAX_STACK_DEPTH = 50;


/**
 * @param {Function} fn The function to start getting the trace from.
 * @return {?string}
 * @private
 */
goog.debug.getNativeStackTrace_ = function(fn) {
  var tempErr = new Error();
  if (Error.captureStackTrace) {
    Error.captureStackTrace(tempErr, fn);
    return String(tempErr.stack);
  } else {
    // IE10, only adds stack traces when an exception is thrown.
    try {
      throw tempErr;
    } catch (e) {
      tempErr = e;
    }
    var stack = tempErr.stack;
    if (stack) {
      return String(stack);
    }
  }
  return null;
};


/**
 * Gets the current stack trace, either starting from the caller or starting
 * from a specified function that's currently on the call stack.
 * @param {Function=} opt_fn Optional function to start getting the trace from.
 *     If not provided, defaults to the function that called this.
 * @return {string} Stack trace.
 * @suppress {es5Strict}
 */
goog.debug.getStacktrace = function(opt_fn) {
  var stack;
  if (goog.STRICT_MODE_COMPATIBLE) {
    // Try to get the stack trace from the environment if it is available.
    var contextFn = opt_fn || goog.debug.getStacktrace;
    stack = goog.debug.getNativeStackTrace_(contextFn);
  }
  if (!stack) {
    // NOTE: browsers that have strict mode support also have native "stack"
    // properties. This function will throw in strict mode.
    stack = goog.debug.getStacktraceHelper_(
        opt_fn || arguments.callee.caller, []);
  }
  return stack;
};


/**
 * Private helper for getStacktrace().
 * @param {Function} fn Function to start getting the trace from.
 * @param {Array<!Function>} visited List of functions visited so far.
 * @return {string} Stack trace starting from function fn.
 * @suppress {es5Strict}
 * @private
 */
goog.debug.getStacktraceHelper_ = function(fn, visited) {
  var sb = [];

  // Circular reference, certain functions like bind seem to cause a recursive
  // loop so we need to catch circular references
  if (goog.array.contains(visited, fn)) {
    sb.push('[...circular reference...]');

  // Traverse the call stack until function not found or max depth is reached
  } else if (fn && visited.length < goog.debug.MAX_STACK_DEPTH) {
    sb.push(goog.debug.getFunctionName(fn) + '(');
    var args = fn.arguments;
    // Args may be null for some special functions such as host objects or eval.
    for (var i = 0; args && i < args.length; i++) {
      if (i > 0) {
        sb.push(', ');
      }
      var argDesc;
      var arg = args[i];
      switch (typeof arg) {
        case 'object':
          argDesc = arg ? 'object' : 'null';
          break;

        case 'string':
          argDesc = arg;
          break;

        case 'number':
          argDesc = String(arg);
          break;

        case 'boolean':
          argDesc = arg ? 'true' : 'false';
          break;

        case 'function':
          argDesc = goog.debug.getFunctionName(arg);
          argDesc = argDesc ? argDesc : '[fn]';
          break;

        case 'undefined':
        default:
          argDesc = typeof arg;
          break;
      }

      if (argDesc.length > 40) {
        argDesc = argDesc.substr(0, 40) + '...';
      }
      sb.push(argDesc);
    }
    visited.push(fn);
    sb.push(')\n');
    /** @preserveTry */
    try {
      sb.push(goog.debug.getStacktraceHelper_(fn.caller, visited));
    } catch (e) {
      sb.push('[exception trying to get caller]\n');
    }

  } else if (fn) {
    sb.push('[...long stack...]');
  } else {
    sb.push('[end]');
  }
  return sb.join('');
};


/**
 * Set a custom function name resolver.
 * @param {function(Function): string} resolver Resolves functions to their
 *     names.
 */
goog.debug.setFunctionResolver = function(resolver) {
  goog.debug.fnNameResolver_ = resolver;
};


/**
 * Gets a function name
 * @param {Function} fn Function to get name of.
 * @return {string} Function's name.
 */
goog.debug.getFunctionName = function(fn) {
  if (goog.debug.fnNameCache_[fn]) {
    return goog.debug.fnNameCache_[fn];
  }
  if (goog.debug.fnNameResolver_) {
    var name = goog.debug.fnNameResolver_(fn);
    if (name) {
      goog.debug.fnNameCache_[fn] = name;
      return name;
    }
  }

  // Heuristically determine function name based on code.
  var functionSource = String(fn);
  if (!goog.debug.fnNameCache_[functionSource]) {
    var matches = /function ([^\(]+)/.exec(functionSource);
    if (matches) {
      var method = matches[1];
      goog.debug.fnNameCache_[functionSource] = method;
    } else {
      goog.debug.fnNameCache_[functionSource] = '[Anonymous]';
    }
  }

  return goog.debug.fnNameCache_[functionSource];
};


/**
 * Makes whitespace visible by replacing it with printable characters.
 * This is useful in finding diffrences between the expected and the actual
 * output strings of a testcase.
 * @param {string} string whose whitespace needs to be made visible.
 * @return {string} string whose whitespace is made visible.
 */
goog.debug.makeWhitespaceVisible = function(string) {
  return string.replace(/ /g, '[_]')
      .replace(/\f/g, '[f]')
      .replace(/\n/g, '[n]\n')
      .replace(/\r/g, '[r]')
      .replace(/\t/g, '[t]');
};


/**
 * Returns the type of a value. If a constructor is passed, and a suitable
 * string cannot be found, 'unknown type name' will be returned.
 *
 * <p>Forked rather than moved from {@link goog.asserts.getType_}
 * to avoid adding a dependency to goog.asserts.
 * @param {*} value A constructor, object, or primitive.
 * @return {string} The best display name for the value, or 'unknown type name'.
 */
goog.debug.runtimeType = function(value) {
  if (value instanceof Function) {
    return value.displayName || value.name || 'unknown type name';
  } else if (value instanceof Object) {
    return value.constructor.displayName || value.constructor.name ||
        Object.prototype.toString.call(value);
  } else {
    return value === null ? 'null' : typeof value;
  }
};


/**
 * Hash map for storing function names that have already been looked up.
 * @type {Object}
 * @private
 */
goog.debug.fnNameCache_ = {};


/**
 * Resolves functions to their names.  Resolved function names will be cached.
 * @type {function(Function):string}
 * @private
 */
goog.debug.fnNameResolver_;

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Definition of the LogRecord class. Please minimize
 * dependencies this file has on other closure classes as any dependency it
 * takes won't be able to use the logging infrastructure.
 *
 */

goog.provide('goog.debug.LogRecord');



/**
 * LogRecord objects are used to pass logging requests between
 * the logging framework and individual log Handlers.
 * @constructor
 * @param {goog.debug.Logger.Level} level One of the level identifiers.
 * @param {string} msg The string message.
 * @param {string} loggerName The name of the source logger.
 * @param {number=} opt_time Time this log record was created if other than now.
 *     If 0, we use #goog.now.
 * @param {number=} opt_sequenceNumber Sequence number of this log record. This
 *     should only be passed in when restoring a log record from persistence.
 */
goog.debug.LogRecord = function(level, msg, loggerName,
    opt_time, opt_sequenceNumber) {
  this.reset(level, msg, loggerName, opt_time, opt_sequenceNumber);
};


/**
 * Time the LogRecord was created.
 * @type {number}
 * @private
 */
goog.debug.LogRecord.prototype.time_;


/**
 * Level of the LogRecord
 * @type {goog.debug.Logger.Level}
 * @private
 */
goog.debug.LogRecord.prototype.level_;


/**
 * Message associated with the record
 * @type {string}
 * @private
 */
goog.debug.LogRecord.prototype.msg_;


/**
 * Name of the logger that created the record.
 * @type {string}
 * @private
 */
goog.debug.LogRecord.prototype.loggerName_;


/**
 * Sequence number for the LogRecord. Each record has a unique sequence number
 * that is greater than all log records created before it.
 * @type {number}
 * @private
 */
goog.debug.LogRecord.prototype.sequenceNumber_ = 0;


/**
 * Exception associated with the record
 * @type {Object}
 * @private
 */
goog.debug.LogRecord.prototype.exception_ = null;


/**
 * @define {boolean} Whether to enable log sequence numbers.
 */
goog.define('goog.debug.LogRecord.ENABLE_SEQUENCE_NUMBERS', true);


/**
 * A sequence counter for assigning increasing sequence numbers to LogRecord
 * objects.
 * @type {number}
 * @private
 */
goog.debug.LogRecord.nextSequenceNumber_ = 0;


/**
 * Sets all fields of the log record.
 * @param {goog.debug.Logger.Level} level One of the level identifiers.
 * @param {string} msg The string message.
 * @param {string} loggerName The name of the source logger.
 * @param {number=} opt_time Time this log record was created if other than now.
 *     If 0, we use #goog.now.
 * @param {number=} opt_sequenceNumber Sequence number of this log record. This
 *     should only be passed in when restoring a log record from persistence.
 */
goog.debug.LogRecord.prototype.reset = function(level, msg, loggerName,
    opt_time, opt_sequenceNumber) {
  if (goog.debug.LogRecord.ENABLE_SEQUENCE_NUMBERS) {
    this.sequenceNumber_ = typeof opt_sequenceNumber == 'number' ?
        opt_sequenceNumber : goog.debug.LogRecord.nextSequenceNumber_++;
  }

  this.time_ = opt_time || goog.now();
  this.level_ = level;
  this.msg_ = msg;
  this.loggerName_ = loggerName;
  delete this.exception_;
};


/**
 * Get the source Logger's name.
 *
 * @return {string} source logger name (may be null).
 */
goog.debug.LogRecord.prototype.getLoggerName = function() {
  return this.loggerName_;
};


/**
 * Get the exception that is part of the log record.
 *
 * @return {Object} the exception.
 */
goog.debug.LogRecord.prototype.getException = function() {
  return this.exception_;
};


/**
 * Set the exception that is part of the log record.
 *
 * @param {Object} exception the exception.
 */
goog.debug.LogRecord.prototype.setException = function(exception) {
  this.exception_ = exception;
};


/**
 * Get the source Logger's name.
 *
 * @param {string} loggerName source logger name (may be null).
 */
goog.debug.LogRecord.prototype.setLoggerName = function(loggerName) {
  this.loggerName_ = loggerName;
};


/**
 * Get the logging message level, for example Level.SEVERE.
 * @return {goog.debug.Logger.Level} the logging message level.
 */
goog.debug.LogRecord.prototype.getLevel = function() {
  return this.level_;
};


/**
 * Set the logging message level, for example Level.SEVERE.
 * @param {goog.debug.Logger.Level} level the logging message level.
 */
goog.debug.LogRecord.prototype.setLevel = function(level) {
  this.level_ = level;
};


/**
 * Get the "raw" log message, before localization or formatting.
 *
 * @return {string} the raw message string.
 */
goog.debug.LogRecord.prototype.getMessage = function() {
  return this.msg_;
};


/**
 * Set the "raw" log message, before localization or formatting.
 *
 * @param {string} msg the raw message string.
 */
goog.debug.LogRecord.prototype.setMessage = function(msg) {
  this.msg_ = msg;
};


/**
 * Get event time in milliseconds since 1970.
 *
 * @return {number} event time in millis since 1970.
 */
goog.debug.LogRecord.prototype.getMillis = function() {
  return this.time_;
};


/**
 * Set event time in milliseconds since 1970.
 *
 * @param {number} time event time in millis since 1970.
 */
goog.debug.LogRecord.prototype.setMillis = function(time) {
  this.time_ = time;
};


/**
 * Get the sequence number.
 * <p>
 * Sequence numbers are normally assigned in the LogRecord
 * constructor, which assigns unique sequence numbers to
 * each new LogRecord in increasing order.
 * @return {number} the sequence number.
 */
goog.debug.LogRecord.prototype.getSequenceNumber = function() {
  return this.sequenceNumber_;
};


// Copyright 2010 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview A buffer for log records. The purpose of this is to improve
 * logging performance by re-using old objects when the buffer becomes full and
 * to eliminate the need for each app to implement their own log buffer. The
 * disadvantage to doing this is that log handlers cannot maintain references to
 * log records and expect that they are not overwriten at a later point.
 *
 * @author agrieve@google.com (Andrew Grieve)
 */

goog.provide('goog.debug.LogBuffer');

goog.require('goog.asserts');
goog.require('goog.debug.LogRecord');



/**
 * Creates the log buffer.
 * @constructor
 * @final
 */
goog.debug.LogBuffer = function() {
  goog.asserts.assert(goog.debug.LogBuffer.isBufferingEnabled(),
      'Cannot use goog.debug.LogBuffer without defining ' +
      'goog.debug.LogBuffer.CAPACITY.');
  this.clear();
};


/**
 * A static method that always returns the same instance of LogBuffer.
 * @return {!goog.debug.LogBuffer} The LogBuffer singleton instance.
 */
goog.debug.LogBuffer.getInstance = function() {
  if (!goog.debug.LogBuffer.instance_) {
    // This function is written with the return statement after the assignment
    // to avoid the jscompiler StripCode bug described in http://b/2608064.
    // After that bug is fixed this can be refactored.
    goog.debug.LogBuffer.instance_ = new goog.debug.LogBuffer();
  }
  return goog.debug.LogBuffer.instance_;
};


/**
 * @define {number} The number of log records to buffer. 0 means disable
 * buffering.
 */
goog.define('goog.debug.LogBuffer.CAPACITY', 0);


/**
 * The array to store the records.
 * @type {!Array<!goog.debug.LogRecord|undefined>}
 * @private
 */
goog.debug.LogBuffer.prototype.buffer_;


/**
 * The index of the most recently added record or -1 if there are no records.
 * @type {number}
 * @private
 */
goog.debug.LogBuffer.prototype.curIndex_;


/**
 * Whether the buffer is at capacity.
 * @type {boolean}
 * @private
 */
goog.debug.LogBuffer.prototype.isFull_;


/**
 * Adds a log record to the buffer, possibly overwriting the oldest record.
 * @param {goog.debug.Logger.Level} level One of the level identifiers.
 * @param {string} msg The string message.
 * @param {string} loggerName The name of the source logger.
 * @return {!goog.debug.LogRecord} The log record.
 */
goog.debug.LogBuffer.prototype.addRecord = function(level, msg, loggerName) {
  var curIndex = (this.curIndex_ + 1) % goog.debug.LogBuffer.CAPACITY;
  this.curIndex_ = curIndex;
  if (this.isFull_) {
    var ret = this.buffer_[curIndex];
    ret.reset(level, msg, loggerName);
    return ret;
  }
  this.isFull_ = curIndex == goog.debug.LogBuffer.CAPACITY - 1;
  return this.buffer_[curIndex] =
      new goog.debug.LogRecord(level, msg, loggerName);
};


/**
 * @return {boolean} Whether the log buffer is enabled.
 */
goog.debug.LogBuffer.isBufferingEnabled = function() {
  return goog.debug.LogBuffer.CAPACITY > 0;
};


/**
 * Removes all buffered log records.
 */
goog.debug.LogBuffer.prototype.clear = function() {
  this.buffer_ = new Array(goog.debug.LogBuffer.CAPACITY);
  this.curIndex_ = -1;
  this.isFull_ = false;
};


/**
 * Calls the given function for each buffered log record, starting with the
 * oldest one.
 * @param {function(!goog.debug.LogRecord)} func The function to call.
 */
goog.debug.LogBuffer.prototype.forEachRecord = function(func) {
  var buffer = this.buffer_;
  // Corner case: no records.
  if (!buffer[0]) {
    return;
  }
  var curIndex = this.curIndex_;
  var i = this.isFull_ ? curIndex : -1;
  do {
    i = (i + 1) % goog.debug.LogBuffer.CAPACITY;
    func(/** @type {!goog.debug.LogRecord} */ (buffer[i]));
  } while (i != curIndex);
};


// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Definition of the Logger class. Please minimize dependencies
 * this file has on other closure classes as any dependency it takes won't be
 * able to use the logging infrastructure.
 *
 * @see ../demos/debug.html
 */

goog.provide('goog.debug.LogManager');
goog.provide('goog.debug.Loggable');
goog.provide('goog.debug.Logger');
goog.provide('goog.debug.Logger.Level');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.debug');
goog.require('goog.debug.LogBuffer');
goog.require('goog.debug.LogRecord');


/**
 * A message value that can be handled by a Logger.
 *
 * Functions are treated like callbacks, but are only called when the event's
 * log level is enabled. This is useful for logging messages that are expensive
 * to construct.
 *
 * @typedef {string|function(): string}
 */
goog.debug.Loggable;



/**
 * The Logger is an object used for logging debug messages. Loggers are
 * normally named, using a hierarchical dot-separated namespace. Logger names
 * can be arbitrary strings, but they should normally be based on the package
 * name or class name of the logged component, such as goog.net.BrowserChannel.
 *
 * The Logger object is loosely based on the java class
 * java.util.logging.Logger. It supports different levels of filtering for
 * different loggers.
 *
 * The logger object should never be instantiated by application code. It
 * should always use the goog.debug.Logger.getLogger function.
 *
 * @constructor
 * @param {string} name The name of the Logger.
 * @final
 */
goog.debug.Logger = function(name) {
  /**
   * Name of the Logger. Generally a dot-separated namespace
   * @private {string}
   */
  this.name_ = name;

  /**
   * Parent Logger.
   * @private {goog.debug.Logger}
   */
  this.parent_ = null;

  /**
   * Level that this logger only filters above. Null indicates it should
   * inherit from the parent.
   * @private {goog.debug.Logger.Level}
   */
  this.level_ = null;

  /**
   * Map of children loggers. The keys are the leaf names of the children and
   * the values are the child loggers.
   * @private {Object}
   */
  this.children_ = null;

  /**
   * Handlers that are listening to this logger.
   * @private {Array<Function>}
   */
  this.handlers_ = null;
};


/** @const */
goog.debug.Logger.ROOT_LOGGER_NAME = '';


/**
 * @define {boolean} Toggles whether loggers other than the root logger can have
 *     log handlers attached to them and whether they can have their log level
 *     set. Logging is a bit faster when this is set to false.
 */
goog.define('goog.debug.Logger.ENABLE_HIERARCHY', true);


if (!goog.debug.Logger.ENABLE_HIERARCHY) {
  /**
   * @type {!Array<Function>}
   * @private
   */
  goog.debug.Logger.rootHandlers_ = [];


  /**
   * @type {goog.debug.Logger.Level}
   * @private
   */
  goog.debug.Logger.rootLevel_;
}



/**
 * The Level class defines a set of standard logging levels that
 * can be used to control logging output.  The logging Level objects
 * are ordered and are specified by ordered integers.  Enabling logging
 * at a given level also enables logging at all higher levels.
 * <p>
 * Clients should normally use the predefined Level constants such
 * as Level.SEVERE.
 * <p>
 * The levels in descending order are:
 * <ul>
 * <li>SEVERE (highest value)
 * <li>WARNING
 * <li>INFO
 * <li>CONFIG
 * <li>FINE
 * <li>FINER
 * <li>FINEST  (lowest value)
 * </ul>
 * In addition there is a level OFF that can be used to turn
 * off logging, and a level ALL that can be used to enable
 * logging of all messages.
 *
 * @param {string} name The name of the level.
 * @param {number} value The numeric value of the level.
 * @constructor
 * @final
 */
goog.debug.Logger.Level = function(name, value) {
  /**
   * The name of the level
   * @type {string}
   */
  this.name = name;

  /**
   * The numeric value of the level
   * @type {number}
   */
  this.value = value;
};


/**
 * @return {string} String representation of the logger level.
 * @override
 */
goog.debug.Logger.Level.prototype.toString = function() {
  return this.name;
};


/**
 * OFF is a special level that can be used to turn off logging.
 * This level is initialized to <CODE>Infinity</CODE>.
 * @type {!goog.debug.Logger.Level}
 */
goog.debug.Logger.Level.OFF =
    new goog.debug.Logger.Level('OFF', Infinity);


/**
 * SHOUT is a message level for extra debugging loudness.
 * This level is initialized to <CODE>1200</CODE>.
 * @type {!goog.debug.Logger.Level}
 */
goog.debug.Logger.Level.SHOUT = new goog.debug.Logger.Level('SHOUT', 1200);


/**
 * SEVERE is a message level indicating a serious failure.
 * This level is initialized to <CODE>1000</CODE>.
 * @type {!goog.debug.Logger.Level}
 */
goog.debug.Logger.Level.SEVERE = new goog.debug.Logger.Level('SEVERE', 1000);


/**
 * WARNING is a message level indicating a potential problem.
 * This level is initialized to <CODE>900</CODE>.
 * @type {!goog.debug.Logger.Level}
 */
goog.debug.Logger.Level.WARNING = new goog.debug.Logger.Level('WARNING', 900);


/**
 * INFO is a message level for informational messages.
 * This level is initialized to <CODE>800</CODE>.
 * @type {!goog.debug.Logger.Level}
 */
goog.debug.Logger.Level.INFO = new goog.debug.Logger.Level('INFO', 800);


/**
 * CONFIG is a message level for static configuration messages.
 * This level is initialized to <CODE>700</CODE>.
 * @type {!goog.debug.Logger.Level}
 */
goog.debug.Logger.Level.CONFIG = new goog.debug.Logger.Level('CONFIG', 700);


/**
 * FINE is a message level providing tracing information.
 * This level is initialized to <CODE>500</CODE>.
 * @type {!goog.debug.Logger.Level}
 */
goog.debug.Logger.Level.FINE = new goog.debug.Logger.Level('FINE', 500);


/**
 * FINER indicates a fairly detailed tracing message.
 * This level is initialized to <CODE>400</CODE>.
 * @type {!goog.debug.Logger.Level}
 */
goog.debug.Logger.Level.FINER = new goog.debug.Logger.Level('FINER', 400);

/**
 * FINEST indicates a highly detailed tracing message.
 * This level is initialized to <CODE>300</CODE>.
 * @type {!goog.debug.Logger.Level}
 */

goog.debug.Logger.Level.FINEST = new goog.debug.Logger.Level('FINEST', 300);


/**
 * ALL indicates that all messages should be logged.
 * This level is initialized to <CODE>0</CODE>.
 * @type {!goog.debug.Logger.Level}
 */
goog.debug.Logger.Level.ALL = new goog.debug.Logger.Level('ALL', 0);


/**
 * The predefined levels.
 * @type {!Array<!goog.debug.Logger.Level>}
 * @final
 */
goog.debug.Logger.Level.PREDEFINED_LEVELS = [
  goog.debug.Logger.Level.OFF,
  goog.debug.Logger.Level.SHOUT,
  goog.debug.Logger.Level.SEVERE,
  goog.debug.Logger.Level.WARNING,
  goog.debug.Logger.Level.INFO,
  goog.debug.Logger.Level.CONFIG,
  goog.debug.Logger.Level.FINE,
  goog.debug.Logger.Level.FINER,
  goog.debug.Logger.Level.FINEST,
  goog.debug.Logger.Level.ALL];


/**
 * A lookup map used to find the level object based on the name or value of
 * the level object.
 * @type {Object}
 * @private
 */
goog.debug.Logger.Level.predefinedLevelsCache_ = null;


/**
 * Creates the predefined levels cache and populates it.
 * @private
 */
goog.debug.Logger.Level.createPredefinedLevelsCache_ = function() {
  goog.debug.Logger.Level.predefinedLevelsCache_ = {};
  for (var i = 0, level; level = goog.debug.Logger.Level.PREDEFINED_LEVELS[i];
       i++) {
    goog.debug.Logger.Level.predefinedLevelsCache_[level.value] = level;
    goog.debug.Logger.Level.predefinedLevelsCache_[level.name] = level;
  }
};


/**
 * Gets the predefined level with the given name.
 * @param {string} name The name of the level.
 * @return {goog.debug.Logger.Level} The level, or null if none found.
 */
goog.debug.Logger.Level.getPredefinedLevel = function(name) {
  if (!goog.debug.Logger.Level.predefinedLevelsCache_) {
    goog.debug.Logger.Level.createPredefinedLevelsCache_();
  }

  return goog.debug.Logger.Level.predefinedLevelsCache_[name] || null;
};


/**
 * Gets the highest predefined level <= #value.
 * @param {number} value Level value.
 * @return {goog.debug.Logger.Level} The level, or null if none found.
 */
goog.debug.Logger.Level.getPredefinedLevelByValue = function(value) {
  if (!goog.debug.Logger.Level.predefinedLevelsCache_) {
    goog.debug.Logger.Level.createPredefinedLevelsCache_();
  }

  if (value in goog.debug.Logger.Level.predefinedLevelsCache_) {
    return goog.debug.Logger.Level.predefinedLevelsCache_[value];
  }

  for (var i = 0; i < goog.debug.Logger.Level.PREDEFINED_LEVELS.length; ++i) {
    var level = goog.debug.Logger.Level.PREDEFINED_LEVELS[i];
    if (level.value <= value) {
      return level;
    }
  }
  return null;
};


/**
 * Finds or creates a logger for a named subsystem. If a logger has already been
 * created with the given name it is returned. Otherwise a new logger is
 * created. If a new logger is created its log level will be configured based
 * on the LogManager configuration and it will configured to also send logging
 * output to its parent's handlers. It will be registered in the LogManager
 * global namespace.
 *
 * @param {string} name A name for the logger. This should be a dot-separated
 * name and should normally be based on the package name or class name of the
 * subsystem, such as goog.net.BrowserChannel.
 * @return {!goog.debug.Logger} The named logger.
 * @deprecated use goog.log instead. http://go/goog-debug-logger-deprecated
 */
goog.debug.Logger.getLogger = function(name) {
  return goog.debug.LogManager.getLogger(name);
};


/**
 * Logs a message to profiling tools, if available.
 * {@see https://developers.google.com/web-toolkit/speedtracer/logging-api}
 * {@see http://msdn.microsoft.com/en-us/library/dd433074(VS.85).aspx}
 * @param {string} msg The message to log.
 */
goog.debug.Logger.logToProfilers = function(msg) {
  // Using goog.global, as loggers might be used in window-less contexts.
  if (goog.global['console']) {
    if (goog.global['console']['timeStamp']) {
      // Logs a message to Firebug, Web Inspector, SpeedTracer, etc.
      goog.global['console']['timeStamp'](msg);
    } else if (goog.global['console']['markTimeline']) {
      // TODO(user): markTimeline is deprecated. Drop this else clause entirely
      // after Chrome M14 hits stable.
      goog.global['console']['markTimeline'](msg);
    }
  }

  if (goog.global['msWriteProfilerMark']) {
    // Logs a message to the Microsoft profiler
    goog.global['msWriteProfilerMark'](msg);
  }
};


/**
 * Gets the name of this logger.
 * @return {string} The name of this logger.
 */
goog.debug.Logger.prototype.getName = function() {
  return this.name_;
};


/**
 * Adds a handler to the logger. This doesn't use the event system because
 * we want to be able to add logging to the event system.
 * @param {Function} handler Handler function to add.
 */
goog.debug.Logger.prototype.addHandler = function(handler) {
  if (goog.debug.LOGGING_ENABLED) {
    if (goog.debug.Logger.ENABLE_HIERARCHY) {
      if (!this.handlers_) {
        this.handlers_ = [];
      }
      this.handlers_.push(handler);
    } else {
      goog.asserts.assert(!this.name_,
          'Cannot call addHandler on a non-root logger when ' +
          'goog.debug.Logger.ENABLE_HIERARCHY is false.');
      goog.debug.Logger.rootHandlers_.push(handler);
    }
  }
};


/**
 * Removes a handler from the logger. This doesn't use the event system because
 * we want to be able to add logging to the event system.
 * @param {Function} handler Handler function to remove.
 * @return {boolean} Whether the handler was removed.
 */
goog.debug.Logger.prototype.removeHandler = function(handler) {
  if (goog.debug.LOGGING_ENABLED) {
    var handlers = goog.debug.Logger.ENABLE_HIERARCHY ? this.handlers_ :
        goog.debug.Logger.rootHandlers_;
    return !!handlers && goog.array.remove(handlers, handler);
  } else {
    return false;
  }
};


/**
 * Returns the parent of this logger.
 * @return {goog.debug.Logger} The parent logger or null if this is the root.
 */
goog.debug.Logger.prototype.getParent = function() {
  return this.parent_;
};


/**
 * Returns the children of this logger as a map of the child name to the logger.
 * @return {!Object} The map where the keys are the child leaf names and the
 *     values are the Logger objects.
 */
goog.debug.Logger.prototype.getChildren = function() {
  if (!this.children_) {
    this.children_ = {};
  }
  return this.children_;
};


/**
 * Set the log level specifying which message levels will be logged by this
 * logger. Message levels lower than this value will be discarded.
 * The level value Level.OFF can be used to turn off logging. If the new level
 * is null, it means that this node should inherit its level from its nearest
 * ancestor with a specific (non-null) level value.
 *
 * @param {goog.debug.Logger.Level} level The new level.
 */
goog.debug.Logger.prototype.setLevel = function(level) {
  if (goog.debug.LOGGING_ENABLED) {
    if (goog.debug.Logger.ENABLE_HIERARCHY) {
      this.level_ = level;
    } else {
      goog.asserts.assert(!this.name_,
          'Cannot call setLevel() on a non-root logger when ' +
          'goog.debug.Logger.ENABLE_HIERARCHY is false.');
      goog.debug.Logger.rootLevel_ = level;
    }
  }
};


/**
 * Gets the log level specifying which message levels will be logged by this
 * logger. Message levels lower than this value will be discarded.
 * The level value Level.OFF can be used to turn off logging. If the level
 * is null, it means that this node should inherit its level from its nearest
 * ancestor with a specific (non-null) level value.
 *
 * @return {goog.debug.Logger.Level} The level.
 */
goog.debug.Logger.prototype.getLevel = function() {
  return goog.debug.LOGGING_ENABLED ?
      this.level_ : goog.debug.Logger.Level.OFF;
};


/**
 * Returns the effective level of the logger based on its ancestors' levels.
 * @return {goog.debug.Logger.Level} The level.
 */
goog.debug.Logger.prototype.getEffectiveLevel = function() {
  if (!goog.debug.LOGGING_ENABLED) {
    return goog.debug.Logger.Level.OFF;
  }

  if (!goog.debug.Logger.ENABLE_HIERARCHY) {
    return goog.debug.Logger.rootLevel_;
  }
  if (this.level_) {
    return this.level_;
  }
  if (this.parent_) {
    return this.parent_.getEffectiveLevel();
  }
  goog.asserts.fail('Root logger has no level set.');
  return null;
};


/**
 * Checks if a message of the given level would actually be logged by this
 * logger. This check is based on the Loggers effective level, which may be
 * inherited from its parent.
 * @param {goog.debug.Logger.Level} level The level to check.
 * @return {boolean} Whether the message would be logged.
 */
goog.debug.Logger.prototype.isLoggable = function(level) {
  return goog.debug.LOGGING_ENABLED &&
      level.value >= this.getEffectiveLevel().value;
};


/**
 * Logs a message. If the logger is currently enabled for the
 * given message level then the given message is forwarded to all the
 * registered output Handler objects.
 * @param {goog.debug.Logger.Level} level One of the level identifiers.
 * @param {goog.debug.Loggable} msg The message to log.
 * @param {Error|Object=} opt_exception An exception associated with the
 *     message.
 */
goog.debug.Logger.prototype.log = function(level, msg, opt_exception) {
  // java caches the effective level, not sure it's necessary here
  if (goog.debug.LOGGING_ENABLED && this.isLoggable(level)) {
    // Message callbacks can be useful when a log message is expensive to build.
    if (goog.isFunction(msg)) {
      msg = msg();
    }

    this.doLogRecord_(this.getLogRecord(level, msg, opt_exception));
  }
};


/**
 * Creates a new log record and adds the exception (if present) to it.
 * @param {goog.debug.Logger.Level} level One of the level identifiers.
 * @param {string} msg The string message.
 * @param {Error|Object=} opt_exception An exception associated with the
 *     message.
 * @return {!goog.debug.LogRecord} A log record.
 * @suppress {es5Strict}
 */
goog.debug.Logger.prototype.getLogRecord = function(
    level, msg, opt_exception) {
  if (goog.debug.LogBuffer.isBufferingEnabled()) {
    var logRecord =
        goog.debug.LogBuffer.getInstance().addRecord(level, msg, this.name_);
  } else {
    logRecord = new goog.debug.LogRecord(level, String(msg), this.name_);
  }
  if (opt_exception) {
    logRecord.setException(opt_exception);
  }
  return logRecord;
};


/**
 * Logs a message at the Logger.Level.SHOUT level.
 * If the logger is currently enabled for the given message level then the
 * given message is forwarded to all the registered output Handler objects.
 * @param {goog.debug.Loggable} msg The message to log.
 * @param {Error=} opt_exception An exception associated with the message.
 */
goog.debug.Logger.prototype.shout = function(msg, opt_exception) {
  if (goog.debug.LOGGING_ENABLED) {
    this.log(goog.debug.Logger.Level.SHOUT, msg, opt_exception);
  }
};


/**
 * Logs a message at the Logger.Level.SEVERE level.
 * If the logger is currently enabled for the given message level then the
 * given message is forwarded to all the registered output Handler objects.
 * @param {goog.debug.Loggable} msg The message to log.
 * @param {Error=} opt_exception An exception associated with the message.
 */
goog.debug.Logger.prototype.severe = function(msg, opt_exception) {
  if (goog.debug.LOGGING_ENABLED) {
    this.log(goog.debug.Logger.Level.SEVERE, msg, opt_exception);
  }
};


/**
 * Logs a message at the Logger.Level.WARNING level.
 * If the logger is currently enabled for the given message level then the
 * given message is forwarded to all the registered output Handler objects.
 * @param {goog.debug.Loggable} msg The message to log.
 * @param {Error=} opt_exception An exception associated with the message.
 */
goog.debug.Logger.prototype.warning = function(msg, opt_exception) {
  if (goog.debug.LOGGING_ENABLED) {
    this.log(goog.debug.Logger.Level.WARNING, msg, opt_exception);
  }
};


/**
 * Logs a message at the Logger.Level.INFO level.
 * If the logger is currently enabled for the given message level then the
 * given message is forwarded to all the registered output Handler objects.
 * @param {goog.debug.Loggable} msg The message to log.
 * @param {Error=} opt_exception An exception associated with the message.
 */
goog.debug.Logger.prototype.info = function(msg, opt_exception) {
  if (goog.debug.LOGGING_ENABLED) {
    this.log(goog.debug.Logger.Level.INFO, msg, opt_exception);
  }
};


/**
 * Logs a message at the Logger.Level.CONFIG level.
 * If the logger is currently enabled for the given message level then the
 * given message is forwarded to all the registered output Handler objects.
 * @param {goog.debug.Loggable} msg The message to log.
 * @param {Error=} opt_exception An exception associated with the message.
 */
goog.debug.Logger.prototype.config = function(msg, opt_exception) {
  if (goog.debug.LOGGING_ENABLED) {
    this.log(goog.debug.Logger.Level.CONFIG, msg, opt_exception);
  }
};


/**
 * Logs a message at the Logger.Level.FINE level.
 * If the logger is currently enabled for the given message level then the
 * given message is forwarded to all the registered output Handler objects.
 * @param {goog.debug.Loggable} msg The message to log.
 * @param {Error=} opt_exception An exception associated with the message.
 */
goog.debug.Logger.prototype.fine = function(msg, opt_exception) {
  if (goog.debug.LOGGING_ENABLED) {
    this.log(goog.debug.Logger.Level.FINE, msg, opt_exception);
  }
};


/**
 * Logs a message at the Logger.Level.FINER level.
 * If the logger is currently enabled for the given message level then the
 * given message is forwarded to all the registered output Handler objects.
 * @param {goog.debug.Loggable} msg The message to log.
 * @param {Error=} opt_exception An exception associated with the message.
 */
goog.debug.Logger.prototype.finer = function(msg, opt_exception) {
  if (goog.debug.LOGGING_ENABLED) {
    this.log(goog.debug.Logger.Level.FINER, msg, opt_exception);
  }
};


/**
 * Logs a message at the Logger.Level.FINEST level.
 * If the logger is currently enabled for the given message level then the
 * given message is forwarded to all the registered output Handler objects.
 * @param {goog.debug.Loggable} msg The message to log.
 * @param {Error=} opt_exception An exception associated with the message.
 */
goog.debug.Logger.prototype.finest = function(msg, opt_exception) {
  if (goog.debug.LOGGING_ENABLED) {
    this.log(goog.debug.Logger.Level.FINEST, msg, opt_exception);
  }
};


/**
 * Logs a LogRecord. If the logger is currently enabled for the
 * given message level then the given message is forwarded to all the
 * registered output Handler objects.
 * @param {goog.debug.LogRecord} logRecord A log record to log.
 */
goog.debug.Logger.prototype.logRecord = function(logRecord) {
  if (goog.debug.LOGGING_ENABLED && this.isLoggable(logRecord.getLevel())) {
    this.doLogRecord_(logRecord);
  }
};


/**
 * Logs a LogRecord.
 * @param {goog.debug.LogRecord} logRecord A log record to log.
 * @private
 */
goog.debug.Logger.prototype.doLogRecord_ = function(logRecord) {
  goog.debug.Logger.logToProfilers('log:' + logRecord.getMessage());
  if (goog.debug.Logger.ENABLE_HIERARCHY) {
    var target = this;
    while (target) {
      target.callPublish_(logRecord);
      target = target.getParent();
    }
  } else {
    for (var i = 0, handler; handler = goog.debug.Logger.rootHandlers_[i++]; ) {
      handler(logRecord);
    }
  }
};


/**
 * Calls the handlers for publish.
 * @param {goog.debug.LogRecord} logRecord The log record to publish.
 * @private
 */
goog.debug.Logger.prototype.callPublish_ = function(logRecord) {
  if (this.handlers_) {
    for (var i = 0, handler; handler = this.handlers_[i]; i++) {
      handler(logRecord);
    }
  }
};


/**
 * Sets the parent of this logger. This is used for setting up the logger tree.
 * @param {goog.debug.Logger} parent The parent logger.
 * @private
 */
goog.debug.Logger.prototype.setParent_ = function(parent) {
  this.parent_ = parent;
};


/**
 * Adds a child to this logger. This is used for setting up the logger tree.
 * @param {string} name The leaf name of the child.
 * @param {goog.debug.Logger} logger The child logger.
 * @private
 */
goog.debug.Logger.prototype.addChild_ = function(name, logger) {
  this.getChildren()[name] = logger;
};


/**
 * There is a single global LogManager object that is used to maintain a set of
 * shared state about Loggers and log services. This is loosely based on the
 * java class java.util.logging.LogManager.
 * @const
 */
goog.debug.LogManager = {};


/**
 * Map of logger names to logger objects.
 *
 * @type {!Object<string, !goog.debug.Logger>}
 * @private
 */
goog.debug.LogManager.loggers_ = {};


/**
 * The root logger which is the root of the logger tree.
 * @type {goog.debug.Logger}
 * @private
 */
goog.debug.LogManager.rootLogger_ = null;


/**
 * Initializes the LogManager if not already initialized.
 */
goog.debug.LogManager.initialize = function() {
  if (!goog.debug.LogManager.rootLogger_) {
    goog.debug.LogManager.rootLogger_ = new goog.debug.Logger(
        goog.debug.Logger.ROOT_LOGGER_NAME);
    goog.debug.LogManager.loggers_[goog.debug.Logger.ROOT_LOGGER_NAME] =
        goog.debug.LogManager.rootLogger_;
    goog.debug.LogManager.rootLogger_.setLevel(goog.debug.Logger.Level.CONFIG);
  }
};


/**
 * Returns all the loggers.
 * @return {!Object<string, !goog.debug.Logger>} Map of logger names to logger
 *     objects.
 */
goog.debug.LogManager.getLoggers = function() {
  return goog.debug.LogManager.loggers_;
};


/**
 * Returns the root of the logger tree namespace, the logger with the empty
 * string as its name.
 *
 * @return {!goog.debug.Logger} The root logger.
 */
goog.debug.LogManager.getRoot = function() {
  goog.debug.LogManager.initialize();
  return /** @type {!goog.debug.Logger} */ (goog.debug.LogManager.rootLogger_);
};


/**
 * Finds a named logger.
 *
 * @param {string} name A name for the logger. This should be a dot-separated
 * name and should normally be based on the package name or class name of the
 * subsystem, such as goog.net.BrowserChannel.
 * @return {!goog.debug.Logger} The named logger.
 */
goog.debug.LogManager.getLogger = function(name) {
  goog.debug.LogManager.initialize();
  var ret = goog.debug.LogManager.loggers_[name];
  return ret || goog.debug.LogManager.createLogger_(name);
};


/**
 * Creates a function that can be passed to goog.debug.catchErrors. The function
 * will log all reported errors using the given logger.
 * @param {goog.debug.Logger=} opt_logger The logger to log the errors to.
 *     Defaults to the root logger.
 * @return {function(Object)} The created function.
 */
goog.debug.LogManager.createFunctionForCatchErrors = function(opt_logger) {
  return function(info) {
    var logger = opt_logger || goog.debug.LogManager.getRoot();
    logger.severe('Error: ' + info.message + ' (' + info.fileName +
                  ' @ Line: ' + info.line + ')');
  };
};


/**
 * Creates the named logger. Will also create the parents of the named logger
 * if they don't yet exist.
 * @param {string} name The name of the logger.
 * @return {!goog.debug.Logger} The named logger.
 * @private
 */
goog.debug.LogManager.createLogger_ = function(name) {
  // find parent logger
  var logger = new goog.debug.Logger(name);
  if (goog.debug.Logger.ENABLE_HIERARCHY) {
    var lastDotIndex = name.lastIndexOf('.');
    var parentName = name.substr(0, lastDotIndex);
    var leafName = name.substr(lastDotIndex + 1);
    var parentLogger = goog.debug.LogManager.getLogger(parentName);

    // tell the parent about the child and the child about the parent
    parentLogger.addChild_(leafName, logger);
    logger.setParent_(parentLogger);
  }

  goog.debug.LogManager.loggers_[name] = logger;
  return logger;
};

// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Definition the goog.debug.RelativeTimeProvider class.
 *
 */

goog.provide('goog.debug.RelativeTimeProvider');



/**
 * A simple object to keep track of a timestamp considered the start of
 * something. The main use is for the logger system to maintain a start time
 * that is occasionally reset. For example, in Gmail, we reset this relative
 * time at the start of a user action so that timings are offset from the
 * beginning of the action. This class also provides a singleton as the default
 * behavior for most use cases is to share the same start time.
 *
 * @constructor
 * @final
 */
goog.debug.RelativeTimeProvider = function() {
  /**
   * The start time.
   * @type {number}
   * @private
   */
  this.relativeTimeStart_ = goog.now();
};


/**
 * Default instance.
 * @type {goog.debug.RelativeTimeProvider}
 * @private
 */
goog.debug.RelativeTimeProvider.defaultInstance_ =
    new goog.debug.RelativeTimeProvider();


/**
 * Sets the start time to the specified time.
 * @param {number} timeStamp The start time.
 */
goog.debug.RelativeTimeProvider.prototype.set = function(timeStamp) {
  this.relativeTimeStart_ = timeStamp;
};


/**
 * Resets the start time to now.
 */
goog.debug.RelativeTimeProvider.prototype.reset = function() {
  this.set(goog.now());
};


/**
 * @return {number} The start time.
 */
goog.debug.RelativeTimeProvider.prototype.get = function() {
  return this.relativeTimeStart_;
};


/**
 * @return {goog.debug.RelativeTimeProvider} The default instance.
 */
goog.debug.RelativeTimeProvider.getDefaultInstance = function() {
  return goog.debug.RelativeTimeProvider.defaultInstance_;
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Definition of various formatters for logging. Please minimize
 * dependencies this file has on other closure classes as any dependency it
 * takes won't be able to use the logging infrastructure.
 *
 */

goog.provide('goog.debug.Formatter');
goog.provide('goog.debug.HtmlFormatter');
goog.provide('goog.debug.TextFormatter');

goog.require('goog.debug');
goog.require('goog.debug.Logger');
goog.require('goog.debug.RelativeTimeProvider');
goog.require('goog.html.SafeHtml');



/**
 * Base class for Formatters. A Formatter is used to format a LogRecord into
 * something that can be displayed to the user.
 *
 * @param {string=} opt_prefix The prefix to place before text records.
 * @constructor
 */
goog.debug.Formatter = function(opt_prefix) {
  this.prefix_ = opt_prefix || '';

  /**
   * A provider that returns the relative start time.
   * @type {goog.debug.RelativeTimeProvider}
   * @private
   */
  this.startTimeProvider_ =
      goog.debug.RelativeTimeProvider.getDefaultInstance();
};


/**
 * Whether to append newlines to the end of formatted log records.
 * @type {boolean}
 */
goog.debug.Formatter.prototype.appendNewline = true;


/**
 * Whether to show absolute time in the DebugWindow.
 * @type {boolean}
 */
goog.debug.Formatter.prototype.showAbsoluteTime = true;


/**
 * Whether to show relative time in the DebugWindow.
 * @type {boolean}
 */
goog.debug.Formatter.prototype.showRelativeTime = true;


/**
 * Whether to show the logger name in the DebugWindow.
 * @type {boolean}
 */
goog.debug.Formatter.prototype.showLoggerName = true;


/**
 * Whether to show the logger exception text.
 * @type {boolean}
 */
goog.debug.Formatter.prototype.showExceptionText = false;


/**
 * Whether to show the severity level.
 * @type {boolean}
 */
goog.debug.Formatter.prototype.showSeverityLevel = false;


/**
 * Formats a record.
 * @param {goog.debug.LogRecord} logRecord the logRecord to format.
 * @return {string} The formatted string.
 */
goog.debug.Formatter.prototype.formatRecord = goog.abstractMethod;


/**
 * Formats a record as SafeHtml.
 * @param {goog.debug.LogRecord} logRecord the logRecord to format.
 * @return {!goog.html.SafeHtml} The formatted string as SafeHtml.
 */
goog.debug.Formatter.prototype.formatRecordAsHtml = goog.abstractMethod;


/**
 * Sets the start time provider. By default, this is the default instance
 * but can be changed.
 * @param {goog.debug.RelativeTimeProvider} provider The provider to use.
 */
goog.debug.Formatter.prototype.setStartTimeProvider = function(provider) {
  this.startTimeProvider_ = provider;
};


/**
 * Returns the start time provider. By default, this is the default instance
 * but can be changed.
 * @return {goog.debug.RelativeTimeProvider} The start time provider.
 */
goog.debug.Formatter.prototype.getStartTimeProvider = function() {
  return this.startTimeProvider_;
};


/**
 * Resets the start relative time.
 */
goog.debug.Formatter.prototype.resetRelativeTimeStart = function() {
  this.startTimeProvider_.reset();
};


/**
 * Returns a string for the time/date of the LogRecord.
 * @param {goog.debug.LogRecord} logRecord The record to get a time stamp for.
 * @return {string} A string representation of the time/date of the LogRecord.
 * @private
 */
goog.debug.Formatter.getDateTimeStamp_ = function(logRecord) {
  var time = new Date(logRecord.getMillis());
  return goog.debug.Formatter.getTwoDigitString_((time.getFullYear() - 2000)) +
         goog.debug.Formatter.getTwoDigitString_((time.getMonth() + 1)) +
         goog.debug.Formatter.getTwoDigitString_(time.getDate()) + ' ' +
         goog.debug.Formatter.getTwoDigitString_(time.getHours()) + ':' +
         goog.debug.Formatter.getTwoDigitString_(time.getMinutes()) + ':' +
         goog.debug.Formatter.getTwoDigitString_(time.getSeconds()) + '.' +
         goog.debug.Formatter.getTwoDigitString_(
             Math.floor(time.getMilliseconds() / 10));
};


/**
 * Returns the number as a two-digit string, meaning it prepends a 0 if the
 * number if less than 10.
 * @param {number} n The number to format.
 * @return {string} A two-digit string representation of {@code n}.
 * @private
 */
goog.debug.Formatter.getTwoDigitString_ = function(n) {
  if (n < 10) {
    return '0' + n;
  }
  return String(n);
};


/**
 * Returns a string for the number of seconds relative to the start time.
 * Prepads with spaces so that anything less than 1000 seconds takes up the
 * same number of characters for better formatting.
 * @param {goog.debug.LogRecord} logRecord The log to compare time to.
 * @param {number} relativeTimeStart The start time to compare to.
 * @return {string} The number of seconds of the LogRecord relative to the
 *     start time.
 * @private
 */
goog.debug.Formatter.getRelativeTime_ = function(logRecord,
                                                 relativeTimeStart) {
  var ms = logRecord.getMillis() - relativeTimeStart;
  var sec = ms / 1000;
  var str = sec.toFixed(3);

  var spacesToPrepend = 0;
  if (sec < 1) {
    spacesToPrepend = 2;
  } else {
    while (sec < 100) {
      spacesToPrepend++;
      sec *= 10;
    }
  }
  while (spacesToPrepend-- > 0) {
    str = ' ' + str;
  }
  return str;
};



/**
 * Formatter that returns formatted html. See formatRecord for the classes
 * it uses for various types of formatted output.
 *
 * @param {string=} opt_prefix The prefix to place before text records.
 * @constructor
 * @extends {goog.debug.Formatter}
 */
goog.debug.HtmlFormatter = function(opt_prefix) {
  goog.debug.Formatter.call(this, opt_prefix);
};
goog.inherits(goog.debug.HtmlFormatter, goog.debug.Formatter);


/**
 * Whether to show the logger exception text
 * @type {boolean}
 * @override
 */
goog.debug.HtmlFormatter.prototype.showExceptionText = true;


/**
 * Formats a record
 * @param {goog.debug.LogRecord} logRecord the logRecord to format.
 * @return {string} The formatted string as html.
 * @override
 */
goog.debug.HtmlFormatter.prototype.formatRecord = function(logRecord) {
  if (!logRecord) {
    return '';
  }
  // OK not to use goog.html.SafeHtml.unwrap() here.
  return this.formatRecordAsHtml(logRecord).getTypedStringValue();
};


/**
 * Formats a record.
 * @param {goog.debug.LogRecord} logRecord the logRecord to format.
 * @return {!goog.html.SafeHtml} The formatted string as SafeHtml.
 * @override
 */
goog.debug.HtmlFormatter.prototype.formatRecordAsHtml = function(logRecord) {
  if (!logRecord) {
    return goog.html.SafeHtml.EMPTY;
  }

  var className;
  switch (logRecord.getLevel().value) {
    case goog.debug.Logger.Level.SHOUT.value:
      className = 'dbg-sh';
      break;
    case goog.debug.Logger.Level.SEVERE.value:
      className = 'dbg-sev';
      break;
    case goog.debug.Logger.Level.WARNING.value:
      className = 'dbg-w';
      break;
    case goog.debug.Logger.Level.INFO.value:
      className = 'dbg-i';
      break;
    case goog.debug.Logger.Level.FINE.value:
    default:
      className = 'dbg-f';
      break;
  }

  // HTML for user defined prefix, time, logger name, and severity.
  var sb = [];
  sb.push(this.prefix_, ' ');
  if (this.showAbsoluteTime) {
    sb.push('[', goog.debug.Formatter.getDateTimeStamp_(logRecord), '] ');
  }
  if (this.showRelativeTime) {
    sb.push('[',
        goog.debug.Formatter.getRelativeTime_(
            logRecord, this.startTimeProvider_.get()),
        's] ');
  }
  if (this.showLoggerName) {
    sb.push('[', logRecord.getLoggerName(), '] ');
  }
  if (this.showSeverityLevel) {
    sb.push('[', logRecord.getLevel().name, '] ');
  }
  var fullPrefixHtml =
      goog.html.SafeHtml.htmlEscapePreservingNewlinesAndSpaces(sb.join(''));

  // HTML for exception text and log record.
  var exceptionHtml = goog.html.SafeHtml.EMPTY;
  if (this.showExceptionText && logRecord.getException()) {
    exceptionHtml = goog.html.SafeHtml.concat(
        goog.html.SafeHtml.create('br'),
        goog.debug.exposeExceptionAsHtml(logRecord.getException()));
  }
  var logRecordHtml = goog.html.SafeHtml.htmlEscapePreservingNewlinesAndSpaces(
      logRecord.getMessage());
  var recordAndExceptionHtml = goog.html.SafeHtml.create(
      'span',
      {'class': className},
      goog.html.SafeHtml.concat(logRecordHtml, exceptionHtml));


  // Combine both pieces of HTML and, if needed, append a final newline.
  var html;
  if (this.appendNewline) {
    html = goog.html.SafeHtml.concat(fullPrefixHtml, recordAndExceptionHtml,
        goog.html.SafeHtml.create('br'));
  } else {
    html = goog.html.SafeHtml.concat(fullPrefixHtml, recordAndExceptionHtml);
  }
  return html;
};



/**
 * Formatter that returns formatted plain text
 *
 * @param {string=} opt_prefix The prefix to place before text records.
 * @constructor
 * @extends {goog.debug.Formatter}
 * @final
 */
goog.debug.TextFormatter = function(opt_prefix) {
  goog.debug.Formatter.call(this, opt_prefix);
};
goog.inherits(goog.debug.TextFormatter, goog.debug.Formatter);


/**
 * Formats a record as text
 * @param {goog.debug.LogRecord} logRecord the logRecord to format.
 * @return {string} The formatted string.
 * @override
 */
goog.debug.TextFormatter.prototype.formatRecord = function(logRecord) {
  var sb = [];
  sb.push(this.prefix_, ' ');
  if (this.showAbsoluteTime) {
    sb.push('[', goog.debug.Formatter.getDateTimeStamp_(logRecord), '] ');
  }
  if (this.showRelativeTime) {
    sb.push('[', goog.debug.Formatter.getRelativeTime_(logRecord,
        this.startTimeProvider_.get()), 's] ');
  }

  if (this.showLoggerName) {
    sb.push('[', logRecord.getLoggerName(), '] ');
  }
  if (this.showSeverityLevel) {
    sb.push('[', logRecord.getLevel().name, '] ');
  }
  sb.push(logRecord.getMessage());
  if (this.showExceptionText) {
    var exception = logRecord.getException();
    if (exception) {
      var exceptionText = exception instanceof Error ?
          exception.message :
          exception.toString();
      sb.push('\n', exceptionText);
    }
  }
  if (this.appendNewline) {
    sb.push('\n');
  }
  return sb.join('');
};


/**
 * Formats a record as text
 * @param {goog.debug.LogRecord} logRecord the logRecord to format.
 * @return {!goog.html.SafeHtml} The formatted string as SafeHtml. This is
 *     just an HTML-escaped version of the text obtained from formatRecord().
 * @override
 */
goog.debug.TextFormatter.prototype.formatRecordAsHtml = function(logRecord) {
  return goog.html.SafeHtml.htmlEscapePreservingNewlinesAndSpaces(
      goog.debug.TextFormatter.prototype.formatRecord(logRecord));
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Simple logger that logs to the window console if available.
 *
 * Has an autoInstall option which can be put into initialization code, which
 * will start logging if "Debug=true" is in document.location.href
 *
 */

goog.provide('goog.debug.Console');

goog.require('goog.debug.LogManager');
goog.require('goog.debug.Logger');
goog.require('goog.debug.TextFormatter');



/**
 * Create and install a log handler that logs to window.console if available
 * @constructor
 */
goog.debug.Console = function() {
  this.publishHandler_ = goog.bind(this.addLogRecord, this);

  /**
   * Formatter for formatted output.
   * @type {!goog.debug.TextFormatter}
   * @private
   */
  this.formatter_ = new goog.debug.TextFormatter();
  this.formatter_.showAbsoluteTime = false;
  this.formatter_.showExceptionText = false;
  // The console logging methods automatically append a newline.
  this.formatter_.appendNewline = false;

  this.isCapturing_ = false;
  this.logBuffer_ = '';

  /**
   * Loggers that we shouldn't output.
   * @type {!Object<boolean>}
   * @private
   */
  this.filteredLoggers_ = {};
};


/**
 * Returns the text formatter used by this console
 * @return {!goog.debug.TextFormatter} The text formatter.
 */
goog.debug.Console.prototype.getFormatter = function() {
  return this.formatter_;
};


/**
 * Sets whether we are currently capturing logger output.
 * @param {boolean} capturing Whether to capture logger output.
 */
goog.debug.Console.prototype.setCapturing = function(capturing) {
  if (capturing == this.isCapturing_) {
    return;
  }

  // attach or detach handler from the root logger
  var rootLogger = goog.debug.LogManager.getRoot();
  if (capturing) {
    rootLogger.addHandler(this.publishHandler_);
  } else {
    rootLogger.removeHandler(this.publishHandler_);
    this.logBuffer = '';
  }
  this.isCapturing_ = capturing;
};


/**
 * Adds a log record.
 * @param {goog.debug.LogRecord} logRecord The log entry.
 */
goog.debug.Console.prototype.addLogRecord = function(logRecord) {

  // Check to see if the log record is filtered or not.
  if (this.filteredLoggers_[logRecord.getLoggerName()]) {
    return;
  }

  var record = this.formatter_.formatRecord(logRecord);
  var console = goog.debug.Console.console_;
  if (console) {
    switch (logRecord.getLevel()) {
      case goog.debug.Logger.Level.SHOUT:
        goog.debug.Console.logToConsole_(console, 'info', record);
        break;
      case goog.debug.Logger.Level.SEVERE:
        goog.debug.Console.logToConsole_(console, 'error', record);
        break;
      case goog.debug.Logger.Level.WARNING:
        goog.debug.Console.logToConsole_(console, 'warn', record);
        break;
      default:
        goog.debug.Console.logToConsole_(console, 'debug', record);
        break;
    }
  } else {
    this.logBuffer_ += record;
  }
};


/**
 * Adds a logger name to be filtered.
 * @param {string} loggerName the logger name to add.
 */
goog.debug.Console.prototype.addFilter = function(loggerName) {
  this.filteredLoggers_[loggerName] = true;
};


/**
 * Removes a logger name to be filtered.
 * @param {string} loggerName the logger name to remove.
 */
goog.debug.Console.prototype.removeFilter = function(loggerName) {
  delete this.filteredLoggers_[loggerName];
};


/**
 * Global console logger instance
 * @type {goog.debug.Console}
 */
goog.debug.Console.instance = null;


/**
 * The console to which to log.  This is a property so it can be mocked out in
 * this unit test for goog.debug.Console. Using goog.global, as console might be
 * used in window-less contexts.
 * @type {Object}
 * @private
 */
goog.debug.Console.console_ = goog.global['console'];


/**
 * Sets the console to which to log.
 * @param {!Object} console The console to which to log.
 */
goog.debug.Console.setConsole = function(console) {
  goog.debug.Console.console_ = console;
};


/**
 * Install the console and start capturing if "Debug=true" is in the page URL
 */
goog.debug.Console.autoInstall = function() {
  if (!goog.debug.Console.instance) {
    goog.debug.Console.instance = new goog.debug.Console();
  }

  if (goog.global.location &&
      goog.global.location.href.indexOf('Debug=true') != -1) {
    goog.debug.Console.instance.setCapturing(true);
  }
};


/**
 * Show an alert with all of the captured debug information.
 * Information is only captured if console is not available
 */
goog.debug.Console.show = function() {
  alert(goog.debug.Console.instance.logBuffer_);
};


/**
 * Logs the record to the console using the given function.  If the function is
 * not available on the console object, the log function is used instead.
 * @param {!Object} console The console object.
 * @param {string} fnName The name of the function to use.
 * @param {string} record The record to log.
 * @private
 */
goog.debug.Console.logToConsole_ = function(console, fnName, record) {
  if (console[fnName]) {
    console[fnName](record);
  } else {
    console.log(record);
  }
};

// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Utility class that monitors viewport size changes.
 *
 * @author attila@google.com (Attila Bodis)
 * @see ../demos/viewportsizemonitor.html
 */

goog.provide('goog.dom.ViewportSizeMonitor');

goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.math.Size');



/**
 * This class can be used to monitor changes in the viewport size.  Instances
 * dispatch a {@link goog.events.EventType.RESIZE} event when the viewport size
 * changes.  Handlers can call {@link goog.dom.ViewportSizeMonitor#getSize} to
 * get the new viewport size.
 *
 * Use this class if you want to execute resize/reflow logic each time the
 * user resizes the browser window.  This class is guaranteed to only dispatch
 * {@code RESIZE} events when the pixel dimensions of the viewport change.
 * (Internet Explorer fires resize events if any element on the page is resized,
 * even if the viewport dimensions are unchanged, which can lead to infinite
 * resize loops.)
 *
 * Example usage:
 *  <pre>
 *    var vsm = new goog.dom.ViewportSizeMonitor();
 *    goog.events.listen(vsm, goog.events.EventType.RESIZE, function(e) {
 *      alert('Viewport size changed to ' + vsm.getSize());
 *    });
 *  </pre>
 *
 * Manually verified on IE6, IE7, FF2, Opera 11, Safari 4 and Chrome.
 *
 * @param {Window=} opt_window The window to monitor; defaults to the window in
 *    which this code is executing.
 * @constructor
 * @extends {goog.events.EventTarget}
 */
goog.dom.ViewportSizeMonitor = function(opt_window) {
  goog.dom.ViewportSizeMonitor.base(this, 'constructor');

  /**
   * The window to monitor. Defaults to the window in which the code is running.
   * @private {Window}
   */
  this.window_ = opt_window || window;

  /**
   * Event listener key for window the window resize handler, as returned by
   * {@link goog.events.listen}.
   * @private {goog.events.Key}
   */
  this.listenerKey_ = goog.events.listen(this.window_,
      goog.events.EventType.RESIZE, this.handleResize_, false, this);

  /**
   * The most recently recorded size of the viewport, in pixels.
   * @private {goog.math.Size}
   */
  this.size_ = goog.dom.getViewportSize(this.window_);
};
goog.inherits(goog.dom.ViewportSizeMonitor, goog.events.EventTarget);


/**
 * Returns a viewport size monitor for the given window.  A new one is created
 * if it doesn't exist already.  This prevents the unnecessary creation of
 * multiple spooling monitors for a window.
 * @param {Window=} opt_window The window to monitor; defaults to the window in
 *     which this code is executing.
 * @return {!goog.dom.ViewportSizeMonitor} Monitor for the given window.
 */
goog.dom.ViewportSizeMonitor.getInstanceForWindow = function(opt_window) {
  var currentWindow = opt_window || window;
  var uid = goog.getUid(currentWindow);

  return goog.dom.ViewportSizeMonitor.windowInstanceMap_[uid] =
      goog.dom.ViewportSizeMonitor.windowInstanceMap_[uid] ||
      new goog.dom.ViewportSizeMonitor(currentWindow);
};


/**
 * Removes and disposes a viewport size monitor for the given window if one
 * exists.
 * @param {Window=} opt_window The window whose monitor should be removed;
 *     defaults to the window in which this code is executing.
 */
goog.dom.ViewportSizeMonitor.removeInstanceForWindow = function(opt_window) {
  var uid = goog.getUid(opt_window || window);

  goog.dispose(goog.dom.ViewportSizeMonitor.windowInstanceMap_[uid]);
  delete goog.dom.ViewportSizeMonitor.windowInstanceMap_[uid];
};


/**
 * Map of window hash code to viewport size monitor for that window, if
 * created.
 * @type {Object<number,goog.dom.ViewportSizeMonitor>}
 * @private
 */
goog.dom.ViewportSizeMonitor.windowInstanceMap_ = {};


/**
 * Returns the most recently recorded size of the viewport, in pixels.  May
 * return null if no window resize event has been handled yet.
 * @return {goog.math.Size} The viewport dimensions, in pixels.
 */
goog.dom.ViewportSizeMonitor.prototype.getSize = function() {
  // Return a clone instead of the original to preserve encapsulation.
  return this.size_ ? this.size_.clone() : null;
};


/** @override */
goog.dom.ViewportSizeMonitor.prototype.disposeInternal = function() {
  goog.dom.ViewportSizeMonitor.superClass_.disposeInternal.call(this);

  if (this.listenerKey_) {
    goog.events.unlistenByKey(this.listenerKey_);
    this.listenerKey_ = null;
  }

  this.window_ = null;
  this.size_ = null;
};


/**
 * Handles window resize events by measuring the dimensions of the
 * viewport and dispatching a {@link goog.events.EventType.RESIZE} event if the
 * current dimensions are different from the previous ones.
 * @param {goog.events.Event} event The window resize event to handle.
 * @private
 */
goog.dom.ViewportSizeMonitor.prototype.handleResize_ = function(event) {
  var size = goog.dom.getViewportSize(this.window_);
  if (!goog.math.Size.equals(size, this.size_)) {
    this.size_ = size;
    this.dispatchEvent(goog.events.EventType.RESIZE);
  }
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Constant declarations for common key codes.
 *
 * @author eae@google.com (Emil A Eklund)
 * @see ../demos/keyhandler.html
 */

goog.provide('goog.events.KeyCodes');

goog.require('goog.userAgent');

goog.forwardDeclare('goog.events.BrowserEvent');


/**
 * Key codes for common characters.
 *
 * This list is not localized and therefore some of the key codes are not
 * correct for non US keyboard layouts. See comments below.
 *
 * @enum {number}
 */
goog.events.KeyCodes = {
  WIN_KEY_FF_LINUX: 0,
  MAC_ENTER: 3,
  BACKSPACE: 8,
  TAB: 9,
  NUM_CENTER: 12,  // NUMLOCK on FF/Safari Mac
  ENTER: 13,
  SHIFT: 16,
  CTRL: 17,
  ALT: 18,
  PAUSE: 19,
  CAPS_LOCK: 20,
  ESC: 27,
  SPACE: 32,
  PAGE_UP: 33,     // also NUM_NORTH_EAST
  PAGE_DOWN: 34,   // also NUM_SOUTH_EAST
  END: 35,         // also NUM_SOUTH_WEST
  HOME: 36,        // also NUM_NORTH_WEST
  LEFT: 37,        // also NUM_WEST
  UP: 38,          // also NUM_NORTH
  RIGHT: 39,       // also NUM_EAST
  DOWN: 40,        // also NUM_SOUTH
  PLUS_SIGN: 43,   // NOT numpad plus
  PRINT_SCREEN: 44,
  INSERT: 45,      // also NUM_INSERT
  DELETE: 46,      // also NUM_DELETE
  ZERO: 48,
  ONE: 49,
  TWO: 50,
  THREE: 51,
  FOUR: 52,
  FIVE: 53,
  SIX: 54,
  SEVEN: 55,
  EIGHT: 56,
  NINE: 57,
  FF_SEMICOLON: 59, // Firefox (Gecko) fires this for semicolon instead of 186
  FF_EQUALS: 61, // Firefox (Gecko) fires this for equals instead of 187
  FF_DASH: 173, // Firefox (Gecko) fires this for dash instead of 189
  QUESTION_MARK: 63, // needs localization
  AT_SIGN: 64,
  A: 65,
  B: 66,
  C: 67,
  D: 68,
  E: 69,
  F: 70,
  G: 71,
  H: 72,
  I: 73,
  J: 74,
  K: 75,
  L: 76,
  M: 77,
  N: 78,
  O: 79,
  P: 80,
  Q: 81,
  R: 82,
  S: 83,
  T: 84,
  U: 85,
  V: 86,
  W: 87,
  X: 88,
  Y: 89,
  Z: 90,
  META: 91, // WIN_KEY_LEFT
  WIN_KEY_RIGHT: 92,
  CONTEXT_MENU: 93,
  NUM_ZERO: 96,
  NUM_ONE: 97,
  NUM_TWO: 98,
  NUM_THREE: 99,
  NUM_FOUR: 100,
  NUM_FIVE: 101,
  NUM_SIX: 102,
  NUM_SEVEN: 103,
  NUM_EIGHT: 104,
  NUM_NINE: 105,
  NUM_MULTIPLY: 106,
  NUM_PLUS: 107,
  NUM_MINUS: 109,
  NUM_PERIOD: 110,
  NUM_DIVISION: 111,
  F1: 112,
  F2: 113,
  F3: 114,
  F4: 115,
  F5: 116,
  F6: 117,
  F7: 118,
  F8: 119,
  F9: 120,
  F10: 121,
  F11: 122,
  F12: 123,
  NUMLOCK: 144,
  SCROLL_LOCK: 145,

  // OS-specific media keys like volume controls and browser controls.
  FIRST_MEDIA_KEY: 166,
  LAST_MEDIA_KEY: 183,

  SEMICOLON: 186,            // needs localization
  DASH: 189,                 // needs localization
  EQUALS: 187,               // needs localization
  COMMA: 188,                // needs localization
  PERIOD: 190,               // needs localization
  SLASH: 191,                // needs localization
  APOSTROPHE: 192,           // needs localization
  TILDE: 192,                // needs localization
  SINGLE_QUOTE: 222,         // needs localization
  OPEN_SQUARE_BRACKET: 219,  // needs localization
  BACKSLASH: 220,            // needs localization
  CLOSE_SQUARE_BRACKET: 221, // needs localization
  WIN_KEY: 224,
  MAC_FF_META: 224, // Firefox (Gecko) fires this for the meta key instead of 91
  MAC_WK_CMD_LEFT: 91,  // WebKit Left Command key fired, same as META
  MAC_WK_CMD_RIGHT: 93, // WebKit Right Command key fired, different from META
  WIN_IME: 229,

  // "Reserved for future use". Some programs (e.g. the SlingPlayer 2.4 ActiveX
  // control) fire this as a hacky way to disable screensavers.
  VK_NONAME: 252,

  // We've seen users whose machines fire this keycode at regular one
  // second intervals. The common thread among these users is that
  // they're all using Dell Inspiron laptops, so we suspect that this
  // indicates a hardware/bios problem.
  // http://en.community.dell.com/support-forums/laptop/f/3518/p/19285957/19523128.aspx
  PHANTOM: 255
};


/**
 * Returns true if the event contains a text modifying key.
 * @param {goog.events.BrowserEvent} e A key event.
 * @return {boolean} Whether it's a text modifying key.
 */
goog.events.KeyCodes.isTextModifyingKeyEvent = function(e) {
  if (e.altKey && !e.ctrlKey ||
      e.metaKey ||
      // Function keys don't generate text
      e.keyCode >= goog.events.KeyCodes.F1 &&
      e.keyCode <= goog.events.KeyCodes.F12) {
    return false;
  }

  // The following keys are quite harmless, even in combination with
  // CTRL, ALT or SHIFT.
  switch (e.keyCode) {
    case goog.events.KeyCodes.ALT:
    case goog.events.KeyCodes.CAPS_LOCK:
    case goog.events.KeyCodes.CONTEXT_MENU:
    case goog.events.KeyCodes.CTRL:
    case goog.events.KeyCodes.DOWN:
    case goog.events.KeyCodes.END:
    case goog.events.KeyCodes.ESC:
    case goog.events.KeyCodes.HOME:
    case goog.events.KeyCodes.INSERT:
    case goog.events.KeyCodes.LEFT:
    case goog.events.KeyCodes.MAC_FF_META:
    case goog.events.KeyCodes.META:
    case goog.events.KeyCodes.NUMLOCK:
    case goog.events.KeyCodes.NUM_CENTER:
    case goog.events.KeyCodes.PAGE_DOWN:
    case goog.events.KeyCodes.PAGE_UP:
    case goog.events.KeyCodes.PAUSE:
    case goog.events.KeyCodes.PHANTOM:
    case goog.events.KeyCodes.PRINT_SCREEN:
    case goog.events.KeyCodes.RIGHT:
    case goog.events.KeyCodes.SCROLL_LOCK:
    case goog.events.KeyCodes.SHIFT:
    case goog.events.KeyCodes.UP:
    case goog.events.KeyCodes.VK_NONAME:
    case goog.events.KeyCodes.WIN_KEY:
    case goog.events.KeyCodes.WIN_KEY_RIGHT:
      return false;
    case goog.events.KeyCodes.WIN_KEY_FF_LINUX:
      return !goog.userAgent.GECKO;
    default:
      return e.keyCode < goog.events.KeyCodes.FIRST_MEDIA_KEY ||
          e.keyCode > goog.events.KeyCodes.LAST_MEDIA_KEY;
  }
};


/**
 * Returns true if the key fires a keypress event in the current browser.
 *
 * Accoridng to MSDN [1] IE only fires keypress events for the following keys:
 * - Letters: A - Z (uppercase and lowercase)
 * - Numerals: 0 - 9
 * - Symbols: ! @ # $ % ^ & * ( ) _ - + = < [ ] { } , . / ? \ | ' ` " ~
 * - System: ESC, SPACEBAR, ENTER
 *
 * That's not entirely correct though, for instance there's no distinction
 * between upper and lower case letters.
 *
 * [1] http://msdn2.microsoft.com/en-us/library/ms536939(VS.85).aspx)
 *
 * Safari is similar to IE, but does not fire keypress for ESC.
 *
 * Additionally, IE6 does not fire keydown or keypress events for letters when
 * the control or alt keys are held down and the shift key is not. IE7 does
 * fire keydown in these cases, though, but not keypress.
 *
 * @param {number} keyCode A key code.
 * @param {number=} opt_heldKeyCode Key code of a currently-held key.
 * @param {boolean=} opt_shiftKey Whether the shift key is held down.
 * @param {boolean=} opt_ctrlKey Whether the control key is held down.
 * @param {boolean=} opt_altKey Whether the alt key is held down.
 * @return {boolean} Whether it's a key that fires a keypress event.
 */
goog.events.KeyCodes.firesKeyPressEvent = function(keyCode, opt_heldKeyCode,
    opt_shiftKey, opt_ctrlKey, opt_altKey) {
  if (!goog.userAgent.IE && !goog.userAgent.EDGE &&
      !(goog.userAgent.WEBKIT && goog.userAgent.isVersionOrHigher('525'))) {
    return true;
  }

  if (goog.userAgent.MAC && opt_altKey) {
    return goog.events.KeyCodes.isCharacterKey(keyCode);
  }

  // Alt but not AltGr which is represented as Alt+Ctrl.
  if (opt_altKey && !opt_ctrlKey) {
    return false;
  }

  // Saves Ctrl or Alt + key for IE and WebKit 525+, which won't fire keypress.
  // Non-IE browsers and WebKit prior to 525 won't get this far so no need to
  // check the user agent.
  if (goog.isNumber(opt_heldKeyCode)) {
    opt_heldKeyCode = goog.events.KeyCodes.normalizeKeyCode(opt_heldKeyCode);
  }
  if (!opt_shiftKey &&
      (opt_heldKeyCode == goog.events.KeyCodes.CTRL ||
       opt_heldKeyCode == goog.events.KeyCodes.ALT ||
       goog.userAgent.MAC &&
       opt_heldKeyCode == goog.events.KeyCodes.META)) {
    return false;
  }

  // Some keys with Ctrl/Shift do not issue keypress in WEBKIT.
  if ((goog.userAgent.WEBKIT || goog.userAgent.EDGE) &&
      opt_ctrlKey && opt_shiftKey) {
    switch (keyCode) {
      case goog.events.KeyCodes.BACKSLASH:
      case goog.events.KeyCodes.OPEN_SQUARE_BRACKET:
      case goog.events.KeyCodes.CLOSE_SQUARE_BRACKET:
      case goog.events.KeyCodes.TILDE:
      case goog.events.KeyCodes.SEMICOLON:
      case goog.events.KeyCodes.DASH:
      case goog.events.KeyCodes.EQUALS:
      case goog.events.KeyCodes.COMMA:
      case goog.events.KeyCodes.PERIOD:
      case goog.events.KeyCodes.SLASH:
      case goog.events.KeyCodes.APOSTROPHE:
      case goog.events.KeyCodes.SINGLE_QUOTE:
        return false;
    }
  }

  // When Ctrl+<somekey> is held in IE, it only fires a keypress once, but it
  // continues to fire keydown events as the event repeats.
  if (goog.userAgent.IE && opt_ctrlKey && opt_heldKeyCode == keyCode) {
    return false;
  }

  switch (keyCode) {
    case goog.events.KeyCodes.ENTER:
      return true;
    case goog.events.KeyCodes.ESC:
      return !(goog.userAgent.WEBKIT || goog.userAgent.EDGE);
  }

  return goog.events.KeyCodes.isCharacterKey(keyCode);
};


/**
 * Returns true if the key produces a character.
 * This does not cover characters on non-US keyboards (Russian, Hebrew, etc.).
 *
 * @param {number} keyCode A key code.
 * @return {boolean} Whether it's a character key.
 */
goog.events.KeyCodes.isCharacterKey = function(keyCode) {
  if (keyCode >= goog.events.KeyCodes.ZERO &&
      keyCode <= goog.events.KeyCodes.NINE) {
    return true;
  }

  if (keyCode >= goog.events.KeyCodes.NUM_ZERO &&
      keyCode <= goog.events.KeyCodes.NUM_MULTIPLY) {
    return true;
  }

  if (keyCode >= goog.events.KeyCodes.A &&
      keyCode <= goog.events.KeyCodes.Z) {
    return true;
  }

  // Safari sends zero key code for non-latin characters.
  if ((goog.userAgent.WEBKIT || goog.userAgent.EDGE) && keyCode == 0) {
    return true;
  }

  switch (keyCode) {
    case goog.events.KeyCodes.SPACE:
    case goog.events.KeyCodes.PLUS_SIGN:
    case goog.events.KeyCodes.QUESTION_MARK:
    case goog.events.KeyCodes.AT_SIGN:
    case goog.events.KeyCodes.NUM_PLUS:
    case goog.events.KeyCodes.NUM_MINUS:
    case goog.events.KeyCodes.NUM_PERIOD:
    case goog.events.KeyCodes.NUM_DIVISION:
    case goog.events.KeyCodes.SEMICOLON:
    case goog.events.KeyCodes.FF_SEMICOLON:
    case goog.events.KeyCodes.DASH:
    case goog.events.KeyCodes.EQUALS:
    case goog.events.KeyCodes.FF_EQUALS:
    case goog.events.KeyCodes.COMMA:
    case goog.events.KeyCodes.PERIOD:
    case goog.events.KeyCodes.SLASH:
    case goog.events.KeyCodes.APOSTROPHE:
    case goog.events.KeyCodes.SINGLE_QUOTE:
    case goog.events.KeyCodes.OPEN_SQUARE_BRACKET:
    case goog.events.KeyCodes.BACKSLASH:
    case goog.events.KeyCodes.CLOSE_SQUARE_BRACKET:
      return true;
    default:
      return false;
  }
};


/**
 * Normalizes key codes from OS/Browser-specific value to the general one.
 * @param {number} keyCode The native key code.
 * @return {number} The normalized key code.
 */
goog.events.KeyCodes.normalizeKeyCode = function(keyCode) {
  if (goog.userAgent.GECKO) {
    return goog.events.KeyCodes.normalizeGeckoKeyCode(keyCode);
  } else if (goog.userAgent.MAC && goog.userAgent.WEBKIT) {
    return goog.events.KeyCodes.normalizeMacWebKitKeyCode(keyCode);
  } else {
    return keyCode;
  }
};


/**
 * Normalizes key codes from their Gecko-specific value to the general one.
 * @param {number} keyCode The native key code.
 * @return {number} The normalized key code.
 */
goog.events.KeyCodes.normalizeGeckoKeyCode = function(keyCode) {
  switch (keyCode) {
    case goog.events.KeyCodes.FF_EQUALS:
      return goog.events.KeyCodes.EQUALS;
    case goog.events.KeyCodes.FF_SEMICOLON:
      return goog.events.KeyCodes.SEMICOLON;
    case goog.events.KeyCodes.FF_DASH:
      return goog.events.KeyCodes.DASH;
    case goog.events.KeyCodes.MAC_FF_META:
      return goog.events.KeyCodes.META;
    case goog.events.KeyCodes.WIN_KEY_FF_LINUX:
      return goog.events.KeyCodes.WIN_KEY;
    default:
      return keyCode;
  }
};


/**
 * Normalizes key codes from their Mac WebKit-specific value to the general one.
 * @param {number} keyCode The native key code.
 * @return {number} The normalized key code.
 */
goog.events.KeyCodes.normalizeMacWebKitKeyCode = function(keyCode) {
  switch (keyCode) {
    case goog.events.KeyCodes.MAC_WK_CMD_RIGHT:  // 93
      return goog.events.KeyCodes.META;          // 91
    default:
      return keyCode;
  }
};

// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview This file contains a class for working with keyboard events
 * that repeat consistently across browsers and platforms. It also unifies the
 * key code so that it is the same in all browsers and platforms.
 *
 * Different web browsers have very different keyboard event handling. Most
 * importantly is that only certain browsers repeat keydown events:
 * IE, Opera, FF/Win32, and Safari 3 repeat keydown events.
 * FF/Mac and Safari 2 do not.
 *
 * For the purposes of this code, "Safari 3" means WebKit 525+, when WebKit
 * decided that they should try to match IE's key handling behavior.
 * Safari 3.0.4, which shipped with Leopard (WebKit 523), has the
 * Safari 2 behavior.
 *
 * Firefox, Safari, Opera prevent on keypress
 *
 * IE prevents on keydown
 *
 * Firefox does not fire keypress for shift, ctrl, alt
 * Firefox does fire keydown for shift, ctrl, alt, meta
 * Firefox does not repeat keydown for shift, ctrl, alt, meta
 *
 * Firefox does not fire keypress for up and down in an input
 *
 * Opera fires keypress for shift, ctrl, alt, meta
 * Opera does not repeat keypress for shift, ctrl, alt, meta
 *
 * Safari 2 and 3 do not fire keypress for shift, ctrl, alt
 * Safari 2 does not fire keydown for shift, ctrl, alt
 * Safari 3 *does* fire keydown for shift, ctrl, alt
 *
 * IE provides the keycode for keyup/down events and the charcode (in the
 * keycode field) for keypress.
 *
 * Mozilla provides the keycode for keyup/down and the charcode for keypress
 * unless it's a non text modifying key in which case the keycode is provided.
 *
 * Safari 3 provides the keycode and charcode for all events.
 *
 * Opera provides the keycode for keyup/down event and either the charcode or
 * the keycode (in the keycode field) for keypress events.
 *
 * Firefox x11 doesn't fire keydown events if a another key is already held down
 * until the first key is released. This can cause a key event to be fired with
 * a keyCode for the first key and a charCode for the second key.
 *
 * Safari in keypress
 *
 *        charCode keyCode which
 * ENTER:       13      13    13
 * F1:       63236   63236 63236
 * F8:       63243   63243 63243
 * ...
 * p:          112     112   112
 * P:           80      80    80
 *
 * Firefox, keypress:
 *
 *        charCode keyCode which
 * ENTER:        0      13    13
 * F1:           0     112     0
 * F8:           0     119     0
 * ...
 * p:          112       0   112
 * P:           80       0    80
 *
 * Opera, Mac+Win32, keypress:
 *
 *         charCode keyCode which
 * ENTER: undefined      13    13
 * F1:    undefined     112     0
 * F8:    undefined     119     0
 * ...
 * p:     undefined     112   112
 * P:     undefined      80    80
 *
 * IE7, keydown
 *
 *         charCode keyCode     which
 * ENTER: undefined      13 undefined
 * F1:    undefined     112 undefined
 * F8:    undefined     119 undefined
 * ...
 * p:     undefined      80 undefined
 * P:     undefined      80 undefined
 *
 * @author arv@google.com (Erik Arvidsson)
 * @author eae@google.com (Emil A Eklund)
 * @see ../demos/keyhandler.html
 */

goog.provide('goog.events.KeyEvent');
goog.provide('goog.events.KeyHandler');
goog.provide('goog.events.KeyHandler.EventType');

goog.require('goog.events');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.events.KeyCodes');
goog.require('goog.userAgent');



/**
 * A wrapper around an element that you want to listen to keyboard events on.
 * @param {Element|Document=} opt_element The element or document to listen on.
 * @param {boolean=} opt_capture Whether to listen for browser events in
 *     capture phase (defaults to false).
 * @constructor
 * @extends {goog.events.EventTarget}
 * @final
 */
goog.events.KeyHandler = function(opt_element, opt_capture) {
  goog.events.EventTarget.call(this);

  if (opt_element) {
    this.attach(opt_element, opt_capture);
  }
};
goog.inherits(goog.events.KeyHandler, goog.events.EventTarget);


/**
 * This is the element that we will listen to the real keyboard events on.
 * @type {Element|Document|null}
 * @private
 */
goog.events.KeyHandler.prototype.element_ = null;


/**
 * The key for the key press listener.
 * @type {goog.events.Key}
 * @private
 */
goog.events.KeyHandler.prototype.keyPressKey_ = null;


/**
 * The key for the key down listener.
 * @type {goog.events.Key}
 * @private
 */
goog.events.KeyHandler.prototype.keyDownKey_ = null;


/**
 * The key for the key up listener.
 * @type {goog.events.Key}
 * @private
 */
goog.events.KeyHandler.prototype.keyUpKey_ = null;


/**
 * Used to detect keyboard repeat events.
 * @private
 * @type {number}
 */
goog.events.KeyHandler.prototype.lastKey_ = -1;


/**
 * Keycode recorded for key down events. As most browsers don't report the
 * keycode in the key press event we need to record it in the key down phase.
 * @private
 * @type {number}
 */
goog.events.KeyHandler.prototype.keyCode_ = -1;


/**
 * Alt key recorded for key down events. FF on Mac does not report the alt key
 * flag in the key press event, we need to record it in the key down phase.
 * @type {boolean}
 * @private
 */
goog.events.KeyHandler.prototype.altKey_ = false;


/**
 * Enum type for the events fired by the key handler
 * @enum {string}
 */
goog.events.KeyHandler.EventType = {
  KEY: 'key'
};


/**
 * An enumeration of key codes that Safari 2 does incorrectly
 * @type {Object}
 * @private
 */
goog.events.KeyHandler.safariKey_ = {
  '3': goog.events.KeyCodes.ENTER, // 13
  '12': goog.events.KeyCodes.NUMLOCK, // 144
  '63232': goog.events.KeyCodes.UP, // 38
  '63233': goog.events.KeyCodes.DOWN, // 40
  '63234': goog.events.KeyCodes.LEFT, // 37
  '63235': goog.events.KeyCodes.RIGHT, // 39
  '63236': goog.events.KeyCodes.F1, // 112
  '63237': goog.events.KeyCodes.F2, // 113
  '63238': goog.events.KeyCodes.F3, // 114
  '63239': goog.events.KeyCodes.F4, // 115
  '63240': goog.events.KeyCodes.F5, // 116
  '63241': goog.events.KeyCodes.F6, // 117
  '63242': goog.events.KeyCodes.F7, // 118
  '63243': goog.events.KeyCodes.F8, // 119
  '63244': goog.events.KeyCodes.F9, // 120
  '63245': goog.events.KeyCodes.F10, // 121
  '63246': goog.events.KeyCodes.F11, // 122
  '63247': goog.events.KeyCodes.F12, // 123
  '63248': goog.events.KeyCodes.PRINT_SCREEN, // 44
  '63272': goog.events.KeyCodes.DELETE, // 46
  '63273': goog.events.KeyCodes.HOME, // 36
  '63275': goog.events.KeyCodes.END, // 35
  '63276': goog.events.KeyCodes.PAGE_UP, // 33
  '63277': goog.events.KeyCodes.PAGE_DOWN, // 34
  '63289': goog.events.KeyCodes.NUMLOCK, // 144
  '63302': goog.events.KeyCodes.INSERT // 45
};


/**
 * An enumeration of key identifiers currently part of the W3C draft for DOM3
 * and their mappings to keyCodes.
 * http://www.w3.org/TR/DOM-Level-3-Events/keyset.html#KeySet-Set
 * This is currently supported in Safari and should be platform independent.
 * @type {Object}
 * @private
 */
goog.events.KeyHandler.keyIdentifier_ = {
  'Up': goog.events.KeyCodes.UP, // 38
  'Down': goog.events.KeyCodes.DOWN, // 40
  'Left': goog.events.KeyCodes.LEFT, // 37
  'Right': goog.events.KeyCodes.RIGHT, // 39
  'Enter': goog.events.KeyCodes.ENTER, // 13
  'F1': goog.events.KeyCodes.F1, // 112
  'F2': goog.events.KeyCodes.F2, // 113
  'F3': goog.events.KeyCodes.F3, // 114
  'F4': goog.events.KeyCodes.F4, // 115
  'F5': goog.events.KeyCodes.F5, // 116
  'F6': goog.events.KeyCodes.F6, // 117
  'F7': goog.events.KeyCodes.F7, // 118
  'F8': goog.events.KeyCodes.F8, // 119
  'F9': goog.events.KeyCodes.F9, // 120
  'F10': goog.events.KeyCodes.F10, // 121
  'F11': goog.events.KeyCodes.F11, // 122
  'F12': goog.events.KeyCodes.F12, // 123
  'U+007F': goog.events.KeyCodes.DELETE, // 46
  'Home': goog.events.KeyCodes.HOME, // 36
  'End': goog.events.KeyCodes.END, // 35
  'PageUp': goog.events.KeyCodes.PAGE_UP, // 33
  'PageDown': goog.events.KeyCodes.PAGE_DOWN, // 34
  'Insert': goog.events.KeyCodes.INSERT // 45
};


/**
 * If true, the KeyEvent fires on keydown. Otherwise, it fires on keypress.
 *
 * @type {boolean}
 * @private
 */
goog.events.KeyHandler.USES_KEYDOWN_ = goog.userAgent.IE ||
    goog.userAgent.EDGE ||
    goog.userAgent.WEBKIT && goog.userAgent.isVersionOrHigher('525');


/**
 * If true, the alt key flag is saved during the key down and reused when
 * handling the key press. FF on Mac does not set the alt flag in the key press
 * event.
 * @type {boolean}
 * @private
 */
goog.events.KeyHandler.SAVE_ALT_FOR_KEYPRESS_ = goog.userAgent.MAC &&
    goog.userAgent.GECKO;


/**
 * Records the keycode for browsers that only returns the keycode for key up/
 * down events. For browser/key combinations that doesn't trigger a key pressed
 * event it also fires the patched key event.
 * @param {goog.events.BrowserEvent} e The key down event.
 * @private
 */
goog.events.KeyHandler.prototype.handleKeyDown_ = function(e) {
  // Ctrl-Tab and Alt-Tab can cause the focus to be moved to another window
  // before we've caught a key-up event.  If the last-key was one of these we
  // reset the state.
  if (goog.userAgent.WEBKIT || goog.userAgent.EDGE) {
    if (this.lastKey_ == goog.events.KeyCodes.CTRL && !e.ctrlKey ||
        this.lastKey_ == goog.events.KeyCodes.ALT && !e.altKey ||
        goog.userAgent.MAC &&
        this.lastKey_ == goog.events.KeyCodes.META && !e.metaKey) {
      this.resetState();
    }
  }

  if (this.lastKey_ == -1) {
    if (e.ctrlKey && e.keyCode != goog.events.KeyCodes.CTRL) {
      this.lastKey_ = goog.events.KeyCodes.CTRL;
    } else if (e.altKey && e.keyCode != goog.events.KeyCodes.ALT) {
      this.lastKey_ = goog.events.KeyCodes.ALT;
    } else if (e.metaKey && e.keyCode != goog.events.KeyCodes.META) {
      this.lastKey_ = goog.events.KeyCodes.META;
    }
  }

  if (goog.events.KeyHandler.USES_KEYDOWN_ &&
      !goog.events.KeyCodes.firesKeyPressEvent(e.keyCode,
          this.lastKey_, e.shiftKey, e.ctrlKey, e.altKey)) {
    this.handleEvent(e);
  } else {
    this.keyCode_ = goog.events.KeyCodes.normalizeKeyCode(e.keyCode);
    if (goog.events.KeyHandler.SAVE_ALT_FOR_KEYPRESS_) {
      this.altKey_ = e.altKey;
    }
  }
};


/**
 * Resets the stored previous values. Needed to be called for webkit which will
 * not generate a key up for meta key operations. This should only be called
 * when having finished with repeat key possiblities.
 */
goog.events.KeyHandler.prototype.resetState = function() {
  this.lastKey_ = -1;
  this.keyCode_ = -1;
};


/**
 * Clears the stored previous key value, resetting the key repeat status. Uses
 * -1 because the Safari 3 Windows beta reports 0 for certain keys (like Home
 * and End.)
 * @param {goog.events.BrowserEvent} e The keyup event.
 * @private
 */
goog.events.KeyHandler.prototype.handleKeyup_ = function(e) {
  this.resetState();
  this.altKey_ = e.altKey;
};


/**
 * Handles the events on the element.
 * @param {goog.events.BrowserEvent} e  The keyboard event sent from the
 *     browser.
 */
goog.events.KeyHandler.prototype.handleEvent = function(e) {
  var be = e.getBrowserEvent();
  var keyCode, charCode;
  var altKey = be.altKey;

  // IE reports the character code in the keyCode field for keypress events.
  // There are two exceptions however, Enter and Escape.
  if (goog.userAgent.IE && e.type == goog.events.EventType.KEYPRESS) {
    keyCode = this.keyCode_;
    charCode = keyCode != goog.events.KeyCodes.ENTER &&
        keyCode != goog.events.KeyCodes.ESC ?
            be.keyCode : 0;

  // Safari reports the character code in the keyCode field for keypress
  // events but also has a charCode field.
  } else if ((goog.userAgent.WEBKIT || goog.userAgent.EDGE) &&
      e.type == goog.events.EventType.KEYPRESS) {
    keyCode = this.keyCode_;
    charCode = be.charCode >= 0 && be.charCode < 63232 &&
        goog.events.KeyCodes.isCharacterKey(keyCode) ?
            be.charCode : 0;

  // Opera reports the keycode or the character code in the keyCode field.
  } else if (goog.userAgent.OPERA && !goog.userAgent.WEBKIT) {
    keyCode = this.keyCode_;
    charCode = goog.events.KeyCodes.isCharacterKey(keyCode) ?
        be.keyCode : 0;

  // Mozilla reports the character code in the charCode field.
  } else {
    keyCode = be.keyCode || this.keyCode_;
    charCode = be.charCode || 0;
    if (goog.events.KeyHandler.SAVE_ALT_FOR_KEYPRESS_) {
      altKey = this.altKey_;
    }
    // On the Mac, shift-/ triggers a question mark char code and no key code
    // (normalized to WIN_KEY), so we synthesize the latter.
    if (goog.userAgent.MAC &&
        charCode == goog.events.KeyCodes.QUESTION_MARK &&
        keyCode == goog.events.KeyCodes.WIN_KEY) {
      keyCode = goog.events.KeyCodes.SLASH;
    }
  }

  keyCode = goog.events.KeyCodes.normalizeKeyCode(keyCode);
  var key = keyCode;
  var keyIdentifier = be.keyIdentifier;

  // Correct the key value for certain browser-specific quirks.
  if (keyCode) {
    if (keyCode >= 63232 && keyCode in goog.events.KeyHandler.safariKey_) {
      // NOTE(nicksantos): Safari 3 has fixed this problem,
      // this is only needed for Safari 2.
      key = goog.events.KeyHandler.safariKey_[keyCode];
    } else {

      // Safari returns 25 for Shift+Tab instead of 9.
      if (keyCode == 25 && e.shiftKey) {
        key = 9;
      }
    }
  } else if (keyIdentifier &&
             keyIdentifier in goog.events.KeyHandler.keyIdentifier_) {
    // This is needed for Safari Windows because it currently doesn't give a
    // keyCode/which for non printable keys.
    key = goog.events.KeyHandler.keyIdentifier_[keyIdentifier];
  }

  // If we get the same keycode as a keydown/keypress without having seen a
  // keyup event, then this event was caused by key repeat.
  var repeat = key == this.lastKey_;
  this.lastKey_ = key;

  var event = new goog.events.KeyEvent(key, charCode, repeat, be);
  event.altKey = altKey;
  this.dispatchEvent(event);
};


/**
 * Returns the element listened on for the real keyboard events.
 * @return {Element|Document|null} The element listened on for the real
 *     keyboard events.
 */
goog.events.KeyHandler.prototype.getElement = function() {
  return this.element_;
};


/**
 * Adds the proper key event listeners to the element.
 * @param {Element|Document} element The element to listen on.
 * @param {boolean=} opt_capture Whether to listen for browser events in
 *     capture phase (defaults to false).
 */
goog.events.KeyHandler.prototype.attach = function(element, opt_capture) {
  if (this.keyUpKey_) {
    this.detach();
  }

  this.element_ = element;

  this.keyPressKey_ = goog.events.listen(this.element_,
                                         goog.events.EventType.KEYPRESS,
                                         this,
                                         opt_capture);

  // Most browsers (Safari 2 being the notable exception) doesn't include the
  // keyCode in keypress events (IE has the char code in the keyCode field and
  // Mozilla only included the keyCode if there's no charCode). Thus we have to
  // listen for keydown to capture the keycode.
  this.keyDownKey_ = goog.events.listen(this.element_,
                                        goog.events.EventType.KEYDOWN,
                                        this.handleKeyDown_,
                                        opt_capture,
                                        this);


  this.keyUpKey_ = goog.events.listen(this.element_,
                                      goog.events.EventType.KEYUP,
                                      this.handleKeyup_,
                                      opt_capture,
                                      this);
};


/**
 * Removes the listeners that may exist.
 */
goog.events.KeyHandler.prototype.detach = function() {
  if (this.keyPressKey_) {
    goog.events.unlistenByKey(this.keyPressKey_);
    goog.events.unlistenByKey(this.keyDownKey_);
    goog.events.unlistenByKey(this.keyUpKey_);
    this.keyPressKey_ = null;
    this.keyDownKey_ = null;
    this.keyUpKey_ = null;
  }
  this.element_ = null;
  this.lastKey_ = -1;
  this.keyCode_ = -1;
};


/** @override */
goog.events.KeyHandler.prototype.disposeInternal = function() {
  goog.events.KeyHandler.superClass_.disposeInternal.call(this);
  this.detach();
};



/**
 * This class is used for the goog.events.KeyHandler.EventType.KEY event and
 * it overrides the key code with the fixed key code.
 * @param {number} keyCode The adjusted key code.
 * @param {number} charCode The unicode character code.
 * @param {boolean} repeat Whether this event was generated by keyboard repeat.
 * @param {Event} browserEvent Browser event object.
 * @constructor
 * @extends {goog.events.BrowserEvent}
 * @final
 */
goog.events.KeyEvent = function(keyCode, charCode, repeat, browserEvent) {
  goog.events.BrowserEvent.call(this, browserEvent);
  this.type = goog.events.KeyHandler.EventType.KEY;

  /**
   * Keycode of key press.
   * @type {number}
   */
  this.keyCode = keyCode;

  /**
   * Unicode character code.
   * @type {number}
   */
  this.charCode = charCode;

  /**
   * True if this event was generated by keyboard auto-repeat (i.e., the user is
   * holding the key down.)
   * @type {boolean}
   */
  this.repeat = repeat;
};
goog.inherits(goog.events.KeyEvent, goog.events.BrowserEvent);

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview This event wrapper will dispatch an event when the user uses
 * the mouse wheel to scroll an element. You can get the direction by checking
 * the deltaX and deltaY properties of the event.
 *
 * This class aims to smooth out inconsistencies between browser platforms with
 * regards to mousewheel events, but we do not cover every possible
 * software/hardware combination out there, some of which occasionally produce
 * very large deltas in mousewheel events. If your application wants to guard
 * against extremely large deltas, use the setMaxDeltaX and setMaxDeltaY APIs
 * to set maximum values that make sense for your application.
 *
 * @author arv@google.com (Erik Arvidsson)
 * @see ../demos/mousewheelhandler.html
 */

goog.provide('goog.events.MouseWheelEvent');
goog.provide('goog.events.MouseWheelHandler');
goog.provide('goog.events.MouseWheelHandler.EventType');

goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.EventTarget');
goog.require('goog.math');
goog.require('goog.style');
goog.require('goog.userAgent');



/**
 * This event handler allows you to catch mouse wheel events in a consistent
 * manner.
 * @param {Element|Document} element The element to listen to the mouse wheel
 *     event on.
 * @param {boolean=} opt_capture Whether to handle the mouse wheel event in
 *     capture phase.
 * @constructor
 * @extends {goog.events.EventTarget}
 */
goog.events.MouseWheelHandler = function(element, opt_capture) {
  goog.events.EventTarget.call(this);

  /**
   * This is the element that we will listen to the real mouse wheel events on.
   * @type {Element|Document}
   * @private
   */
  this.element_ = element;

  var rtlElement = goog.dom.isElement(this.element_) ?
      /** @type {Element} */ (this.element_) :
      (this.element_ ? /** @type {Document} */ (this.element_).body : null);

  /**
   * True if the element exists and is RTL, false otherwise.
   * @type {boolean}
   * @private
   */
  this.isRtl_ = !!rtlElement && goog.style.isRightToLeft(rtlElement);

  var type = goog.userAgent.GECKO ? 'DOMMouseScroll' : 'mousewheel';

  /**
   * The key returned from the goog.events.listen.
   * @type {goog.events.Key}
   * @private
   */
  this.listenKey_ = goog.events.listen(this.element_, type, this, opt_capture);
};
goog.inherits(goog.events.MouseWheelHandler, goog.events.EventTarget);


/**
 * Enum type for the events fired by the mouse wheel handler.
 * @enum {string}
 */
goog.events.MouseWheelHandler.EventType = {
  MOUSEWHEEL: 'mousewheel'
};


/**
 * Optional maximum magnitude for x delta on each mousewheel event.
 * @type {number|undefined}
 * @private
 */
goog.events.MouseWheelHandler.prototype.maxDeltaX_;


/**
 * Optional maximum magnitude for y delta on each mousewheel event.
 * @type {number|undefined}
 * @private
 */
goog.events.MouseWheelHandler.prototype.maxDeltaY_;


/**
 * @param {number} maxDeltaX Maximum magnitude for x delta on each mousewheel
 *     event. Should be non-negative.
 */
goog.events.MouseWheelHandler.prototype.setMaxDeltaX = function(maxDeltaX) {
  this.maxDeltaX_ = maxDeltaX;
};


/**
 * @param {number} maxDeltaY Maximum magnitude for y delta on each mousewheel
 *     event. Should be non-negative.
 */
goog.events.MouseWheelHandler.prototype.setMaxDeltaY = function(maxDeltaY) {
  this.maxDeltaY_ = maxDeltaY;
};


/**
 * Handles the events on the element.
 * @param {goog.events.BrowserEvent} e The underlying browser event.
 */
goog.events.MouseWheelHandler.prototype.handleEvent = function(e) {
  var deltaX = 0;
  var deltaY = 0;
  var detail = 0;
  var be = e.getBrowserEvent();
  if (be.type == 'mousewheel') {
    var wheelDeltaScaleFactor = 1;
    if (goog.userAgent.IE ||
        goog.userAgent.WEBKIT &&
        (goog.userAgent.WINDOWS || goog.userAgent.isVersionOrHigher('532.0'))) {
      // In IE we get a multiple of 120; we adjust to a multiple of 3 to
      // represent number of lines scrolled (like Gecko).
      // Newer versions of Webkit match IE behavior, and WebKit on
      // Windows also matches IE behavior.
      // See bug https://bugs.webkit.org/show_bug.cgi?id=24368
      wheelDeltaScaleFactor = 40;
    }

    detail = goog.events.MouseWheelHandler.smartScale_(
        -be.wheelDelta, wheelDeltaScaleFactor);
    if (goog.isDef(be.wheelDeltaX)) {
      // Webkit has two properties to indicate directional scroll, and
      // can scroll both directions at once.
      deltaX = goog.events.MouseWheelHandler.smartScale_(
          -be.wheelDeltaX, wheelDeltaScaleFactor);
      deltaY = goog.events.MouseWheelHandler.smartScale_(
          -be.wheelDeltaY, wheelDeltaScaleFactor);
    } else {
      deltaY = detail;
    }

    // Historical note: Opera (pre 9.5) used to negate the detail value.
  } else { // Gecko
    // Gecko returns multiple of 3 (representing the number of lines scrolled)
    detail = be.detail;

    // Gecko sometimes returns really big values if the user changes settings to
    // scroll a whole page per scroll
    if (detail > 100) {
      detail = 3;
    } else if (detail < -100) {
      detail = -3;
    }

    // Firefox 3.1 adds an axis field to the event to indicate direction of
    // scroll.  See https://developer.mozilla.org/en/Gecko-Specific_DOM_Events
    if (goog.isDef(be.axis) && be.axis === be.HORIZONTAL_AXIS) {
      deltaX = detail;
    } else {
      deltaY = detail;
    }
  }

  if (goog.isNumber(this.maxDeltaX_)) {
    deltaX = goog.math.clamp(deltaX, -this.maxDeltaX_, this.maxDeltaX_);
  }
  if (goog.isNumber(this.maxDeltaY_)) {
    deltaY = goog.math.clamp(deltaY, -this.maxDeltaY_, this.maxDeltaY_);
  }
  // Don't clamp 'detail', since it could be ambiguous which axis it refers to
  // and because it's informally deprecated anyways.

  // For horizontal scrolling we need to flip the value for RTL grids.
  if (this.isRtl_) {
    deltaX = -deltaX;
  }
  var newEvent = new goog.events.MouseWheelEvent(detail, be, deltaX, deltaY);
  this.dispatchEvent(newEvent);
};


/**
 * Helper for scaling down a mousewheel delta by a scale factor, if appropriate.
 * @param {number} mouseWheelDelta Delta from a mouse wheel event. Expected to
 *     be an integer.
 * @param {number} scaleFactor Factor to scale the delta down by. Expected to
 *     be an integer.
 * @return {number} Scaled-down delta value, or the original delta if the
 *     scaleFactor does not appear to be applicable.
 * @private
 */
goog.events.MouseWheelHandler.smartScale_ = function(mouseWheelDelta,
    scaleFactor) {
  // The basic problem here is that in Webkit on Mac and Linux, we can get two
  // very different types of mousewheel events: from continuous devices
  // (touchpads, Mighty Mouse) or non-continuous devices (normal wheel mice).
  //
  // Non-continuous devices in Webkit get their wheel deltas scaled up to
  // behave like IE. Continuous devices return much smaller unscaled values
  // (which most of the time will not be cleanly divisible by the IE scale
  // factor), so we should not try to normalize them down.
  //
  // Detailed discussion:
  //   https://bugs.webkit.org/show_bug.cgi?id=29601
  //   http://trac.webkit.org/browser/trunk/WebKit/chromium/src/mac/WebInputEventFactory.mm#L1063
  if (goog.userAgent.WEBKIT &&
      (goog.userAgent.MAC || goog.userAgent.LINUX) &&
      (mouseWheelDelta % scaleFactor) != 0) {
    return mouseWheelDelta;
  } else {
    return mouseWheelDelta / scaleFactor;
  }
};


/** @override */
goog.events.MouseWheelHandler.prototype.disposeInternal = function() {
  goog.events.MouseWheelHandler.superClass_.disposeInternal.call(this);
  goog.events.unlistenByKey(this.listenKey_);
  this.listenKey_ = null;
};



/**
 * A base class for mouse wheel events. This is used with the
 * MouseWheelHandler.
 *
 * @param {number} detail The number of rows the user scrolled.
 * @param {Event} browserEvent Browser event object.
 * @param {number} deltaX The number of rows the user scrolled in the X
 *     direction.
 * @param {number} deltaY The number of rows the user scrolled in the Y
 *     direction.
 * @constructor
 * @extends {goog.events.BrowserEvent}
 * @final
 */
goog.events.MouseWheelEvent = function(detail, browserEvent, deltaX, deltaY) {
  goog.events.BrowserEvent.call(this, browserEvent);

  this.type = goog.events.MouseWheelHandler.EventType.MOUSEWHEEL;

  /**
   * The number of lines the user scrolled
   * @type {number}
   * NOTE: Informally deprecated. Use deltaX and deltaY instead, they provide
   * more information.
   */
  this.detail = detail;

  /**
   * The number of "lines" scrolled in the X direction.
   *
   * Note that not all browsers provide enough information to distinguish
   * horizontal and vertical scroll events, so for these unsupported browsers,
   * we will always have a deltaX of 0, even if the user scrolled their mouse
   * wheel or trackpad sideways.
   *
   * Currently supported browsers are Webkit and Firefox 3.1 or later.
   *
   * @type {number}
   */
  this.deltaX = deltaX;

  /**
   * The number of lines scrolled in the Y direction.
   * @type {number}
   */
  this.deltaY = deltaY;
};
goog.inherits(goog.events.MouseWheelEvent, goog.events.BrowserEvent);

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Basic strippable logging definitions.
 * @see http://go/closurelogging
 *
 * @author johnlenz@google.com (John Lenz)
 */

goog.provide('goog.log');
goog.provide('goog.log.Level');
goog.provide('goog.log.LogRecord');
goog.provide('goog.log.Logger');

goog.require('goog.debug');
goog.require('goog.debug.LogManager');
goog.require('goog.debug.LogRecord');
goog.require('goog.debug.Logger');


/** @define {boolean} Whether logging is enabled. */
goog.define('goog.log.ENABLED', goog.debug.LOGGING_ENABLED);


/** @const */
goog.log.ROOT_LOGGER_NAME = goog.debug.Logger.ROOT_LOGGER_NAME;



/**
 * @constructor
 * @final
 */
goog.log.Logger = goog.debug.Logger;



/**
 * @constructor
 * @final
 */
goog.log.Level = goog.debug.Logger.Level;



/**
 * @constructor
 * @final
 */
goog.log.LogRecord = goog.debug.LogRecord;


/**
 * Finds or creates a logger for a named subsystem. If a logger has already been
 * created with the given name it is returned. Otherwise a new logger is
 * created. If a new logger is created its log level will be configured based
 * on the goog.debug.LogManager configuration and it will configured to also
 * send logging output to its parent's handlers.
 * @see goog.debug.LogManager
 *
 * @param {string} name A name for the logger. This should be a dot-separated
 *     name and should normally be based on the package name or class name of
 *     the subsystem, such as goog.net.BrowserChannel.
 * @param {goog.log.Level=} opt_level If provided, override the
 *     default logging level with the provided level.
 * @return {goog.log.Logger} The named logger or null if logging is disabled.
 */
goog.log.getLogger = function(name, opt_level) {
  if (goog.log.ENABLED) {
    var logger = goog.debug.LogManager.getLogger(name);
    if (opt_level && logger) {
      logger.setLevel(opt_level);
    }
    return logger;
  } else {
    return null;
  }
};


// TODO(johnlenz): try to tighten the types to these functions.
/**
 * Adds a handler to the logger. This doesn't use the event system because
 * we want to be able to add logging to the event system.
 * @param {goog.log.Logger} logger
 * @param {Function} handler Handler function to add.
 */
goog.log.addHandler = function(logger, handler) {
  if (goog.log.ENABLED && logger) {
    logger.addHandler(handler);
  }
};


/**
 * Removes a handler from the logger. This doesn't use the event system because
 * we want to be able to add logging to the event system.
 * @param {goog.log.Logger} logger
 * @param {Function} handler Handler function to remove.
 * @return {boolean} Whether the handler was removed.
 */
goog.log.removeHandler = function(logger, handler) {
  if (goog.log.ENABLED && logger) {
    return logger.removeHandler(handler);
  } else {
    return false;
  }
};


/**
 * Logs a message. If the logger is currently enabled for the
 * given message level then the given message is forwarded to all the
 * registered output Handler objects.
 * @param {goog.log.Logger} logger
 * @param {goog.log.Level} level One of the level identifiers.
 * @param {goog.debug.Loggable} msg The message to log.
 * @param {Error|Object=} opt_exception An exception associated with the
 *     message.
 */
goog.log.log = function(logger, level, msg, opt_exception) {
  if (goog.log.ENABLED && logger) {
    logger.log(level, msg, opt_exception);
  }
};


/**
 * Logs a message at the Level.SEVERE level.
 * If the logger is currently enabled for the given message level then the
 * given message is forwarded to all the registered output Handler objects.
 * @param {goog.log.Logger} logger
 * @param {goog.debug.Loggable} msg The message to log.
 * @param {Error=} opt_exception An exception associated with the message.
 */
goog.log.error = function(logger, msg, opt_exception) {
  if (goog.log.ENABLED && logger) {
    logger.severe(msg, opt_exception);
  }
};


/**
 * Logs a message at the Level.WARNING level.
 * If the logger is currently enabled for the given message level then the
 * given message is forwarded to all the registered output Handler objects.
 * @param {goog.log.Logger} logger
 * @param {goog.debug.Loggable} msg The message to log.
 * @param {Error=} opt_exception An exception associated with the message.
 */
goog.log.warning = function(logger, msg, opt_exception) {
  if (goog.log.ENABLED && logger) {
    logger.warning(msg, opt_exception);
  }
};


/**
 * Logs a message at the Level.INFO level.
 * If the logger is currently enabled for the given message level then the
 * given message is forwarded to all the registered output Handler objects.
 * @param {goog.log.Logger} logger
 * @param {goog.debug.Loggable} msg The message to log.
 * @param {Error=} opt_exception An exception associated with the message.
 */
goog.log.info = function(logger, msg, opt_exception) {
  if (goog.log.ENABLED && logger) {
    logger.info(msg, opt_exception);
  }
};


/**
 * Logs a message at the Level.Fine level.
 * If the logger is currently enabled for the given message level then the
 * given message is forwarded to all the registered output Handler objects.
 * @param {goog.log.Logger} logger
 * @param {goog.debug.Loggable} msg The message to log.
 * @param {Error=} opt_exception An exception associated with the message.
 */
goog.log.fine = function(logger, msg, opt_exception) {
  if (goog.log.ENABLED && logger) {
    logger.fine(msg, opt_exception);
  }
};

// Based on https://github.com/Polymer/PointerEvents

// Copyright (c) 2013 The Polymer Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

goog.provide('ol.pointer.PointerEvent');


goog.require('goog.events');
goog.require('goog.events.Event');


/**
 * A class for pointer events.
 *
 * This class is used as an abstraction for mouse events,
 * touch events and even native pointer events.
 *
 * @constructor
 * @extends {goog.events.Event}
 * @param {string} type The type of the event to create.
 * @param {goog.events.BrowserEvent} browserEvent The event.
 * @param {Object.<string, ?>=} opt_eventDict An optional dictionary of
 *    initial event properties.
 */
ol.pointer.PointerEvent = function(type, browserEvent, opt_eventDict) {
  goog.base(this, type);

  /**
   * @const
   * @type {goog.events.BrowserEvent}
   */
  this.browserEvent = browserEvent;

  var eventDict = opt_eventDict ? opt_eventDict : {};

  /**
   * @type {number}
   */
  this.buttons = this.getButtons_(eventDict);

  /**
   * @type {number}
   */
  this.pressure = this.getPressure_(eventDict, this.buttons);

  // MouseEvent related properties

  /**
   * @type {boolean}
   */
  this.bubbles = 'bubbles' in eventDict ? eventDict['bubbles'] : false;

  /**
   * @type {boolean}
   */
  this.cancelable = 'cancelable' in eventDict ? eventDict['cancelable'] : false;

  /**
   * @type {Object}
   */
  this.view = 'view' in eventDict ? eventDict['view'] : null;

  /**
   * @type {number}
   */
  this.detail = 'detail' in eventDict ? eventDict['detail'] : null;

  /**
   * @type {number}
   */
  this.screenX = 'screenX' in eventDict ? eventDict['screenX'] : 0;

  /**
   * @type {number}
   */
  this.screenY = 'screenY' in eventDict ? eventDict['screenY'] : 0;

  /**
   * @type {number}
   */
  this.clientX = 'clientX' in eventDict ? eventDict['clientX'] : 0;

  /**
   * @type {number}
   */
  this.clientY = 'clientY' in eventDict ? eventDict['clientY'] : 0;

  /**
   * @type {boolean}
   */
  this.ctrlKey = 'ctrlKey' in eventDict ? eventDict['ctrlKey'] : false;

  /**
   * @type {boolean}
   */
  this.altKey = 'altKey' in eventDict ? eventDict['altKey'] : false;

  /**
   * @type {boolean}
   */
  this.shiftKey = 'shiftKey' in eventDict ? eventDict['shiftKey'] : false;

  /**
   * @type {boolean}
   */
  this.metaKey = 'metaKey' in eventDict ? eventDict['metaKey'] : false;

  /**
   * @type {number}
   */
  this.button = 'button' in eventDict ? eventDict['button'] : 0;

  /**
   * @type {Node}
   */
  this.relatedTarget = 'relatedTarget' in eventDict ?
      eventDict['relatedTarget'] : null;

  // PointerEvent related properties

  /**
   * @const
   * @type {number}
   */
  this.pointerId = 'pointerId' in eventDict ? eventDict['pointerId'] : 0;

  /**
   * @type {number}
   */
  this.width = 'width' in eventDict ? eventDict['width'] : 0;

  /**
   * @type {number}
   */
  this.height = 'height' in eventDict ? eventDict['height'] : 0;

  /**
   * @type {number}
   */
  this.tiltX = 'tiltX' in eventDict ? eventDict['tiltX'] : 0;

  /**
   * @type {number}
   */
  this.tiltY = 'tiltY' in eventDict ? eventDict['tiltY'] : 0;

  /**
   * @type {string}
   */
  this.pointerType = 'pointerType' in eventDict ? eventDict['pointerType'] : '';

  /**
   * @type {number}
   */
  this.hwTimestamp = 'hwTimestamp' in eventDict ? eventDict['hwTimestamp'] : 0;

  /**
   * @type {boolean}
   */
  this.isPrimary = 'isPrimary' in eventDict ? eventDict['isPrimary'] : false;

  // keep the semantics of preventDefault
  if (browserEvent.preventDefault) {
    this.preventDefault = function() {
      browserEvent.preventDefault();
    };
  }
};
goog.inherits(ol.pointer.PointerEvent, goog.events.Event);


/**
 * @private
 * @param {Object.<string, ?>} eventDict The event dictionary.
 * @return {number} Button indicator.
 */
ol.pointer.PointerEvent.prototype.getButtons_ = function(eventDict) {
  // According to the w3c spec,
  // http://www.w3.org/TR/DOM-Level-3-Events/#events-MouseEvent-button
  // MouseEvent.button == 0 can mean either no mouse button depressed, or the
  // left mouse button depressed.
  //
  // As of now, the only way to distinguish between the two states of
  // MouseEvent.button is by using the deprecated MouseEvent.which property, as
  // this maps mouse buttons to positive integers > 0, and uses 0 to mean that
  // no mouse button is held.
  //
  // MouseEvent.which is derived from MouseEvent.button at MouseEvent creation,
  // but initMouseEvent does not expose an argument with which to set
  // MouseEvent.which. Calling initMouseEvent with a buttonArg of 0 will set
  // MouseEvent.button == 0 and MouseEvent.which == 1, breaking the expectations
  // of app developers.
  //
  // The only way to propagate the correct state of MouseEvent.which and
  // MouseEvent.button to a new MouseEvent.button == 0 and MouseEvent.which == 0
  // is to call initMouseEvent with a buttonArg value of -1.
  //
  // This is fixed with DOM Level 4's use of buttons
  var buttons;
  if (eventDict.buttons || ol.pointer.PointerEvent.HAS_BUTTONS) {
    buttons = eventDict.buttons;
  } else {
    switch (eventDict.which) {
      case 1: buttons = 1; break;
      case 2: buttons = 4; break;
      case 3: buttons = 2; break;
      default: buttons = 0;
    }
  }
  return buttons;
};


/**
 * @private
 * @param {Object.<string, ?>} eventDict The event dictionary.
 * @param {number} buttons Button indicator.
 * @return {number} The pressure.
 */
ol.pointer.PointerEvent.prototype.getPressure_ = function(eventDict, buttons) {
  // Spec requires that pointers without pressure specified use 0.5 for down
  // state and 0 for up state.
  var pressure = 0;
  if (eventDict.pressure) {
    pressure = eventDict.pressure;
  } else {
    pressure = buttons ? 0.5 : 0;
  }
  return pressure;
};


/**
 * Is the `buttons` property supported?
 * @type {boolean}
 */
ol.pointer.PointerEvent.HAS_BUTTONS = false;


/**
 * Checks if the `buttons` property is supported.
 */
(function() {
  try {
    var ev = new MouseEvent('click', {buttons: 1});
    ol.pointer.PointerEvent.HAS_BUTTONS = ev.buttons === 1;
  } catch (e) {
    // pass
  }
})();

// FIXME add tests for browser features (Modernizr?)

goog.provide('ol.dom');
goog.provide('ol.dom.BrowserFeature');

goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.userAgent');
goog.require('goog.vec.Mat4');
goog.require('ol');


/**
 * Create an html canvas element and returns its 2d context.
 * @param {number=} opt_width Canvas width.
 * @param {number=} opt_height Canvas height.
 * @return {CanvasRenderingContext2D} The context.
 */
ol.dom.createCanvasContext2D = function(opt_width, opt_height) {
  var canvas = document.createElement('CANVAS');
  if (opt_width) {
    canvas.width = opt_width;
  }
  if (opt_height) {
    canvas.height = opt_height;
  }
  return canvas.getContext('2d');
};


/**
 * Detect 2d transform.
 * Adapted from http://stackoverflow.com/q/5661671/130442
 * http://caniuse.com/#feat=transforms2d
 * @return {boolean}
 */
ol.dom.canUseCssTransform = (function() {
  var canUseCssTransform;
  return function() {
    if (canUseCssTransform === undefined) {
      goog.asserts.assert(document.body,
          'document.body should not be null');
      if (!goog.global.getComputedStyle) {
        // this browser is ancient
        canUseCssTransform = false;
      } else {
        var el = document.createElement('P'),
            has2d,
            transforms = {
              'webkitTransform': '-webkit-transform',
              'OTransform': '-o-transform',
              'msTransform': '-ms-transform',
              'MozTransform': '-moz-transform',
              'transform': 'transform'
            };
        document.body.appendChild(el);
        for (var t in transforms) {
          if (t in el.style) {
            el.style[t] = 'translate(1px,1px)';
            has2d = goog.global.getComputedStyle(el).getPropertyValue(
                transforms[t]);
          }
        }
        goog.dom.removeNode(el);

        canUseCssTransform = (has2d && has2d !== 'none');
      }
    }
    return canUseCssTransform;
  };
}());


/**
 * Detect 3d transform.
 * Adapted from http://stackoverflow.com/q/5661671/130442
 * http://caniuse.com/#feat=transforms3d
 * @return {boolean}
 */
ol.dom.canUseCssTransform3D = (function() {
  var canUseCssTransform3D;
  return function() {
    if (canUseCssTransform3D === undefined) {
      goog.asserts.assert(document.body,
          'document.body should not be null');
      if (!goog.global.getComputedStyle) {
        // this browser is ancient
        canUseCssTransform3D = false;
      } else {
        var el = document.createElement('P'),
            has3d,
            transforms = {
              'webkitTransform': '-webkit-transform',
              'OTransform': '-o-transform',
              'msTransform': '-ms-transform',
              'MozTransform': '-moz-transform',
              'transform': 'transform'
            };
        document.body.appendChild(el);
        for (var t in transforms) {
          if (t in el.style) {
            el.style[t] = 'translate3d(1px,1px,1px)';
            has3d = goog.global.getComputedStyle(el).getPropertyValue(
                transforms[t]);
          }
        }
        goog.dom.removeNode(el);

        canUseCssTransform3D = (has3d && has3d !== 'none');
      }
    }
    return canUseCssTransform3D;
  };
}());


/**
 * @param {Element} element Element.
 * @param {string} value Value.
 */
ol.dom.setTransform = function(element, value) {
  var style = element.style;
  style.WebkitTransform = value;
  style.MozTransform = value;
  style.OTransform = value;
  style.msTransform = value;
  style.transform = value;

  // IE 9+ seems to assume transform-origin: 100% 100%; for some unknown reason
  if (goog.userAgent.IE && goog.userAgent.isVersionOrHigher('9.0')) {
    element.style.transformOrigin = '0 0';
  }
};


/**
 * @param {!Element} element Element.
 * @param {goog.vec.Mat4.Number} transform Matrix.
 * @param {number=} opt_precision Precision.
 */
ol.dom.transformElement2D = function(element, transform, opt_precision) {
  // using matrix() causes gaps in Chrome and Firefox on Mac OS X, so prefer
  // matrix3d()
  var i;
  if (ol.dom.canUseCssTransform3D()) {
    var value3D;

    if (opt_precision !== undefined) {
      /** @type {Array.<string>} */
      var strings3D = new Array(16);
      for (i = 0; i < 16; ++i) {
        strings3D[i] = transform[i].toFixed(opt_precision);
      }
      value3D = strings3D.join(',');
    } else {
      value3D = transform.join(',');
    }
    ol.dom.setTransform(element, 'matrix3d(' + value3D + ')');
  } else if (ol.dom.canUseCssTransform()) {
    /** @type {Array.<number>} */
    var transform2D = [
      goog.vec.Mat4.getElement(transform, 0, 0),
      goog.vec.Mat4.getElement(transform, 1, 0),
      goog.vec.Mat4.getElement(transform, 0, 1),
      goog.vec.Mat4.getElement(transform, 1, 1),
      goog.vec.Mat4.getElement(transform, 0, 3),
      goog.vec.Mat4.getElement(transform, 1, 3)
    ];
    var value2D;
    if (opt_precision !== undefined) {
      /** @type {Array.<string>} */
      var strings2D = new Array(6);
      for (i = 0; i < 6; ++i) {
        strings2D[i] = transform2D[i].toFixed(opt_precision);
      }
      value2D = strings2D.join(',');
    } else {
      value2D = transform2D.join(',');
    }
    ol.dom.setTransform(element, 'matrix(' + value2D + ')');
  } else {
    element.style.left =
        Math.round(goog.vec.Mat4.getElement(transform, 0, 3)) + 'px';
    element.style.top =
        Math.round(goog.vec.Mat4.getElement(transform, 1, 3)) + 'px';

    // TODO: Add scaling here. This isn't quite as simple as multiplying
    // width/height, because that only changes the container size, not the
    // content size.
  }
};


/**
 * Get the current computed width for the given element including margin,
 * padding and border.
 * Equivalent to jQuery's `$(el).outerWidth(true)`.
 * @param {!Element} element Element.
 * @return {number} The width.
 */
ol.dom.outerWidth = function(element) {
  var width = element.offsetWidth;
  var style = element.currentStyle || window.getComputedStyle(element);
  width += parseInt(style.marginLeft, 10) + parseInt(style.marginRight, 10);

  return width;
};


/**
 * Get the current computed height for the given element including margin,
 * padding and border.
 * Equivalent to jQuery's `$(el).outerHeight(true)`.
 * @param {!Element} element Element.
 * @return {number} The height.
 */
ol.dom.outerHeight = function(element) {
  var height = element.offsetHeight;
  var style = element.currentStyle || window.getComputedStyle(element);
  height += parseInt(style.marginTop, 10) + parseInt(style.marginBottom, 10);

  return height;
};

goog.provide('ol.webgl');
goog.provide('ol.webgl.WebGLContextEventType');


/**
 * @const
 * @private
 * @type {Array.<string>}
 */
ol.webgl.CONTEXT_IDS_ = [
  'experimental-webgl',
  'webgl',
  'webkit-3d',
  'moz-webgl'
];


/**
 * @enum {string}
 */
ol.webgl.WebGLContextEventType = {
  LOST: 'webglcontextlost',
  RESTORED: 'webglcontextrestored'
};


/**
 * @param {HTMLCanvasElement} canvas Canvas.
 * @param {Object=} opt_attributes Attributes.
 * @return {WebGLRenderingContext} WebGL rendering context.
 */
ol.webgl.getContext = function(canvas, opt_attributes) {
  var context, i, ii = ol.webgl.CONTEXT_IDS_.length;
  for (i = 0; i < ii; ++i) {
    try {
      context = canvas.getContext(ol.webgl.CONTEXT_IDS_[i], opt_attributes);
      if (context) {
        return /** @type {!WebGLRenderingContext} */ (context);
      }
    } catch (e) {
      // pass
    }
  }
  return null;
};

goog.provide('ol.has');

goog.require('ol');
goog.require('ol.dom');
goog.require('ol.webgl');


/**
 * The ratio between physical pixels and device-independent pixels
 * (dips) on the device (`window.devicePixelRatio`).
 * @const
 * @type {number}
 * @api stable
 */
ol.has.DEVICE_PIXEL_RATIO = goog.global.devicePixelRatio || 1;


/**
 * True if the browser's Canvas implementation implements {get,set}LineDash.
 * @type {boolean}
 */
ol.has.CANVAS_LINE_DASH = false;


/**
 * True if both the library and browser support Canvas.  Always `false`
 * if `ol.ENABLE_CANVAS` is set to `false` at compile time.
 * @const
 * @type {boolean}
 * @api stable
 */
ol.has.CANVAS = ol.ENABLE_CANVAS && (
    /**
     * @return {boolean} Canvas supported.
     */
    function() {
      if (!('HTMLCanvasElement' in goog.global)) {
        return false;
      }
      try {
        var context = ol.dom.createCanvasContext2D();
        if (!context) {
          return false;
        } else {
          if (context.setLineDash !== undefined) {
            ol.has.CANVAS_LINE_DASH = true;
          }
          return true;
        }
      } catch (e) {
        return false;
      }
    })();


/**
 * Indicates if DeviceOrientation is supported in the user's browser.
 * @const
 * @type {boolean}
 * @api stable
 */
ol.has.DEVICE_ORIENTATION = 'DeviceOrientationEvent' in goog.global;


/**
 * True if `ol.ENABLE_DOM` is set to `true` at compile time.
 * @const
 * @type {boolean}
 */
ol.has.DOM = ol.ENABLE_DOM;


/**
 * Is HTML5 geolocation supported in the current browser?
 * @const
 * @type {boolean}
 * @api stable
 */
ol.has.GEOLOCATION = 'geolocation' in goog.global.navigator;


/**
 * True if browser supports touch events.
 * @const
 * @type {boolean}
 * @api stable
 */
ol.has.TOUCH = ol.ASSUME_TOUCH || 'ontouchstart' in goog.global;


/**
 * True if browser supports pointer events.
 * @const
 * @type {boolean}
 */
ol.has.POINTER = 'PointerEvent' in goog.global;


/**
 * True if browser supports ms pointer events (IE 10).
 * @const
 * @type {boolean}
 */
ol.has.MSPOINTER = !!(goog.global.navigator.msPointerEnabled);


/**
 * True if both OpenLayers and browser support WebGL.  Always `false`
 * if `ol.ENABLE_WEBGL` is set to `false` at compile time.
 * @const
 * @type {boolean}
 * @api stable
 */
ol.has.WEBGL;


(function() {
  if (ol.ENABLE_WEBGL) {
    var hasWebGL = false;
    var textureSize;
    var /** @type {Array.<string>} */ extensions = [];

    if ('WebGLRenderingContext' in goog.global) {
      try {
        var canvas = /** @type {HTMLCanvasElement} */
            (document.createElement('CANVAS'));
        var gl = ol.webgl.getContext(canvas, {
          failIfMajorPerformanceCaveat: true
        });
        if (gl) {
          hasWebGL = true;
          textureSize = /** @type {number} */
              (gl.getParameter(gl.MAX_TEXTURE_SIZE));
          extensions = gl.getSupportedExtensions();
        }
      } catch (e) {
        // pass
      }
    }
    ol.has.WEBGL = hasWebGL;
    ol.WEBGL_EXTENSIONS = extensions;
    ol.WEBGL_MAX_TEXTURE_SIZE = textureSize;
  }
})();

goog.provide('ol.pointer.EventSource');

goog.require('goog.events.BrowserEvent');


/**
 * @param {ol.pointer.PointerEventHandler} dispatcher Event handler.
 * @param {!Object.<string, function(goog.events.BrowserEvent)>} mapping Event
 *     mapping.
 * @constructor
 */
ol.pointer.EventSource = function(dispatcher, mapping) {
  /**
   * @type {ol.pointer.PointerEventHandler}
   */
  this.dispatcher = dispatcher;

  /**
   * @private
   * @const
   * @type {!Object.<string, function(goog.events.BrowserEvent)>}
   */
  this.mapping_ = mapping;
};


/**
 * List of events supported by this source.
 * @return {Array.<string>} Event names
 */
ol.pointer.EventSource.prototype.getEvents = function() {
  return Object.keys(this.mapping_);
};


/**
 * Returns a mapping between the supported event types and
 * the handlers that should handle an event.
 * @return {Object.<string, function(goog.events.BrowserEvent)>}
 *         Event/Handler mapping
 */
ol.pointer.EventSource.prototype.getMapping = function() {
  return this.mapping_;
};


/**
 * Returns the handler that should handle a given event type.
 * @param {string} eventType The event type.
 * @return {function(goog.events.BrowserEvent)} Handler
 */
ol.pointer.EventSource.prototype.getHandlerForEvent = function(eventType) {
  return this.mapping_[eventType];
};

// Based on https://github.com/Polymer/PointerEvents

// Copyright (c) 2013 The Polymer Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

goog.provide('ol.pointer.MouseSource');

goog.require('ol.pointer.EventSource');


/**
 * @param {ol.pointer.PointerEventHandler} dispatcher Event handler.
 * @constructor
 * @extends {ol.pointer.EventSource}
 */
ol.pointer.MouseSource = function(dispatcher) {
  var mapping = {
    'mousedown': this.mousedown,
    'mousemove': this.mousemove,
    'mouseup': this.mouseup,
    'mouseover': this.mouseover,
    'mouseout': this.mouseout
  };
  goog.base(this, dispatcher, mapping);

  /**
   * @const
   * @type {!Object.<string, goog.events.BrowserEvent|Object>}
   */
  this.pointerMap = dispatcher.pointerMap;

  /**
   * @const
   * @type {Array.<ol.Pixel>}
   */
  this.lastTouches = [];
};
goog.inherits(ol.pointer.MouseSource, ol.pointer.EventSource);


/**
 * @const
 * @type {number}
 */
ol.pointer.MouseSource.POINTER_ID = 1;


/**
 * @const
 * @type {string}
 */
ol.pointer.MouseSource.POINTER_TYPE = 'mouse';


/**
 * Radius around touchend that swallows mouse events.
 *
 * @const
 * @type {number}
 */
ol.pointer.MouseSource.DEDUP_DIST = 25;


/**
 * Detect if a mouse event was simulated from a touch by
 * checking if previously there was a touch event at the
 * same position.
 *
 * FIXME - Known problem with the native Android browser on
 * Samsung GT-I9100 (Android 4.1.2):
 * In case the page is scrolled, this function does not work
 * correctly when a canvas is used (WebGL or canvas renderer).
 * Mouse listeners on canvas elements (for this browser), create
 * two mouse events: One 'good' and one 'bad' one (on other browsers or
 * when a div is used, there is only one event). For the 'bad' one,
 * clientX/clientY and also pageX/pageY are wrong when the page
 * is scrolled. Because of that, this function can not detect if
 * the events were simulated from a touch event. As result, a
 * pointer event at a wrong position is dispatched, which confuses
 * the map interactions.
 * It is unclear, how one can get the correct position for the event
 * or detect that the positions are invalid.
 *
 * @private
 * @param {goog.events.BrowserEvent} inEvent The in event.
 * @return {boolean} True, if the event was generated by a touch.
 */
ol.pointer.MouseSource.prototype.isEventSimulatedFromTouch_ = function(inEvent) {
  var lts = this.lastTouches;
  var x = inEvent.clientX, y = inEvent.clientY;
  for (var i = 0, l = lts.length, t; i < l && (t = lts[i]); i++) {
    // simulated mouse events will be swallowed near a primary touchend
    var dx = Math.abs(x - t[0]), dy = Math.abs(y - t[1]);
    if (dx <= ol.pointer.MouseSource.DEDUP_DIST &&
        dy <= ol.pointer.MouseSource.DEDUP_DIST) {
      return true;
    }
  }
  return false;
};


/**
 * Creates a copy of the original event that will be used
 * for the fake pointer event.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 * @param {ol.pointer.PointerEventHandler} dispatcher Event handler.
 * @return {Object} The copied event.
 */
ol.pointer.MouseSource.prepareEvent = function(inEvent, dispatcher) {
  var e = dispatcher.cloneEvent(inEvent, inEvent.getBrowserEvent());

  // forward mouse preventDefault
  var pd = e.preventDefault;
  e.preventDefault = function() {
    inEvent.preventDefault();
    pd();
  };

  e.pointerId = ol.pointer.MouseSource.POINTER_ID;
  e.isPrimary = true;
  e.pointerType = ol.pointer.MouseSource.POINTER_TYPE;

  return e;
};


/**
 * Handler for `mousedown`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.MouseSource.prototype.mousedown = function(inEvent) {
  if (!this.isEventSimulatedFromTouch_(inEvent)) {
    // TODO(dfreedman) workaround for some elements not sending mouseup
    // http://crbug/149091
    if (ol.pointer.MouseSource.POINTER_ID.toString() in this.pointerMap) {
      this.cancel(inEvent);
    }
    var e = ol.pointer.MouseSource.prepareEvent(inEvent, this.dispatcher);
    this.pointerMap[ol.pointer.MouseSource.POINTER_ID.toString()] = inEvent;
    this.dispatcher.down(e, inEvent);
  }
};


/**
 * Handler for `mousemove`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.MouseSource.prototype.mousemove = function(inEvent) {
  if (!this.isEventSimulatedFromTouch_(inEvent)) {
    var e = ol.pointer.MouseSource.prepareEvent(inEvent, this.dispatcher);
    this.dispatcher.move(e, inEvent);
  }
};


/**
 * Handler for `mouseup`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.MouseSource.prototype.mouseup = function(inEvent) {
  if (!this.isEventSimulatedFromTouch_(inEvent)) {
    var p = this.pointerMap[ol.pointer.MouseSource.POINTER_ID.toString()];

    if (p && p.button === inEvent.button) {
      var e = ol.pointer.MouseSource.prepareEvent(inEvent, this.dispatcher);
      this.dispatcher.up(e, inEvent);
      this.cleanupMouse();
    }
  }
};


/**
 * Handler for `mouseover`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.MouseSource.prototype.mouseover = function(inEvent) {
  if (!this.isEventSimulatedFromTouch_(inEvent)) {
    var e = ol.pointer.MouseSource.prepareEvent(inEvent, this.dispatcher);
    this.dispatcher.enterOver(e, inEvent);
  }
};


/**
 * Handler for `mouseout`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.MouseSource.prototype.mouseout = function(inEvent) {
  if (!this.isEventSimulatedFromTouch_(inEvent)) {
    var e = ol.pointer.MouseSource.prepareEvent(inEvent, this.dispatcher);
    this.dispatcher.leaveOut(e, inEvent);
  }
};


/**
 * Dispatches a `pointercancel` event.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.MouseSource.prototype.cancel = function(inEvent) {
  var e = ol.pointer.MouseSource.prepareEvent(inEvent, this.dispatcher);
  this.dispatcher.cancel(e, inEvent);
  this.cleanupMouse();
};


/**
 * Remove the mouse from the list of active pointers.
 */
ol.pointer.MouseSource.prototype.cleanupMouse = function() {
  delete this.pointerMap[ol.pointer.MouseSource.POINTER_ID.toString()];
};

// Based on https://github.com/Polymer/PointerEvents

// Copyright (c) 2013 The Polymer Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

goog.provide('ol.pointer.MsSource');

goog.require('ol.pointer.EventSource');


/**
 * @param {ol.pointer.PointerEventHandler} dispatcher Event handler.
 * @constructor
 * @extends {ol.pointer.EventSource}
 */
ol.pointer.MsSource = function(dispatcher) {
  var mapping = {
    'MSPointerDown': this.msPointerDown,
    'MSPointerMove': this.msPointerMove,
    'MSPointerUp': this.msPointerUp,
    'MSPointerOut': this.msPointerOut,
    'MSPointerOver': this.msPointerOver,
    'MSPointerCancel': this.msPointerCancel,
    'MSGotPointerCapture': this.msGotPointerCapture,
    'MSLostPointerCapture': this.msLostPointerCapture
  };
  goog.base(this, dispatcher, mapping);

  /**
   * @const
   * @type {!Object.<string, goog.events.BrowserEvent|Object>}
   */
  this.pointerMap = dispatcher.pointerMap;

  /**
   * @const
   * @type {Array.<string>}
   */
  this.POINTER_TYPES = [
    '',
    'unavailable',
    'touch',
    'pen',
    'mouse'
  ];
};
goog.inherits(ol.pointer.MsSource, ol.pointer.EventSource);


/**
 * Creates a copy of the original event that will be used
 * for the fake pointer event.
 *
 * @private
 * @param {goog.events.BrowserEvent} inEvent The in event.
 * @return {Object} The copied event.
 */
ol.pointer.MsSource.prototype.prepareEvent_ = function(inEvent) {
  var e = inEvent;
  if (goog.isNumber(inEvent.getBrowserEvent().pointerType)) {
    e = this.dispatcher.cloneEvent(inEvent, inEvent.getBrowserEvent());
    e.pointerType = this.POINTER_TYPES[inEvent.getBrowserEvent().pointerType];
  }

  return e;
};


/**
 * Remove this pointer from the list of active pointers.
 * @param {number} pointerId Pointer identifier.
 */
ol.pointer.MsSource.prototype.cleanup = function(pointerId) {
  delete this.pointerMap[pointerId.toString()];
};


/**
 * Handler for `msPointerDown`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.MsSource.prototype.msPointerDown = function(inEvent) {
  this.pointerMap[inEvent.getBrowserEvent().pointerId.toString()] = inEvent;
  var e = this.prepareEvent_(inEvent);
  this.dispatcher.down(e, inEvent);
};


/**
 * Handler for `msPointerMove`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.MsSource.prototype.msPointerMove = function(inEvent) {
  var e = this.prepareEvent_(inEvent);
  this.dispatcher.move(e, inEvent);
};


/**
 * Handler for `msPointerUp`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.MsSource.prototype.msPointerUp = function(inEvent) {
  var e = this.prepareEvent_(inEvent);
  this.dispatcher.up(e, inEvent);
  this.cleanup(inEvent.getBrowserEvent().pointerId);
};


/**
 * Handler for `msPointerOut`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.MsSource.prototype.msPointerOut = function(inEvent) {
  var e = this.prepareEvent_(inEvent);
  this.dispatcher.leaveOut(e, inEvent);
};


/**
 * Handler for `msPointerOver`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.MsSource.prototype.msPointerOver = function(inEvent) {
  var e = this.prepareEvent_(inEvent);
  this.dispatcher.enterOver(e, inEvent);
};


/**
 * Handler for `msPointerCancel`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.MsSource.prototype.msPointerCancel = function(inEvent) {
  var e = this.prepareEvent_(inEvent);
  this.dispatcher.cancel(e, inEvent);
  this.cleanup(inEvent.getBrowserEvent().pointerId);
};


/**
 * Handler for `msLostPointerCapture`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.MsSource.prototype.msLostPointerCapture = function(inEvent) {
  var e = this.dispatcher.makeEvent('lostpointercapture',
      inEvent.getBrowserEvent(), inEvent);
  this.dispatcher.dispatchEvent(e);
};


/**
 * Handler for `msGotPointerCapture`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.MsSource.prototype.msGotPointerCapture = function(inEvent) {
  var e = this.dispatcher.makeEvent('gotpointercapture',
      inEvent.getBrowserEvent(), inEvent);
  this.dispatcher.dispatchEvent(e);
};

// Based on https://github.com/Polymer/PointerEvents

// Copyright (c) 2013 The Polymer Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

goog.provide('ol.pointer.NativeSource');

goog.require('ol.pointer.EventSource');


/**
 * @param {ol.pointer.PointerEventHandler} dispatcher Event handler.
 * @constructor
 * @extends {ol.pointer.EventSource}
 */
ol.pointer.NativeSource = function(dispatcher) {
  var mapping = {
    'pointerdown': this.pointerDown,
    'pointermove': this.pointerMove,
    'pointerup': this.pointerUp,
    'pointerout': this.pointerOut,
    'pointerover': this.pointerOver,
    'pointercancel': this.pointerCancel,
    'gotpointercapture': this.gotPointerCapture,
    'lostpointercapture': this.lostPointerCapture
  };
  goog.base(this, dispatcher, mapping);
};
goog.inherits(ol.pointer.NativeSource, ol.pointer.EventSource);


/**
 * Handler for `pointerdown`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.NativeSource.prototype.pointerDown = function(inEvent) {
  this.dispatcher.fireNativeEvent(inEvent);
};


/**
 * Handler for `pointermove`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.NativeSource.prototype.pointerMove = function(inEvent) {
  this.dispatcher.fireNativeEvent(inEvent);
};


/**
 * Handler for `pointerup`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.NativeSource.prototype.pointerUp = function(inEvent) {
  this.dispatcher.fireNativeEvent(inEvent);
};


/**
 * Handler for `pointerout`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.NativeSource.prototype.pointerOut = function(inEvent) {
  this.dispatcher.fireNativeEvent(inEvent);
};


/**
 * Handler for `pointerover`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.NativeSource.prototype.pointerOver = function(inEvent) {
  this.dispatcher.fireNativeEvent(inEvent);
};


/**
 * Handler for `pointercancel`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.NativeSource.prototype.pointerCancel = function(inEvent) {
  this.dispatcher.fireNativeEvent(inEvent);
};


/**
 * Handler for `lostpointercapture`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.NativeSource.prototype.lostPointerCapture = function(inEvent) {
  this.dispatcher.fireNativeEvent(inEvent);
};


/**
 * Handler for `gotpointercapture`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.NativeSource.prototype.gotPointerCapture = function(inEvent) {
  this.dispatcher.fireNativeEvent(inEvent);
};

// Based on https://github.com/Polymer/PointerEvents

// Copyright (c) 2013 The Polymer Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

goog.provide('ol.pointer.TouchSource');

goog.require('goog.array');
goog.require('goog.object');
goog.require('ol');
goog.require('ol.pointer.EventSource');
goog.require('ol.pointer.MouseSource');


/**
 * @constructor
 * @param {ol.pointer.PointerEventHandler} dispatcher The event handler.
 * @param {ol.pointer.MouseSource} mouseSource Mouse source.
 * @extends {ol.pointer.EventSource}
 */
ol.pointer.TouchSource = function(dispatcher, mouseSource) {
  var mapping = {
    'touchstart': this.touchstart,
    'touchmove': this.touchmove,
    'touchend': this.touchend,
    'touchcancel': this.touchcancel
  };
  goog.base(this, dispatcher, mapping);

  /**
   * @const
   * @type {!Object.<string, goog.events.BrowserEvent|Object>}
   */
  this.pointerMap = dispatcher.pointerMap;

  /**
   * @const
   * @type {ol.pointer.MouseSource}
   */
  this.mouseSource = mouseSource;

  /**
   * @private
   * @type {number|undefined}
   */
  this.firstTouchId_ = undefined;

  /**
   * @private
   * @type {number}
   */
  this.clickCount_ = 0;

  /**
   * @private
   * @type {number|undefined}
   */
  this.resetId_ = undefined;
};
goog.inherits(ol.pointer.TouchSource, ol.pointer.EventSource);


/**
 * Mouse event timeout: This should be long enough to
 * ignore compat mouse events made by touch.
 * @const
 * @type {number}
 */
ol.pointer.TouchSource.DEDUP_TIMEOUT = 2500;


/**
 * @const
 * @type {number}
 */
ol.pointer.TouchSource.CLICK_COUNT_TIMEOUT = 200;


/**
 * @const
 * @type {string}
 */
ol.pointer.TouchSource.POINTER_TYPE = 'touch';


/**
 * @private
 * @param {Touch} inTouch The in touch.
 * @return {boolean} True, if this is the primary touch.
 */
ol.pointer.TouchSource.prototype.isPrimaryTouch_ = function(inTouch) {
  return this.firstTouchId_ === inTouch.identifier;
};


/**
 * Set primary touch if there are no pointers, or the only pointer is the mouse.
 * @param {Touch} inTouch The in touch.
 * @private
 */
ol.pointer.TouchSource.prototype.setPrimaryTouch_ = function(inTouch) {
  var count = goog.object.getCount(this.pointerMap);
  if (count === 0 || (count === 1 &&
      ol.pointer.MouseSource.POINTER_ID.toString() in this.pointerMap)) {
    this.firstTouchId_ = inTouch.identifier;
    this.cancelResetClickCount_();
  }
};


/**
 * @private
 * @param {Object} inPointer The in pointer object.
 */
ol.pointer.TouchSource.prototype.removePrimaryPointer_ = function(inPointer) {
  if (inPointer.isPrimary) {
    this.firstTouchId_ = undefined;
    this.resetClickCount_();
  }
};


/**
 * @private
 */
ol.pointer.TouchSource.prototype.resetClickCount_ = function() {
  this.resetId_ = goog.global.setTimeout(
      this.resetClickCountHandler_.bind(this),
      ol.pointer.TouchSource.CLICK_COUNT_TIMEOUT);
};


/**
 * @private
 */
ol.pointer.TouchSource.prototype.resetClickCountHandler_ = function() {
  this.clickCount_ = 0;
  this.resetId_ = undefined;
};


/**
 * @private
 */
ol.pointer.TouchSource.prototype.cancelResetClickCount_ = function() {
  if (this.resetId_ !== undefined) {
    goog.global.clearTimeout(this.resetId_);
  }
};


/**
 * @private
 * @param {goog.events.BrowserEvent} browserEvent Browser event
 * @param {Touch} inTouch Touch event
 * @return {Object} A pointer object.
 */
ol.pointer.TouchSource.prototype.touchToPointer_ = function(browserEvent, inTouch) {
  var e = this.dispatcher.cloneEvent(browserEvent, inTouch);
  // Spec specifies that pointerId 1 is reserved for Mouse.
  // Touch identifiers can start at 0.
  // Add 2 to the touch identifier for compatibility.
  e.pointerId = inTouch.identifier + 2;
  // TODO: check if this is necessary?
  //e.target = findTarget(e);
  e.bubbles = true;
  e.cancelable = true;
  e.detail = this.clickCount_;
  e.button = 0;
  e.buttons = 1;
  e.width = inTouch.webkitRadiusX || inTouch.radiusX || 0;
  e.height = inTouch.webkitRadiusY || inTouch.radiusY || 0;
  e.pressure = inTouch.webkitForce || inTouch.force || 0.5;
  e.isPrimary = this.isPrimaryTouch_(inTouch);
  e.pointerType = ol.pointer.TouchSource.POINTER_TYPE;

  // make sure that the properties that are different for
  // each `Touch` object are not copied from the BrowserEvent object
  e.clientX = inTouch.clientX;
  e.clientY = inTouch.clientY;
  e.screenX = inTouch.screenX;
  e.screenY = inTouch.screenY;

  return e;
};


/**
 * @private
 * @param {goog.events.BrowserEvent} inEvent Touch event
 * @param {function(goog.events.BrowserEvent, Object)} inFunction In function.
 */
ol.pointer.TouchSource.prototype.processTouches_ = function(inEvent, inFunction) {
  var touches = Array.prototype.slice.call(
      inEvent.getBrowserEvent().changedTouches);
  var count = touches.length;
  function preventDefault() {
    inEvent.preventDefault();
  }
  var i, pointer;
  for (i = 0; i < count; ++i) {
    pointer = this.touchToPointer_(inEvent, touches[i]);
    // forward touch preventDefaults
    pointer.preventDefault = preventDefault;
    inFunction.call(this, inEvent, pointer);
  }
};


/**
 * @private
 * @param {TouchList} touchList The touch list.
 * @param {number} searchId Search identifier.
 * @return {boolean} True, if the `Touch` with the given id is in the list.
 */
ol.pointer.TouchSource.prototype.findTouch_ = function(touchList, searchId) {
  var l = touchList.length;
  var touch;
  for (var i = 0; i < l; i++) {
    touch = touchList[i];
    if (touch.identifier === searchId) {
      return true;
    }
  }
  return false;
};


/**
 * In some instances, a touchstart can happen without a touchend. This
 * leaves the pointermap in a broken state.
 * Therefore, on every touchstart, we remove the touches that did not fire a
 * touchend event.
 * To keep state globally consistent, we fire a pointercancel for
 * this "abandoned" touch
 *
 * @private
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.TouchSource.prototype.vacuumTouches_ = function(inEvent) {
  var touchList = inEvent.getBrowserEvent().touches;
  // pointerMap.getCount() should be < touchList.length here,
  // as the touchstart has not been processed yet.
  var keys = Object.keys(this.pointerMap);
  var count = keys.length;
  if (count >= touchList.length) {
    var d = [];
    var i, key, value;
    for (i = 0; i < count; ++i) {
      key = keys[i];
      value = this.pointerMap[key];
      // Never remove pointerId == 1, which is mouse.
      // Touch identifiers are 2 smaller than their pointerId, which is the
      // index in pointermap.
      if (key != ol.pointer.MouseSource.POINTER_ID &&
          !this.findTouch_(touchList, key - 2)) {
        d.push(value.out);
      }
    }
    for (i = 0; i < d.length; ++i) {
      this.cancelOut_(inEvent, d[i]);
    }
  }
};


/**
 * Handler for `touchstart`, triggers `pointerover`,
 * `pointerenter` and `pointerdown` events.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.TouchSource.prototype.touchstart = function(inEvent) {
  this.vacuumTouches_(inEvent);
  this.setPrimaryTouch_(inEvent.getBrowserEvent().changedTouches[0]);
  this.dedupSynthMouse_(inEvent);
  this.clickCount_++;
  this.processTouches_(inEvent, this.overDown_);
};


/**
 * @private
 * @param {goog.events.BrowserEvent} browserEvent The event.
 * @param {Object} inPointer The in pointer object.
 */
ol.pointer.TouchSource.prototype.overDown_ = function(browserEvent, inPointer) {
  this.pointerMap[inPointer.pointerId] = {
    target: inPointer.target,
    out: inPointer,
    outTarget: inPointer.target
  };
  this.dispatcher.over(inPointer, browserEvent);
  this.dispatcher.enter(inPointer, browserEvent);
  this.dispatcher.down(inPointer, browserEvent);
};


/**
 * Handler for `touchmove`.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.TouchSource.prototype.touchmove = function(inEvent) {
  inEvent.preventDefault();
  this.processTouches_(inEvent, this.moveOverOut_);
};


/**
 * @private
 * @param {goog.events.BrowserEvent} browserEvent The event.
 * @param {Object} inPointer The in pointer.
 */
ol.pointer.TouchSource.prototype.moveOverOut_ = function(browserEvent, inPointer) {
  var event = inPointer;
  var pointer = this.pointerMap[event.pointerId];
  // a finger drifted off the screen, ignore it
  if (!pointer) {
    return;
  }
  var outEvent = pointer.out;
  var outTarget = pointer.outTarget;
  this.dispatcher.move(event, browserEvent);
  if (outEvent && outTarget !== event.target) {
    outEvent.relatedTarget = event.target;
    event.relatedTarget = outTarget;
    // recover from retargeting by shadow
    outEvent.target = outTarget;
    if (event.target) {
      this.dispatcher.leaveOut(outEvent, browserEvent);
      this.dispatcher.enterOver(event, browserEvent);
    } else {
      // clean up case when finger leaves the screen
      event.target = outTarget;
      event.relatedTarget = null;
      this.cancelOut_(browserEvent, event);
    }
  }
  pointer.out = event;
  pointer.outTarget = event.target;
};


/**
 * Handler for `touchend`, triggers `pointerup`,
 * `pointerout` and `pointerleave` events.
 *
 * @param {goog.events.BrowserEvent} inEvent The event.
 */
ol.pointer.TouchSource.prototype.touchend = function(inEvent) {
  this.dedupSynthMouse_(inEvent);
  this.processTouches_(inEvent, this.upOut_);
};


/**
 * @private
 * @param {goog.events.BrowserEvent} browserEvent An event.
 * @param {Object} inPointer The inPointer object.
 */
ol.pointer.TouchSource.prototype.upOut_ = function(browserEvent, inPointer) {
  this.dispatcher.up(inPointer, browserEvent);
  this.dispatcher.out(inPointer, browserEvent);
  this.dispatcher.leave(inPointer, browserEvent);
  this.cleanUpPointer_(inPointer);
};


/**
 * Handler for `touchcancel`, triggers `pointercancel`,
 * `pointerout` and `pointerleave` events.
 *
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.TouchSource.prototype.touchcancel = function(inEvent) {
  this.processTouches_(inEvent, this.cancelOut_);
};


/**
 * @private
 * @param {goog.events.BrowserEvent} browserEvent The event.
 * @param {Object} inPointer The in pointer.
 */
ol.pointer.TouchSource.prototype.cancelOut_ = function(browserEvent, inPointer) {
  this.dispatcher.cancel(inPointer, browserEvent);
  this.dispatcher.out(inPointer, browserEvent);
  this.dispatcher.leave(inPointer, browserEvent);
  this.cleanUpPointer_(inPointer);
};


/**
 * @private
 * @param {Object} inPointer The inPointer object.
 */
ol.pointer.TouchSource.prototype.cleanUpPointer_ = function(inPointer) {
  delete this.pointerMap[inPointer.pointerId];
  this.removePrimaryPointer_(inPointer);
};


/**
 * Prevent synth mouse events from creating pointer events.
 *
 * @private
 * @param {goog.events.BrowserEvent} inEvent The in event.
 */
ol.pointer.TouchSource.prototype.dedupSynthMouse_ = function(inEvent) {
  var lts = this.mouseSource.lastTouches;
  var t = inEvent.getBrowserEvent().changedTouches[0];
  // only the primary finger will synth mouse events
  if (this.isPrimaryTouch_(t)) {
    // remember x/y of last touch
    var lt = /** @type {ol.Pixel} */ ([t.clientX, t.clientY]);
    lts.push(lt);

    goog.global.setTimeout(function() {
      // remove touch after timeout
      goog.array.remove(lts, lt);
    }, ol.pointer.TouchSource.DEDUP_TIMEOUT);
  }
};

// Based on https://github.com/Polymer/PointerEvents

// Copyright (c) 2013 The Polymer Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the following disclaimer
// in the documentation and/or other materials provided with the
// distribution.
// * Neither the name of Google Inc. nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

goog.provide('ol.pointer.PointerEventHandler');

goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.EventTarget');

goog.require('ol.has');
goog.require('ol.pointer.MouseSource');
goog.require('ol.pointer.MsSource');
goog.require('ol.pointer.NativeSource');
goog.require('ol.pointer.PointerEvent');
goog.require('ol.pointer.TouchSource');


/**
 * @constructor
 * @extends {goog.events.EventTarget}
 * @param {Element|HTMLDocument} element Viewport element.
 */
ol.pointer.PointerEventHandler = function(element) {
  goog.base(this);

  /**
   * @const
   * @private
   * @type {Element|HTMLDocument}
   */
  this.element_ = element;

  /**
   * @const
   * @type {!Object.<string, goog.events.BrowserEvent|Object>}
   */
  this.pointerMap = {};

  /**
   * @type {Object.<string, function(goog.events.BrowserEvent)>}
   * @private
   */
  this.eventMap_ = {};

  /**
   * @type {Array.<ol.pointer.EventSource>}
   * @private
   */
  this.eventSourceList_ = [];

  this.registerSources();
};
goog.inherits(ol.pointer.PointerEventHandler, goog.events.EventTarget);


/**
 * Set up the event sources (mouse, touch and native pointers)
 * that generate pointer events.
 */
ol.pointer.PointerEventHandler.prototype.registerSources = function() {
  if (ol.has.POINTER) {
    this.registerSource('native', new ol.pointer.NativeSource(this));
  } else if (ol.has.MSPOINTER) {
    this.registerSource('ms', new ol.pointer.MsSource(this));
  } else {
    var mouseSource = new ol.pointer.MouseSource(this);
    this.registerSource('mouse', mouseSource);

    if (ol.has.TOUCH) {
      this.registerSource('touch',
          new ol.pointer.TouchSource(this, mouseSource));
    }
  }

  // register events on the viewport element
  this.register_();
};


/**
 * Add a new event source that will generate pointer events.
 *
 * @param {string} name A name for the event source
 * @param {ol.pointer.EventSource} source The source event.
 */
ol.pointer.PointerEventHandler.prototype.registerSource = function(name, source) {
  var s = source;
  var newEvents = s.getEvents();

  if (newEvents) {
    newEvents.forEach(function(e) {
      var handler = s.getHandlerForEvent(e);

      if (handler) {
        this.eventMap_[e] = handler.bind(s);
      }
    }, this);
    this.eventSourceList_.push(s);
  }
};


/**
 * Set up the events for all registered event sources.
 * @private
 */
ol.pointer.PointerEventHandler.prototype.register_ = function() {
  var l = this.eventSourceList_.length;
  var eventSource;
  for (var i = 0; i < l; i++) {
    eventSource = this.eventSourceList_[i];
    this.addEvents_(eventSource.getEvents());
  }
};


/**
 * Remove all registered events.
 * @private
 */
ol.pointer.PointerEventHandler.prototype.unregister_ = function() {
  var l = this.eventSourceList_.length;
  var eventSource;
  for (var i = 0; i < l; i++) {
    eventSource = this.eventSourceList_[i];
    this.removeEvents_(eventSource.getEvents());
  }
};


/**
 * Calls the right handler for a new event.
 * @private
 * @param {goog.events.BrowserEvent} inEvent Browser event.
 */
ol.pointer.PointerEventHandler.prototype.eventHandler_ = function(inEvent) {
  var type = inEvent.type;
  var handler = this.eventMap_[type];
  if (handler) {
    handler(inEvent);
  }
};


/**
 * Setup listeners for the given events.
 * @private
 * @param {Array.<string>} events List of events.
 */
ol.pointer.PointerEventHandler.prototype.addEvents_ = function(events) {
  events.forEach(function(eventName) {
    goog.events.listen(this.element_, eventName,
        this.eventHandler_, false, this);
  }, this);
};


/**
 * Unregister listeners for the given events.
 * @private
 * @param {Array.<string>} events List of events.
 */
ol.pointer.PointerEventHandler.prototype.removeEvents_ = function(events) {
  events.forEach(function(e) {
    goog.events.unlisten(this.element_, e,
        this.eventHandler_, false, this);
  }, this);
};


/**
 * Returns a snapshot of inEvent, with writable properties.
 *
 * @param {goog.events.BrowserEvent} browserEvent Browser event.
 * @param {Event|Touch} inEvent An event that contains
 *    properties to copy.
 * @return {Object} An object containing shallow copies of
 *    `inEvent`'s properties.
 */
ol.pointer.PointerEventHandler.prototype.cloneEvent = function(browserEvent, inEvent) {
  var eventCopy = {}, p;
  for (var i = 0, ii = ol.pointer.CLONE_PROPS.length; i < ii; i++) {
    p = ol.pointer.CLONE_PROPS[i][0];
    eventCopy[p] =
        browserEvent[p] ||
        inEvent[p] ||
        ol.pointer.CLONE_PROPS[i][1];
  }

  return eventCopy;
};


// EVENTS


/**
 * Triggers a 'pointerdown' event.
 * @param {Object} pointerEventData Pointer event data.
 * @param {goog.events.BrowserEvent} browserEvent The event.
 */
ol.pointer.PointerEventHandler.prototype.down = function(pointerEventData, browserEvent) {
  this.fireEvent(ol.pointer.EventType.POINTERDOWN,
      pointerEventData, browserEvent);
};


/**
 * Triggers a 'pointermove' event.
 * @param {Object} pointerEventData Pointer event data.
 * @param {goog.events.BrowserEvent} browserEvent The event.
 */
ol.pointer.PointerEventHandler.prototype.move = function(pointerEventData, browserEvent) {
  this.fireEvent(ol.pointer.EventType.POINTERMOVE,
      pointerEventData, browserEvent);
};


/**
 * Triggers a 'pointerup' event.
 * @param {Object} pointerEventData Pointer event data.
 * @param {goog.events.BrowserEvent} browserEvent The event.
 */
ol.pointer.PointerEventHandler.prototype.up = function(pointerEventData, browserEvent) {
  this.fireEvent(ol.pointer.EventType.POINTERUP,
      pointerEventData, browserEvent);
};


/**
 * Triggers a 'pointerenter' event.
 * @param {Object} pointerEventData Pointer event data.
 * @param {goog.events.BrowserEvent} browserEvent The event.
 */
ol.pointer.PointerEventHandler.prototype.enter = function(pointerEventData, browserEvent) {
  pointerEventData.bubbles = false;
  this.fireEvent(ol.pointer.EventType.POINTERENTER,
      pointerEventData, browserEvent);
};


/**
 * Triggers a 'pointerleave' event.
 * @param {Object} pointerEventData Pointer event data.
 * @param {goog.events.BrowserEvent} browserEvent The event.
 */
ol.pointer.PointerEventHandler.prototype.leave = function(pointerEventData, browserEvent) {
  pointerEventData.bubbles = false;
  this.fireEvent(ol.pointer.EventType.POINTERLEAVE,
      pointerEventData, browserEvent);
};


/**
 * Triggers a 'pointerover' event.
 * @param {Object} pointerEventData Pointer event data.
 * @param {goog.events.BrowserEvent} browserEvent The event.
 */
ol.pointer.PointerEventHandler.prototype.over = function(pointerEventData, browserEvent) {
  pointerEventData.bubbles = true;
  this.fireEvent(ol.pointer.EventType.POINTEROVER,
      pointerEventData, browserEvent);
};


/**
 * Triggers a 'pointerout' event.
 * @param {Object} pointerEventData Pointer event data.
 * @param {goog.events.BrowserEvent} browserEvent The event.
 */
ol.pointer.PointerEventHandler.prototype.out = function(pointerEventData, browserEvent) {
  pointerEventData.bubbles = true;
  this.fireEvent(ol.pointer.EventType.POINTEROUT,
      pointerEventData, browserEvent);
};


/**
 * Triggers a 'pointercancel' event.
 * @param {Object} pointerEventData Pointer event data.
 * @param {goog.events.BrowserEvent} browserEvent The event.
 */
ol.pointer.PointerEventHandler.prototype.cancel = function(pointerEventData, browserEvent) {
  this.fireEvent(ol.pointer.EventType.POINTERCANCEL,
      pointerEventData, browserEvent);
};


/**
 * Triggers a combination of 'pointerout' and 'pointerleave' events.
 * @param {Object} pointerEventData Pointer event data.
 * @param {goog.events.BrowserEvent} browserEvent The event.
 */
ol.pointer.PointerEventHandler.prototype.leaveOut = function(pointerEventData, browserEvent) {
  this.out(pointerEventData, browserEvent);
  if (!this.contains_(
      pointerEventData.target,
      pointerEventData.relatedTarget)) {
    this.leave(pointerEventData, browserEvent);
  }
};


/**
 * Triggers a combination of 'pointerover' and 'pointerevents' events.
 * @param {Object} pointerEventData Pointer event data.
 * @param {goog.events.BrowserEvent} browserEvent The event.
 */
ol.pointer.PointerEventHandler.prototype.enterOver = function(pointerEventData, browserEvent) {
  this.over(pointerEventData, browserEvent);
  if (!this.contains_(
      pointerEventData.target,
      pointerEventData.relatedTarget)) {
    this.enter(pointerEventData, browserEvent);
  }
};


/**
 * @private
 * @param {Element} container The container element.
 * @param {Element} contained The contained element.
 * @return {boolean} Returns true if the container element
 *   contains the other element.
 */
ol.pointer.PointerEventHandler.prototype.contains_ = function(container, contained) {
  if (!contained) {
    return false;
  }
  return goog.dom.contains(container, contained);
};


// EVENT CREATION AND TRACKING
/**
 * Creates a new Event of type `inType`, based on the information in
 * `pointerEventData`.
 *
 * @param {string} inType A string representing the type of event to create.
 * @param {Object} pointerEventData Pointer event data.
 * @param {goog.events.BrowserEvent} browserEvent The event.
 * @return {ol.pointer.PointerEvent} A PointerEvent of type `inType`.
 */
ol.pointer.PointerEventHandler.prototype.makeEvent = function(inType, pointerEventData, browserEvent) {
  return new ol.pointer.PointerEvent(inType, browserEvent, pointerEventData);
};


/**
 * Make and dispatch an event in one call.
 * @param {string} inType A string representing the type of event.
 * @param {Object} pointerEventData Pointer event data.
 * @param {goog.events.BrowserEvent} browserEvent The event.
 */
ol.pointer.PointerEventHandler.prototype.fireEvent = function(inType, pointerEventData, browserEvent) {
  var e = this.makeEvent(inType, pointerEventData, browserEvent);
  this.dispatchEvent(e);
};


/**
 * Creates a pointer event from a native pointer event
 * and dispatches this event.
 * @param {goog.events.BrowserEvent} nativeEvent A platform event with a target.
 */
ol.pointer.PointerEventHandler.prototype.fireNativeEvent = function(nativeEvent) {
  var e = this.makeEvent(nativeEvent.type, nativeEvent.getBrowserEvent(),
      nativeEvent);
  this.dispatchEvent(e);
};


/**
 * Wrap a native mouse event into a pointer event.
 * This proxy method is required for the legacy IE support.
 * @param {string} eventType The pointer event type.
 * @param {goog.events.BrowserEvent} browserEvent The event.
 * @return {ol.pointer.PointerEvent} The wrapped event.
 */
ol.pointer.PointerEventHandler.prototype.wrapMouseEvent = function(eventType, browserEvent) {
  var pointerEvent = this.makeEvent(
      eventType,
      ol.pointer.MouseSource.prepareEvent(browserEvent, this),
      browserEvent
      );
  return pointerEvent;
};


/**
 * @inheritDoc
 */
ol.pointer.PointerEventHandler.prototype.disposeInternal = function() {
  this.unregister_();
  goog.base(this, 'disposeInternal');
};


/**
 * Constants for event names.
 * @enum {string}
 */
ol.pointer.EventType = {
  POINTERMOVE: 'pointermove',
  POINTERDOWN: 'pointerdown',
  POINTERUP: 'pointerup',
  POINTEROVER: 'pointerover',
  POINTEROUT: 'pointerout',
  POINTERENTER: 'pointerenter',
  POINTERLEAVE: 'pointerleave',
  POINTERCANCEL: 'pointercancel'
};


/**
 * Properties to copy when cloning an event, with default values.
 * @type {Array.<Array>}
 */
ol.pointer.CLONE_PROPS = [
  // MouseEvent
  ['bubbles', false],
  ['cancelable', false],
  ['view', null],
  ['detail', null],
  ['screenX', 0],
  ['screenY', 0],
  ['clientX', 0],
  ['clientY', 0],
  ['ctrlKey', false],
  ['altKey', false],
  ['shiftKey', false],
  ['metaKey', false],
  ['button', 0],
  ['relatedTarget', null],
  // DOM Level 3
  ['buttons', 0],
  // PointerEvent
  ['pointerId', 0],
  ['width', 0],
  ['height', 0],
  ['pressure', 0],
  ['tiltX', 0],
  ['tiltY', 0],
  ['pointerType', ''],
  ['hwTimestamp', 0],
  ['isPrimary', false],
  // event instance
  ['type', ''],
  ['target', null],
  ['currentTarget', null],
  ['which', 0]
];

goog.provide('ol.MapBrowserEvent');
goog.provide('ol.MapBrowserEvent.EventType');
goog.provide('ol.MapBrowserEventHandler');
goog.provide('ol.MapBrowserPointerEvent');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.object');
goog.require('ol');
goog.require('ol.Coordinate');
goog.require('ol.MapEvent');
goog.require('ol.Pixel');
goog.require('ol.pointer.PointerEvent');
goog.require('ol.pointer.PointerEventHandler');


/**
 * @classdesc
 * Events emitted as map browser events are instances of this type.
 * See {@link ol.Map} for which events trigger a map browser event.
 *
 * @constructor
 * @extends {ol.MapEvent}
 * @implements {oli.MapBrowserEvent}
 * @param {string} type Event type.
 * @param {ol.Map} map Map.
 * @param {goog.events.BrowserEvent} browserEvent Browser event.
 * @param {boolean=} opt_dragging Is the map currently being dragged?
 * @param {?olx.FrameState=} opt_frameState Frame state.
 */
ol.MapBrowserEvent = function(type, map, browserEvent, opt_dragging,
    opt_frameState) {

  goog.base(this, type, map, opt_frameState);

  /**
   * @const
   * @type {goog.events.BrowserEvent}
   */
  this.browserEvent = browserEvent;

  /**
   * The original browser event.
   * @const
   * @type {Event}
   * @api stable
   */
  this.originalEvent = browserEvent.getBrowserEvent();

  /**
   * The pixel of the original browser event.
   * @type {ol.Pixel}
   * @api stable
   */
  this.pixel = map.getEventPixel(this.originalEvent);

  /**
   * The coordinate of the original browser event.
   * @type {ol.Coordinate}
   * @api stable
   */
  this.coordinate = map.getCoordinateFromPixel(this.pixel);

  /**
   * Indicates if the map is currently being dragged. Only set for
   * `POINTERDRAG` and `POINTERMOVE` events. Default is `false`.
   *
   * @type {boolean}
   * @api stable
   */
  this.dragging = opt_dragging !== undefined ? opt_dragging : false;

};
goog.inherits(ol.MapBrowserEvent, ol.MapEvent);


/**
 * Prevents the default browser action.
 * @see https://developer.mozilla.org/en-US/docs/Web/API/event.preventDefault
 * @override
 * @api stable
 */
ol.MapBrowserEvent.prototype.preventDefault = function() {
  goog.base(this, 'preventDefault');
  this.browserEvent.preventDefault();
};


/**
 * Prevents further propagation of the current event.
 * @see https://developer.mozilla.org/en-US/docs/Web/API/event.stopPropagation
 * @override
 * @api stable
 */
ol.MapBrowserEvent.prototype.stopPropagation = function() {
  goog.base(this, 'stopPropagation');
  this.browserEvent.stopPropagation();
};


/**
 * @constructor
 * @extends {ol.MapBrowserEvent}
 * @param {string} type Event type.
 * @param {ol.Map} map Map.
 * @param {ol.pointer.PointerEvent} pointerEvent Pointer event.
 * @param {boolean=} opt_dragging Is the map currently being dragged?
 * @param {?olx.FrameState=} opt_frameState Frame state.
 */
ol.MapBrowserPointerEvent = function(type, map, pointerEvent, opt_dragging,
    opt_frameState) {

  goog.base(this, type, map, pointerEvent.browserEvent, opt_dragging,
      opt_frameState);

  /**
   * @const
   * @type {ol.pointer.PointerEvent}
   */
  this.pointerEvent = pointerEvent;

};
goog.inherits(ol.MapBrowserPointerEvent, ol.MapBrowserEvent);


/**
 * @param {ol.Map} map The map with the viewport to listen to events on.
 * @constructor
 * @extends {goog.events.EventTarget}
 */
ol.MapBrowserEventHandler = function(map) {

  goog.base(this);

  /**
   * This is the element that we will listen to the real events on.
   * @type {ol.Map}
   * @private
   */
  this.map_ = map;

  /**
   * @type {number}
   * @private
   */
  this.clickTimeoutId_ = 0;

  /**
   * @type {boolean}
   * @private
   */
  this.dragging_ = false;

  /**
   * @type {Array.<goog.events.Key>}
   * @private
   */
  this.dragListenerKeys_ = null;

  /**
   * @type {goog.events.Key}
   * @private
   */
  this.pointerdownListenerKey_ = null;

  /**
   * The most recent "down" type event (or null if none have occurred).
   * Set on pointerdown.
   * @type {ol.pointer.PointerEvent}
   * @private
   */
  this.down_ = null;

  var element = this.map_.getViewport();

  /**
   * @type {number}
   * @private
   */
  this.activePointers_ = 0;

  /**
   * @type {Object.<number, boolean>}
   * @private
   */
  this.trackedTouches_ = {};

  /**
   * Event handler which generates pointer events for
   * the viewport element.
   *
   * @type {ol.pointer.PointerEventHandler}
   * @private
   */
  this.pointerEventHandler_ = new ol.pointer.PointerEventHandler(element);

  /**
   * Event handler which generates pointer events for
   * the document (used when dragging).
   *
   * @type {ol.pointer.PointerEventHandler}
   * @private
   */
  this.documentPointerEventHandler_ = null;

  this.pointerdownListenerKey_ = goog.events.listen(this.pointerEventHandler_,
      ol.pointer.EventType.POINTERDOWN,
      this.handlePointerDown_, false, this);

  this.relayedListenerKey_ = goog.events.listen(this.pointerEventHandler_,
      ol.pointer.EventType.POINTERMOVE,
      this.relayEvent_, false, this);

};
goog.inherits(ol.MapBrowserEventHandler, goog.events.EventTarget);


/**
 * @param {ol.pointer.PointerEvent} pointerEvent Pointer event.
 * @private
 */
ol.MapBrowserEventHandler.prototype.emulateClick_ = function(pointerEvent) {
  var newEvent;
  newEvent = new ol.MapBrowserPointerEvent(
      ol.MapBrowserEvent.EventType.CLICK, this.map_, pointerEvent);
  this.dispatchEvent(newEvent);
  if (this.clickTimeoutId_ !== 0) {
    // double-click
    goog.global.clearTimeout(this.clickTimeoutId_);
    this.clickTimeoutId_ = 0;
    newEvent = new ol.MapBrowserPointerEvent(
        ol.MapBrowserEvent.EventType.DBLCLICK, this.map_, pointerEvent);
    this.dispatchEvent(newEvent);
  } else {
    // click
    this.clickTimeoutId_ = goog.global.setTimeout(function() {
      this.clickTimeoutId_ = 0;
      var newEvent = new ol.MapBrowserPointerEvent(
          ol.MapBrowserEvent.EventType.SINGLECLICK, this.map_, pointerEvent);
      this.dispatchEvent(newEvent);
    }.bind(this), 250);
  }
};


/**
 * Keeps track on how many pointers are currently active.
 *
 * @param {ol.pointer.PointerEvent} pointerEvent Pointer event.
 * @private
 */
ol.MapBrowserEventHandler.prototype.updateActivePointers_ = function(pointerEvent) {
  var event = pointerEvent;

  if (event.type == ol.MapBrowserEvent.EventType.POINTERUP ||
      event.type == ol.MapBrowserEvent.EventType.POINTERCANCEL) {
    delete this.trackedTouches_[event.pointerId];
  } else if (event.type == ol.MapBrowserEvent.EventType.POINTERDOWN) {
    this.trackedTouches_[event.pointerId] = true;
  }
  this.activePointers_ = goog.object.getCount(this.trackedTouches_);
};


/**
 * @param {ol.pointer.PointerEvent} pointerEvent Pointer event.
 * @private
 */
ol.MapBrowserEventHandler.prototype.handlePointerUp_ = function(pointerEvent) {
  this.updateActivePointers_(pointerEvent);
  var newEvent = new ol.MapBrowserPointerEvent(
      ol.MapBrowserEvent.EventType.POINTERUP, this.map_, pointerEvent);
  this.dispatchEvent(newEvent);

  // We emulate click events on left mouse button click, touch contact, and pen
  // contact. isMouseActionButton returns true in these cases (evt.button is set
  // to 0).
  // See http://www.w3.org/TR/pointerevents/#button-states
  if (!this.dragging_ && this.isMouseActionButton_(pointerEvent)) {
    goog.asserts.assert(this.down_, 'this.down_ must be truthy');
    this.emulateClick_(this.down_);
  }

  goog.asserts.assert(this.activePointers_ >= 0,
      'this.activePointers_ should be equal to or larger than 0');
  if (this.activePointers_ === 0) {
    this.dragListenerKeys_.forEach(goog.events.unlistenByKey);
    this.dragListenerKeys_ = null;
    this.dragging_ = false;
    this.down_ = null;
    goog.dispose(this.documentPointerEventHandler_);
    this.documentPointerEventHandler_ = null;
  }
};


/**
 * @param {ol.pointer.PointerEvent} pointerEvent Pointer event.
 * @return {boolean} If the left mouse button was pressed.
 * @private
 */
ol.MapBrowserEventHandler.prototype.isMouseActionButton_ = function(pointerEvent) {
  return pointerEvent.button === 0;
};


/**
 * @param {ol.pointer.PointerEvent} pointerEvent Pointer event.
 * @private
 */
ol.MapBrowserEventHandler.prototype.handlePointerDown_ = function(pointerEvent) {
  this.updateActivePointers_(pointerEvent);
  var newEvent = new ol.MapBrowserPointerEvent(
      ol.MapBrowserEvent.EventType.POINTERDOWN, this.map_, pointerEvent);
  this.dispatchEvent(newEvent);

  this.down_ = pointerEvent;

  if (!this.dragListenerKeys_) {
    /* Set up a pointer event handler on the `document`,
     * which is required when the pointer is moved outside
     * the viewport when dragging.
     */
    this.documentPointerEventHandler_ =
        new ol.pointer.PointerEventHandler(document);

    this.dragListenerKeys_ = [
      goog.events.listen(this.documentPointerEventHandler_,
          ol.MapBrowserEvent.EventType.POINTERMOVE,
          this.handlePointerMove_, false, this),
      goog.events.listen(this.documentPointerEventHandler_,
          ol.MapBrowserEvent.EventType.POINTERUP,
          this.handlePointerUp_, false, this),
      /* Note that the listener for `pointercancel is set up on
       * `pointerEventHandler_` and not `documentPointerEventHandler_` like
       * the `pointerup` and `pointermove` listeners.
       *
       * The reason for this is the following: `TouchSource.vacuumTouches_()`
       * issues `pointercancel` events, when there was no `touchend` for a
       * `touchstart`. Now, let's say a first `touchstart` is registered on
       * `pointerEventHandler_`. The `documentPointerEventHandler_` is set up.
       * But `documentPointerEventHandler_` doesn't know about the first
       * `touchstart`. If there is no `touchend` for the `touchstart`, we can
       * only receive a `touchcancel` from `pointerEventHandler_`, because it is
       * only registered there.
       */
      goog.events.listen(this.pointerEventHandler_,
          ol.MapBrowserEvent.EventType.POINTERCANCEL,
          this.handlePointerUp_, false, this)
    ];
  }
};


/**
 * @param {ol.pointer.PointerEvent} pointerEvent Pointer event.
 * @private
 */
ol.MapBrowserEventHandler.prototype.handlePointerMove_ = function(pointerEvent) {
  // Fix IE10 on windows Surface : When you tap the tablet, it triggers
  // multiple pointermove events between pointerdown and pointerup with
  // the exact same coordinates of the pointerdown event. To avoid a
  // 'false' touchmove event to be dispatched , we test if the pointer
  // effectively moved.
  if (this.isMoving_(pointerEvent)) {
    this.dragging_ = true;
    var newEvent = new ol.MapBrowserPointerEvent(
        ol.MapBrowserEvent.EventType.POINTERDRAG, this.map_, pointerEvent,
        this.dragging_);
    this.dispatchEvent(newEvent);
  }

  // Some native android browser triggers mousemove events during small period
  // of time. See: https://code.google.com/p/android/issues/detail?id=5491 or
  // https://code.google.com/p/android/issues/detail?id=19827
  // ex: Galaxy Tab P3110 + Android 4.1.1
  pointerEvent.preventDefault();
};


/**
 * Wrap and relay a pointer event.  Note that this requires that the type
 * string for the MapBrowserPointerEvent matches the PointerEvent type.
 * @param {ol.pointer.PointerEvent} pointerEvent Pointer event.
 * @private
 */
ol.MapBrowserEventHandler.prototype.relayEvent_ = function(pointerEvent) {
  var dragging = !!(this.down_ && this.isMoving_(pointerEvent));
  this.dispatchEvent(new ol.MapBrowserPointerEvent(
      pointerEvent.type, this.map_, pointerEvent, dragging));
};


/**
 * @param {ol.pointer.PointerEvent} pointerEvent Pointer event.
 * @return {boolean} Is moving.
 * @private
 */
ol.MapBrowserEventHandler.prototype.isMoving_ = function(pointerEvent) {
  return pointerEvent.clientX != this.down_.clientX ||
      pointerEvent.clientY != this.down_.clientY;
};


/**
 * @inheritDoc
 */
ol.MapBrowserEventHandler.prototype.disposeInternal = function() {
  if (this.relayedListenerKey_) {
    goog.events.unlistenByKey(this.relayedListenerKey_);
    this.relayedListenerKey_ = null;
  }
  if (this.pointerdownListenerKey_) {
    goog.events.unlistenByKey(this.pointerdownListenerKey_);
    this.pointerdownListenerKey_ = null;
  }
  if (this.dragListenerKeys_) {
    this.dragListenerKeys_.forEach(goog.events.unlistenByKey);
    this.dragListenerKeys_ = null;
  }
  if (this.documentPointerEventHandler_) {
    goog.dispose(this.documentPointerEventHandler_);
    this.documentPointerEventHandler_ = null;
  }
  if (this.pointerEventHandler_) {
    goog.dispose(this.pointerEventHandler_);
    this.pointerEventHandler_ = null;
  }
  goog.base(this, 'disposeInternal');
};


/**
 * Constants for event names.
 * @enum {string}
 */
ol.MapBrowserEvent.EventType = {

  /**
   * A true single click with no dragging and no double click. Note that this
   * event is delayed by 250 ms to ensure that it is not a double click.
   * @event ol.MapBrowserEvent#singleclick
   * @api stable
   */
  SINGLECLICK: 'singleclick',

  /**
   * A click with no dragging. A double click will fire two of this.
   * @event ol.MapBrowserEvent#click
   * @api stable
   */
  CLICK: goog.events.EventType.CLICK,

  /**
   * A true double click, with no dragging.
   * @event ol.MapBrowserEvent#dblclick
   * @api stable
   */
  DBLCLICK: goog.events.EventType.DBLCLICK,

  /**
   * Triggered when a pointer is dragged.
   * @event ol.MapBrowserEvent#pointerdrag
   * @api
   */
  POINTERDRAG: 'pointerdrag',

  /**
   * Triggered when a pointer is moved. Note that on touch devices this is
   * triggered when the map is panned, so is not the same as mousemove.
   * @event ol.MapBrowserEvent#pointermove
   * @api stable
   */
  POINTERMOVE: 'pointermove',

  POINTERDOWN: 'pointerdown',
  POINTERUP: 'pointerup',
  POINTEROVER: 'pointerover',
  POINTEROUT: 'pointerout',
  POINTERENTER: 'pointerenter',
  POINTERLEAVE: 'pointerleave',
  POINTERCANCEL: 'pointercancel'
};

goog.provide('ol.layer.Base');
goog.provide('ol.layer.LayerProperty');
goog.provide('ol.layer.LayerState');

goog.require('goog.object');
goog.require('ol');
goog.require('ol.Object');
goog.require('ol.math');
goog.require('ol.source.State');


/**
 * @enum {string}
 */
ol.layer.LayerProperty = {
  OPACITY: 'opacity',
  VISIBLE: 'visible',
  EXTENT: 'extent',
  Z_INDEX: 'zIndex',
  MAX_RESOLUTION: 'maxResolution',
  MIN_RESOLUTION: 'minResolution',
  SOURCE: 'source'
};


/**
 * @typedef {{layer: ol.layer.Layer,
 *            opacity: number,
 *            sourceState: ol.source.State,
 *            visible: boolean,
 *            managed: boolean,
 *            extent: (ol.Extent|undefined),
 *            zIndex: number,
 *            maxResolution: number,
 *            minResolution: number}}
 */
ol.layer.LayerState;


/**
 * @classdesc
 * Abstract base class; normally only used for creating subclasses and not
 * instantiated in apps.
 * Note that with `ol.layer.Base` and all its subclasses, any property set in
 * the options is set as a {@link ol.Object} property on the layer object, so
 * is observable, and has get/set accessors.
 *
 * @constructor
 * @extends {ol.Object}
 * @param {olx.layer.BaseOptions} options Layer options.
 * @api stable
 */
ol.layer.Base = function(options) {

  goog.base(this);

  /**
   * @type {Object.<string, *>}
   */
  var properties = goog.object.clone(options);
  properties[ol.layer.LayerProperty.OPACITY] =
      options.opacity !== undefined ? options.opacity : 1;
  properties[ol.layer.LayerProperty.VISIBLE] =
      options.visible !== undefined ? options.visible : true;
  properties[ol.layer.LayerProperty.Z_INDEX] =
      options.zIndex !== undefined ? options.zIndex : 0;
  properties[ol.layer.LayerProperty.MAX_RESOLUTION] =
      options.maxResolution !== undefined ? options.maxResolution : Infinity;
  properties[ol.layer.LayerProperty.MIN_RESOLUTION] =
      options.minResolution !== undefined ? options.minResolution : 0;

  this.setProperties(properties);
};
goog.inherits(ol.layer.Base, ol.Object);


/**
 * @return {ol.layer.LayerState} Layer state.
 */
ol.layer.Base.prototype.getLayerState = function() {
  var opacity = this.getOpacity();
  var sourceState = this.getSourceState();
  var visible = this.getVisible();
  var extent = this.getExtent();
  var zIndex = this.getZIndex();
  var maxResolution = this.getMaxResolution();
  var minResolution = this.getMinResolution();
  return {
    layer: /** @type {ol.layer.Layer} */ (this),
    opacity: ol.math.clamp(opacity, 0, 1),
    sourceState: sourceState,
    visible: visible,
    managed: true,
    extent: extent,
    zIndex: zIndex,
    maxResolution: maxResolution,
    minResolution: Math.max(minResolution, 0)
  };
};


/**
 * @param {Array.<ol.layer.Layer>=} opt_array Array of layers (to be
 *     modified in place).
 * @return {Array.<ol.layer.Layer>} Array of layers.
 */
ol.layer.Base.prototype.getLayersArray = goog.abstractMethod;


/**
 * @param {Array.<ol.layer.LayerState>=} opt_states Optional list of layer
 *     states (to be modified in place).
 * @return {Array.<ol.layer.LayerState>} List of layer states.
 */
ol.layer.Base.prototype.getLayerStatesArray = goog.abstractMethod;


/**
 * Return the {@link ol.Extent extent} of the layer or `undefined` if it
 * will be visible regardless of extent.
 * @return {ol.Extent|undefined} The layer extent.
 * @observable
 * @api stable
 */
ol.layer.Base.prototype.getExtent = function() {
  return /** @type {ol.Extent|undefined} */ (
      this.get(ol.layer.LayerProperty.EXTENT));
};


/**
 * Return the maximum resolution of the layer.
 * @return {number} The maximum resolution of the layer.
 * @observable
 * @api stable
 */
ol.layer.Base.prototype.getMaxResolution = function() {
  return /** @type {number} */ (
      this.get(ol.layer.LayerProperty.MAX_RESOLUTION));
};


/**
 * Return the minimum resolution of the layer.
 * @return {number} The minimum resolution of the layer.
 * @observable
 * @api stable
 */
ol.layer.Base.prototype.getMinResolution = function() {
  return /** @type {number} */ (
      this.get(ol.layer.LayerProperty.MIN_RESOLUTION));
};


/**
 * Return the opacity of the layer (between 0 and 1).
 * @return {number} The opacity of the layer.
 * @observable
 * @api stable
 */
ol.layer.Base.prototype.getOpacity = function() {
  return /** @type {number} */ (this.get(ol.layer.LayerProperty.OPACITY));
};


/**
 * @return {ol.source.State} Source state.
 */
ol.layer.Base.prototype.getSourceState = goog.abstractMethod;


/**
 * Return the visibility of the layer (`true` or `false`).
 * @return {boolean} The visibility of the layer.
 * @observable
 * @api stable
 */
ol.layer.Base.prototype.getVisible = function() {
  return /** @type {boolean} */ (this.get(ol.layer.LayerProperty.VISIBLE));
};


/**
 * Return the Z-index of the layer, which is used to order layers before
 * rendering. The default Z-index is 0.
 * @return {number} The Z-index of the layer.
 * @observable
 * @api
 */
ol.layer.Base.prototype.getZIndex = function() {
  return /** @type {number} */ (this.get(ol.layer.LayerProperty.Z_INDEX));
};


/**
 * Set the extent at which the layer is visible.  If `undefined`, the layer
 * will be visible at all extents.
 * @param {ol.Extent|undefined} extent The extent of the layer.
 * @observable
 * @api stable
 */
ol.layer.Base.prototype.setExtent = function(extent) {
  this.set(ol.layer.LayerProperty.EXTENT, extent);
};


/**
 * Set the maximum resolution at which the layer is visible.
 * @param {number} maxResolution The maximum resolution of the layer.
 * @observable
 * @api stable
 */
ol.layer.Base.prototype.setMaxResolution = function(maxResolution) {
  this.set(ol.layer.LayerProperty.MAX_RESOLUTION, maxResolution);
};


/**
 * Set the minimum resolution at which the layer is visible.
 * @param {number} minResolution The minimum resolution of the layer.
 * @observable
 * @api stable
 */
ol.layer.Base.prototype.setMinResolution = function(minResolution) {
  this.set(ol.layer.LayerProperty.MIN_RESOLUTION, minResolution);
};


/**
 * Set the opacity of the layer, allowed values range from 0 to 1.
 * @param {number} opacity The opacity of the layer.
 * @observable
 * @api stable
 */
ol.layer.Base.prototype.setOpacity = function(opacity) {
  this.set(ol.layer.LayerProperty.OPACITY, opacity);
};


/**
 * Set the visibility of the layer (`true` or `false`).
 * @param {boolean} visible The visibility of the layer.
 * @observable
 * @api stable
 */
ol.layer.Base.prototype.setVisible = function(visible) {
  this.set(ol.layer.LayerProperty.VISIBLE, visible);
};


/**
 * Set Z-index of the layer, which is used to order layers before rendering.
 * The default Z-index is 0.
 * @param {number} zindex The z-index of the layer.
 * @observable
 * @api
 */
ol.layer.Base.prototype.setZIndex = function(zindex) {
  this.set(ol.layer.LayerProperty.Z_INDEX, zindex);
};

goog.provide('ol.render.VectorContext');


/**
 * Context for drawing geometries.  A vector context is available on render
 * events and does not need to be constructed directly.
 * @constructor
 * @struct
 * @api
 */
ol.render.VectorContext = function() {
};


/**
 * @param {number} zIndex Z index.
 * @param {function(ol.render.VectorContext)} callback Callback.
 */
ol.render.VectorContext.prototype.drawAsync = goog.abstractMethod;


/**
 * @param {ol.geom.Circle} circleGeometry Circle geometry.
 * @param {ol.Feature} feature Feature,
 */
ol.render.VectorContext.prototype.drawCircleGeometry = goog.abstractMethod;


/**
 * @param {ol.Feature} feature Feature.
 * @param {ol.style.Style} style Style.
 */
ol.render.VectorContext.prototype.drawFeature = goog.abstractMethod;


/**
 * @param {ol.geom.GeometryCollection} geometryCollectionGeometry Geometry
 *     collection.
 * @param {ol.Feature} feature Feature.
 */
ol.render.VectorContext.prototype.drawGeometryCollectionGeometry =
    goog.abstractMethod;


/**
 * @param {ol.geom.LineString|ol.render.Feature} lineStringGeometry Line
 *     string geometry.
 * @param {ol.Feature|ol.render.Feature} feature Feature.
 */
ol.render.VectorContext.prototype.drawLineStringGeometry =
    goog.abstractMethod;


/**
 * @param {ol.geom.MultiLineString|ol.render.Feature} multiLineStringGeometry
 *     MultiLineString geometry.
 * @param {ol.Feature|ol.render.Feature} feature Feature.
 */
ol.render.VectorContext.prototype.drawMultiLineStringGeometry =
    goog.abstractMethod;


/**
 * @param {ol.geom.MultiPoint|ol.render.Feature} multiPointGeometry MultiPoint
 *     geometry.
 * @param {ol.Feature|ol.render.Feature} feature Feature.
 */
ol.render.VectorContext.prototype.drawMultiPointGeometry = goog.abstractMethod;


/**
 * @param {ol.geom.MultiPolygon} multiPolygonGeometry MultiPolygon geometry.
 * @param {ol.Feature} feature Feature.
 */
ol.render.VectorContext.prototype.drawMultiPolygonGeometry =
    goog.abstractMethod;


/**
 * @param {ol.geom.Point|ol.render.Feature} pointGeometry Point geometry.
 * @param {ol.Feature|ol.render.Feature} feature Feature.
 */
ol.render.VectorContext.prototype.drawPointGeometry = goog.abstractMethod;


/**
 * @param {ol.geom.Polygon|ol.render.Feature} polygonGeometry Polygon
 *     geometry.
 * @param {ol.Feature|ol.render.Feature} feature Feature.
 */
ol.render.VectorContext.prototype.drawPolygonGeometry = goog.abstractMethod;


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {ol.geom.Geometry|ol.render.Feature} geometry Geometry.
 * @param {ol.Feature|ol.render.Feature} feature Feature.
 */
ol.render.VectorContext.prototype.drawText = goog.abstractMethod;


/**
 * @param {ol.style.Fill} fillStyle Fill style.
 * @param {ol.style.Stroke} strokeStyle Stroke style.
 */
ol.render.VectorContext.prototype.setFillStrokeStyle = goog.abstractMethod;


/**
 * @param {ol.style.Image} imageStyle Image style.
 */
ol.render.VectorContext.prototype.setImageStyle = goog.abstractMethod;


/**
 * @param {ol.style.Text} textStyle Text style.
 */
ol.render.VectorContext.prototype.setTextStyle = goog.abstractMethod;

goog.provide('ol.render.Event');
goog.provide('ol.render.EventType');

goog.require('goog.events.Event');
goog.require('ol.render.VectorContext');


/**
 * @enum {string}
 */
ol.render.EventType = {
  /**
   * @event ol.render.Event#postcompose
   * @api
   */
  POSTCOMPOSE: 'postcompose',
  /**
   * @event ol.render.Event#precompose
   * @api
   */
  PRECOMPOSE: 'precompose',
  /**
   * @event ol.render.Event#render
   * @api
   */
  RENDER: 'render'
};


/**
 * @constructor
 * @extends {goog.events.Event}
 * @implements {oli.render.Event}
 * @param {ol.render.EventType} type Type.
 * @param {Object=} opt_target Target.
 * @param {ol.render.VectorContext=} opt_vectorContext Vector context.
 * @param {olx.FrameState=} opt_frameState Frame state.
 * @param {?CanvasRenderingContext2D=} opt_context Context.
 * @param {?ol.webgl.Context=} opt_glContext WebGL Context.
 */
ol.render.Event = function(
    type, opt_target, opt_vectorContext, opt_frameState, opt_context,
    opt_glContext) {

  goog.base(this, type, opt_target);

  /**
   * For canvas, this is an instance of {@link ol.render.canvas.Immediate}.
   * @type {ol.render.VectorContext|undefined}
   * @api
   */
  this.vectorContext = opt_vectorContext;

  /**
   * An object representing the current render frame state.
   * @type {olx.FrameState|undefined}
   * @api
   */
  this.frameState = opt_frameState;

  /**
   * Canvas context. Only available when a Canvas renderer is used, null
   * otherwise.
   * @type {CanvasRenderingContext2D|null|undefined}
   * @api
   */
  this.context = opt_context;

  /**
   * WebGL context. Only available when a WebGL renderer is used, null
   * otherwise.
   * @type {ol.webgl.Context|null|undefined}
   * @api
   */
  this.glContext = opt_glContext;

};
goog.inherits(ol.render.Event, goog.events.Event);

goog.provide('ol.layer.Layer');

goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.object');
goog.require('ol');
goog.require('ol.Object');
goog.require('ol.layer.Base');
goog.require('ol.layer.LayerProperty');
goog.require('ol.render.EventType');
goog.require('ol.source.State');


/**
 * @classdesc
 * Abstract base class; normally only used for creating subclasses and not
 * instantiated in apps.
 * A visual representation of raster or vector map data.
 * Layers group together those properties that pertain to how the data is to be
 * displayed, irrespective of the source of that data.
 *
 * Layers are usually added to a map with {@link ol.Map#addLayer}. Components
 * like {@link ol.interaction.Select} use unmanaged layers internally. These
 * unmanaged layers are associated with the map using
 * {@link ol.layer.Layer#setMap} instead.
 *
 * A generic `change` event is fired when the state of the source changes.
 *
 * @constructor
 * @extends {ol.layer.Base}
 * @fires ol.render.Event
 * @param {olx.layer.LayerOptions} options Layer options.
 * @api stable
 */
ol.layer.Layer = function(options) {

  var baseOptions = goog.object.clone(options);
  delete baseOptions.source;

  goog.base(this, /** @type {olx.layer.LayerOptions} */ (baseOptions));

  /**
   * @private
   * @type {goog.events.Key}
   */
  this.mapPrecomposeKey_ = null;

  /**
   * @private
   * @type {goog.events.Key}
   */
  this.mapRenderKey_ = null;

  /**
   * @private
   * @type {goog.events.Key}
   */
  this.sourceChangeKey_ = null;

  if (options.map) {
    this.setMap(options.map);
  }

  goog.events.listen(this,
      ol.Object.getChangeEventType(ol.layer.LayerProperty.SOURCE),
      this.handleSourcePropertyChange_, false, this);

  var source = options.source ? options.source : null;
  this.setSource(source);
};
goog.inherits(ol.layer.Layer, ol.layer.Base);


/**
 * Return `true` if the layer is visible, and if the passed resolution is
 * between the layer's minResolution and maxResolution. The comparison is
 * inclusive for `minResolution` and exclusive for `maxResolution`.
 * @param {ol.layer.LayerState} layerState Layer state.
 * @param {number} resolution Resolution.
 * @return {boolean} The layer is visible at the given resolution.
 */
ol.layer.Layer.visibleAtResolution = function(layerState, resolution) {
  return layerState.visible && resolution >= layerState.minResolution &&
      resolution < layerState.maxResolution;
};


/**
 * @inheritDoc
 */
ol.layer.Layer.prototype.getLayersArray = function(opt_array) {
  var array = opt_array ? opt_array : [];
  array.push(this);
  return array;
};


/**
 * @inheritDoc
 */
ol.layer.Layer.prototype.getLayerStatesArray = function(opt_states) {
  var states = opt_states ? opt_states : [];
  states.push(this.getLayerState());
  return states;
};


/**
 * Get the layer source.
 * @return {ol.source.Source} The layer source (or `null` if not yet set).
 * @observable
 * @api stable
 */
ol.layer.Layer.prototype.getSource = function() {
  var source = this.get(ol.layer.LayerProperty.SOURCE);
  return /** @type {ol.source.Source} */ (source) || null;
};


/**
  * @inheritDoc
  */
ol.layer.Layer.prototype.getSourceState = function() {
  var source = this.getSource();
  return !source ? ol.source.State.UNDEFINED : source.getState();
};


/**
 * @private
 */
ol.layer.Layer.prototype.handleSourceChange_ = function() {
  this.changed();
};


/**
 * @private
 */
ol.layer.Layer.prototype.handleSourcePropertyChange_ = function() {
  if (this.sourceChangeKey_) {
    goog.events.unlistenByKey(this.sourceChangeKey_);
    this.sourceChangeKey_ = null;
  }
  var source = this.getSource();
  if (source) {
    this.sourceChangeKey_ = goog.events.listen(source,
        goog.events.EventType.CHANGE, this.handleSourceChange_, false, this);
  }
  this.changed();
};


/**
 * Sets the layer to be rendered on top of other layers on a map. The map will
 * not manage this layer in its layers collection, and the callback in
 * {@link ol.Map#forEachLayerAtPixel} will receive `null` as layer. This
 * is useful for temporary layers. To remove an unmanaged layer from the map,
 * use `#setMap(null)`.
 *
 * To add the layer to a map and have it managed by the map, use
 * {@link ol.Map#addLayer} instead.
 * @param {ol.Map} map Map.
 * @api
 */
ol.layer.Layer.prototype.setMap = function(map) {
  goog.events.unlistenByKey(this.mapPrecomposeKey_);
  this.mapPrecomposeKey_ = null;
  if (!map) {
    this.changed();
  }
  goog.events.unlistenByKey(this.mapRenderKey_);
  this.mapRenderKey_ = null;
  if (map) {
    this.mapPrecomposeKey_ = goog.events.listen(
        map, ol.render.EventType.PRECOMPOSE, function(evt) {
          var layerState = this.getLayerState();
          layerState.managed = false;
          layerState.zIndex = Infinity;
          evt.frameState.layerStatesArray.push(layerState);
          evt.frameState.layerStates[goog.getUid(this)] = layerState;
        }, false, this);
    this.mapRenderKey_ = goog.events.listen(
        this, goog.events.EventType.CHANGE, map.render, false, map);
    this.changed();
  }
};


/**
 * Set the layer source.
 * @param {ol.source.Source} source The layer source.
 * @observable
 * @api stable
 */
ol.layer.Layer.prototype.setSource = function(source) {
  this.set(ol.layer.LayerProperty.SOURCE, source);
};

goog.provide('ol.ImageBase');
goog.provide('ol.ImageState');

goog.require('goog.asserts');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('ol.Attribution');
goog.require('ol.Extent');


/**
 * @enum {number}
 */
ol.ImageState = {
  IDLE: 0,
  LOADING: 1,
  LOADED: 2,
  ERROR: 3
};


/**
 * @constructor
 * @extends {goog.events.EventTarget}
 * @param {ol.Extent} extent Extent.
 * @param {number|undefined} resolution Resolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {ol.ImageState} state State.
 * @param {Array.<ol.Attribution>} attributions Attributions.
 */
ol.ImageBase = function(extent, resolution, pixelRatio, state, attributions) {

  goog.base(this);

  /**
   * @private
   * @type {Array.<ol.Attribution>}
   */
  this.attributions_ = attributions;

  /**
   * @protected
   * @type {ol.Extent}
   */
  this.extent = extent;

  /**
   * @private
   * @type {number}
   */
  this.pixelRatio_ = pixelRatio;

  /**
   * @protected
   * @type {number|undefined}
   */
  this.resolution = resolution;

  /**
   * @protected
   * @type {ol.ImageState}
   */
  this.state = state;

};
goog.inherits(ol.ImageBase, goog.events.EventTarget);


/**
 * @protected
 */
ol.ImageBase.prototype.changed = function() {
  this.dispatchEvent(goog.events.EventType.CHANGE);
};


/**
 * @return {Array.<ol.Attribution>} Attributions.
 */
ol.ImageBase.prototype.getAttributions = function() {
  return this.attributions_;
};


/**
 * @return {ol.Extent} Extent.
 */
ol.ImageBase.prototype.getExtent = function() {
  return this.extent;
};


/**
 * @param {Object=} opt_context Object.
 * @return {HTMLCanvasElement|Image|HTMLVideoElement} Image.
 */
ol.ImageBase.prototype.getImage = goog.abstractMethod;


/**
 * @return {number} PixelRatio.
 */
ol.ImageBase.prototype.getPixelRatio = function() {
  return this.pixelRatio_;
};


/**
 * @return {number} Resolution.
 */
ol.ImageBase.prototype.getResolution = function() {
  goog.asserts.assert(this.resolution !== undefined, 'resolution not yet set');
  return this.resolution;
};


/**
 * @return {ol.ImageState} State.
 */
ol.ImageBase.prototype.getState = function() {
  return this.state;
};


/**
 * Load not yet loaded URI.
 */
ol.ImageBase.prototype.load = goog.abstractMethod;

goog.provide('ol.vec.Mat4');
goog.provide('ol.vec.Mat4.Number');

goog.require('goog.vec.Mat4');


/**
 * A alias for the goog.vec.Number type.
 * @typedef {goog.vec.Number}
 */
ol.vec.Mat4.Number;


/**
 * @param {!goog.vec.Mat4.Number} mat Matrix.
 * @param {number} translateX1 Translate X1.
 * @param {number} translateY1 Translate Y1.
 * @param {number} scaleX Scale X.
 * @param {number} scaleY Scale Y.
 * @param {number} rotation Rotation.
 * @param {number} translateX2 Translate X2.
 * @param {number} translateY2 Translate Y2.
 * @return {!goog.vec.Mat4.Number} Matrix.
 */
ol.vec.Mat4.makeTransform2D = function(mat, translateX1, translateY1,
    scaleX, scaleY, rotation, translateX2, translateY2) {
  goog.vec.Mat4.makeIdentity(mat);
  if (translateX1 !== 0 || translateY1 !== 0) {
    goog.vec.Mat4.translate(mat, translateX1, translateY1, 0);
  }
  if (scaleX != 1 || scaleY != 1) {
    goog.vec.Mat4.scale(mat, scaleX, scaleY, 1);
  }
  if (rotation !== 0) {
    goog.vec.Mat4.rotateZ(mat, rotation);
  }
  if (translateX2 !== 0 || translateY2 !== 0) {
    goog.vec.Mat4.translate(mat, translateX2, translateY2, 0);
  }
  return mat;
};


/**
 * Returns true if mat1 and mat2 represent the same 2D transformation.
 * @param {goog.vec.Mat4.Number} mat1 Matrix 1.
 * @param {goog.vec.Mat4.Number} mat2 Matrix 2.
 * @return {boolean} Equal 2D.
 */
ol.vec.Mat4.equals2D = function(mat1, mat2) {
  return (
      goog.vec.Mat4.getElement(mat1, 0, 0) ==
      goog.vec.Mat4.getElement(mat2, 0, 0) &&
      goog.vec.Mat4.getElement(mat1, 1, 0) ==
      goog.vec.Mat4.getElement(mat2, 1, 0) &&
      goog.vec.Mat4.getElement(mat1, 0, 1) ==
      goog.vec.Mat4.getElement(mat2, 0, 1) &&
      goog.vec.Mat4.getElement(mat1, 1, 1) ==
      goog.vec.Mat4.getElement(mat2, 1, 1) &&
      goog.vec.Mat4.getElement(mat1, 0, 3) ==
      goog.vec.Mat4.getElement(mat2, 0, 3) &&
      goog.vec.Mat4.getElement(mat1, 1, 3) ==
      goog.vec.Mat4.getElement(mat2, 1, 3));
};


/**
 * Transforms the given vector with the given matrix storing the resulting,
 * transformed vector into resultVec. The input vector is multiplied against the
 * upper 2x4 matrix omitting the projective component.
 *
 * @param {goog.vec.Mat4.Number} mat The matrix supplying the transformation.
 * @param {Array.<number>} vec The 3 element vector to transform.
 * @param {Array.<number>} resultVec The 3 element vector to receive the results
 *     (may be vec).
 * @return {Array.<number>} return resultVec so that operations can be
 *     chained together.
 */
ol.vec.Mat4.multVec2 = function(mat, vec, resultVec) {
  var m00 = goog.vec.Mat4.getElement(mat, 0, 0);
  var m10 = goog.vec.Mat4.getElement(mat, 1, 0);
  var m01 = goog.vec.Mat4.getElement(mat, 0, 1);
  var m11 = goog.vec.Mat4.getElement(mat, 1, 1);
  var m03 = goog.vec.Mat4.getElement(mat, 0, 3);
  var m13 = goog.vec.Mat4.getElement(mat, 1, 3);
  var x = vec[0], y = vec[1];
  resultVec[0] = m00 * x + m01 * y + m03;
  resultVec[1] = m10 * x + m11 * y + m13;
  return resultVec;
};

goog.provide('ol.renderer.Layer');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.functions');
goog.require('ol');
goog.require('ol.ImageState');
goog.require('ol.Observable');
goog.require('ol.TileRange');
goog.require('ol.TileState');
goog.require('ol.layer.Layer');
goog.require('ol.source.Source');
goog.require('ol.source.State');
goog.require('ol.source.Tile');
goog.require('ol.vec.Mat4');


/**
 * @constructor
 * @extends {ol.Observable}
 * @param {ol.layer.Layer} layer Layer.
 * @struct
 */
ol.renderer.Layer = function(layer) {

  goog.base(this);

  /**
   * @private
   * @type {ol.layer.Layer}
   */
  this.layer_ = layer;


};
goog.inherits(ol.renderer.Layer, ol.Observable);


/**
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {olx.FrameState} frameState Frame state.
 * @param {function(this: S, (ol.Feature|ol.render.Feature), ol.layer.Layer): T}
 *     callback Feature callback.
 * @param {S} thisArg Value to use as `this` when executing `callback`.
 * @return {T|undefined} Callback result.
 * @template S,T
 */
ol.renderer.Layer.prototype.forEachFeatureAtCoordinate = ol.nullFunction;


/**
 * @param {ol.Pixel} pixel Pixel.
 * @param {olx.FrameState} frameState Frame state.
 * @param {function(this: S, ol.layer.Layer): T} callback Layer callback.
 * @param {S} thisArg Value to use as `this` when executing `callback`.
 * @return {T|undefined} Callback result.
 * @template S,T
 */
ol.renderer.Layer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) {
  var coordinate = pixel.slice();
  ol.vec.Mat4.multVec2(
      frameState.pixelToCoordinateMatrix, coordinate, coordinate);

  var hasFeature = this.forEachFeatureAtCoordinate(
      coordinate, frameState, goog.functions.TRUE, this);

  if (hasFeature) {
    return callback.call(thisArg, this.layer_);
  } else {
    return undefined;
  }
};


/**
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {olx.FrameState} frameState Frame state.
 * @return {boolean} Is there a feature at the given coordinate?
 */
ol.renderer.Layer.prototype.hasFeatureAtCoordinate = goog.functions.FALSE;


/**
 * Create a function that adds loaded tiles to the tile lookup.
 * @param {ol.source.Tile} source Tile source.
 * @param {ol.proj.Projection} projection Projection of the tiles.
 * @param {Object.<number, Object.<string, ol.Tile>>} tiles Lookup of loaded
 *     tiles by zoom level.
 * @return {function(number, ol.TileRange):boolean} A function that can be
 *     called with a zoom level and a tile range to add loaded tiles to the
 *     lookup.
 * @protected
 */
ol.renderer.Layer.prototype.createLoadedTileFinder = function(source, projection, tiles) {
  return (
      /**
       * @param {number} zoom Zoom level.
       * @param {ol.TileRange} tileRange Tile range.
       * @return {boolean} The tile range is fully loaded.
       */
      function(zoom, tileRange) {
        function callback(tile) {
          if (!tiles[zoom]) {
            tiles[zoom] = {};
          }
          tiles[zoom][tile.tileCoord.toString()] = tile;
        }
        return source.forEachLoadedTile(projection, zoom, tileRange, callback);
      });
};


/**
 * @return {ol.layer.Layer} Layer.
 */
ol.renderer.Layer.prototype.getLayer = function() {
  return this.layer_;
};


/**
 * Handle changes in image state.
 * @param {goog.events.Event} event Image change event.
 * @private
 */
ol.renderer.Layer.prototype.handleImageChange_ = function(event) {
  var image = /** @type {ol.Image} */ (event.target);
  if (image.getState() === ol.ImageState.LOADED) {
    this.renderIfReadyAndVisible();
  }
};


/**
 * Load the image if not already loaded, and register the image change
 * listener if needed.
 * @param {ol.ImageBase} image Image.
 * @return {boolean} `true` if the image is already loaded, `false`
 *     otherwise.
 * @protected
 */
ol.renderer.Layer.prototype.loadImage = function(image) {
  var imageState = image.getState();
  if (imageState != ol.ImageState.LOADED &&
      imageState != ol.ImageState.ERROR) {
    // the image is either "idle" or "loading", register the change
    // listener (a noop if the listener was already registered)
    goog.asserts.assert(imageState == ol.ImageState.IDLE ||
        imageState == ol.ImageState.LOADING,
        'imageState is "idle" or "loading"');
    goog.events.listen(image, goog.events.EventType.CHANGE,
        this.handleImageChange_, false, this);
  }
  if (imageState == ol.ImageState.IDLE) {
    image.load();
    imageState = image.getState();
    goog.asserts.assert(imageState == ol.ImageState.LOADING ||
        imageState == ol.ImageState.LOADED,
        'imageState is "loading" or "loaded"');
  }
  return imageState == ol.ImageState.LOADED;
};


/**
 * @protected
 */
ol.renderer.Layer.prototype.renderIfReadyAndVisible = function() {
  var layer = this.getLayer();
  if (layer.getVisible() && layer.getSourceState() == ol.source.State.READY) {
    this.changed();
  }
};


/**
 * @param {olx.FrameState} frameState Frame state.
 * @param {ol.source.Tile} tileSource Tile source.
 * @protected
 */
ol.renderer.Layer.prototype.scheduleExpireCache = function(frameState, tileSource) {
  if (tileSource.canExpireCache()) {
    frameState.postRenderFunctions.push(
        goog.partial(
            /**
             * @param {ol.source.Tile} tileSource Tile source.
             * @param {ol.Map} map Map.
             * @param {olx.FrameState} frameState Frame state.
             */
            function(tileSource, map, frameState) {
              var tileSourceKey = goog.getUid(tileSource).toString();
              tileSource.expireCache(frameState.viewState.projection,
                                     frameState.usedTiles[tileSourceKey]);
            }, tileSource));
  }
};


/**
 * @param {Object.<string, ol.Attribution>} attributionsSet Attributions
 *     set (target).
 * @param {Array.<ol.Attribution>} attributions Attributions (source).
 * @protected
 */
ol.renderer.Layer.prototype.updateAttributions = function(attributionsSet, attributions) {
  if (attributions) {
    var attribution, i, ii;
    for (i = 0, ii = attributions.length; i < ii; ++i) {
      attribution = attributions[i];
      attributionsSet[goog.getUid(attribution).toString()] = attribution;
    }
  }
};


/**
 * @param {olx.FrameState} frameState Frame state.
 * @param {ol.source.Source} source Source.
 * @protected
 */
ol.renderer.Layer.prototype.updateLogos = function(frameState, source) {
  var logo = source.getLogo();
  if (logo !== undefined) {
    if (goog.isString(logo)) {
      frameState.logos[logo] = '';
    } else if (goog.isObject(logo)) {
      goog.asserts.assertString(logo.href, 'logo.href is a string');
      goog.asserts.assertString(logo.src, 'logo.src is a string');
      frameState.logos[logo.src] = logo.href;
    }
  }
};


/**
 * @param {Object.<string, Object.<string, ol.TileRange>>} usedTiles Used tiles.
 * @param {ol.source.Tile} tileSource Tile source.
 * @param {number} z Z.
 * @param {ol.TileRange} tileRange Tile range.
 * @protected
 */
ol.renderer.Layer.prototype.updateUsedTiles = function(usedTiles, tileSource, z, tileRange) {
  // FIXME should we use tilesToDrawByZ instead?
  var tileSourceKey = goog.getUid(tileSource).toString();
  var zKey = z.toString();
  if (tileSourceKey in usedTiles) {
    if (zKey in usedTiles[tileSourceKey]) {
      usedTiles[tileSourceKey][zKey].extend(tileRange);
    } else {
      usedTiles[tileSourceKey][zKey] = tileRange;
    }
  } else {
    usedTiles[tileSourceKey] = {};
    usedTiles[tileSourceKey][zKey] = tileRange;
  }
};


/**
 * @param {ol.Coordinate} center Center.
 * @param {number} resolution Resolution.
 * @param {ol.Size} size Size.
 * @protected
 * @return {ol.Coordinate} Snapped center.
 */
ol.renderer.Layer.prototype.snapCenterToPixel = function(center, resolution, size) {
  return [
    resolution * (Math.round(center[0] / resolution) + (size[0] % 2) / 2),
    resolution * (Math.round(center[1] / resolution) + (size[1] % 2) / 2)
  ];
};


/**
 * Manage tile pyramid.
 * This function performs a number of functions related to the tiles at the
 * current zoom and lower zoom levels:
 * - registers idle tiles in frameState.wantedTiles so that they are not
 *   discarded by the tile queue
 * - enqueues missing tiles
 * @param {olx.FrameState} frameState Frame state.
 * @param {ol.source.Tile} tileSource Tile source.
 * @param {ol.tilegrid.TileGrid} tileGrid Tile grid.
 * @param {number} pixelRatio Pixel ratio.
 * @param {ol.proj.Projection} projection Projection.
 * @param {ol.Extent} extent Extent.
 * @param {number} currentZ Current Z.
 * @param {number} preload Load low resolution tiles up to 'preload' levels.
 * @param {function(this: T, ol.Tile)=} opt_tileCallback Tile callback.
 * @param {T=} opt_this Object to use as `this` in `opt_tileCallback`.
 * @protected
 * @template T
 */
ol.renderer.Layer.prototype.manageTilePyramid = function(
    frameState, tileSource, tileGrid, pixelRatio, projection, extent,
    currentZ, preload, opt_tileCallback, opt_this) {
  var tileSourceKey = goog.getUid(tileSource).toString();
  if (!(tileSourceKey in frameState.wantedTiles)) {
    frameState.wantedTiles[tileSourceKey] = {};
  }
  var wantedTiles = frameState.wantedTiles[tileSourceKey];
  var tileQueue = frameState.tileQueue;
  var minZoom = tileGrid.getMinZoom();
  var tile, tileRange, tileResolution, x, y, z;
  for (z = currentZ; z >= minZoom; --z) {
    tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z, tileRange);
    tileResolution = tileGrid.getResolution(z);
    for (x = tileRange.minX; x <= tileRange.maxX; ++x) {
      for (y = tileRange.minY; y <= tileRange.maxY; ++y) {
        if (currentZ - z <= preload) {
          tile = tileSource.getTile(z, x, y, pixelRatio, projection);
          if (tile.getState() == ol.TileState.IDLE) {
            wantedTiles[tile.tileCoord.toString()] = true;
            if (!tileQueue.isKeyQueued(tile.getKey())) {
              tileQueue.enqueue([tile, tileSourceKey,
                tileGrid.getTileCoordCenter(tile.tileCoord), tileResolution]);
            }
          }
          if (opt_tileCallback !== undefined) {
            opt_tileCallback.call(opt_this, tile);
          }
        } else {
          tileSource.useTile(z, x, y, projection);
        }
      }
    }
  }
};

goog.provide('ol.style.Image');
goog.provide('ol.style.ImageState');


/**
 * @enum {number}
 */
ol.style.ImageState = {
  IDLE: 0,
  LOADING: 1,
  LOADED: 2,
  ERROR: 3
};


/**
 * @typedef {{opacity: number,
 *            rotateWithView: boolean,
 *            rotation: number,
 *            scale: number,
 *            snapToPixel: boolean}}
 */
ol.style.ImageOptions;


/**
 * @classdesc
 * A base class used for creating subclasses and not instantiated in
 * apps. Base class for {@link ol.style.Icon}, {@link ol.style.Circle} and
 * {@link ol.style.RegularShape}.
 *
 * @constructor
 * @param {ol.style.ImageOptions} options Options.
 * @api
 */
ol.style.Image = function(options) {

  /**
   * @private
   * @type {number}
   */
  this.opacity_ = options.opacity;

  /**
   * @private
   * @type {boolean}
   */
  this.rotateWithView_ = options.rotateWithView;

  /**
   * @private
   * @type {number}
   */
  this.rotation_ = options.rotation;

  /**
   * @private
   * @type {number}
   */
  this.scale_ = options.scale;

  /**
   * @private
   * @type {boolean}
   */
  this.snapToPixel_ = options.snapToPixel;

};


/**
 * Get the symbolizer opacity.
 * @return {number} Opacity.
 * @api
 */
ol.style.Image.prototype.getOpacity = function() {
  return this.opacity_;
};


/**
 * Determine whether the symbolizer rotates with the map.
 * @return {boolean} Rotate with map.
 * @api
 */
ol.style.Image.prototype.getRotateWithView = function() {
  return this.rotateWithView_;
};


/**
 * Get the symoblizer rotation.
 * @return {number} Rotation.
 * @api
 */
ol.style.Image.prototype.getRotation = function() {
  return this.rotation_;
};


/**
 * Get the symbolizer scale.
 * @return {number} Scale.
 * @api
 */
ol.style.Image.prototype.getScale = function() {
  return this.scale_;
};


/**
 * Determine whether the symbolizer should be snapped to a pixel.
 * @return {boolean} The symbolizer should snap to a pixel.
 * @api
 */
ol.style.Image.prototype.getSnapToPixel = function() {
  return this.snapToPixel_;
};


/**
 * Get the anchor point.  The anchor determines the center point for the
 * symbolizer.  Its units are determined by `anchorXUnits` and `anchorYUnits`.
 * @function
 * @return {Array.<number>} Anchor.
 */
ol.style.Image.prototype.getAnchor = goog.abstractMethod;


/**
 * Get the image element for the symbolizer.
 * @function
 * @param {number} pixelRatio Pixel ratio.
 * @return {HTMLCanvasElement|HTMLVideoElement|Image} Image element.
 */
ol.style.Image.prototype.getImage = goog.abstractMethod;


/**
 * @param {number} pixelRatio Pixel ratio.
 * @return {HTMLCanvasElement|HTMLVideoElement|Image} Image element.
 */
ol.style.Image.prototype.getHitDetectionImage = goog.abstractMethod;


/**
 * @return {ol.style.ImageState} Image state.
 */
ol.style.Image.prototype.getImageState = goog.abstractMethod;


/**
 * @return {ol.Size} Image size.
 */
ol.style.Image.prototype.getImageSize = goog.abstractMethod;


/**
 * @return {ol.Size} Size of the hit-detection image.
 */
ol.style.Image.prototype.getHitDetectionImageSize = goog.abstractMethod;


/**
 * Get the origin of the symbolizer.
 * @function
 * @return {Array.<number>} Origin.
 */
ol.style.Image.prototype.getOrigin = goog.abstractMethod;


/**
 * Get the size of the symbolizer (in pixels).
 * @function
 * @return {ol.Size} Size.
 */
ol.style.Image.prototype.getSize = goog.abstractMethod;


/**
 * Set the opacity.
 *
 * @param {number} opacity Opacity.
 * @api
 */
ol.style.Image.prototype.setOpacity = function(opacity) {
  this.opacity_ = opacity;
};


/**
 * Set whether to rotate the style with the view.
 *
 * @param {boolean} rotateWithView Rotate with map.
 */
ol.style.Image.prototype.setRotateWithView = function(rotateWithView) {
  this.rotateWithView_ = rotateWithView;
};


/**
 * Set the rotation.
 *
 * @param {number} rotation Rotation.
 * @api
 */
ol.style.Image.prototype.setRotation = function(rotation) {
  this.rotation_ = rotation;
};


/**
 * Set the scale.
 *
 * @param {number} scale Scale.
 * @api
 */
ol.style.Image.prototype.setScale = function(scale) {
  this.scale_ = scale;
};


/**
 * Set whether to snap the image to the closest pixel.
 *
 * @param {boolean} snapToPixel Snap to pixel?
 */
ol.style.Image.prototype.setSnapToPixel = function(snapToPixel) {
  this.snapToPixel_ = snapToPixel;
};


/**
 * @param {function(this: T, goog.events.Event)} listener Listener function.
 * @param {T} thisArg Value to use as `this` when executing `listener`.
 * @return {goog.events.Key|undefined} Listener key.
 * @template T
 */
ol.style.Image.prototype.listenImageChange = goog.abstractMethod;


/**
 * Load not yet loaded URI.
 */
ol.style.Image.prototype.load = goog.abstractMethod;


/**
 * @param {function(this: T, goog.events.Event)} listener Listener function.
 * @param {T} thisArg Value to use as `this` when executing `listener`.
 * @template T
 */
ol.style.Image.prototype.unlistenImageChange = goog.abstractMethod;

goog.provide('ol.style.Icon');
goog.provide('ol.style.IconAnchorUnits');
goog.provide('ol.style.IconImageCache');
goog.provide('ol.style.IconOrigin');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('ol.color');
goog.require('ol.dom');
goog.require('ol.style.Image');
goog.require('ol.style.ImageState');


/**
 * Icon anchor units. One of 'fraction', 'pixels'.
 * @enum {string}
 * @api
 */
ol.style.IconAnchorUnits = {
  FRACTION: 'fraction',
  PIXELS: 'pixels'
};


/**
 * Icon origin. One of 'bottom-left', 'bottom-right', 'top-left', 'top-right'.
 * @enum {string}
 * @api
 */
ol.style.IconOrigin = {
  BOTTOM_LEFT: 'bottom-left',
  BOTTOM_RIGHT: 'bottom-right',
  TOP_LEFT: 'top-left',
  TOP_RIGHT: 'top-right'
};


/**
 * @classdesc
 * Set icon style for vector features.
 *
 * @constructor
 * @param {olx.style.IconOptions=} opt_options Options.
 * @extends {ol.style.Image}
 * @api
 */
ol.style.Icon = function(opt_options) {

  var options = opt_options || {};

  /**
   * @private
   * @type {Array.<number>}
   */
  this.anchor_ = options.anchor !== undefined ? options.anchor : [0.5, 0.5];

  /**
   * @private
   * @type {Array.<number>}
   */
  this.normalizedAnchor_ = null;

  /**
   * @private
   * @type {ol.style.IconOrigin}
   */
  this.anchorOrigin_ = options.anchorOrigin !== undefined ?
      options.anchorOrigin : ol.style.IconOrigin.TOP_LEFT;

  /**
   * @private
   * @type {ol.style.IconAnchorUnits}
   */
  this.anchorXUnits_ = options.anchorXUnits !== undefined ?
      options.anchorXUnits : ol.style.IconAnchorUnits.FRACTION;

  /**
   * @private
   * @type {ol.style.IconAnchorUnits}
   */
  this.anchorYUnits_ = options.anchorYUnits !== undefined ?
      options.anchorYUnits : ol.style.IconAnchorUnits.FRACTION;

  /**
   * @type {?string}
   */
  var crossOrigin =
      options.crossOrigin !== undefined ? options.crossOrigin : null;

  /**
   * @type {Image|HTMLCanvasElement}
   */
  var image = options.img !== undefined ? options.img : null;

  /**
   * @type {ol.Size}
   */
  var imgSize = options.imgSize !== undefined ? options.imgSize : null;

  /**
   * @type {string|undefined}
   */
  var src = options.src;

  goog.asserts.assert(!(src !== undefined && image),
      'image and src can not provided at the same time');
  goog.asserts.assert(
      src === undefined || (src !== undefined && !imgSize),
      'imgSize should not be set when src is provided');
  goog.asserts.assert(
      !image || (image && imgSize),
      'imgSize must be set when image is provided');

  if ((src === undefined || src.length === 0) && image) {
    src = image.src || goog.getUid(image).toString();
  }
  goog.asserts.assert(src !== undefined && src.length > 0,
      'must provide a defined and non-empty src or image');

  /**
   * @type {ol.style.ImageState}
   */
  var imageState = options.src !== undefined ?
      ol.style.ImageState.IDLE : ol.style.ImageState.LOADED;

  /**
   * @type {ol.Color}
   */
  var color = options.color !== undefined ? ol.color.asArray(options.color) :
      null;

  /**
   * @private
   * @type {ol.style.IconImage_}
   */
  this.iconImage_ = ol.style.IconImage_.get(
      image, src, imgSize, crossOrigin, imageState, color);

  /**
   * @private
   * @type {Array.<number>}
   */
  this.offset_ = options.offset !== undefined ? options.offset : [0, 0];

  /**
   * @private
   * @type {ol.style.IconOrigin}
   */
  this.offsetOrigin_ = options.offsetOrigin !== undefined ?
      options.offsetOrigin : ol.style.IconOrigin.TOP_LEFT;

  /**
   * @private
   * @type {Array.<number>}
   */
  this.origin_ = null;

  /**
   * @private
   * @type {ol.Size}
   */
  this.size_ = options.size !== undefined ? options.size : null;

  /**
   * @type {number}
   */
  var opacity = options.opacity !== undefined ? options.opacity : 1;

  /**
   * @type {boolean}
   */
  var rotateWithView = options.rotateWithView !== undefined ?
      options.rotateWithView : false;

  /**
   * @type {number}
   */
  var rotation = options.rotation !== undefined ? options.rotation : 0;

  /**
   * @type {number}
   */
  var scale = options.scale !== undefined ? options.scale : 1;

  /**
   * @type {boolean}
   */
  var snapToPixel = options.snapToPixel !== undefined ?
      options.snapToPixel : true;

  goog.base(this, {
    opacity: opacity,
    rotation: rotation,
    scale: scale,
    snapToPixel: snapToPixel,
    rotateWithView: rotateWithView
  });

};
goog.inherits(ol.style.Icon, ol.style.Image);


/**
 * @inheritDoc
 * @api
 */
ol.style.Icon.prototype.getAnchor = function() {
  if (this.normalizedAnchor_) {
    return this.normalizedAnchor_;
  }
  var anchor = this.anchor_;
  var size = this.getSize();
  if (this.anchorXUnits_ == ol.style.IconAnchorUnits.FRACTION ||
      this.anchorYUnits_ == ol.style.IconAnchorUnits.FRACTION) {
    if (!size) {
      return null;
    }
    anchor = this.anchor_.slice();
    if (this.anchorXUnits_ == ol.style.IconAnchorUnits.FRACTION) {
      anchor[0] *= size[0];
    }
    if (this.anchorYUnits_ == ol.style.IconAnchorUnits.FRACTION) {
      anchor[1] *= size[1];
    }
  }

  if (this.anchorOrigin_ != ol.style.IconOrigin.TOP_LEFT) {
    if (!size) {
      return null;
    }
    if (anchor === this.anchor_) {
      anchor = this.anchor_.slice();
    }
    if (this.anchorOrigin_ == ol.style.IconOrigin.TOP_RIGHT ||
        this.anchorOrigin_ == ol.style.IconOrigin.BOTTOM_RIGHT) {
      anchor[0] = -anchor[0] + size[0];
    }
    if (this.anchorOrigin_ == ol.style.IconOrigin.BOTTOM_LEFT ||
        this.anchorOrigin_ == ol.style.IconOrigin.BOTTOM_RIGHT) {
      anchor[1] = -anchor[1] + size[1];
    }
  }
  this.normalizedAnchor_ = anchor;
  return this.normalizedAnchor_;
};


/**
 * Get the image icon.
 * @param {number} pixelRatio Pixel ratio.
 * @return {Image|HTMLCanvasElement} Image or Canvas element.
 * @api
 */
ol.style.Icon.prototype.getImage = function(pixelRatio) {
  return this.iconImage_.getImage(pixelRatio);
};


/**
 * Real Image size used.
 * @return {ol.Size} Size.
 */
ol.style.Icon.prototype.getImageSize = function() {
  return this.iconImage_.getSize();
};


/**
 * @inheritDoc
 */
ol.style.Icon.prototype.getHitDetectionImageSize = function() {
  return this.getImageSize();
};


/**
 * @inheritDoc
 */
ol.style.Icon.prototype.getImageState = function() {
  return this.iconImage_.getImageState();
};


/**
 * @inheritDoc
 */
ol.style.Icon.prototype.getHitDetectionImage = function(pixelRatio) {
  return this.iconImage_.getHitDetectionImage(pixelRatio);
};


/**
 * @inheritDoc
 * @api
 */
ol.style.Icon.prototype.getOrigin = function() {
  if (this.origin_) {
    return this.origin_;
  }
  var offset = this.offset_;

  if (this.offsetOrigin_ != ol.style.IconOrigin.TOP_LEFT) {
    var size = this.getSize();
    var iconImageSize = this.iconImage_.getSize();
    if (!size || !iconImageSize) {
      return null;
    }
    offset = offset.slice();
    if (this.offsetOrigin_ == ol.style.IconOrigin.TOP_RIGHT ||
        this.offsetOrigin_ == ol.style.IconOrigin.BOTTOM_RIGHT) {
      offset[0] = iconImageSize[0] - size[0] - offset[0];
    }
    if (this.offsetOrigin_ == ol.style.IconOrigin.BOTTOM_LEFT ||
        this.offsetOrigin_ == ol.style.IconOrigin.BOTTOM_RIGHT) {
      offset[1] = iconImageSize[1] - size[1] - offset[1];
    }
  }
  this.origin_ = offset;
  return this.origin_;
};


/**
 * Get the image URL.
 * @return {string|undefined} Image src.
 * @api
 */
ol.style.Icon.prototype.getSrc = function() {
  return this.iconImage_.getSrc();
};


/**
 * @inheritDoc
 * @api
 */
ol.style.Icon.prototype.getSize = function() {
  return !this.size_ ? this.iconImage_.getSize() : this.size_;
};


/**
 * @inheritDoc
 */
ol.style.Icon.prototype.listenImageChange = function(listener, thisArg) {
  return goog.events.listen(this.iconImage_, goog.events.EventType.CHANGE,
      listener, false, thisArg);
};


/**
 * Load not yet loaded URI.
 * When rendering a feature with an icon style, the vector renderer will
 * automatically call this method. However, you might want to call this
 * method yourself for preloading or other purposes.
 * @api
 */
ol.style.Icon.prototype.load = function() {
  this.iconImage_.load();
};


/**
 * @inheritDoc
 */
ol.style.Icon.prototype.unlistenImageChange = function(listener, thisArg) {
  goog.events.unlisten(this.iconImage_, goog.events.EventType.CHANGE,
      listener, false, thisArg);
};


/**
 * @constructor
 * @param {Image|HTMLCanvasElement} image Image.
 * @param {string|undefined} src Src.
 * @param {ol.Size} size Size.
 * @param {?string} crossOrigin Cross origin.
 * @param {ol.style.ImageState} imageState Image state.
 * @param {ol.Color} color Color.
 * @extends {goog.events.EventTarget}
 * @private
 */
ol.style.IconImage_ = function(image, src, size, crossOrigin, imageState,
                               color) {

  goog.base(this);

  /**
   * @private
   * @type {Image|HTMLCanvasElement}
   */
  this.hitDetectionImage_ = null;

  /**
   * @private
   * @type {Image|HTMLCanvasElement}
   */
  this.image_ = !image ? new Image() : image;

  if (crossOrigin !== null) {
    this.image_.crossOrigin = crossOrigin;
  }

  /**
   * @private
   * @type {HTMLCanvasElement}
   */
  this.canvas_ = color ?
      /** @type {HTMLCanvasElement} */ (document.createElement('CANVAS')) :
      null;

  /**
   * @private
   * @type {ol.Color}
   */
  this.color_ = color;

  /**
   * @private
   * @type {Array.<goog.events.Key>}
   */
  this.imageListenerKeys_ = null;

  /**
   * @private
   * @type {ol.style.ImageState}
   */
  this.imageState_ = imageState;

  /**
   * @private
   * @type {ol.Size}
   */
  this.size_ = size;

  /**
   * @private
   * @type {string|undefined}
   */
  this.src_ = src;

  /**
   * @private
   * @type {boolean}
   */
  this.tainting_ = false;
  if (this.imageState_ == ol.style.ImageState.LOADED) {
    this.determineTainting_();
  }

};
goog.inherits(ol.style.IconImage_, goog.events.EventTarget);


/**
 * @param {Image|HTMLCanvasElement} image Image.
 * @param {string} src Src.
 * @param {ol.Size} size Size.
 * @param {?string} crossOrigin Cross origin.
 * @param {ol.style.ImageState} imageState Image state.
 * @param {ol.Color} color Color.
 * @return {ol.style.IconImage_} Icon image.
 */
ol.style.IconImage_.get = function(image, src, size, crossOrigin, imageState,
                                   color) {
  var iconImageCache = ol.style.IconImageCache.getInstance();
  var iconImage = iconImageCache.get(src, crossOrigin, color);
  if (!iconImage) {
    iconImage = new ol.style.IconImage_(
        image, src, size, crossOrigin, imageState, color);
    iconImageCache.set(src, crossOrigin, color, iconImage);
  }
  return iconImage;
};


/**
 * @private
 */
ol.style.IconImage_.prototype.determineTainting_ = function() {
  var context = ol.dom.createCanvasContext2D(1, 1);
  try {
    context.drawImage(this.image_, 0, 0);
    context.getImageData(0, 0, 1, 1);
  } catch (e) {
    this.tainting_ = true;
  }
};


/**
 * @private
 */
ol.style.IconImage_.prototype.dispatchChangeEvent_ = function() {
  this.dispatchEvent(goog.events.EventType.CHANGE);
};


/**
 * @private
 */
ol.style.IconImage_.prototype.handleImageError_ = function() {
  this.imageState_ = ol.style.ImageState.ERROR;
  this.unlistenImage_();
  this.dispatchChangeEvent_();
};


/**
 * @private
 */
ol.style.IconImage_.prototype.handleImageLoad_ = function() {
  this.imageState_ = ol.style.ImageState.LOADED;
  this.size_ = [this.image_.width, this.image_.height];
  this.unlistenImage_();
  this.determineTainting_();
  this.replaceColor_();
  this.dispatchChangeEvent_();
};


/**
 * @param {number} pixelRatio Pixel ratio.
 * @return {Image|HTMLCanvasElement} Image or Canvas element.
 */
ol.style.IconImage_.prototype.getImage = function(pixelRatio) {
  return this.canvas_ ? this.canvas_ : this.image_;
};


/**
 * @return {ol.style.ImageState} Image state.
 */
ol.style.IconImage_.prototype.getImageState = function() {
  return this.imageState_;
};


/**
 * @param {number} pixelRatio Pixel ratio.
 * @return {Image|HTMLCanvasElement} Image element.
 */
ol.style.IconImage_.prototype.getHitDetectionImage = function(pixelRatio) {
  if (!this.hitDetectionImage_) {
    if (this.tainting_) {
      var width = this.size_[0];
      var height = this.size_[1];
      var context = ol.dom.createCanvasContext2D(width, height);
      context.fillRect(0, 0, width, height);
      this.hitDetectionImage_ = context.canvas;
    } else {
      this.hitDetectionImage_ = this.image_;
    }
  }
  return this.hitDetectionImage_;
};


/**
 * @return {ol.Size} Image size.
 */
ol.style.IconImage_.prototype.getSize = function() {
  return this.size_;
};


/**
 * @return {string|undefined} Image src.
 */
ol.style.IconImage_.prototype.getSrc = function() {
  return this.src_;
};


/**
 * Load not yet loaded URI.
 */
ol.style.IconImage_.prototype.load = function() {
  if (this.imageState_ == ol.style.ImageState.IDLE) {
    goog.asserts.assert(this.src_ !== undefined,
        'this.src_ must not be undefined');
    goog.asserts.assert(!this.imageListenerKeys_,
        'no listener keys existing');
    this.imageState_ = ol.style.ImageState.LOADING;
    this.imageListenerKeys_ = [
      goog.events.listenOnce(this.image_, goog.events.EventType.ERROR,
          this.handleImageError_, false, this),
      goog.events.listenOnce(this.image_, goog.events.EventType.LOAD,
          this.handleImageLoad_, false, this)
    ];
    try {
      this.image_.src = this.src_;
    } catch (e) {
      this.handleImageError_();
    }
  }
};


/**
 * @private
 */
ol.style.IconImage_.prototype.replaceColor_ = function() {
  if (this.tainting_ || this.color_ === null) {
    return;
  }

  goog.asserts.assert(this.canvas_ !== null,
      'this.canvas_ must not be null');

  this.canvas_.width = this.image_.width;
  this.canvas_.height = this.image_.height;

  var ctx = this.canvas_.getContext('2d');
  ctx.drawImage(this.image_, 0, 0);

  var imgData = ctx.getImageData(0, 0, this.image_.width, this.image_.height);
  var data = imgData.data;
  var r = this.color_[0] / 255.0;
  var g = this.color_[1] / 255.0;
  var b = this.color_[2] / 255.0;

  for (var i = 0, ii = data.length; i < ii; i += 4) {
    data[i] *= r;
    data[i + 1] *= g;
    data[i + 2] *= b;
  }
  ctx.putImageData(imgData, 0, 0);
};


/**
 * Discards event handlers which listen for load completion or errors.
 *
 * @private
 */
ol.style.IconImage_.prototype.unlistenImage_ = function() {
  goog.asserts.assert(this.imageListenerKeys_,
      'we must have listeners registered');
  this.imageListenerKeys_.forEach(goog.events.unlistenByKey);
  this.imageListenerKeys_ = null;
};


/**
 * @constructor
 */
ol.style.IconImageCache = function() {

  /**
   * @type {Object.<string, ol.style.IconImage_>}
   * @private
   */
  this.cache_ = {};

  /**
   * @type {number}
   * @private
   */
  this.cacheSize_ = 0;

  /**
   * @const
   * @type {number}
   * @private
   */
  this.maxCacheSize_ = 32;
};
goog.addSingletonGetter(ol.style.IconImageCache);


/**
 * @param {string} src Src.
 * @param {?string} crossOrigin Cross origin.
 * @param {ol.Color} color Color.
 * @return {string} Cache key.
 */
ol.style.IconImageCache.getKey = function(src, crossOrigin, color) {
  goog.asserts.assert(crossOrigin !== undefined,
      'argument crossOrigin must be defined');
  var colorString = color ? ol.color.asString(color) : 'null';
  return crossOrigin + ':' + src + ':' + colorString;
};


/**
 * FIXME empty description for jsdoc
 */
ol.style.IconImageCache.prototype.clear = function() {
  this.cache_ = {};
  this.cacheSize_ = 0;
};


/**
 * FIXME empty description for jsdoc
 */
ol.style.IconImageCache.prototype.expire = function() {
  if (this.cacheSize_ > this.maxCacheSize_) {
    var i = 0;
    var key, iconImage;
    for (key in this.cache_) {
      iconImage = this.cache_[key];
      if ((i++ & 3) === 0 && !goog.events.hasListener(iconImage)) {
        delete this.cache_[key];
        --this.cacheSize_;
      }
    }
  }
};


/**
 * @param {string} src Src.
 * @param {?string} crossOrigin Cross origin.
 * @param {ol.Color} color Color.
 * @return {ol.style.IconImage_} Icon image.
 */
ol.style.IconImageCache.prototype.get = function(src, crossOrigin, color) {
  var key = ol.style.IconImageCache.getKey(src, crossOrigin, color);
  return key in this.cache_ ? this.cache_[key] : null;
};


/**
 * @param {string} src Src.
 * @param {?string} crossOrigin Cross origin.
 * @param {ol.Color} color Color.
 * @param {ol.style.IconImage_} iconImage Icon image.
 */
ol.style.IconImageCache.prototype.set = function(src, crossOrigin, color,
                                                 iconImage) {
  var key = ol.style.IconImageCache.getKey(src, crossOrigin, color);
  this.cache_[key] = iconImage;
  ++this.cacheSize_;
};

goog.provide('ol.RendererType');
goog.provide('ol.renderer.Map');

goog.require('goog.Disposable');
goog.require('goog.asserts');
goog.require('goog.dispose');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.functions');
goog.require('goog.object');
goog.require('goog.vec.Mat4');
goog.require('ol');
goog.require('ol.extent');
goog.require('ol.layer.Layer');
goog.require('ol.renderer.Layer');
goog.require('ol.style.IconImageCache');
goog.require('ol.vec.Mat4');


/**
 * Available renderers: `'canvas'`, `'dom'` or `'webgl'`.
 * @enum {string}
 * @api stable
 */
ol.RendererType = {
  CANVAS: 'canvas',
  DOM: 'dom',
  WEBGL: 'webgl'
};


/**
 * @constructor
 * @extends {goog.Disposable}
 * @param {Element} container Container.
 * @param {ol.Map} map Map.
 * @struct
 */
ol.renderer.Map = function(container, map) {

  goog.base(this);


  /**
   * @private
   * @type {ol.Map}
   */
  this.map_ = map;

  /**
   * @private
   * @type {Object.<string, ol.renderer.Layer>}
   */
  this.layerRenderers_ = {};

  /**
   * @private
   * @type {Object.<string, goog.events.Key>}
   */
  this.layerRendererListeners_ = {};

};
goog.inherits(ol.renderer.Map, goog.Disposable);


/**
 * @param {olx.FrameState} frameState FrameState.
 * @protected
 */
ol.renderer.Map.prototype.calculateMatrices2D = function(frameState) {
  var viewState = frameState.viewState;
  var coordinateToPixelMatrix = frameState.coordinateToPixelMatrix;
  goog.asserts.assert(coordinateToPixelMatrix,
      'frameState has a coordinateToPixelMatrix');
  ol.vec.Mat4.makeTransform2D(coordinateToPixelMatrix,
      frameState.size[0] / 2, frameState.size[1] / 2,
      1 / viewState.resolution, -1 / viewState.resolution,
      -viewState.rotation,
      -viewState.center[0], -viewState.center[1]);
  var inverted = goog.vec.Mat4.invert(
      coordinateToPixelMatrix, frameState.pixelToCoordinateMatrix);
  goog.asserts.assert(inverted, 'matrix could be inverted');
};


/**
 * @param {ol.layer.Layer} layer Layer.
 * @protected
 * @return {ol.renderer.Layer} layerRenderer Layer renderer.
 */
ol.renderer.Map.prototype.createLayerRenderer = goog.abstractMethod;


/**
 * @inheritDoc
 */
ol.renderer.Map.prototype.disposeInternal = function() {
  goog.object.forEach(this.layerRenderers_, goog.dispose);
  goog.base(this, 'disposeInternal');
};


/**
 * @param {ol.Map} map Map.
 * @param {olx.FrameState} frameState Frame state.
 * @private
 */
ol.renderer.Map.expireIconCache_ = function(map, frameState) {
  ol.style.IconImageCache.getInstance().expire();
};


/**
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {olx.FrameState} frameState FrameState.
 * @param {function(this: S, (ol.Feature|ol.render.Feature),
 *     ol.layer.Layer): T} callback Feature callback.
 * @param {S} thisArg Value to use as `this` when executing `callback`.
 * @param {function(this: U, ol.layer.Layer): boolean} layerFilter Layer filter
 *     function, only layers which are visible and for which this function
 *     returns `true` will be tested for features.  By default, all visible
 *     layers will be tested.
 * @param {U} thisArg2 Value to use as `this` when executing `layerFilter`.
 * @return {T|undefined} Callback result.
 * @template S,T,U
 */
ol.renderer.Map.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg,
        layerFilter, thisArg2) {
  var result;
  var viewState = frameState.viewState;
  var viewResolution = viewState.resolution;

  /** @type {Object.<string, boolean>} */
  var features = {};

  /**
   * @param {ol.Feature|ol.render.Feature} feature Feature.
   * @return {?} Callback result.
   */
  function forEachFeatureAtCoordinate(feature) {
    goog.asserts.assert(feature !== undefined, 'received a feature');
    var key = goog.getUid(feature).toString();
    if (!(key in features)) {
      features[key] = true;
      return callback.call(thisArg, feature, null);
    }
  }

  var projection = viewState.projection;

  var translatedCoordinate = coordinate;
  if (projection.canWrapX()) {
    var projectionExtent = projection.getExtent();
    var worldWidth = ol.extent.getWidth(projectionExtent);
    var x = coordinate[0];
    if (x < projectionExtent[0] || x > projectionExtent[2]) {
      var worldsAway = Math.ceil((projectionExtent[0] - x) / worldWidth);
      translatedCoordinate = [x + worldWidth * worldsAway, coordinate[1]];
    }
  }

  var layerStates = frameState.layerStatesArray;
  var numLayers = layerStates.length;
  var i;
  for (i = numLayers - 1; i >= 0; --i) {
    var layerState = layerStates[i];
    var layer = layerState.layer;
    if (ol.layer.Layer.visibleAtResolution(layerState, viewResolution) &&
        layerFilter.call(thisArg2, layer)) {
      var layerRenderer = this.getLayerRenderer(layer);
      if (layer.getSource()) {
        result = layerRenderer.forEachFeatureAtCoordinate(
            layer.getSource().getWrapX() ? translatedCoordinate : coordinate,
            frameState,
            layerState.managed ? callback : forEachFeatureAtCoordinate,
            thisArg);
      }
      if (result) {
        return result;
      }
    }
  }
  return undefined;
};


/**
 * @param {ol.Pixel} pixel Pixel.
 * @param {olx.FrameState} frameState FrameState.
 * @param {function(this: S, ol.layer.Layer): T} callback Layer
 *     callback.
 * @param {S} thisArg Value to use as `this` when executing `callback`.
 * @param {function(this: U, ol.layer.Layer): boolean} layerFilter Layer filter
 *     function, only layers which are visible and for which this function
 *     returns `true` will be tested for features.  By default, all visible
 *     layers will be tested.
 * @param {U} thisArg2 Value to use as `this` when executing `layerFilter`.
 * @return {T|undefined} Callback result.
 * @template S,T,U
 */
ol.renderer.Map.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg,
        layerFilter, thisArg2) {
  var result;
  var viewState = frameState.viewState;
  var viewResolution = viewState.resolution;

  var layerStates = frameState.layerStatesArray;
  var numLayers = layerStates.length;
  var i;
  for (i = numLayers - 1; i >= 0; --i) {
    var layerState = layerStates[i];
    var layer = layerState.layer;
    if (ol.layer.Layer.visibleAtResolution(layerState, viewResolution) &&
        layerFilter.call(thisArg2, layer)) {
      var layerRenderer = this.getLayerRenderer(layer);
      result = layerRenderer.forEachLayerAtPixel(
          pixel, frameState, callback, thisArg);
      if (result) {
        return result;
      }
    }
  }
  return undefined;
};


/**
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {olx.FrameState} frameState FrameState.
 * @param {function(this: U, ol.layer.Layer): boolean} layerFilter Layer filter
 *     function, only layers which are visible and for which this function
 *     returns `true` will be tested for features.  By default, all visible
 *     layers will be tested.
 * @param {U} thisArg Value to use as `this` when executing `layerFilter`.
 * @return {boolean} Is there a feature at the given coordinate?
 * @template U
 */
ol.renderer.Map.prototype.hasFeatureAtCoordinate = function(coordinate, frameState, layerFilter, thisArg) {
  var hasFeature = this.forEachFeatureAtCoordinate(
      coordinate, frameState, goog.functions.TRUE, this, layerFilter, thisArg);

  return hasFeature !== undefined;
};


/**
 * @param {ol.layer.Layer} layer Layer.
 * @protected
 * @return {ol.renderer.Layer} Layer renderer.
 */
ol.renderer.Map.prototype.getLayerRenderer = function(layer) {
  var layerKey = goog.getUid(layer).toString();
  if (layerKey in this.layerRenderers_) {
    return this.layerRenderers_[layerKey];
  } else {
    var layerRenderer = this.createLayerRenderer(layer);
    this.layerRenderers_[layerKey] = layerRenderer;
    this.layerRendererListeners_[layerKey] = goog.events.listen(layerRenderer,
        goog.events.EventType.CHANGE, this.handleLayerRendererChange_,
        false, this);

    return layerRenderer;
  }
};


/**
 * @param {string} layerKey Layer key.
 * @protected
 * @return {ol.renderer.Layer} Layer renderer.
 */
ol.renderer.Map.prototype.getLayerRendererByKey = function(layerKey) {
  goog.asserts.assert(layerKey in this.layerRenderers_,
      'given layerKey (%s) exists in layerRenderers', layerKey);
  return this.layerRenderers_[layerKey];
};


/**
 * @protected
 * @return {Object.<number, ol.renderer.Layer>} Layer renderers.
 */
ol.renderer.Map.prototype.getLayerRenderers = function() {
  return this.layerRenderers_;
};


/**
 * @return {ol.Map} Map.
 */
ol.renderer.Map.prototype.getMap = function() {
  return this.map_;
};


/**
 * @return {string} Type
 */
ol.renderer.Map.prototype.getType = goog.abstractMethod;


/**
 * Handle changes in a layer renderer.
 * @private
 */
ol.renderer.Map.prototype.handleLayerRendererChange_ = function() {
  this.map_.render();
};


/**
 * @param {string} layerKey Layer key.
 * @return {ol.renderer.Layer} Layer renderer.
 * @private
 */
ol.renderer.Map.prototype.removeLayerRendererByKey_ = function(layerKey) {
  goog.asserts.assert(layerKey in this.layerRenderers_,
      'given layerKey (%s) exists in layerRenderers', layerKey);
  var layerRenderer = this.layerRenderers_[layerKey];
  delete this.layerRenderers_[layerKey];

  goog.asserts.assert(layerKey in this.layerRendererListeners_,
      'given layerKey (%s) exists in layerRendererListeners', layerKey);
  goog.events.unlistenByKey(this.layerRendererListeners_[layerKey]);
  delete this.layerRendererListeners_[layerKey];

  return layerRenderer;
};


/**
 * Render.
 * @param {?olx.FrameState} frameState Frame state.
 */
ol.renderer.Map.prototype.renderFrame = ol.nullFunction;


/**
 * @param {ol.Map} map Map.
 * @param {olx.FrameState} frameState Frame state.
 * @private
 */
ol.renderer.Map.prototype.removeUnusedLayerRenderers_ = function(map, frameState) {
  var layerKey;
  for (layerKey in this.layerRenderers_) {
    if (!frameState || !(layerKey in frameState.layerStates)) {
      goog.dispose(this.removeLayerRendererByKey_(layerKey));
    }
  }
};


/**
 * @param {olx.FrameState} frameState Frame state.
 * @protected
 */
ol.renderer.Map.prototype.scheduleExpireIconCache = function(frameState) {
  frameState.postRenderFunctions.push(ol.renderer.Map.expireIconCache_);
};


/**
 * @param {!olx.FrameState} frameState Frame state.
 * @protected
 */
ol.renderer.Map.prototype.scheduleRemoveUnusedLayerRenderers = function(frameState) {
  var layerKey;
  for (layerKey in this.layerRenderers_) {
    if (!(layerKey in frameState.layerStates)) {
      frameState.postRenderFunctions.push(
          this.removeUnusedLayerRenderers_.bind(this));
      return;
    }
  }
};


/**
 * @param {ol.layer.LayerState} state1 First layer state.
 * @param {ol.layer.LayerState} state2 Second layer state.
 * @return {number} The zIndex difference.
 */
ol.renderer.Map.sortByZIndex = function(state1, state2) {
  return state1.zIndex - state2.zIndex;
};

goog.provide('ol.structs.PriorityQueue');

goog.require('goog.asserts');
goog.require('goog.object');


/**
 * Priority queue.
 *
 * The implementation is inspired from the Closure Library's Heap class and
 * Python's heapq module.
 *
 * @see http://closure-library.googlecode.com/svn/docs/closure_goog_structs_heap.js.source.html
 * @see http://hg.python.org/cpython/file/2.7/Lib/heapq.py
 *
 * @constructor
 * @param {function(T): number} priorityFunction Priority function.
 * @param {function(T): string} keyFunction Key function.
 * @struct
 * @template T
 */
ol.structs.PriorityQueue = function(priorityFunction, keyFunction) {

  /**
   * @type {function(T): number}
   * @private
   */
  this.priorityFunction_ = priorityFunction;

  /**
   * @type {function(T): string}
   * @private
   */
  this.keyFunction_ = keyFunction;

  /**
   * @type {Array.<T>}
   * @private
   */
  this.elements_ = [];

  /**
   * @type {Array.<number>}
   * @private
   */
  this.priorities_ = [];

  /**
   * @type {Object.<string, boolean>}
   * @private
   */
  this.queuedElements_ = {};

};


/**
 * @const
 * @type {number}
 */
ol.structs.PriorityQueue.DROP = Infinity;


/**
 * FIXME empty description for jsdoc
 */
ol.structs.PriorityQueue.prototype.assertValid = function() {
  var elements = this.elements_;
  var priorities = this.priorities_;
  var n = elements.length;
  goog.asserts.assert(priorities.length == n);
  var i, priority;
  for (i = 0; i < (n >> 1) - 1; ++i) {
    priority = priorities[i];
    goog.asserts.assert(priority <= priorities[this.getLeftChildIndex_(i)],
        'priority smaller than or equal to priority of left child (%s <= %s)',
        priority, priorities[this.getLeftChildIndex_(i)]);
    goog.asserts.assert(priority <= priorities[this.getRightChildIndex_(i)],
        'priority smaller than or equal to priority of right child (%s <= %s)',
        priority, priorities[this.getRightChildIndex_(i)]);
  }
};


/**
 * FIXME empty description for jsdoc
 */
ol.structs.PriorityQueue.prototype.clear = function() {
  this.elements_.length = 0;
  this.priorities_.length = 0;
  goog.object.clear(this.queuedElements_);
};


/**
 * Remove and return the highest-priority element. O(log N).
 * @return {T} Element.
 */
ol.structs.PriorityQueue.prototype.dequeue = function() {
  var elements = this.elements_;
  goog.asserts.assert(elements.length > 0,
      'must have elements in order to be able to dequeue');
  var priorities = this.priorities_;
  var element = elements[0];
  if (elements.length == 1) {
    elements.length = 0;
    priorities.length = 0;
  } else {
    elements[0] = elements.pop();
    priorities[0] = priorities.pop();
    this.siftUp_(0);
  }
  var elementKey = this.keyFunction_(element);
  goog.asserts.assert(elementKey in this.queuedElements_,
      'key %s is not listed as queued', elementKey);
  delete this.queuedElements_[elementKey];
  return element;
};


/**
 * Enqueue an element. O(log N).
 * @param {T} element Element.
 * @return {boolean} The element was added to the queue.
 */
ol.structs.PriorityQueue.prototype.enqueue = function(element) {
  goog.asserts.assert(!(this.keyFunction_(element) in this.queuedElements_),
      'key %s is already listed as queued', this.keyFunction_(element));
  var priority = this.priorityFunction_(element);
  if (priority != ol.structs.PriorityQueue.DROP) {
    this.elements_.push(element);
    this.priorities_.push(priority);
    this.queuedElements_[this.keyFunction_(element)] = true;
    this.siftDown_(0, this.elements_.length - 1);
    return true;
  }
  return false;
};


/**
 * @return {number} Count.
 */
ol.structs.PriorityQueue.prototype.getCount = function() {
  return this.elements_.length;
};


/**
 * Gets the index of the left child of the node at the given index.
 * @param {number} index The index of the node to get the left child for.
 * @return {number} The index of the left child.
 * @private
 */
ol.structs.PriorityQueue.prototype.getLeftChildIndex_ = function(index) {
  return index * 2 + 1;
};


/**
 * Gets the index of the right child of the node at the given index.
 * @param {number} index The index of the node to get the right child for.
 * @return {number} The index of the right child.
 * @private
 */
ol.structs.PriorityQueue.prototype.getRightChildIndex_ = function(index) {
  return index * 2 + 2;
};


/**
 * Gets the index of the parent of the node at the given index.
 * @param {number} index The index of the node to get the parent for.
 * @return {number} The index of the parent.
 * @private
 */
ol.structs.PriorityQueue.prototype.getParentIndex_ = function(index) {
  return (index - 1) >> 1;
};


/**
 * Make this a heap. O(N).
 * @private
 */
ol.structs.PriorityQueue.prototype.heapify_ = function() {
  var i;
  for (i = (this.elements_.length >> 1) - 1; i >= 0; i--) {
    this.siftUp_(i);
  }
};


/**
 * @return {boolean} Is empty.
 */
ol.structs.PriorityQueue.prototype.isEmpty = function() {
  return this.elements_.length === 0;
};


/**
 * @param {string} key Key.
 * @return {boolean} Is key queued.
 */
ol.structs.PriorityQueue.prototype.isKeyQueued = function(key) {
  return key in this.queuedElements_;
};


/**
 * @param {T} element Element.
 * @return {boolean} Is queued.
 */
ol.structs.PriorityQueue.prototype.isQueued = function(element) {
  return this.isKeyQueued(this.keyFunction_(element));
};


/**
 * @param {number} index The index of the node to move down.
 * @private
 */
ol.structs.PriorityQueue.prototype.siftUp_ = function(index) {
  var elements = this.elements_;
  var priorities = this.priorities_;
  var count = elements.length;
  var element = elements[index];
  var priority = priorities[index];
  var startIndex = index;

  while (index < (count >> 1)) {
    var lIndex = this.getLeftChildIndex_(index);
    var rIndex = this.getRightChildIndex_(index);

    var smallerChildIndex = rIndex < count &&
        priorities[rIndex] < priorities[lIndex] ?
        rIndex : lIndex;

    elements[index] = elements[smallerChildIndex];
    priorities[index] = priorities[smallerChildIndex];
    index = smallerChildIndex;
  }

  elements[index] = element;
  priorities[index] = priority;
  this.siftDown_(startIndex, index);
};


/**
 * @param {number} startIndex The index of the root.
 * @param {number} index The index of the node to move up.
 * @private
 */
ol.structs.PriorityQueue.prototype.siftDown_ = function(startIndex, index) {
  var elements = this.elements_;
  var priorities = this.priorities_;
  var element = elements[index];
  var priority = priorities[index];

  while (index > startIndex) {
    var parentIndex = this.getParentIndex_(index);
    if (priorities[parentIndex] > priority) {
      elements[index] = elements[parentIndex];
      priorities[index] = priorities[parentIndex];
      index = parentIndex;
    } else {
      break;
    }
  }
  elements[index] = element;
  priorities[index] = priority;
};


/**
 * FIXME empty description for jsdoc
 */
ol.structs.PriorityQueue.prototype.reprioritize = function() {
  var priorityFunction = this.priorityFunction_;
  var elements = this.elements_;
  var priorities = this.priorities_;
  var index = 0;
  var n = elements.length;
  var element, i, priority;
  for (i = 0; i < n; ++i) {
    element = elements[i];
    priority = priorityFunction(element);
    if (priority == ol.structs.PriorityQueue.DROP) {
      delete this.queuedElements_[this.keyFunction_(element)];
    } else {
      priorities[index] = priority;
      elements[index++] = element;
    }
  }
  elements.length = index;
  priorities.length = index;
  this.heapify_();
};

goog.provide('ol.TilePriorityFunction');
goog.provide('ol.TileQueue');

goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('ol.Coordinate');
goog.require('ol.TileState');
goog.require('ol.structs.PriorityQueue');


/**
 * @typedef {function(ol.Tile, string, ol.Coordinate, number): number}
 */
ol.TilePriorityFunction;


/**
 * @constructor
 * @extends {ol.structs.PriorityQueue.<Array>}
 * @param {ol.TilePriorityFunction} tilePriorityFunction
 *     Tile priority function.
 * @param {function(): ?} tileChangeCallback
 *     Function called on each tile change event.
 * @struct
 */
ol.TileQueue = function(tilePriorityFunction, tileChangeCallback) {

  goog.base(
      this,
      /**
       * @param {Array} element Element.
       * @return {number} Priority.
       */
      function(element) {
        return tilePriorityFunction.apply(null, element);
      },
      /**
       * @param {Array} element Element.
       * @return {string} Key.
       */
      function(element) {
        return /** @type {ol.Tile} */ (element[0]).getKey();
      });

  /**
   * @private
   * @type {function(): ?}
   */
  this.tileChangeCallback_ = tileChangeCallback;

  /**
   * @private
   * @type {number}
   */
  this.tilesLoading_ = 0;

  /**
   * @private
   * @type {Object.<string,boolean>}
   */
  this.tilesLoadingKeys_ = {};

};
goog.inherits(ol.TileQueue, ol.structs.PriorityQueue);


/**
 * @inheritDoc
 */
ol.TileQueue.prototype.enqueue = function(element) {
  var added = goog.base(this, 'enqueue', element);
  if (added) {
    var tile = element[0];
    goog.events.listen(tile, goog.events.EventType.CHANGE,
        this.handleTileChange, false, this);
  }
  return added;
};


/**
 * @return {number} Number of tiles loading.
 */
ol.TileQueue.prototype.getTilesLoading = function() {
  return this.tilesLoading_;
};


/**
 * @param {goog.events.Event} event Event.
 * @protected
 */
ol.TileQueue.prototype.handleTileChange = function(event) {
  var tile = /** @type {ol.Tile} */ (event.target);
  var state = tile.getState();
  if (state === ol.TileState.LOADED || state === ol.TileState.ERROR ||
      state === ol.TileState.EMPTY) {
    goog.events.unlisten(tile, goog.events.EventType.CHANGE,
        this.handleTileChange, false, this);
    var tileKey = tile.getKey();
    if (tileKey in this.tilesLoadingKeys_) {
      delete this.tilesLoadingKeys_[tileKey];
      --this.tilesLoading_;
    }
    this.tileChangeCallback_();
  }
};


/**
 * @param {number} maxTotalLoading Maximum number tiles to load simultaneously.
 * @param {number} maxNewLoads Maximum number of new tiles to load.
 */
ol.TileQueue.prototype.loadMoreTiles = function(maxTotalLoading, maxNewLoads) {
  var newLoads = 0;
  var tile;
  while (this.tilesLoading_ < maxTotalLoading && newLoads < maxNewLoads &&
         this.getCount() > 0) {
    tile = /** @type {ol.Tile} */ (this.dequeue()[0]);
    if (tile.getState() === ol.TileState.IDLE) {
      this.tilesLoadingKeys_[tile.getKey()] = true;
      ++this.tilesLoading_;
      ++newLoads;
      tile.load();
    }
  }
};

goog.provide('ol.Kinetic');

goog.require('ol.Coordinate');
goog.require('ol.PreRenderFunction');
goog.require('ol.animation');


/**
 * @classdesc
 * Implementation of inertial deceleration for map movement.
 *
 * @constructor
 * @param {number} decay Rate of decay (must be negative).
 * @param {number} minVelocity Minimum velocity (pixels/millisecond).
 * @param {number} delay Delay to consider to calculate the kinetic
 *     initial values (milliseconds).
 * @struct
 * @api
 */
ol.Kinetic = function(decay, minVelocity, delay) {

  /**
   * @private
   * @type {number}
   */
  this.decay_ = decay;

  /**
   * @private
   * @type {number}
   */
  this.minVelocity_ = minVelocity;

  /**
   * @private
   * @type {number}
   */
  this.delay_ = delay;

  /**
   * @private
   * @type {Array.<number>}
   */
  this.points_ = [];

  /**
   * @private
   * @type {number}
   */
  this.angle_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.initialVelocity_ = 0;
};


/**
 * FIXME empty description for jsdoc
 */
ol.Kinetic.prototype.begin = function() {
  this.points_.length = 0;
  this.angle_ = 0;
  this.initialVelocity_ = 0;
};


/**
 * @param {number} x X.
 * @param {number} y Y.
 */
ol.Kinetic.prototype.update = function(x, y) {
  this.points_.push(x, y, Date.now());
};


/**
 * @return {boolean} Whether we should do kinetic animation.
 */
ol.Kinetic.prototype.end = function() {
  if (this.points_.length < 6) {
    // at least 2 points are required (i.e. there must be at least 6 elements
    // in the array)
    return false;
  }
  var delay = Date.now() - this.delay_;
  var lastIndex = this.points_.length - 3;
  if (this.points_[lastIndex + 2] < delay) {
    // the last tracked point is too old, which means that the user stopped
    // panning before releasing the map
    return false;
  }

  // get the first point which still falls into the delay time
  var firstIndex = lastIndex - 3;
  while (firstIndex > 0 && this.points_[firstIndex + 2] > delay) {
    firstIndex -= 3;
  }
  var duration = this.points_[lastIndex + 2] - this.points_[firstIndex + 2];
  var dx = this.points_[lastIndex] - this.points_[firstIndex];
  var dy = this.points_[lastIndex + 1] - this.points_[firstIndex + 1];
  this.angle_ = Math.atan2(dy, dx);
  this.initialVelocity_ = Math.sqrt(dx * dx + dy * dy) / duration;
  return this.initialVelocity_ > this.minVelocity_;
};


/**
 * @param {ol.Coordinate} source Source coordinate for the animation.
 * @return {ol.PreRenderFunction} Pre-render function for kinetic animation.
 */
ol.Kinetic.prototype.pan = function(source) {
  var decay = this.decay_;
  var initialVelocity = this.initialVelocity_;
  var velocity = this.minVelocity_ - initialVelocity;
  var duration = this.getDuration_();
  var easingFunction = (
      /**
       * @param {number} t T.
       * @return {number} Easing.
       */
      function(t) {
        return initialVelocity * (Math.exp((decay * t) * duration) - 1) /
            velocity;
      });
  return ol.animation.pan({
    source: source,
    duration: duration,
    easing: easingFunction
  });
};


/**
 * @private
 * @return {number} Duration of animation (milliseconds).
 */
ol.Kinetic.prototype.getDuration_ = function() {
  return Math.log(this.minVelocity_ / this.initialVelocity_) / this.decay_;
};


/**
 * @return {number} Total distance travelled (pixels).
 */
ol.Kinetic.prototype.getDistance = function() {
  return (this.minVelocity_ - this.initialVelocity_) / this.decay_;
};


/**
 * @return {number} Angle of the kinetic panning animation (radians).
 */
ol.Kinetic.prototype.getAngle = function() {
  return this.angle_;
};

// FIXME factor out key precondition (shift et. al)

goog.provide('ol.interaction.Interaction');
goog.provide('ol.interaction.InteractionProperty');

goog.require('ol');
goog.require('ol.MapBrowserEvent');
goog.require('ol.Object');
goog.require('ol.animation');
goog.require('ol.easing');


/**
 * @enum {string}
 */
ol.interaction.InteractionProperty = {
  ACTIVE: 'active'
};


/**
 * @classdesc
 * Abstract base class; normally only used for creating subclasses and not
 * instantiated in apps.
 * User actions that change the state of the map. Some are similar to controls,
 * but are not associated with a DOM element.
 * For example, {@link ol.interaction.KeyboardZoom} is functionally the same as
 * {@link ol.control.Zoom}, but triggered by a keyboard event not a button
 * element event.
 * Although interactions do not have a DOM element, some of them do render
 * vectors and so are visible on the screen.
 *
 * @constructor
 * @param {olx.interaction.InteractionOptions} options Options.
 * @extends {ol.Object}
 * @api
 */
ol.interaction.Interaction = function(options) {

  goog.base(this);

  /**
   * @private
   * @type {ol.Map}
   */
  this.map_ = null;

  this.setActive(true);

  /**
   * @type {function(ol.MapBrowserEvent):boolean}
   */
  this.handleEvent = options.handleEvent;

};
goog.inherits(ol.interaction.Interaction, ol.Object);


/**
 * Return whether the interaction is currently active.
 * @return {boolean} `true` if the interaction is active, `false` otherwise.
 * @observable
 * @api
 */
ol.interaction.Interaction.prototype.getActive = function() {
  return /** @type {boolean} */ (
      this.get(ol.interaction.InteractionProperty.ACTIVE));
};


/**
 * Get the map associated with this interaction.
 * @return {ol.Map} Map.
 * @api
 */
ol.interaction.Interaction.prototype.getMap = function() {
  return this.map_;
};


/**
 * Activate or deactivate the interaction.
 * @param {boolean} active Active.
 * @observable
 * @api
 */
ol.interaction.Interaction.prototype.setActive = function(active) {
  this.set(ol.interaction.InteractionProperty.ACTIVE, active);
};


/**
 * Remove the interaction from its current map and attach it to the new map.
 * Subclasses may set up event handlers to get notified about changes to
 * the map here.
 * @param {ol.Map} map Map.
 */
ol.interaction.Interaction.prototype.setMap = function(map) {
  this.map_ = map;
};


/**
 * @param {ol.Map} map Map.
 * @param {ol.View} view View.
 * @param {ol.Coordinate} delta Delta.
 * @param {number=} opt_duration Duration.
 */
ol.interaction.Interaction.pan = function(map, view, delta, opt_duration) {
  var currentCenter = view.getCenter();
  if (currentCenter) {
    if (opt_duration && opt_duration > 0) {
      map.beforeRender(ol.animation.pan({
        source: currentCenter,
        duration: opt_duration,
        easing: ol.easing.linear
      }));
    }
    var center = view.constrainCenter(
        [currentCenter[0] + delta[0], currentCenter[1] + delta[1]]);
    view.setCenter(center);
  }
};


/**
 * @param {ol.Map} map Map.
 * @param {ol.View} view View.
 * @param {number|undefined} rotation Rotation.
 * @param {ol.Coordinate=} opt_anchor Anchor coordinate.
 * @param {number=} opt_duration Duration.
 */
ol.interaction.Interaction.rotate = function(map, view, rotation, opt_anchor, opt_duration) {
  rotation = view.constrainRotation(rotation, 0);
  ol.interaction.Interaction.rotateWithoutConstraints(
      map, view, rotation, opt_anchor, opt_duration);
};


/**
 * @param {ol.Map} map Map.
 * @param {ol.View} view View.
 * @param {number|undefined} rotation Rotation.
 * @param {ol.Coordinate=} opt_anchor Anchor coordinate.
 * @param {number=} opt_duration Duration.
 */
ol.interaction.Interaction.rotateWithoutConstraints = function(map, view, rotation, opt_anchor, opt_duration) {
  if (rotation !== undefined) {
    var currentRotation = view.getRotation();
    var currentCenter = view.getCenter();
    if (currentRotation !== undefined && currentCenter &&
        opt_duration && opt_duration > 0) {
      map.beforeRender(ol.animation.rotate({
        rotation: currentRotation,
        duration: opt_duration,
        easing: ol.easing.easeOut
      }));
      if (opt_anchor) {
        map.beforeRender(ol.animation.pan({
          source: currentCenter,
          duration: opt_duration,
          easing: ol.easing.easeOut
        }));
      }
    }
    view.rotate(rotation, opt_anchor);
  }
};


/**
 * @param {ol.Map} map Map.
 * @param {ol.View} view View.
 * @param {number|undefined} resolution Resolution to go to.
 * @param {ol.Coordinate=} opt_anchor Anchor coordinate.
 * @param {number=} opt_duration Duration.
 * @param {number=} opt_direction Zooming direction; > 0 indicates
 *     zooming out, in which case the constraints system will select
 *     the largest nearest resolution; < 0 indicates zooming in, in
 *     which case the constraints system will select the smallest
 *     nearest resolution; == 0 indicates that the zooming direction
 *     is unknown/not relevant, in which case the constraints system
 *     will select the nearest resolution. If not defined 0 is
 *     assumed.
 */
ol.interaction.Interaction.zoom = function(map, view, resolution, opt_anchor, opt_duration, opt_direction) {
  resolution = view.constrainResolution(resolution, 0, opt_direction);
  ol.interaction.Interaction.zoomWithoutConstraints(
      map, view, resolution, opt_anchor, opt_duration);
};


/**
 * @param {ol.Map} map Map.
 * @param {ol.View} view View.
 * @param {number} delta Delta from previous zoom level.
 * @param {ol.Coordinate=} opt_anchor Anchor coordinate.
 * @param {number=} opt_duration Duration.
 */
ol.interaction.Interaction.zoomByDelta = function(map, view, delta, opt_anchor, opt_duration) {
  var currentResolution = view.getResolution();
  var resolution = view.constrainResolution(currentResolution, delta, 0);
  ol.interaction.Interaction.zoomWithoutConstraints(
      map, view, resolution, opt_anchor, opt_duration);
};


/**
 * @param {ol.Map} map Map.
 * @param {ol.View} view View.
 * @param {number|undefined} resolution Resolution to go to.
 * @param {ol.Coordinate=} opt_anchor Anchor coordinate.
 * @param {number=} opt_duration Duration.
 */
ol.interaction.Interaction.zoomWithoutConstraints = function(map, view, resolution, opt_anchor, opt_duration) {
  if (resolution) {
    var currentResolution = view.getResolution();
    var currentCenter = view.getCenter();
    if (currentResolution !== undefined && currentCenter &&
        resolution !== currentResolution &&
        opt_duration && opt_duration > 0) {
      map.beforeRender(ol.animation.zoom({
        resolution: currentResolution,
        duration: opt_duration,
        easing: ol.easing.easeOut
      }));
      if (opt_anchor) {
        map.beforeRender(ol.animation.pan({
          source: currentCenter,
          duration: opt_duration,
          easing: ol.easing.easeOut
        }));
      }
    }
    if (opt_anchor) {
      var center = view.calculateCenterZoom(resolution, opt_anchor);
      view.setCenter(center);
    }
    view.setResolution(resolution);
  }
};

goog.provide('ol.interaction.DoubleClickZoom');

goog.require('goog.asserts');
goog.require('ol.MapBrowserEvent');
goog.require('ol.MapBrowserEvent.EventType');
goog.require('ol.interaction.Interaction');


/**
 * @classdesc
 * Allows the user to zoom by double-clicking on the map.
 *
 * @constructor
 * @extends {ol.interaction.Interaction}
 * @param {olx.interaction.DoubleClickZoomOptions=} opt_options Options.
 * @api stable
 */
ol.interaction.DoubleClickZoom = function(opt_options) {

  var options = opt_options ? opt_options : {};

  /**
   * @private
   * @type {number}
   */
  this.delta_ = options.delta ? options.delta : 1;

  goog.base(this, {
    handleEvent: ol.interaction.DoubleClickZoom.handleEvent
  });

  /**
   * @private
   * @type {number}
   */
  this.duration_ = options.duration !== undefined ? options.duration : 250;

};
goog.inherits(ol.interaction.DoubleClickZoom, ol.interaction.Interaction);


/**
 * Handles the {@link ol.MapBrowserEvent map browser event} (if it was a
 * doubleclick) and eventually zooms the map.
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} `false` to stop event propagation.
 * @this {ol.interaction.DoubleClickZoom}
 * @api
 */
ol.interaction.DoubleClickZoom.handleEvent = function(mapBrowserEvent) {
  var stopEvent = false;
  var browserEvent = mapBrowserEvent.browserEvent;
  if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.DBLCLICK) {
    var map = mapBrowserEvent.map;
    var anchor = mapBrowserEvent.coordinate;
    var delta = browserEvent.shiftKey ? -this.delta_ : this.delta_;
    var view = map.getView();
    goog.asserts.assert(view, 'map must have a view');
    ol.interaction.Interaction.zoomByDelta(
        map, view, delta, anchor, this.duration_);
    mapBrowserEvent.preventDefault();
    stopEvent = true;
  }
  return !stopEvent;
};

goog.provide('ol.events.ConditionType');
goog.provide('ol.events.condition');

goog.require('goog.asserts');
goog.require('goog.functions');
goog.require('ol.MapBrowserEvent.EventType');
goog.require('ol.MapBrowserPointerEvent');


/**
 * A function that takes an {@link ol.MapBrowserEvent} and returns a
 * `{boolean}`. If the condition is met, true should be returned.
 *
 * @typedef {function(ol.MapBrowserEvent): boolean}
 * @api stable
 */
ol.events.ConditionType;


/**
 * Return `true` if only the alt-key is pressed, `false` otherwise (e.g. when
 * additionally the shift-key is pressed).
 *
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} True if only the alt key is pressed.
 * @api stable
 */
ol.events.condition.altKeyOnly = function(mapBrowserEvent) {
  var browserEvent = mapBrowserEvent.browserEvent;
  return (
      browserEvent.altKey &&
      !browserEvent.platformModifierKey &&
      !browserEvent.shiftKey);
};


/**
 * Return `true` if only the alt-key and shift-key is pressed, `false` otherwise
 * (e.g. when additionally the platform-modifier-key is pressed).
 *
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} True if only the alt and shift keys are pressed.
 * @api stable
 */
ol.events.condition.altShiftKeysOnly = function(mapBrowserEvent) {
  var browserEvent = mapBrowserEvent.browserEvent;
  return (
      browserEvent.altKey &&
      !browserEvent.platformModifierKey &&
      browserEvent.shiftKey);
};


/**
 * Return always true.
 *
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} True.
 * @function
 * @api stable
 */
ol.events.condition.always = goog.functions.TRUE;


/**
 * Return `true` if the event is a `click` event, `false` otherwise.
 *
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} True if the event is a map `click` event.
 * @api stable
 */
ol.events.condition.click = function(mapBrowserEvent) {
  return mapBrowserEvent.type == ol.MapBrowserEvent.EventType.CLICK;
};


/**
 * Return always false.
 *
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} False.
 * @function
 * @api stable
 */
ol.events.condition.never = goog.functions.FALSE;


/**
 * Return `true` if the browser event is a `pointermove` event, `false`
 * otherwise.
 *
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} True if the browser event is a `pointermove` event.
 * @api
 */
ol.events.condition.pointerMove = function(mapBrowserEvent) {
  return mapBrowserEvent.type == 'pointermove';
};


/**
 * Return `true` if the event is a map `singleclick` event, `false` otherwise.
 *
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} True if the event is a map `singleclick` event.
 * @api stable
 */
ol.events.condition.singleClick = function(mapBrowserEvent) {
  return mapBrowserEvent.type == ol.MapBrowserEvent.EventType.SINGLECLICK;
};


/**
 * Return `true` if the event is a map `dblclick` event, `false` otherwise.
 *
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} True if the event is a map `dblclick` event.
 * @api stable
 */
ol.events.condition.doubleClick = function(mapBrowserEvent) {
  return mapBrowserEvent.type == ol.MapBrowserEvent.EventType.DBLCLICK;
};


/**
 * Return `true` if no modifier key (alt-, shift- or platform-modifier-key) is
 * pressed.
 *
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} True only if there no modifier keys are pressed.
 * @api stable
 */
ol.events.condition.noModifierKeys = function(mapBrowserEvent) {
  var browserEvent = mapBrowserEvent.browserEvent;
  return (
      !browserEvent.altKey &&
      !browserEvent.platformModifierKey &&
      !browserEvent.shiftKey);
};


/**
 * Return `true` if only the platform-modifier-key (the meta-key on Mac,
 * ctrl-key otherwise) is pressed, `false` otherwise (e.g. when additionally
 * the shift-key is pressed).
 *
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} True if only the platform modifier key is pressed.
 * @api stable
 */
ol.events.condition.platformModifierKeyOnly = function(mapBrowserEvent) {
  var browserEvent = mapBrowserEvent.browserEvent;
  return (
      !browserEvent.altKey &&
      browserEvent.platformModifierKey &&
      !browserEvent.shiftKey);
};


/**
 * Return `true` if only the shift-key is pressed, `false` otherwise (e.g. when
 * additionally the alt-key is pressed).
 *
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} True if only the shift key is pressed.
 * @api stable
 */
ol.events.condition.shiftKeyOnly = function(mapBrowserEvent) {
  var browserEvent = mapBrowserEvent.browserEvent;
  return (
      !browserEvent.altKey &&
      !browserEvent.platformModifierKey &&
      browserEvent.shiftKey);
};


/**
 * Return `true` if the target element is not editable, i.e. not a `<input>`-,
 * `<select>`- or `<textarea>`-element, `false` otherwise.
 *
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} True only if the target element is not editable.
 * @api
 */
ol.events.condition.targetNotEditable = function(mapBrowserEvent) {
  var target = mapBrowserEvent.browserEvent.target;
  goog.asserts.assertInstanceof(target, Element,
      'target should be an Element');
  var tagName = target.tagName;
  return (
      tagName !== 'INPUT' &&
      tagName !== 'SELECT' &&
      tagName !== 'TEXTAREA');
};


/**
 * Return `true` if the event originates from a mouse device.
 *
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Map browser event.
 * @return {boolean} True if the event originates from a mouse device.
 * @api stable
 */
ol.events.condition.mouseOnly = function(mapBrowserEvent) {
  // see http://www.w3.org/TR/pointerevents/#widl-PointerEvent-pointerType
  return mapBrowserEvent.pointerEvent.pointerType == 'mouse';
};

goog.provide('ol.interaction.Pointer');

goog.require('goog.functions');
goog.require('goog.object');
goog.require('ol');
goog.require('ol.MapBrowserEvent.EventType');
goog.require('ol.MapBrowserPointerEvent');
goog.require('ol.Pixel');
goog.require('ol.interaction.Interaction');


/**
 * @classdesc
 * Base class that calls user-defined functions on `down`, `move` and `up`
 * events. This class also manages "drag sequences".
 *
 * When the `handleDownEvent` user function returns `true` a drag sequence is
 * started. During a drag sequence the `handleDragEvent` user function is
 * called on `move` events. The drag sequence ends when the `handleUpEvent`
 * user function is called and returns `false`.
 *
 * @constructor
 * @param {olx.interaction.PointerOptions=} opt_options Options.
 * @extends {ol.interaction.Interaction}
 * @api
 */
ol.interaction.Pointer = function(opt_options) {

  var options = opt_options ? opt_options : {};

  var handleEvent = options.handleEvent ?
      options.handleEvent : ol.interaction.Pointer.handleEvent;

  goog.base(this, {
    handleEvent: handleEvent
  });

  /**
   * @type {function(ol.MapBrowserPointerEvent):boolean}
   * @private
   */
  this.handleDownEvent_ = options.handleDownEvent ?
      options.handleDownEvent : ol.interaction.Pointer.handleDownEvent;

  /**
   * @type {function(ol.MapBrowserPointerEvent)}
   * @private
   */
  this.handleDragEvent_ = options.handleDragEvent ?
      options.handleDragEvent : ol.interaction.Pointer.handleDragEvent;

  /**
   * @type {function(ol.MapBrowserPointerEvent)}
   * @private
   */
  this.handleMoveEvent_ = options.handleMoveEvent ?
      options.handleMoveEvent : ol.interaction.Pointer.handleMoveEvent;

  /**
   * @type {function(ol.MapBrowserPointerEvent):boolean}
   * @private
   */
  this.handleUpEvent_ = options.handleUpEvent ?
      options.handleUpEvent : ol.interaction.Pointer.handleUpEvent;

  /**
   * @type {boolean}
   * @protected
   */
  this.handlingDownUpSequence = false;

  /**
   * @type {Object.<number, ol.pointer.PointerEvent>}
   * @private
   */
  this.trackedPointers_ = {};

  /**
   * @type {Array.<ol.pointer.PointerEvent>}
   * @protected
   */
  this.targetPointers = [];

};
goog.inherits(ol.interaction.Pointer, ol.interaction.Interaction);


/**
 * @param {Array.<ol.pointer.PointerEvent>} pointerEvents List of events.
 * @return {ol.Pixel} Centroid pixel.
 */
ol.interaction.Pointer.centroid = function(pointerEvents) {
  var length = pointerEvents.length;
  var clientX = 0;
  var clientY = 0;
  for (var i = 0; i < length; i++) {
    clientX += pointerEvents[i].clientX;
    clientY += pointerEvents[i].clientY;
  }
  return [clientX / length, clientY / length];
};


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boolean} Whether the event is a pointerdown, pointerdrag
 *     or pointerup event.
 * @private
 */
ol.interaction.Pointer.prototype.isPointerDraggingEvent_ = function(mapBrowserEvent) {
  var type = mapBrowserEvent.type;
  return (
      type === ol.MapBrowserEvent.EventType.POINTERDOWN ||
      type === ol.MapBrowserEvent.EventType.POINTERDRAG ||
      type === ol.MapBrowserEvent.EventType.POINTERUP);
};


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @private
 */
ol.interaction.Pointer.prototype.updateTrackedPointers_ = function(mapBrowserEvent) {
  if (this.isPointerDraggingEvent_(mapBrowserEvent)) {
    var event = mapBrowserEvent.pointerEvent;

    if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.POINTERUP) {
      delete this.trackedPointers_[event.pointerId];
    } else if (mapBrowserEvent.type ==
        ol.MapBrowserEvent.EventType.POINTERDOWN) {
      this.trackedPointers_[event.pointerId] = event;
    } else if (event.pointerId in this.trackedPointers_) {
      // update only when there was a pointerdown event for this pointer
      this.trackedPointers_[event.pointerId] = event;
    }
    this.targetPointers = goog.object.getValues(this.trackedPointers_);
  }
};


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @this {ol.interaction.Pointer}
 */
ol.interaction.Pointer.handleDragEvent = ol.nullFunction;


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boolean} Capture dragging.
 * @this {ol.interaction.Pointer}
 */
ol.interaction.Pointer.handleUpEvent = goog.functions.FALSE;


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boolean} Capture dragging.
 * @this {ol.interaction.Pointer}
 */
ol.interaction.Pointer.handleDownEvent = goog.functions.FALSE;


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @this {ol.interaction.Pointer}
 */
ol.interaction.Pointer.handleMoveEvent = ol.nullFunction;


/**
 * Handles the {@link ol.MapBrowserEvent map browser event} and may call into
 * other functions, if event sequences like e.g. 'drag' or 'down-up' etc. are
 * detected.
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} `false` to stop event propagation.
 * @this {ol.interaction.Pointer}
 * @api
 */
ol.interaction.Pointer.handleEvent = function(mapBrowserEvent) {
  if (!(mapBrowserEvent instanceof ol.MapBrowserPointerEvent)) {
    return true;
  }

  var stopEvent = false;
  this.updateTrackedPointers_(mapBrowserEvent);
  if (this.handlingDownUpSequence) {
    if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.POINTERDRAG) {
      this.handleDragEvent_(mapBrowserEvent);
    } else if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.POINTERUP) {
      this.handlingDownUpSequence = this.handleUpEvent_(mapBrowserEvent);
    }
  }
  if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.POINTERDOWN) {
    var handled = this.handleDownEvent_(mapBrowserEvent);
    this.handlingDownUpSequence = handled;
    stopEvent = this.shouldStopEvent(handled);
  } else if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.POINTERMOVE) {
    this.handleMoveEvent_(mapBrowserEvent);
  }
  return !stopEvent;
};


/**
 * This method is used to determine if "down" events should be propagated to
 * other interactions or should be stopped.
 *
 * The method receives the return code of the "handleDownEvent" function.
 *
 * By default this function is the "identity" function. It's overidden in
 * child classes.
 *
 * @param {boolean} handled Was the event handled by the interaction?
 * @return {boolean} Should the event be stopped?
 * @protected
 */
ol.interaction.Pointer.prototype.shouldStopEvent = goog.functions.identity;

goog.provide('ol.interaction.DragPan');

goog.require('goog.asserts');
goog.require('ol.Kinetic');
goog.require('ol.Pixel');
goog.require('ol.PreRenderFunction');
goog.require('ol.ViewHint');
goog.require('ol.coordinate');
goog.require('ol.events.condition');
goog.require('ol.interaction.Pointer');


/**
 * @classdesc
 * Allows the user to pan the map by dragging the map.
 *
 * @constructor
 * @extends {ol.interaction.Pointer}
 * @param {olx.interaction.DragPanOptions=} opt_options Options.
 * @api stable
 */
ol.interaction.DragPan = function(opt_options) {

  goog.base(this, {
    handleDownEvent: ol.interaction.DragPan.handleDownEvent_,
    handleDragEvent: ol.interaction.DragPan.handleDragEvent_,
    handleUpEvent: ol.interaction.DragPan.handleUpEvent_
  });

  var options = opt_options ? opt_options : {};

  /**
   * @private
   * @type {ol.Kinetic|undefined}
   */
  this.kinetic_ = options.kinetic;

  /**
   * @private
   * @type {?ol.PreRenderFunction}
   */
  this.kineticPreRenderFn_ = null;

  /**
   * @type {ol.Pixel}
   */
  this.lastCentroid = null;

  /**
   * @private
   * @type {ol.events.ConditionType}
   */
  this.condition_ = options.condition ?
      options.condition : ol.events.condition.noModifierKeys;

  /**
   * @private
   * @type {boolean}
   */
  this.noKinetic_ = false;

};
goog.inherits(ol.interaction.DragPan, ol.interaction.Pointer);


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @this {ol.interaction.DragPan}
 * @private
 */
ol.interaction.DragPan.handleDragEvent_ = function(mapBrowserEvent) {
  goog.asserts.assert(this.targetPointers.length >= 1,
      'the length of this.targetPointers should be more than 1');
  var centroid =
      ol.interaction.Pointer.centroid(this.targetPointers);
  if (this.kinetic_) {
    this.kinetic_.update(centroid[0], centroid[1]);
  }
  if (this.lastCentroid) {
    var deltaX = this.lastCentroid[0] - centroid[0];
    var deltaY = centroid[1] - this.lastCentroid[1];
    var map = mapBrowserEvent.map;
    var view = map.getView();
    var viewState = view.getState();
    var center = [deltaX, deltaY];
    ol.coordinate.scale(center, viewState.resolution);
    ol.coordinate.rotate(center, viewState.rotation);
    ol.coordinate.add(center, viewState.center);
    center = view.constrainCenter(center);
    map.render();
    view.setCenter(center);
  }
  this.lastCentroid = centroid;
};


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boolean} Stop drag sequence?
 * @this {ol.interaction.DragPan}
 * @private
 */
ol.interaction.DragPan.handleUpEvent_ = function(mapBrowserEvent) {
  var map = mapBrowserEvent.map;
  var view = map.getView();
  if (this.targetPointers.length === 0) {
    if (!this.noKinetic_ && this.kinetic_ && this.kinetic_.end()) {
      var distance = this.kinetic_.getDistance();
      var angle = this.kinetic_.getAngle();
      var center = view.getCenter();
      goog.asserts.assert(center !== undefined, 'center should be defined');
      this.kineticPreRenderFn_ = this.kinetic_.pan(center);
      map.beforeRender(this.kineticPreRenderFn_);
      var centerpx = map.getPixelFromCoordinate(center);
      var dest = map.getCoordinateFromPixel([
        centerpx[0] - distance * Math.cos(angle),
        centerpx[1] - distance * Math.sin(angle)
      ]);
      dest = view.constrainCenter(dest);
      view.setCenter(dest);
    }
    view.setHint(ol.ViewHint.INTERACTING, -1);
    map.render();
    return false;
  } else {
    this.lastCentroid = null;
    return true;
  }
};


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boolean} Start drag sequence?
 * @this {ol.interaction.DragPan}
 * @private
 */
ol.interaction.DragPan.handleDownEvent_ = function(mapBrowserEvent) {
  if (this.targetPointers.length > 0 && this.condition_(mapBrowserEvent)) {
    var map = mapBrowserEvent.map;
    var view = map.getView();
    this.lastCentroid = null;
    if (!this.handlingDownUpSequence) {
      view.setHint(ol.ViewHint.INTERACTING, 1);
    }
    map.render();
    if (this.kineticPreRenderFn_ &&
        map.removePreRenderFunction(this.kineticPreRenderFn_)) {
      view.setCenter(mapBrowserEvent.frameState.viewState.center);
      this.kineticPreRenderFn_ = null;
    }
    if (this.kinetic_) {
      this.kinetic_.begin();
    }
    // No kinetic as soon as more than one pointer on the screen is
    // detected. This is to prevent nasty pans after pinch.
    this.noKinetic_ = this.targetPointers.length > 1;
    return true;
  } else {
    return false;
  }
};


/**
 * @inheritDoc
 */
ol.interaction.DragPan.prototype.shouldStopEvent = goog.functions.FALSE;

goog.provide('ol.interaction.DragRotate');

goog.require('ol');
goog.require('ol.ViewHint');
goog.require('ol.events.ConditionType');
goog.require('ol.events.condition');
goog.require('ol.interaction.Interaction');
goog.require('ol.interaction.Pointer');


/**
 * @classdesc
 * Allows the user to rotate the map by clicking and dragging on the map,
 * normally combined with an {@link ol.events.condition} that limits
 * it to when the alt and shift keys are held down.
 *
 * This interaction is only supported for mouse devices.
 *
 * @constructor
 * @extends {ol.interaction.Pointer}
 * @param {olx.interaction.DragRotateOptions=} opt_options Options.
 * @api stable
 */
ol.interaction.DragRotate = function(opt_options) {

  var options = opt_options ? opt_options : {};

  goog.base(this, {
    handleDownEvent: ol.interaction.DragRotate.handleDownEvent_,
    handleDragEvent: ol.interaction.DragRotate.handleDragEvent_,
    handleUpEvent: ol.interaction.DragRotate.handleUpEvent_
  });

  /**
   * @private
   * @type {ol.events.ConditionType}
   */
  this.condition_ = options.condition ?
      options.condition : ol.events.condition.altShiftKeysOnly;

  /**
   * @private
   * @type {number|undefined}
   */
  this.lastAngle_ = undefined;

  /**
   * @private
   * @type {number}
   */
  this.duration_ = options.duration !== undefined ? options.duration : 250;
};
goog.inherits(ol.interaction.DragRotate, ol.interaction.Pointer);


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @this {ol.interaction.DragRotate}
 * @private
 */
ol.interaction.DragRotate.handleDragEvent_ = function(mapBrowserEvent) {
  if (!ol.events.condition.mouseOnly(mapBrowserEvent)) {
    return;
  }

  var map = mapBrowserEvent.map;
  var size = map.getSize();
  var offset = mapBrowserEvent.pixel;
  var theta =
      Math.atan2(size[1] / 2 - offset[1], offset[0] - size[0] / 2);
  if (this.lastAngle_ !== undefined) {
    var delta = theta - this.lastAngle_;
    var view = map.getView();
    var rotation = view.getRotation();
    map.render();
    ol.interaction.Interaction.rotateWithoutConstraints(
        map, view, rotation - delta);
  }
  this.lastAngle_ = theta;
};


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boolean} Stop drag sequence?
 * @this {ol.interaction.DragRotate}
 * @private
 */
ol.interaction.DragRotate.handleUpEvent_ = function(mapBrowserEvent) {
  if (!ol.events.condition.mouseOnly(mapBrowserEvent)) {
    return true;
  }

  var map = mapBrowserEvent.map;
  var view = map.getView();
  view.setHint(ol.ViewHint.INTERACTING, -1);
  var rotation = view.getRotation();
  ol.interaction.Interaction.rotate(map, view, rotation,
      undefined, this.duration_);
  return false;
};


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boolean} Start drag sequence?
 * @this {ol.interaction.DragRotate}
 * @private
 */
ol.interaction.DragRotate.handleDownEvent_ = function(mapBrowserEvent) {
  if (!ol.events.condition.mouseOnly(mapBrowserEvent)) {
    return false;
  }

  var browserEvent = mapBrowserEvent.browserEvent;
  if (browserEvent.isMouseActionButton() && this.condition_(mapBrowserEvent)) {
    var map = mapBrowserEvent.map;
    map.getView().setHint(ol.ViewHint.INTERACTING, 1);
    map.render();
    this.lastAngle_ = undefined;
    return true;
  } else {
    return false;
  }
};


/**
 * @inheritDoc
 */
ol.interaction.DragRotate.prototype.shouldStopEvent = goog.functions.FALSE;

// FIXME add rotation

goog.provide('ol.render.Box');

goog.require('goog.Disposable');
goog.require('goog.asserts');
goog.require('ol.geom.Polygon');


/**
 * @constructor
 * @extends {goog.Disposable}
 * @param {string} className CSS class name.
 */
ol.render.Box = function(className) {

  /**
   * @type {ol.geom.Polygon}
   * @private
   */
  this.geometry_ = null;

  /**
   * @type {HTMLDivElement}
   * @private
   */
  this.element_ = /** @type {HTMLDivElement} */ (document.createElement('div'));
  this.element_.style.position = 'absolute';
  this.element_.className = 'ol-box ' + className;

  /**
   * @private
   * @type {ol.Map}
   */
  this.map_ = null;

  /**
   * @private
   * @type {ol.Pixel}
   */
  this.startPixel_ = null;

  /**
   * @private
   * @type {ol.Pixel}
   */
  this.endPixel_ = null;

};
goog.inherits(ol.render.Box, goog.Disposable);


/**
 * @inheritDoc
 */
ol.render.Box.prototype.disposeInternal = function() {
  this.setMap(null);
  goog.base(this, 'disposeInternal');
};


/**
 * @private
 */
ol.render.Box.prototype.render_ = function() {
  var startPixel = this.startPixel_;
  var endPixel = this.endPixel_;
  goog.asserts.assert(startPixel, 'this.startPixel_ must be truthy');
  goog.asserts.assert(endPixel, 'this.endPixel_ must be truthy');
  var px = 'px';
  var style = this.element_.style;
  style.left = Math.min(startPixel[0], endPixel[0]) + px;
  style.top = Math.min(startPixel[1], endPixel[1]) + px;
  style.width = Math.abs(endPixel[0] - startPixel[0]) + px;
  style.height = Math.abs(endPixel[1] - startPixel[1]) + px;
};


/**
 * @param {ol.Map} map Map.
 */
ol.render.Box.prototype.setMap = function(map) {
  if (this.map_) {
    this.map_.getOverlayContainer().removeChild(this.element_);
    var style = this.element_.style;
    style.left = style.top = style.width = style.height = 'inherit';
  }
  this.map_ = map;
  if (this.map_) {
    this.map_.getOverlayContainer().appendChild(this.element_);
  }
};


/**
 * @param {ol.Pixel} startPixel Start pixel.
 * @param {ol.Pixel} endPixel End pixel.
 */
ol.render.Box.prototype.setPixels = function(startPixel, endPixel) {
  this.startPixel_ = startPixel;
  this.endPixel_ = endPixel;
  this.createOrUpdateGeometry();
  this.render_();
};


/**
 * Creates or updates the cached geometry.
 */
ol.render.Box.prototype.createOrUpdateGeometry = function() {
  goog.asserts.assert(this.startPixel_,
      'this.startPixel_ must be truthy');
  goog.asserts.assert(this.endPixel_,
      'this.endPixel_ must be truthy');
  goog.asserts.assert(this.map_, 'this.map_ must be truthy');
  var startPixel = this.startPixel_;
  var endPixel = this.endPixel_;
  var pixels = [
    startPixel,
    [startPixel[0], endPixel[1]],
    endPixel,
    [endPixel[0], startPixel[1]]
  ];
  var coordinates = pixels.map(this.map_.getCoordinateFromPixel, this.map_);
  // close the polygon
  coordinates[4] = coordinates[0].slice();
  if (!this.geometry_) {
    this.geometry_ = new ol.geom.Polygon([coordinates]);
  } else {
    this.geometry_.setCoordinates([coordinates]);
  }
};


/**
 * @return {ol.geom.Polygon} Geometry.
 */
ol.render.Box.prototype.getGeometry = function() {
  return this.geometry_;
};

// FIXME draw drag box
goog.provide('ol.DragBoxEvent');
goog.provide('ol.interaction.DragBox');

goog.require('goog.events.Event');
goog.require('ol');
goog.require('ol.events.ConditionType');
goog.require('ol.events.condition');
goog.require('ol.interaction.Pointer');
goog.require('ol.render.Box');


/**
 * @const
 * @type {number}
 */
ol.DRAG_BOX_HYSTERESIS_PIXELS_SQUARED =
    ol.DRAG_BOX_HYSTERESIS_PIXELS *
    ol.DRAG_BOX_HYSTERESIS_PIXELS;


/**
 * @enum {string}
 */
ol.DragBoxEventType = {
  /**
   * Triggered upon drag box start.
   * @event ol.DragBoxEvent#boxstart
   * @api stable
   */
  BOXSTART: 'boxstart',

  /**
   * Triggered on drag when box is active.
   * @event ol.DragBoxEvent#boxdrag
   * @api
   */
  BOXDRAG: 'boxdrag',

  /**
   * Triggered upon drag box end.
   * @event ol.DragBoxEvent#boxend
   * @api stable
   */
  BOXEND: 'boxend'
};


/**
 * @classdesc
 * Events emitted by {@link ol.interaction.DragBox} instances are instances of
 * this type.
 *
 * @param {string} type The event type.
 * @param {ol.Coordinate} coordinate The event coordinate.
 * @param {ol.MapBrowserEvent} mapBrowserEvent Originating event.
 * @extends {goog.events.Event}
 * @constructor
 * @implements {oli.DragBoxEvent}
 */
ol.DragBoxEvent = function(type, coordinate, mapBrowserEvent) {
  goog.base(this, type);

  /**
   * The coordinate of the drag event.
   * @const
   * @type {ol.Coordinate}
   * @api stable
   */
  this.coordinate = coordinate;

  /**
   * @const
   * @type {ol.MapBrowserEvent}
   * @api
   */
  this.mapBrowserEvent = mapBrowserEvent;

};
goog.inherits(ol.DragBoxEvent, goog.events.Event);


/**
 * A function that takes a {@link ol.MapBrowserEvent} and two
 * {@link ol.Pixel}s and returns a `{boolean}`. If the condition is met,
 * true should be returned.
 * @typedef {function(ol.MapBrowserEvent, ol.Pixel, ol.Pixel):boolean}
 * @api
 */
ol.interaction.DragBoxEndConditionType;


/**
 * @classdesc
 * Allows the user to draw a vector box by clicking and dragging on the map,
 * normally combined with an {@link ol.events.condition} that limits
 * it to when the shift or other key is held down. This is used, for example,
 * for zooming to a specific area of the map
 * (see {@link ol.interaction.DragZoom} and
 * {@link ol.interaction.DragRotateAndZoom}).
 *
 * This interaction is only supported for mouse devices.
 *
 * @constructor
 * @extends {ol.interaction.Pointer}
 * @fires ol.DragBoxEvent
 * @param {olx.interaction.DragBoxOptions=} opt_options Options.
 * @api stable
 */
ol.interaction.DragBox = function(opt_options) {

  goog.base(this, {
    handleDownEvent: ol.interaction.DragBox.handleDownEvent_,
    handleDragEvent: ol.interaction.DragBox.handleDragEvent_,
    handleUpEvent: ol.interaction.DragBox.handleUpEvent_
  });

  var options = opt_options ? opt_options : {};

  /**
   * @type {ol.render.Box}
   * @private
   */
  this.box_ = new ol.render.Box(options.className || 'ol-dragbox');

  /**
   * @type {ol.Pixel}
   * @private
   */
  this.startPixel_ = null;

  /**
   * @private
   * @type {ol.events.ConditionType}
   */
  this.condition_ = options.condition ?
      options.condition : ol.events.condition.always;

  /**
   * @private
   * @type {ol.interaction.DragBoxEndConditionType}
   */
  this.boxEndCondition_ = options.boxEndCondition ?
      options.boxEndCondition : ol.interaction.DragBox.defaultBoxEndCondition;
};
goog.inherits(ol.interaction.DragBox, ol.interaction.Pointer);


/**
 * The default condition for determining whether the boxend event
 * should fire.
 * @param  {ol.MapBrowserEvent} mapBrowserEvent The originating MapBrowserEvent
 *  leading to the box end.
 * @param  {ol.Pixel} startPixel      The starting pixel of the box.
 * @param  {ol.Pixel} endPixel        The end pixel of the box.
 * @return {boolean} Whether or not the boxend condition should be fired.
 */
ol.interaction.DragBox.defaultBoxEndCondition = function(mapBrowserEvent,
    startPixel, endPixel) {
  var width = endPixel[0] - startPixel[0];
  var height = endPixel[1] - startPixel[1];
  return width * width + height * height >=
      ol.DRAG_BOX_HYSTERESIS_PIXELS_SQUARED;
};


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @this {ol.interaction.DragBox}
 * @private
 */
ol.interaction.DragBox.handleDragEvent_ = function(mapBrowserEvent) {
  if (!ol.events.condition.mouseOnly(mapBrowserEvent)) {
    return;
  }

  this.box_.setPixels(this.startPixel_, mapBrowserEvent.pixel);

  this.dispatchEvent(new ol.DragBoxEvent(ol.DragBoxEventType.BOXDRAG,
    mapBrowserEvent.coordinate, mapBrowserEvent));
};


/**
 * Returns geometry of last drawn box.
 * @return {ol.geom.Polygon} Geometry.
 * @api stable
 */
ol.interaction.DragBox.prototype.getGeometry = function() {
  return this.box_.getGeometry();
};


/**
 * To be overriden by child classes.
 * FIXME: use constructor option instead of relying on overridding.
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @protected
 */
ol.interaction.DragBox.prototype.onBoxEnd = ol.nullFunction;


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boolean} Stop drag sequence?
 * @this {ol.interaction.DragBox}
 * @private
 */
ol.interaction.DragBox.handleUpEvent_ = function(mapBrowserEvent) {
  if (!ol.events.condition.mouseOnly(mapBrowserEvent)) {
    return true;
  }

  this.box_.setMap(null);

  if (this.boxEndCondition_(mapBrowserEvent,
      this.startPixel_, mapBrowserEvent.pixel)) {
    this.onBoxEnd(mapBrowserEvent);
    this.dispatchEvent(new ol.DragBoxEvent(ol.DragBoxEventType.BOXEND,
        mapBrowserEvent.coordinate, mapBrowserEvent));
  }
  return false;
};


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boolean} Start drag sequence?
 * @this {ol.interaction.DragBox}
 * @private
 */
ol.interaction.DragBox.handleDownEvent_ = function(mapBrowserEvent) {
  if (!ol.events.condition.mouseOnly(mapBrowserEvent)) {
    return false;
  }

  var browserEvent = mapBrowserEvent.browserEvent;
  if (browserEvent.isMouseActionButton() && this.condition_(mapBrowserEvent)) {
    this.startPixel_ = mapBrowserEvent.pixel;
    this.box_.setMap(mapBrowserEvent.map);
    this.box_.setPixels(this.startPixel_, this.startPixel_);
    this.dispatchEvent(new ol.DragBoxEvent(ol.DragBoxEventType.BOXSTART,
        mapBrowserEvent.coordinate, mapBrowserEvent));
    return true;
  } else {
    return false;
  }
};

goog.provide('ol.interaction.DragZoom');

goog.require('goog.asserts');
goog.require('ol.animation');
goog.require('ol.easing');
goog.require('ol.events.condition');
goog.require('ol.extent');
goog.require('ol.interaction.DragBox');


/**
 * @classdesc
 * Allows the user to zoom the map by clicking and dragging on the map,
 * normally combined with an {@link ol.events.condition} that limits
 * it to when a key, shift by default, is held down.
 *
 * To change the style of the box, use CSS and the `.ol-dragzoom` selector, or
 * your custom one configured with `className`.
 *
 * @constructor
 * @extends {ol.interaction.DragBox}
 * @param {olx.interaction.DragZoomOptions=} opt_options Options.
 * @api stable
 */
ol.interaction.DragZoom = function(opt_options) {
  var options = opt_options ? opt_options : {};

  var condition = options.condition ?
      options.condition : ol.events.condition.shiftKeyOnly;

  /**
   * @private
   * @type {number}
   */
  this.duration_ = options.duration !== undefined ? options.duration : 200;

  goog.base(this, {
    condition: condition,
    className: options.className || 'ol-dragzoom'
  });

};
goog.inherits(ol.interaction.DragZoom, ol.interaction.DragBox);


/**
 * @inheritDoc
 */
ol.interaction.DragZoom.prototype.onBoxEnd = function() {
  var map = this.getMap();

  var view = map.getView();
  goog.asserts.assert(view, 'map must have view');

  var size = map.getSize();
  goog.asserts.assert(size !== undefined, 'size should be defined');

  var extent = this.getGeometry().getExtent();

  var resolution = view.constrainResolution(
      view.getResolutionForExtent(extent, size));

  var currentResolution = view.getResolution();
  goog.asserts.assert(currentResolution !== undefined, 'res should be defined');

  var currentCenter = view.getCenter();
  goog.asserts.assert(currentCenter !== undefined, 'center should be defined');

  map.beforeRender(ol.animation.zoom({
    resolution: currentResolution,
    duration: this.duration_,
    easing: ol.easing.easeOut
  }));
  map.beforeRender(ol.animation.pan({
    source: currentCenter,
    duration: this.duration_,
    easing: ol.easing.easeOut
  }));

  view.setCenter(ol.extent.getCenter(extent));
  view.setResolution(resolution);
};

goog.provide('ol.interaction.KeyboardPan');

goog.require('goog.asserts');
goog.require('goog.events.KeyCodes');
goog.require('goog.events.KeyHandler.EventType');
goog.require('goog.functions');
goog.require('ol');
goog.require('ol.coordinate');
goog.require('ol.events.ConditionType');
goog.require('ol.events.condition');
goog.require('ol.interaction.Interaction');


/**
 * @classdesc
 * Allows the user to pan the map using keyboard arrows.
 * Note that, although this interaction is by default included in maps,
 * the keys can only be used when browser focus is on the element to which
 * the keyboard events are attached. By default, this is the map div,
 * though you can change this with the `keyboardEventTarget` in
 * {@link ol.Map}. `document` never loses focus but, for any other element,
 * focus will have to be on, and returned to, this element if the keys are to
 * function.
 * See also {@link ol.interaction.KeyboardZoom}.
 *
 * @constructor
 * @extends {ol.interaction.Interaction}
 * @param {olx.interaction.KeyboardPanOptions=} opt_options Options.
 * @api stable
 */
ol.interaction.KeyboardPan = function(opt_options) {

  goog.base(this, {
    handleEvent: ol.interaction.KeyboardPan.handleEvent
  });

  var options = opt_options || {};

  /**
   * @private
   * @type {ol.events.ConditionType}
   */
  this.condition_ = options.condition !== undefined ?
      options.condition :
      goog.functions.and(ol.events.condition.noModifierKeys,
          ol.events.condition.targetNotEditable);

  /**
   * @private
   * @type {number}
   */
  this.duration_ = options.duration !== undefined ? options.duration : 100;

  /**
   * @private
   * @type {number}
   */
  this.pixelDelta_ = options.pixelDelta !== undefined ?
      options.pixelDelta : 128;

};
goog.inherits(ol.interaction.KeyboardPan, ol.interaction.Interaction);


/**
 * Handles the {@link ol.MapBrowserEvent map browser event} if it was a
 * `KeyEvent`, and decides the direction to pan to (if an arrow key was
 * pressed).
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} `false` to stop event propagation.
 * @this {ol.interaction.KeyboardPan}
 * @api
 */
ol.interaction.KeyboardPan.handleEvent = function(mapBrowserEvent) {
  var stopEvent = false;
  if (mapBrowserEvent.type == goog.events.KeyHandler.EventType.KEY) {
    var keyEvent = /** @type {goog.events.KeyEvent} */
        (mapBrowserEvent.browserEvent);
    var keyCode = keyEvent.keyCode;
    if (this.condition_(mapBrowserEvent) &&
        (keyCode == goog.events.KeyCodes.DOWN ||
        keyCode == goog.events.KeyCodes.LEFT ||
        keyCode == goog.events.KeyCodes.RIGHT ||
        keyCode == goog.events.KeyCodes.UP)) {
      var map = mapBrowserEvent.map;
      var view = map.getView();
      goog.asserts.assert(view, 'map must have view');
      var mapUnitsDelta = view.getResolution() * this.pixelDelta_;
      var deltaX = 0, deltaY = 0;
      if (keyCode == goog.events.KeyCodes.DOWN) {
        deltaY = -mapUnitsDelta;
      } else if (keyCode == goog.events.KeyCodes.LEFT) {
        deltaX = -mapUnitsDelta;
      } else if (keyCode == goog.events.KeyCodes.RIGHT) {
        deltaX = mapUnitsDelta;
      } else {
        deltaY = mapUnitsDelta;
      }
      var delta = [deltaX, deltaY];
      ol.coordinate.rotate(delta, view.getRotation());
      ol.interaction.Interaction.pan(map, view, delta, this.duration_);
      mapBrowserEvent.preventDefault();
      stopEvent = true;
    }
  }
  return !stopEvent;
};

goog.provide('ol.interaction.KeyboardZoom');

goog.require('goog.asserts');
goog.require('goog.events.KeyHandler.EventType');
goog.require('ol.events.ConditionType');
goog.require('ol.events.condition');
goog.require('ol.interaction.Interaction');


/**
 * @classdesc
 * Allows the user to zoom the map using keyboard + and -.
 * Note that, although this interaction is by default included in maps,
 * the keys can only be used when browser focus is on the element to which
 * the keyboard events are attached. By default, this is the map div,
 * though you can change this with the `keyboardEventTarget` in
 * {@link ol.Map}. `document` never loses focus but, for any other element,
 * focus will have to be on, and returned to, this element if the keys are to
 * function.
 * See also {@link ol.interaction.KeyboardPan}.
 *
 * @constructor
 * @param {olx.interaction.KeyboardZoomOptions=} opt_options Options.
 * @extends {ol.interaction.Interaction}
 * @api stable
 */
ol.interaction.KeyboardZoom = function(opt_options) {

  goog.base(this, {
    handleEvent: ol.interaction.KeyboardZoom.handleEvent
  });

  var options = opt_options ? opt_options : {};

  /**
   * @private
   * @type {ol.events.ConditionType}
   */
  this.condition_ = options.condition ? options.condition :
          ol.events.condition.targetNotEditable;

  /**
   * @private
   * @type {number}
   */
  this.delta_ = options.delta ? options.delta : 1;

  /**
   * @private
   * @type {number}
   */
  this.duration_ = options.duration !== undefined ? options.duration : 100;

};
goog.inherits(ol.interaction.KeyboardZoom, ol.interaction.Interaction);


/**
 * Handles the {@link ol.MapBrowserEvent map browser event} if it was a
 * `KeyEvent`, and decides whether to zoom in or out (depending on whether the
 * key pressed was '+' or '-').
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} `false` to stop event propagation.
 * @this {ol.interaction.KeyboardZoom}
 * @api
 */
ol.interaction.KeyboardZoom.handleEvent = function(mapBrowserEvent) {
  var stopEvent = false;
  if (mapBrowserEvent.type == goog.events.KeyHandler.EventType.KEY) {
    var keyEvent = /** @type {goog.events.KeyEvent} */
        (mapBrowserEvent.browserEvent);
    var charCode = keyEvent.charCode;
    if (this.condition_(mapBrowserEvent) &&
        (charCode == '+'.charCodeAt(0) || charCode == '-'.charCodeAt(0))) {
      var map = mapBrowserEvent.map;
      var delta = (charCode == '+'.charCodeAt(0)) ? this.delta_ : -this.delta_;
      map.render();
      var view = map.getView();
      goog.asserts.assert(view, 'map must have view');
      ol.interaction.Interaction.zoomByDelta(
          map, view, delta, undefined, this.duration_);
      mapBrowserEvent.preventDefault();
      stopEvent = true;
    }
  }
  return !stopEvent;
};

goog.provide('ol.interaction.MouseWheelZoom');

goog.require('goog.asserts');
goog.require('goog.events.MouseWheelEvent');
goog.require('goog.events.MouseWheelHandler.EventType');
goog.require('ol');
goog.require('ol.Coordinate');
goog.require('ol.interaction.Interaction');
goog.require('ol.math');


/**
 * @classdesc
 * Allows the user to zoom the map by scrolling the mouse wheel.
 *
 * @constructor
 * @extends {ol.interaction.Interaction}
 * @param {olx.interaction.MouseWheelZoomOptions=} opt_options Options.
 * @api stable
 */
ol.interaction.MouseWheelZoom = function(opt_options) {

  goog.base(this, {
    handleEvent: ol.interaction.MouseWheelZoom.handleEvent
  });

  var options = opt_options || {};

  /**
   * @private
   * @type {number}
   */
  this.delta_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.duration_ = options.duration !== undefined ? options.duration : 250;

  /**
   * @private
   * @type {boolean}
   */
  this.useAnchor_ = options.useAnchor !== undefined ? options.useAnchor : true;

  /**
   * @private
   * @type {?ol.Coordinate}
   */
  this.lastAnchor_ = null;

  /**
   * @private
   * @type {number|undefined}
   */
  this.startTime_ = undefined;

  /**
   * @private
   * @type {number|undefined}
   */
  this.timeoutId_ = undefined;

};
goog.inherits(ol.interaction.MouseWheelZoom, ol.interaction.Interaction);


/**
 * Handles the {@link ol.MapBrowserEvent map browser event} (if it was a
 * mousewheel-event) and eventually zooms the map.
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} `false` to stop event propagation.
 * @this {ol.interaction.MouseWheelZoom}
 * @api
 */
ol.interaction.MouseWheelZoom.handleEvent = function(mapBrowserEvent) {
  var stopEvent = false;
  if (mapBrowserEvent.type ==
      goog.events.MouseWheelHandler.EventType.MOUSEWHEEL) {
    var map = mapBrowserEvent.map;
    var mouseWheelEvent = mapBrowserEvent.browserEvent;
    goog.asserts.assertInstanceof(mouseWheelEvent, goog.events.MouseWheelEvent,
        'mouseWheelEvent should be of type MouseWheelEvent');

    if (this.useAnchor_) {
      this.lastAnchor_ = mapBrowserEvent.coordinate;
    }

    this.delta_ += mouseWheelEvent.deltaY;

    if (this.startTime_ === undefined) {
      this.startTime_ = Date.now();
    }

    var duration = ol.MOUSEWHEELZOOM_TIMEOUT_DURATION;
    var timeLeft = Math.max(duration - (Date.now() - this.startTime_), 0);

    goog.global.clearTimeout(this.timeoutId_);
    this.timeoutId_ = goog.global.setTimeout(
        this.doZoom_.bind(this, map), timeLeft);

    mapBrowserEvent.preventDefault();
    stopEvent = true;
  }
  return !stopEvent;
};


/**
 * @private
 * @param {ol.Map} map Map.
 */
ol.interaction.MouseWheelZoom.prototype.doZoom_ = function(map) {
  var maxDelta = ol.MOUSEWHEELZOOM_MAXDELTA;
  var delta = ol.math.clamp(this.delta_, -maxDelta, maxDelta);

  var view = map.getView();
  goog.asserts.assert(view, 'map must have view');

  map.render();
  ol.interaction.Interaction.zoomByDelta(map, view, -delta, this.lastAnchor_,
      this.duration_);

  this.delta_ = 0;
  this.lastAnchor_ = null;
  this.startTime_ = undefined;
  this.timeoutId_ = undefined;
};


/**
 * Enable or disable using the mouse's location as an anchor when zooming
 * @param {boolean} useAnchor true to zoom to the mouse's location, false
 * to zoom to the center of the map
 * @api
 */
ol.interaction.MouseWheelZoom.prototype.setMouseAnchor = function(useAnchor) {
  this.useAnchor_ = useAnchor;
  if (!useAnchor) {
    this.lastAnchor_ = null;
  }
};

goog.provide('ol.interaction.PinchRotate');

goog.require('goog.asserts');
goog.require('goog.functions');
goog.require('goog.style');
goog.require('ol');
goog.require('ol.Coordinate');
goog.require('ol.ViewHint');
goog.require('ol.interaction.Interaction');
goog.require('ol.interaction.Pointer');


/**
 * @classdesc
 * Allows the user to rotate the map by twisting with two fingers
 * on a touch screen.
 *
 * @constructor
 * @extends {ol.interaction.Pointer}
 * @param {olx.interaction.PinchRotateOptions=} opt_options Options.
 * @api stable
 */
ol.interaction.PinchRotate = function(opt_options) {

  goog.base(this, {
    handleDownEvent: ol.interaction.PinchRotate.handleDownEvent_,
    handleDragEvent: ol.interaction.PinchRotate.handleDragEvent_,
    handleUpEvent: ol.interaction.PinchRotate.handleUpEvent_
  });

  var options = opt_options || {};

  /**
   * @private
   * @type {ol.Coordinate}
   */
  this.anchor_ = null;

  /**
   * @private
   * @type {number|undefined}
   */
  this.lastAngle_ = undefined;

  /**
   * @private
   * @type {boolean}
   */
  this.rotating_ = false;

  /**
   * @private
   * @type {number}
   */
  this.rotationDelta_ = 0.0;

  /**
   * @private
   * @type {number}
   */
  this.threshold_ = options.threshold !== undefined ? options.threshold : 0.3;

  /**
   * @private
   * @type {number}
   */
  this.duration_ = options.duration !== undefined ? options.duration : 250;

};
goog.inherits(ol.interaction.PinchRotate, ol.interaction.Pointer);


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @this {ol.interaction.PinchRotate}
 * @private
 */
ol.interaction.PinchRotate.handleDragEvent_ = function(mapBrowserEvent) {
  goog.asserts.assert(this.targetPointers.length >= 2,
      'length of this.targetPointers should be greater than or equal to 2');
  var rotationDelta = 0.0;

  var touch0 = this.targetPointers[0];
  var touch1 = this.targetPointers[1];

  // angle between touches
  var angle = Math.atan2(
      touch1.clientY - touch0.clientY,
      touch1.clientX - touch0.clientX);

  if (this.lastAngle_ !== undefined) {
    var delta = angle - this.lastAngle_;
    this.rotationDelta_ += delta;
    if (!this.rotating_ &&
        Math.abs(this.rotationDelta_) > this.threshold_) {
      this.rotating_ = true;
    }
    rotationDelta = delta;
  }
  this.lastAngle_ = angle;

  var map = mapBrowserEvent.map;

  // rotate anchor point.
  // FIXME: should be the intersection point between the lines:
  //     touch0,touch1 and previousTouch0,previousTouch1
  var viewportPosition = goog.style.getClientPosition(map.getViewport());
  var centroid =
      ol.interaction.Pointer.centroid(this.targetPointers);
  centroid[0] -= viewportPosition.x;
  centroid[1] -= viewportPosition.y;
  this.anchor_ = map.getCoordinateFromPixel(centroid);

  // rotate
  if (this.rotating_) {
    var view = map.getView();
    var rotation = view.getRotation();
    map.render();
    ol.interaction.Interaction.rotateWithoutConstraints(map, view,
        rotation + rotationDelta, this.anchor_);
  }
};


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boolean} Stop drag sequence?
 * @this {ol.interaction.PinchRotate}
 * @private
 */
ol.interaction.PinchRotate.handleUpEvent_ = function(mapBrowserEvent) {
  if (this.targetPointers.length < 2) {
    var map = mapBrowserEvent.map;
    var view = map.getView();
    view.setHint(ol.ViewHint.INTERACTING, -1);
    if (this.rotating_) {
      var rotation = view.getRotation();
      ol.interaction.Interaction.rotate(
          map, view, rotation, this.anchor_, this.duration_);
    }
    return false;
  } else {
    return true;
  }
};


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boolean} Start drag sequence?
 * @this {ol.interaction.PinchRotate}
 * @private
 */
ol.interaction.PinchRotate.handleDownEvent_ = function(mapBrowserEvent) {
  if (this.targetPointers.length >= 2) {
    var map = mapBrowserEvent.map;
    this.anchor_ = null;
    this.lastAngle_ = undefined;
    this.rotating_ = false;
    this.rotationDelta_ = 0.0;
    if (!this.handlingDownUpSequence) {
      map.getView().setHint(ol.ViewHint.INTERACTING, 1);
    }
    map.render();
    return true;
  } else {
    return false;
  }
};


/**
 * @inheritDoc
 */
ol.interaction.PinchRotate.prototype.shouldStopEvent = goog.functions.FALSE;

goog.provide('ol.interaction.PinchZoom');

goog.require('goog.asserts');
goog.require('goog.functions');
goog.require('goog.style');
goog.require('ol');
goog.require('ol.Coordinate');
goog.require('ol.ViewHint');
goog.require('ol.interaction.Interaction');
goog.require('ol.interaction.Pointer');


/**
 * @classdesc
 * Allows the user to zoom the map by pinching with two fingers
 * on a touch screen.
 *
 * @constructor
 * @extends {ol.interaction.Pointer}
 * @param {olx.interaction.PinchZoomOptions=} opt_options Options.
 * @api stable
 */
ol.interaction.PinchZoom = function(opt_options) {

  goog.base(this, {
    handleDownEvent: ol.interaction.PinchZoom.handleDownEvent_,
    handleDragEvent: ol.interaction.PinchZoom.handleDragEvent_,
    handleUpEvent: ol.interaction.PinchZoom.handleUpEvent_
  });

  var options = opt_options ? opt_options : {};

  /**
   * @private
   * @type {ol.Coordinate}
   */
  this.anchor_ = null;

  /**
   * @private
   * @type {number}
   */
  this.duration_ = options.duration !== undefined ? options.duration : 400;

  /**
   * @private
   * @type {number|undefined}
   */
  this.lastDistance_ = undefined;

  /**
   * @private
   * @type {number}
   */
  this.lastScaleDelta_ = 1;

};
goog.inherits(ol.interaction.PinchZoom, ol.interaction.Pointer);


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @this {ol.interaction.PinchZoom}
 * @private
 */
ol.interaction.PinchZoom.handleDragEvent_ = function(mapBrowserEvent) {
  goog.asserts.assert(this.targetPointers.length >= 2,
      'length of this.targetPointers should be 2 or more');
  var scaleDelta = 1.0;

  var touch0 = this.targetPointers[0];
  var touch1 = this.targetPointers[1];
  var dx = touch0.clientX - touch1.clientX;
  var dy = touch0.clientY - touch1.clientY;

  // distance between touches
  var distance = Math.sqrt(dx * dx + dy * dy);

  if (this.lastDistance_ !== undefined) {
    scaleDelta = this.lastDistance_ / distance;
  }
  this.lastDistance_ = distance;
  if (scaleDelta != 1.0) {
    this.lastScaleDelta_ = scaleDelta;
  }

  var map = mapBrowserEvent.map;
  var view = map.getView();
  var resolution = view.getResolution();

  // scale anchor point.
  var viewportPosition = goog.style.getClientPosition(map.getViewport());
  var centroid =
      ol.interaction.Pointer.centroid(this.targetPointers);
  centroid[0] -= viewportPosition.x;
  centroid[1] -= viewportPosition.y;
  this.anchor_ = map.getCoordinateFromPixel(centroid);

  // scale, bypass the resolution constraint
  map.render();
  ol.interaction.Interaction.zoomWithoutConstraints(
      map, view, resolution * scaleDelta, this.anchor_);

};


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boolean} Stop drag sequence?
 * @this {ol.interaction.PinchZoom}
 * @private
 */
ol.interaction.PinchZoom.handleUpEvent_ = function(mapBrowserEvent) {
  if (this.targetPointers.length < 2) {
    var map = mapBrowserEvent.map;
    var view = map.getView();
    view.setHint(ol.ViewHint.INTERACTING, -1);
    var resolution = view.getResolution();
    // Zoom to final resolution, with an animation, and provide a
    // direction not to zoom out/in if user was pinching in/out.
    // Direction is > 0 if pinching out, and < 0 if pinching in.
    var direction = this.lastScaleDelta_ - 1;
    ol.interaction.Interaction.zoom(map, view, resolution,
        this.anchor_, this.duration_, direction);
    return false;
  } else {
    return true;
  }
};


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boolean} Start drag sequence?
 * @this {ol.interaction.PinchZoom}
 * @private
 */
ol.interaction.PinchZoom.handleDownEvent_ = function(mapBrowserEvent) {
  if (this.targetPointers.length >= 2) {
    var map = mapBrowserEvent.map;
    this.anchor_ = null;
    this.lastDistance_ = undefined;
    this.lastScaleDelta_ = 1;
    if (!this.handlingDownUpSequence) {
      map.getView().setHint(ol.ViewHint.INTERACTING, 1);
    }
    map.render();
    return true;
  } else {
    return false;
  }
};


/**
 * @inheritDoc
 */
ol.interaction.PinchZoom.prototype.shouldStopEvent = goog.functions.FALSE;

goog.provide('ol.interaction');

goog.require('ol');
goog.require('ol.Collection');
goog.require('ol.Kinetic');
goog.require('ol.interaction.DoubleClickZoom');
goog.require('ol.interaction.DragPan');
goog.require('ol.interaction.DragRotate');
goog.require('ol.interaction.DragZoom');
goog.require('ol.interaction.KeyboardPan');
goog.require('ol.interaction.KeyboardZoom');
goog.require('ol.interaction.MouseWheelZoom');
goog.require('ol.interaction.PinchRotate');
goog.require('ol.interaction.PinchZoom');


/**
 * Set of interactions included in maps by default. Specific interactions can be
 * excluded by setting the appropriate option to false in the constructor
 * options, but the order of the interactions is fixed.  If you want to specify
 * a different order for interactions, you will need to create your own
 * {@link ol.interaction.Interaction} instances and insert them into a
 * {@link ol.Collection} in the order you want before creating your
 * {@link ol.Map} instance. The default set of interactions, in sequence, is:
 * * {@link ol.interaction.DragRotate}
 * * {@link ol.interaction.DoubleClickZoom}
 * * {@link ol.interaction.DragPan}
 * * {@link ol.interaction.PinchRotate}
 * * {@link ol.interaction.PinchZoom}
 * * {@link ol.interaction.KeyboardPan}
 * * {@link ol.interaction.KeyboardZoom}
 * * {@link ol.interaction.MouseWheelZoom}
 * * {@link ol.interaction.DragZoom}
 *
 * @param {olx.interaction.DefaultsOptions=} opt_options Defaults options.
 * @return {ol.Collection.<ol.interaction.Interaction>} A collection of
 * interactions to be used with the ol.Map constructor's interactions option.
 * @api stable
 */
ol.interaction.defaults = function(opt_options) {

  var options = opt_options ? opt_options : {};

  var interactions = new ol.Collection();

  var kinetic = new ol.Kinetic(-0.005, 0.05, 100);

  var altShiftDragRotate = options.altShiftDragRotate !== undefined ?
      options.altShiftDragRotate : true;
  if (altShiftDragRotate) {
    interactions.push(new ol.interaction.DragRotate());
  }

  var doubleClickZoom = options.doubleClickZoom !== undefined ?
      options.doubleClickZoom : true;
  if (doubleClickZoom) {
    interactions.push(new ol.interaction.DoubleClickZoom({
      delta: options.zoomDelta,
      duration: options.zoomDuration
    }));
  }

  var dragPan = options.dragPan !== undefined ? options.dragPan : true;
  if (dragPan) {
    interactions.push(new ol.interaction.DragPan({
      kinetic: kinetic
    }));
  }

  var pinchRotate = options.pinchRotate !== undefined ? options.pinchRotate :
      true;
  if (pinchRotate) {
    interactions.push(new ol.interaction.PinchRotate());
  }

  var pinchZoom = options.pinchZoom !== undefined ? options.pinchZoom : true;
  if (pinchZoom) {
    interactions.push(new ol.interaction.PinchZoom({
      duration: options.zoomDuration
    }));
  }

  var keyboard = options.keyboard !== undefined ? options.keyboard : true;
  if (keyboard) {
    interactions.push(new ol.interaction.KeyboardPan());
    interactions.push(new ol.interaction.KeyboardZoom({
      delta: options.zoomDelta,
      duration: options.zoomDuration
    }));
  }

  var mouseWheelZoom = options.mouseWheelZoom !== undefined ?
      options.mouseWheelZoom : true;
  if (mouseWheelZoom) {
    interactions.push(new ol.interaction.MouseWheelZoom({
      duration: options.zoomDuration
    }));
  }

  var shiftDragZoom = options.shiftDragZoom !== undefined ?
      options.shiftDragZoom : true;
  if (shiftDragZoom) {
    interactions.push(new ol.interaction.DragZoom({
      duration: options.zoomDuration
    }));
  }

  return interactions;

};

goog.provide('ol.layer.Group');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.object');
goog.require('ol.Collection');
goog.require('ol.CollectionEvent');
goog.require('ol.CollectionEventType');
goog.require('ol.Object');
goog.require('ol.ObjectEventType');
goog.require('ol.extent');
goog.require('ol.layer.Base');
goog.require('ol.source.State');


/**
 * @enum {string}
 */
ol.layer.GroupProperty = {
  LAYERS: 'layers'
};


/**
 * @classdesc
 * A {@link ol.Collection} of layers that are handled together.
 *
 * A generic `change` event is triggered when the group/Collection changes.
 *
 * @constructor
 * @extends {ol.layer.Base}
 * @param {olx.layer.GroupOptions=} opt_options Layer options.
 * @api stable
 */
ol.layer.Group = function(opt_options) {

  var options = opt_options || {};
  var baseOptions = /** @type {olx.layer.GroupOptions} */
      (goog.object.clone(options));
  delete baseOptions.layers;

  var layers = options.layers;

  goog.base(this, baseOptions);

  /**
   * @private
   * @type {Array.<goog.events.Key>}
   */
  this.layersListenerKeys_ = [];

  /**
   * @private
   * @type {Object.<string, Array.<goog.events.Key>>}
   */
  this.listenerKeys_ = {};

  goog.events.listen(this,
      ol.Object.getChangeEventType(ol.layer.GroupProperty.LAYERS),
      this.handleLayersChanged_, false, this);

  if (layers) {
    if (goog.isArray(layers)) {
      layers = new ol.Collection(layers.slice());
    } else {
      goog.asserts.assertInstanceof(layers, ol.Collection,
          'layers should be an ol.Collection');
      layers = layers;
    }
  } else {
    layers = new ol.Collection();
  }

  this.setLayers(layers);

};
goog.inherits(ol.layer.Group, ol.layer.Base);


/**
 * @private
 */
ol.layer.Group.prototype.handleLayerChange_ = function() {
  if (this.getVisible()) {
    this.changed();
  }
};


/**
 * @param {goog.events.Event} event Event.
 * @private
 */
ol.layer.Group.prototype.handleLayersChanged_ = function(event) {
  this.layersListenerKeys_.forEach(goog.events.unlistenByKey);
  this.layersListenerKeys_.length = 0;

  var layers = this.getLayers();
  this.layersListenerKeys_.push(
      goog.events.listen(layers, ol.CollectionEventType.ADD,
          this.handleLayersAdd_, false, this),
      goog.events.listen(layers, ol.CollectionEventType.REMOVE,
          this.handleLayersRemove_, false, this));

  goog.object.forEach(this.listenerKeys_, function(keys) {
    keys.forEach(goog.events.unlistenByKey);
  });
  goog.object.clear(this.listenerKeys_);

  var layersArray = layers.getArray();
  var i, ii, layer;
  for (i = 0, ii = layersArray.length; i < ii; i++) {
    layer = layersArray[i];
    this.listenerKeys_[goog.getUid(layer).toString()] = [
      goog.events.listen(layer, ol.ObjectEventType.PROPERTYCHANGE,
          this.handleLayerChange_, false, this),
      goog.events.listen(layer, goog.events.EventType.CHANGE,
          this.handleLayerChange_, false, this)
    ];
  }

  this.changed();
};


/**
 * @param {ol.CollectionEvent} collectionEvent Collection event.
 * @private
 */
ol.layer.Group.prototype.handleLayersAdd_ = function(collectionEvent) {
  var layer = /** @type {ol.layer.Base} */ (collectionEvent.element);
  var key = goog.getUid(layer).toString();
  goog.asserts.assert(!(key in this.listenerKeys_),
      'listeners already registered');
  this.listenerKeys_[key] = [
    goog.events.listen(layer, ol.ObjectEventType.PROPERTYCHANGE,
        this.handleLayerChange_, false, this),
    goog.events.listen(layer, goog.events.EventType.CHANGE,
        this.handleLayerChange_, false, this)
  ];
  this.changed();
};


/**
 * @param {ol.CollectionEvent} collectionEvent Collection event.
 * @private
 */
ol.layer.Group.prototype.handleLayersRemove_ = function(collectionEvent) {
  var layer = /** @type {ol.layer.Base} */ (collectionEvent.element);
  var key = goog.getUid(layer).toString();
  goog.asserts.assert(key in this.listenerKeys_, 'no listeners to unregister');
  this.listenerKeys_[key].forEach(goog.events.unlistenByKey);
  delete this.listenerKeys_[key];
  this.changed();
};


/**
 * Returns the {@link ol.Collection collection} of {@link ol.layer.Layer layers}
 * in this group.
 * @return {!ol.Collection.<ol.layer.Base>} Collection of
 *   {@link ol.layer.Base layers} that are part of this group.
 * @observable
 * @api stable
 */
ol.layer.Group.prototype.getLayers = function() {
  return /** @type {!ol.Collection.<ol.layer.Base>} */ (this.get(
      ol.layer.GroupProperty.LAYERS));
};


/**
 * Set the {@link ol.Collection collection} of {@link ol.layer.Layer layers}
 * in this group.
 * @param {!ol.Collection.<ol.layer.Base>} layers Collection of
 *   {@link ol.layer.Base layers} that are part of this group.
 * @observable
 * @api stable
 */
ol.layer.Group.prototype.setLayers = function(layers) {
  this.set(ol.layer.GroupProperty.LAYERS, layers);
};


/**
 * @inheritDoc
 */
ol.layer.Group.prototype.getLayersArray = function(opt_array) {
  var array = opt_array !== undefined ? opt_array : [];
  this.getLayers().forEach(function(layer) {
    layer.getLayersArray(array);
  });
  return array;
};


/**
 * @inheritDoc
 */
ol.layer.Group.prototype.getLayerStatesArray = function(opt_states) {
  var states = opt_states !== undefined ? opt_states : [];

  var pos = states.length;

  this.getLayers().forEach(function(layer) {
    layer.getLayerStatesArray(states);
  });

  var ownLayerState = this.getLayerState();
  var i, ii, layerState;
  for (i = pos, ii = states.length; i < ii; i++) {
    layerState = states[i];
    layerState.opacity *= ownLayerState.opacity;
    layerState.visible = layerState.visible && ownLayerState.visible;
    layerState.maxResolution = Math.min(
        layerState.maxResolution, ownLayerState.maxResolution);
    layerState.minResolution = Math.max(
        layerState.minResolution, ownLayerState.minResolution);
    if (ownLayerState.extent !== undefined) {
      if (layerState.extent !== undefined) {
        layerState.extent = ol.extent.getIntersection(
            layerState.extent, ownLayerState.extent);
      } else {
        layerState.extent = ownLayerState.extent;
      }
    }
  }

  return states;
};


/**
 * @inheritDoc
 */
ol.layer.Group.prototype.getSourceState = function() {
  return ol.source.State.READY;
};

goog.provide('ol.proj.EPSG3857');

goog.require('goog.asserts');
goog.require('ol.math');
goog.require('ol.proj');
goog.require('ol.proj.Projection');
goog.require('ol.proj.Units');


/**
 * @classdesc
 * Projection object for web/spherical Mercator (EPSG:3857).
 *
 * @constructor
 * @extends {ol.proj.Projection}
 * @param {string} code Code.
 * @private
 */
ol.proj.EPSG3857_ = function(code) {
  goog.base(this, {
    code: code,
    units: ol.proj.Units.METERS,
    extent: ol.proj.EPSG3857.EXTENT,
    global: true,
    worldExtent: ol.proj.EPSG3857.WORLD_EXTENT
  });
};
goog.inherits(ol.proj.EPSG3857_, ol.proj.Projection);


/**
 * @inheritDoc
 */
ol.proj.EPSG3857_.prototype.getPointResolution = function(resolution, point) {
  return resolution / ol.math.cosh(point[1] / ol.proj.EPSG3857.RADIUS);
};


/**
 * @const
 * @type {number}
 */
ol.proj.EPSG3857.RADIUS = 6378137;


/**
 * @const
 * @type {number}
 */
ol.proj.EPSG3857.HALF_SIZE = Math.PI * ol.proj.EPSG3857.RADIUS;


/**
 * @const
 * @type {ol.Extent}
 */
ol.proj.EPSG3857.EXTENT = [
  -ol.proj.EPSG3857.HALF_SIZE, -ol.proj.EPSG3857.HALF_SIZE,
  ol.proj.EPSG3857.HALF_SIZE, ol.proj.EPSG3857.HALF_SIZE
];


/**
 * @const
 * @type {ol.Extent}
 */
ol.proj.EPSG3857.WORLD_EXTENT = [-180, -85, 180, 85];


/**
 * Lists several projection codes with the same meaning as EPSG:3857.
 *
 * @type {Array.<string>}
 */
ol.proj.EPSG3857.CODES = [
  'EPSG:3857',
  'EPSG:102100',
  'EPSG:102113',
  'EPSG:900913',
  'urn:ogc:def:crs:EPSG:6.18:3:3857',
  'urn:ogc:def:crs:EPSG::3857',
  'http://www.opengis.net/gml/srs/epsg.xml#3857'
];


/**
 * Projections equal to EPSG:3857.
 *
 * @const
 * @type {Array.<ol.proj.Projection>}
 */
ol.proj.EPSG3857.PROJECTIONS = ol.proj.EPSG3857.CODES.map(function(code) {
  return new ol.proj.EPSG3857_(code);
});


/**
 * Transformation from EPSG:4326 to EPSG:3857.
 *
 * @param {Array.<number>} input Input array of coordinate values.
 * @param {Array.<number>=} opt_output Output array of coordinate values.
 * @param {number=} opt_dimension Dimension (default is `2`).
 * @return {Array.<number>} Output array of coordinate values.
 */
ol.proj.EPSG3857.fromEPSG4326 = function(input, opt_output, opt_dimension) {
  var length = input.length,
      dimension = opt_dimension > 1 ? opt_dimension : 2,
      output = opt_output;
  if (output === undefined) {
    if (dimension > 2) {
      // preserve values beyond second dimension
      output = input.slice();
    } else {
      output = new Array(length);
    }
  }
  goog.asserts.assert(output.length % dimension === 0,
      'modulus of output.length with dimension should be 0');
  for (var i = 0; i < length; i += dimension) {
    output[i] = ol.proj.EPSG3857.RADIUS * Math.PI * input[i] / 180;
    output[i + 1] = ol.proj.EPSG3857.RADIUS *
        Math.log(Math.tan(Math.PI * (input[i + 1] + 90) / 360));
  }
  return output;
};


/**
 * Transformation from EPSG:3857 to EPSG:4326.
 *
 * @param {Array.<number>} input Input array of coordinate values.
 * @param {Array.<number>=} opt_output Output array of coordinate values.
 * @param {number=} opt_dimension Dimension (default is `2`).
 * @return {Array.<number>} Output array of coordinate values.
 */
ol.proj.EPSG3857.toEPSG4326 = function(input, opt_output, opt_dimension) {
  var length = input.length,
      dimension = opt_dimension > 1 ? opt_dimension : 2,
      output = opt_output;
  if (output === undefined) {
    if (dimension > 2) {
      // preserve values beyond second dimension
      output = input.slice();
    } else {
      output = new Array(length);
    }
  }
  goog.asserts.assert(output.length % dimension === 0,
      'modulus of output.length with dimension should be 0');
  for (var i = 0; i < length; i += dimension) {
    output[i] = 180 * input[i] / (ol.proj.EPSG3857.RADIUS * Math.PI);
    output[i + 1] = 360 * Math.atan(
        Math.exp(input[i + 1] / ol.proj.EPSG3857.RADIUS)) / Math.PI - 90;
  }
  return output;
};

goog.provide('ol.sphere.WGS84');

goog.require('ol.Sphere');


/**
 * A sphere with radius equal to the semi-major axis of the WGS84 ellipsoid.
 * @const
 * @type {ol.Sphere}
 */
ol.sphere.WGS84 = new ol.Sphere(6378137);

goog.provide('ol.proj.EPSG4326');

goog.require('ol.proj');
goog.require('ol.proj.Projection');
goog.require('ol.proj.Units');
goog.require('ol.sphere.WGS84');


/**
 * @classdesc
 * Projection object for WGS84 geographic coordinates (EPSG:4326).
 *
 * Note that OpenLayers does not strictly comply with the EPSG definition.
 * The EPSG registry defines 4326 as a CRS for Latitude,Longitude (y,x).
 * OpenLayers treats EPSG:4326 as a pseudo-projection, with x,y coordinates.
 *
 * @constructor
 * @extends {ol.proj.Projection}
 * @param {string} code Code.
 * @param {string=} opt_axisOrientation Axis orientation.
 * @private
 */
ol.proj.EPSG4326_ = function(code, opt_axisOrientation) {
  goog.base(this, {
    code: code,
    units: ol.proj.Units.DEGREES,
    extent: ol.proj.EPSG4326.EXTENT,
    axisOrientation: opt_axisOrientation,
    global: true,
    metersPerUnit: ol.proj.EPSG4326.METERS_PER_UNIT,
    worldExtent: ol.proj.EPSG4326.EXTENT
  });
};
goog.inherits(ol.proj.EPSG4326_, ol.proj.Projection);


/**
 * @inheritDoc
 */
ol.proj.EPSG4326_.prototype.getPointResolution = function(resolution, point) {
  return resolution;
};


/**
 * Extent of the EPSG:4326 projection which is the whole world.
 *
 * @const
 * @type {ol.Extent}
 */
ol.proj.EPSG4326.EXTENT = [-180, -90, 180, 90];


/**
 * @const
 * @type {number}
 */
ol.proj.EPSG4326.METERS_PER_UNIT = Math.PI * ol.sphere.WGS84.radius / 180;


/**
 * Projections equal to EPSG:4326.
 *
 * @const
 * @type {Array.<ol.proj.Projection>}
 */
ol.proj.EPSG4326.PROJECTIONS = [
  new ol.proj.EPSG4326_('CRS:84'),
  new ol.proj.EPSG4326_('EPSG:4326', 'neu'),
  new ol.proj.EPSG4326_('urn:ogc:def:crs:EPSG::4326', 'neu'),
  new ol.proj.EPSG4326_('urn:ogc:def:crs:EPSG:6.6:4326', 'neu'),
  new ol.proj.EPSG4326_('urn:ogc:def:crs:OGC:1.3:CRS84'),
  new ol.proj.EPSG4326_('urn:ogc:def:crs:OGC:2:84'),
  new ol.proj.EPSG4326_('http://www.opengis.net/gml/srs/epsg.xml#4326', 'neu'),
  new ol.proj.EPSG4326_('urn:x-ogc:def:crs:EPSG:4326', 'neu')
];

goog.provide('ol.proj.common');

goog.require('ol.proj');
goog.require('ol.proj.EPSG3857');
goog.require('ol.proj.EPSG4326');


/**
 * FIXME empty description for jsdoc
 * @api
 */
ol.proj.common.add = function() {
  // Add transformations that don't alter coordinates to convert within set of
  // projections with equal meaning.
  ol.proj.addEquivalentProjections(ol.proj.EPSG3857.PROJECTIONS);
  ol.proj.addEquivalentProjections(ol.proj.EPSG4326.PROJECTIONS);
  // Add transformations to convert EPSG:4326 like coordinates to EPSG:3857 like
  // coordinates and back.
  ol.proj.addEquivalentTransforms(
      ol.proj.EPSG4326.PROJECTIONS,
      ol.proj.EPSG3857.PROJECTIONS,
      ol.proj.EPSG3857.fromEPSG4326,
      ol.proj.EPSG3857.toEPSG4326);
};

goog.provide('ol.layer.Image');

goog.require('ol.layer.Layer');


/**
 * @classdesc
 * Server-rendered images that are available for arbitrary extents and
 * resolutions.
 * Note that any property set in the options is set as a {@link ol.Object}
 * property on the layer object; for example, setting `title: 'My Title'` in the
 * options means that `title` is observable, and has get/set accessors.
 *
 * @constructor
 * @extends {ol.layer.Layer}
 * @fires ol.render.Event
 * @param {olx.layer.ImageOptions=} opt_options Layer options.
 * @api stable
 */
ol.layer.Image = function(opt_options) {
  var options = opt_options ? opt_options : {};
  goog.base(this,  /** @type {olx.layer.LayerOptions} */ (options));
};
goog.inherits(ol.layer.Image, ol.layer.Layer);


/**
 * Return the associated {@link ol.source.Image source} of the image layer.
 * @function
 * @return {ol.source.Image} Source.
 * @api stable
 */
ol.layer.Image.prototype.getSource;

goog.provide('ol.layer.Tile');

goog.require('goog.object');
goog.require('ol');
goog.require('ol.layer.Layer');


/**
 * @enum {string}
 */
ol.layer.TileProperty = {
  PRELOAD: 'preload',
  USE_INTERIM_TILES_ON_ERROR: 'useInterimTilesOnError'
};


/**
 * @classdesc
 * For layer sources that provide pre-rendered, tiled images in grids that are
 * organized by zoom levels for specific resolutions.
 * Note that any property set in the options is set as a {@link ol.Object}
 * property on the layer object; for example, setting `title: 'My Title'` in the
 * options means that `title` is observable, and has get/set accessors.
 *
 * @constructor
 * @extends {ol.layer.Layer}
 * @fires ol.render.Event
 * @param {olx.layer.TileOptions=} opt_options Tile layer options.
 * @api stable
 */
ol.layer.Tile = function(opt_options) {
  var options = opt_options ? opt_options : {};

  var baseOptions = goog.object.clone(options);

  delete baseOptions.preload;
  delete baseOptions.useInterimTilesOnError;
  goog.base(this,  /** @type {olx.layer.LayerOptions} */ (baseOptions));

  this.setPreload(options.preload !== undefined ? options.preload : 0);
  this.setUseInterimTilesOnError(options.useInterimTilesOnError !== undefined ?
      options.useInterimTilesOnError : true);
};
goog.inherits(ol.layer.Tile, ol.layer.Layer);


/**
 * Return the level as number to which we will preload tiles up to.
 * @return {number} The level to preload tiles up to.
 * @observable
 * @api
 */
ol.layer.Tile.prototype.getPreload = function() {
  return /** @type {number} */ (this.get(ol.layer.TileProperty.PRELOAD));
};


/**
 * Return the associated {@link ol.source.Tile tilesource} of the layer.
 * @function
 * @return {ol.source.Tile} Source.
 * @api stable
 */
ol.layer.Tile.prototype.getSource;


/**
 * Set the level as number to which we will preload tiles up to.
 * @param {number} preload The level to preload tiles up to.
 * @observable
 * @api
 */
ol.layer.Tile.prototype.setPreload = function(preload) {
  this.set(ol.layer.TileProperty.PRELOAD, preload);
};


/**
 * Whether we use interim tiles on error.
 * @return {boolean} Use interim tiles on error.
 * @observable
 * @api
 */
ol.layer.Tile.prototype.getUseInterimTilesOnError = function() {
  return /** @type {boolean} */ (
      this.get(ol.layer.TileProperty.USE_INTERIM_TILES_ON_ERROR));
};


/**
 * Set whether we use interim tiles on error.
 * @param {boolean} useInterimTilesOnError Use interim tiles on error.
 * @observable
 * @api
 */
ol.layer.Tile.prototype.setUseInterimTilesOnError = function(useInterimTilesOnError) {
  this.set(
      ol.layer.TileProperty.USE_INTERIM_TILES_ON_ERROR, useInterimTilesOnError);
};

goog.provide('ol.render.canvas');


/**
 * @typedef {{fillStyle: string}}
 */
ol.render.canvas.FillState;


/**
 * @typedef {{lineCap: string,
 *            lineDash: Array.<number>,
 *            lineJoin: string,
 *            lineWidth: number,
 *            miterLimit: number,
 *            strokeStyle: string}}
 */
ol.render.canvas.StrokeState;


/**
 * @typedef {{font: string,
 *            textAlign: string,
 *            textBaseline: string}}
 */
ol.render.canvas.TextState;


/**
 * @const
 * @type {string}
 */
ol.render.canvas.defaultFont = '10px sans-serif';


/**
 * @const
 * @type {ol.Color}
 */
ol.render.canvas.defaultFillStyle = [0, 0, 0, 1];


/**
 * @const
 * @type {string}
 */
ol.render.canvas.defaultLineCap = 'round';


/**
 * @const
 * @type {Array.<number>}
 */
ol.render.canvas.defaultLineDash = [];


/**
 * @const
 * @type {string}
 */
ol.render.canvas.defaultLineJoin = 'round';


/**
 * @const
 * @type {number}
 */
ol.render.canvas.defaultMiterLimit = 10;


/**
 * @const
 * @type {ol.Color}
 */
ol.render.canvas.defaultStrokeStyle = [0, 0, 0, 1];


/**
 * @const
 * @type {string}
 */
ol.render.canvas.defaultTextAlign = 'center';


/**
 * @const
 * @type {string}
 */
ol.render.canvas.defaultTextBaseline = 'middle';


/**
 * @const
 * @type {number}
 */
ol.render.canvas.defaultLineWidth = 1;

goog.provide('ol.style.Fill');

goog.require('ol.color');


/**
 * @classdesc
 * Set fill style for vector features.
 *
 * @constructor
 * @param {olx.style.FillOptions=} opt_options Options.
 * @api
 */
ol.style.Fill = function(opt_options) {

  var options = opt_options || {};

  /**
   * @private
   * @type {ol.Color|string}
   */
  this.color_ = options.color !== undefined ? options.color : null;

  /**
   * @private
   * @type {string|undefined}
   */
  this.checksum_ = undefined;
};


/**
 * Get the fill color.
 * @return {ol.Color|string} Color.
 * @api
 */
ol.style.Fill.prototype.getColor = function() {
  return this.color_;
};


/**
 * Set the color.
 *
 * @param {ol.Color|string} color Color.
 * @api
 */
ol.style.Fill.prototype.setColor = function(color) {
  this.color_ = color;
  this.checksum_ = undefined;
};


/**
 * @return {string} The checksum.
 */
ol.style.Fill.prototype.getChecksum = function() {
  if (this.checksum_ === undefined) {
    this.checksum_ = 'f' + (this.color_ ?
        ol.color.asString(this.color_) : '-');
  }

  return this.checksum_;
};

// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Namespace with crypto related helper functions.
 */

goog.provide('goog.crypt');

goog.require('goog.array');
goog.require('goog.asserts');


/**
 * Turns a string into an array of bytes; a "byte" being a JS number in the
 * range 0-255.
 * @param {string} str String value to arrify.
 * @return {!Array<number>} Array of numbers corresponding to the
 *     UCS character codes of each character in str.
 */
goog.crypt.stringToByteArray = function(str) {
  var output = [], p = 0;
  for (var i = 0; i < str.length; i++) {
    var c = str.charCodeAt(i);
    while (c > 0xff) {
      output[p++] = c & 0xff;
      c >>= 8;
    }
    output[p++] = c;
  }
  return output;
};


/**
 * Turns an array of numbers into the string given by the concatenation of the
 * characters to which the numbers correspond.
 * @param {Array<number>} bytes Array of numbers representing characters.
 * @return {string} Stringification of the array.
 */
goog.crypt.byteArrayToString = function(bytes) {
  var CHUNK_SIZE = 8192;

  // Special-case the simple case for speed's sake.
  if (bytes.length <= CHUNK_SIZE) {
    return String.fromCharCode.apply(null, bytes);
  }

  // The remaining logic splits conversion by chunks since
  // Function#apply() has a maximum parameter count.
  // See discussion: http://goo.gl/LrWmZ9

  var str = '';
  for (var i = 0; i < bytes.length; i += CHUNK_SIZE) {
    var chunk = goog.array.slice(bytes, i, i + CHUNK_SIZE);
    str += String.fromCharCode.apply(null, chunk);
  }
  return str;
};


/**
 * Turns an array of numbers into the hex string given by the concatenation of
 * the hex values to which the numbers correspond.
 * @param {Uint8Array|Array<number>} array Array of numbers representing
 *     characters.
 * @return {string} Hex string.
 */
goog.crypt.byteArrayToHex = function(array) {
  return goog.array.map(array, function(numByte) {
    var hexByte = numByte.toString(16);
    return hexByte.length > 1 ? hexByte : '0' + hexByte;
  }).join('');
};


/**
 * Converts a hex string into an integer array.
 * @param {string} hexString Hex string of 16-bit integers (two characters
 *     per integer).
 * @return {!Array<number>} Array of {0,255} integers for the given string.
 */
goog.crypt.hexToByteArray = function(hexString) {
  goog.asserts.assert(hexString.length % 2 == 0,
                      'Key string length must be multiple of 2');
  var arr = [];
  for (var i = 0; i < hexString.length; i += 2) {
    arr.push(parseInt(hexString.substring(i, i + 2), 16));
  }
  return arr;
};


/**
 * Converts a JS string to a UTF-8 "byte" array.
 * @param {string} str 16-bit unicode string.
 * @return {!Array<number>} UTF-8 byte array.
 */
goog.crypt.stringToUtf8ByteArray = function(str) {
  // TODO(user): Use native implementations if/when available
  var out = [], p = 0;
  for (var i = 0; i < str.length; i++) {
    var c = str.charCodeAt(i);
    if (c < 128) {
      out[p++] = c;
    } else if (c < 2048) {
      out[p++] = (c >> 6) | 192;
      out[p++] = (c & 63) | 128;
    } else {
      out[p++] = (c >> 12) | 224;
      out[p++] = ((c >> 6) & 63) | 128;
      out[p++] = (c & 63) | 128;
    }
  }
  return out;
};


/**
 * Converts a UTF-8 byte array to JavaScript's 16-bit Unicode.
 * @param {Uint8Array|Array<number>} bytes UTF-8 byte array.
 * @return {string} 16-bit Unicode string.
 */
goog.crypt.utf8ByteArrayToString = function(bytes) {
  // TODO(user): Use native implementations if/when available
  var out = [], pos = 0, c = 0;
  while (pos < bytes.length) {
    var c1 = bytes[pos++];
    if (c1 < 128) {
      out[c++] = String.fromCharCode(c1);
    } else if (c1 > 191 && c1 < 224) {
      var c2 = bytes[pos++];
      out[c++] = String.fromCharCode((c1 & 31) << 6 | c2 & 63);
    } else {
      var c2 = bytes[pos++];
      var c3 = bytes[pos++];
      out[c++] = String.fromCharCode(
          (c1 & 15) << 12 | (c2 & 63) << 6 | c3 & 63);
    }
  }
  return out.join('');
};


/**
 * XOR two byte arrays.
 * @param {!ArrayBufferView|!Array<number>} bytes1 Byte array 1.
 * @param {!ArrayBufferView|!Array<number>} bytes2 Byte array 2.
 * @return {!Array<number>} Resulting XOR of the two byte arrays.
 */
goog.crypt.xorByteArray = function(bytes1, bytes2) {
  goog.asserts.assert(
      bytes1.length == bytes2.length,
      'XOR array lengths must match');

  var result = [];
  for (var i = 0; i < bytes1.length; i++) {
    result.push(bytes1[i] ^ bytes2[i]);
  }
  return result;
};

// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Abstract cryptographic hash interface.
 *
 * See goog.crypt.Sha1 and goog.crypt.Md5 for sample implementations.
 *
 */

goog.provide('goog.crypt.Hash');



/**
 * Create a cryptographic hash instance.
 *
 * @constructor
 * @struct
 */
goog.crypt.Hash = function() {
  /**
   * The block size for the hasher.
   * @type {number}
   */
  this.blockSize = -1;
};


/**
 * Resets the internal accumulator.
 */
goog.crypt.Hash.prototype.reset = goog.abstractMethod;


/**
 * Adds a byte array (array with values in [0-255] range) or a string (might
 * only contain 8-bit, i.e., Latin1 characters) to the internal accumulator.
 *
 * Many hash functions operate on blocks of data and implement optimizations
 * when a full chunk of data is readily available. Hence it is often preferable
 * to provide large chunks of data (a kilobyte or more) than to repeatedly
 * call the update method with few tens of bytes. If this is not possible, or
 * not feasible, it might be good to provide data in multiplies of hash block
 * size (often 64 bytes). Please see the implementation and performance tests
 * of your favourite hash.
 *
 * @param {Array<number>|Uint8Array|string} bytes Data used for the update.
 * @param {number=} opt_length Number of bytes to use.
 */
goog.crypt.Hash.prototype.update = goog.abstractMethod;


/**
 * @return {!Array<number>} The finalized hash computed
 *     from the internal accumulator.
 */
goog.crypt.Hash.prototype.digest = goog.abstractMethod;

// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview MD5 cryptographic hash.
 * Implementation of http://tools.ietf.org/html/rfc1321 with common
 * optimizations and tweaks (see http://en.wikipedia.org/wiki/MD5).
 *
 * Usage:
 *   var md5 = new goog.crypt.Md5();
 *   md5.update(bytes);
 *   var hash = md5.digest();
 *
 * Performance:
 *   Chrome 23              ~680 Mbit/s
 *   Chrome 13 (in a VM)    ~250 Mbit/s
 *   Firefox 6.0 (in a VM)  ~100 Mbit/s
 *   IE9 (in a VM)           ~27 Mbit/s
 *   Firefox 3.6             ~15 Mbit/s
 *   IE8 (in a VM)           ~13 Mbit/s
 *
 */

goog.provide('goog.crypt.Md5');

goog.require('goog.crypt.Hash');



/**
 * MD5 cryptographic hash constructor.
 * @constructor
 * @extends {goog.crypt.Hash}
 * @final
 * @struct
 */
goog.crypt.Md5 = function() {
  goog.crypt.Md5.base(this, 'constructor');

  this.blockSize = 512 / 8;

  /**
   * Holds the current values of accumulated A-D variables (MD buffer).
   * @type {!Array<number>}
   * @private
   */
  this.chain_ = new Array(4);

  /**
   * A buffer holding the data until the whole block can be processed.
   * @type {!Array<number>}
   * @private
   */
  this.block_ = new Array(this.blockSize);

  /**
   * The length of yet-unprocessed data as collected in the block.
   * @type {number}
   * @private
   */
  this.blockLength_ = 0;

  /**
   * The total length of the message so far.
   * @type {number}
   * @private
   */
  this.totalLength_ = 0;

  this.reset();
};
goog.inherits(goog.crypt.Md5, goog.crypt.Hash);


/**
 * Integer rotation constants used by the abbreviated implementation.
 * They are hardcoded in the unrolled implementation, so it is left
 * here commented out.
 * @type {Array<number>}
 * @private
 *
goog.crypt.Md5.S_ = [
  7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22, 7, 12, 17, 22,
  5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20, 5, 9, 14, 20,
  4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23, 4, 11, 16, 23,
  6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21, 6, 10, 15, 21
];
 */

/**
 * Sine function constants used by the abbreviated implementation.
 * They are hardcoded in the unrolled implementation, so it is left
 * here commented out.
 * @type {Array<number>}
 * @private
 *
goog.crypt.Md5.T_ = [
  0xd76aa478, 0xe8c7b756, 0x242070db, 0xc1bdceee,
  0xf57c0faf, 0x4787c62a, 0xa8304613, 0xfd469501,
  0x698098d8, 0x8b44f7af, 0xffff5bb1, 0x895cd7be,
  0x6b901122, 0xfd987193, 0xa679438e, 0x49b40821,
  0xf61e2562, 0xc040b340, 0x265e5a51, 0xe9b6c7aa,
  0xd62f105d, 0x02441453, 0xd8a1e681, 0xe7d3fbc8,
  0x21e1cde6, 0xc33707d6, 0xf4d50d87, 0x455a14ed,
  0xa9e3e905, 0xfcefa3f8, 0x676f02d9, 0x8d2a4c8a,
  0xfffa3942, 0x8771f681, 0x6d9d6122, 0xfde5380c,
  0xa4beea44, 0x4bdecfa9, 0xf6bb4b60, 0xbebfbc70,
  0x289b7ec6, 0xeaa127fa, 0xd4ef3085, 0x04881d05,
  0xd9d4d039, 0xe6db99e5, 0x1fa27cf8, 0xc4ac5665,
  0xf4292244, 0x432aff97, 0xab9423a7, 0xfc93a039,
  0x655b59c3, 0x8f0ccc92, 0xffeff47d, 0x85845dd1,
  0x6fa87e4f, 0xfe2ce6e0, 0xa3014314, 0x4e0811a1,
  0xf7537e82, 0xbd3af235, 0x2ad7d2bb, 0xeb86d391
];
 */


/** @override */
goog.crypt.Md5.prototype.reset = function() {
  this.chain_[0] = 0x67452301;
  this.chain_[1] = 0xefcdab89;
  this.chain_[2] = 0x98badcfe;
  this.chain_[3] = 0x10325476;

  this.blockLength_ = 0;
  this.totalLength_ = 0;
};


/**
 * Internal compress helper function. It takes a block of data (64 bytes)
 * and updates the accumulator.
 * @param {Array<number>|Uint8Array|string} buf The block to compress.
 * @param {number=} opt_offset Offset of the block in the buffer.
 * @private
 */
goog.crypt.Md5.prototype.compress_ = function(buf, opt_offset) {
  if (!opt_offset) {
    opt_offset = 0;
  }

  // We allocate the array every time, but it's cheap in practice.
  var X = new Array(16);

  // Get 16 little endian words. It is not worth unrolling this for Chrome 11.
  if (goog.isString(buf)) {
    for (var i = 0; i < 16; ++i) {
      X[i] = (buf.charCodeAt(opt_offset++)) |
             (buf.charCodeAt(opt_offset++) << 8) |
             (buf.charCodeAt(opt_offset++) << 16) |
             (buf.charCodeAt(opt_offset++) << 24);
    }
  } else {
    for (var i = 0; i < 16; ++i) {
      X[i] = (buf[opt_offset++]) |
             (buf[opt_offset++] << 8) |
             (buf[opt_offset++] << 16) |
             (buf[opt_offset++] << 24);
    }
  }

  var A = this.chain_[0];
  var B = this.chain_[1];
  var C = this.chain_[2];
  var D = this.chain_[3];
  var sum = 0;

  /*
   * This is an abbreviated implementation, it is left here commented out for
   * reference purposes. See below for an unrolled version in use.
   *
  var f, n, tmp;
  for (var i = 0; i < 64; ++i) {

    if (i < 16) {
      f = (D ^ (B & (C ^ D)));
      n = i;
    } else if (i < 32) {
      f = (C ^ (D & (B ^ C)));
      n = (5 * i + 1) % 16;
    } else if (i < 48) {
      f = (B ^ C ^ D);
      n = (3 * i + 5) % 16;
    } else {
      f = (C ^ (B | (~D)));
      n = (7 * i) % 16;
    }

    tmp = D;
    D = C;
    C = B;
    sum = (A + f + goog.crypt.Md5.T_[i] + X[n]) & 0xffffffff;
    B += ((sum << goog.crypt.Md5.S_[i]) & 0xffffffff) |
         (sum >>> (32 - goog.crypt.Md5.S_[i]));
    A = tmp;
  }
   */

  /*
   * This is an unrolled MD5 implementation, which gives ~30% speedup compared
   * to the abbreviated implementation above, as measured on Chrome 11. It is
   * important to keep 32-bit croppings to minimum and inline the integer
   * rotation.
   */
  sum = (A + (D ^ (B & (C ^ D))) + X[0] + 0xd76aa478) & 0xffffffff;
  A = B + (((sum << 7) & 0xffffffff) | (sum >>> 25));
  sum = (D + (C ^ (A & (B ^ C))) + X[1] + 0xe8c7b756) & 0xffffffff;
  D = A + (((sum << 12) & 0xffffffff) | (sum >>> 20));
  sum = (C + (B ^ (D & (A ^ B))) + X[2] + 0x242070db) & 0xffffffff;
  C = D + (((sum << 17) & 0xffffffff) | (sum >>> 15));
  sum = (B + (A ^ (C & (D ^ A))) + X[3] + 0xc1bdceee) & 0xffffffff;
  B = C + (((sum << 22) & 0xffffffff) | (sum >>> 10));
  sum = (A + (D ^ (B & (C ^ D))) + X[4] + 0xf57c0faf) & 0xffffffff;
  A = B + (((sum << 7) & 0xffffffff) | (sum >>> 25));
  sum = (D + (C ^ (A & (B ^ C))) + X[5] + 0x4787c62a) & 0xffffffff;
  D = A + (((sum << 12) & 0xffffffff) | (sum >>> 20));
  sum = (C + (B ^ (D & (A ^ B))) + X[6] + 0xa8304613) & 0xffffffff;
  C = D + (((sum << 17) & 0xffffffff) | (sum >>> 15));
  sum = (B + (A ^ (C & (D ^ A))) + X[7] + 0xfd469501) & 0xffffffff;
  B = C + (((sum << 22) & 0xffffffff) | (sum >>> 10));
  sum = (A + (D ^ (B & (C ^ D))) + X[8] + 0x698098d8) & 0xffffffff;
  A = B + (((sum << 7) & 0xffffffff) | (sum >>> 25));
  sum = (D + (C ^ (A & (B ^ C))) + X[9] + 0x8b44f7af) & 0xffffffff;
  D = A + (((sum << 12) & 0xffffffff) | (sum >>> 20));
  sum = (C + (B ^ (D & (A ^ B))) + X[10] + 0xffff5bb1) & 0xffffffff;
  C = D + (((sum << 17) & 0xffffffff) | (sum >>> 15));
  sum = (B + (A ^ (C & (D ^ A))) + X[11] + 0x895cd7be) & 0xffffffff;
  B = C + (((sum << 22) & 0xffffffff) | (sum >>> 10));
  sum = (A + (D ^ (B & (C ^ D))) + X[12] + 0x6b901122) & 0xffffffff;
  A = B + (((sum << 7) & 0xffffffff) | (sum >>> 25));
  sum = (D + (C ^ (A & (B ^ C))) + X[13] + 0xfd987193) & 0xffffffff;
  D = A + (((sum << 12) & 0xffffffff) | (sum >>> 20));
  sum = (C + (B ^ (D & (A ^ B))) + X[14] + 0xa679438e) & 0xffffffff;
  C = D + (((sum << 17) & 0xffffffff) | (sum >>> 15));
  sum = (B + (A ^ (C & (D ^ A))) + X[15] + 0x49b40821) & 0xffffffff;
  B = C + (((sum << 22) & 0xffffffff) | (sum >>> 10));
  sum = (A + (C ^ (D & (B ^ C))) + X[1] + 0xf61e2562) & 0xffffffff;
  A = B + (((sum << 5) & 0xffffffff) | (sum >>> 27));
  sum = (D + (B ^ (C & (A ^ B))) + X[6] + 0xc040b340) & 0xffffffff;
  D = A + (((sum << 9) & 0xffffffff) | (sum >>> 23));
  sum = (C + (A ^ (B & (D ^ A))) + X[11] + 0x265e5a51) & 0xffffffff;
  C = D + (((sum << 14) & 0xffffffff) | (sum >>> 18));
  sum = (B + (D ^ (A & (C ^ D))) + X[0] + 0xe9b6c7aa) & 0xffffffff;
  B = C + (((sum << 20) & 0xffffffff) | (sum >>> 12));
  sum = (A + (C ^ (D & (B ^ C))) + X[5] + 0xd62f105d) & 0xffffffff;
  A = B + (((sum << 5) & 0xffffffff) | (sum >>> 27));
  sum = (D + (B ^ (C & (A ^ B))) + X[10] + 0x02441453) & 0xffffffff;
  D = A + (((sum << 9) & 0xffffffff) | (sum >>> 23));
  sum = (C + (A ^ (B & (D ^ A))) + X[15] + 0xd8a1e681) & 0xffffffff;
  C = D + (((sum << 14) & 0xffffffff) | (sum >>> 18));
  sum = (B + (D ^ (A & (C ^ D))) + X[4] + 0xe7d3fbc8) & 0xffffffff;
  B = C + (((sum << 20) & 0xffffffff) | (sum >>> 12));
  sum = (A + (C ^ (D & (B ^ C))) + X[9] + 0x21e1cde6) & 0xffffffff;
  A = B + (((sum << 5) & 0xffffffff) | (sum >>> 27));
  sum = (D + (B ^ (C & (A ^ B))) + X[14] + 0xc33707d6) & 0xffffffff;
  D = A + (((sum << 9) & 0xffffffff) | (sum >>> 23));
  sum = (C + (A ^ (B & (D ^ A))) + X[3] + 0xf4d50d87) & 0xffffffff;
  C = D + (((sum << 14) & 0xffffffff) | (sum >>> 18));
  sum = (B + (D ^ (A & (C ^ D))) + X[8] + 0x455a14ed) & 0xffffffff;
  B = C + (((sum << 20) & 0xffffffff) | (sum >>> 12));
  sum = (A + (C ^ (D & (B ^ C))) + X[13] + 0xa9e3e905) & 0xffffffff;
  A = B + (((sum << 5) & 0xffffffff) | (sum >>> 27));
  sum = (D + (B ^ (C & (A ^ B))) + X[2] + 0xfcefa3f8) & 0xffffffff;
  D = A + (((sum << 9) & 0xffffffff) | (sum >>> 23));
  sum = (C + (A ^ (B & (D ^ A))) + X[7] + 0x676f02d9) & 0xffffffff;
  C = D + (((sum << 14) & 0xffffffff) | (sum >>> 18));
  sum = (B + (D ^ (A & (C ^ D))) + X[12] + 0x8d2a4c8a) & 0xffffffff;
  B = C + (((sum << 20) & 0xffffffff) | (sum >>> 12));
  sum = (A + (B ^ C ^ D) + X[5] + 0xfffa3942) & 0xffffffff;
  A = B + (((sum << 4) & 0xffffffff) | (sum >>> 28));
  sum = (D + (A ^ B ^ C) + X[8] + 0x8771f681) & 0xffffffff;
  D = A + (((sum << 11) & 0xffffffff) | (sum >>> 21));
  sum = (C + (D ^ A ^ B) + X[11] + 0x6d9d6122) & 0xffffffff;
  C = D + (((sum << 16) & 0xffffffff) | (sum >>> 16));
  sum = (B + (C ^ D ^ A) + X[14] + 0xfde5380c) & 0xffffffff;
  B = C + (((sum << 23) & 0xffffffff) | (sum >>> 9));
  sum = (A + (B ^ C ^ D) + X[1] + 0xa4beea44) & 0xffffffff;
  A = B + (((sum << 4) & 0xffffffff) | (sum >>> 28));
  sum = (D + (A ^ B ^ C) + X[4] + 0x4bdecfa9) & 0xffffffff;
  D = A + (((sum << 11) & 0xffffffff) | (sum >>> 21));
  sum = (C + (D ^ A ^ B) + X[7] + 0xf6bb4b60) & 0xffffffff;
  C = D + (((sum << 16) & 0xffffffff) | (sum >>> 16));
  sum = (B + (C ^ D ^ A) + X[10] + 0xbebfbc70) & 0xffffffff;
  B = C + (((sum << 23) & 0xffffffff) | (sum >>> 9));
  sum = (A + (B ^ C ^ D) + X[13] + 0x289b7ec6) & 0xffffffff;
  A = B + (((sum << 4) & 0xffffffff) | (sum >>> 28));
  sum = (D + (A ^ B ^ C) + X[0] + 0xeaa127fa) & 0xffffffff;
  D = A + (((sum << 11) & 0xffffffff) | (sum >>> 21));
  sum = (C + (D ^ A ^ B) + X[3] + 0xd4ef3085) & 0xffffffff;
  C = D + (((sum << 16) & 0xffffffff) | (sum >>> 16));
  sum = (B + (C ^ D ^ A) + X[6] + 0x04881d05) & 0xffffffff;
  B = C + (((sum << 23) & 0xffffffff) | (sum >>> 9));
  sum = (A + (B ^ C ^ D) + X[9] + 0xd9d4d039) & 0xffffffff;
  A = B + (((sum << 4) & 0xffffffff) | (sum >>> 28));
  sum = (D + (A ^ B ^ C) + X[12] + 0xe6db99e5) & 0xffffffff;
  D = A + (((sum << 11) & 0xffffffff) | (sum >>> 21));
  sum = (C + (D ^ A ^ B) + X[15] + 0x1fa27cf8) & 0xffffffff;
  C = D + (((sum << 16) & 0xffffffff) | (sum >>> 16));
  sum = (B + (C ^ D ^ A) + X[2] + 0xc4ac5665) & 0xffffffff;
  B = C + (((sum << 23) & 0xffffffff) | (sum >>> 9));
  sum = (A + (C ^ (B | (~D))) + X[0] + 0xf4292244) & 0xffffffff;
  A = B + (((sum << 6) & 0xffffffff) | (sum >>> 26));
  sum = (D + (B ^ (A | (~C))) + X[7] + 0x432aff97) & 0xffffffff;
  D = A + (((sum << 10) & 0xffffffff) | (sum >>> 22));
  sum = (C + (A ^ (D | (~B))) + X[14] + 0xab9423a7) & 0xffffffff;
  C = D + (((sum << 15) & 0xffffffff) | (sum >>> 17));
  sum = (B + (D ^ (C | (~A))) + X[5] + 0xfc93a039) & 0xffffffff;
  B = C + (((sum << 21) & 0xffffffff) | (sum >>> 11));
  sum = (A + (C ^ (B | (~D))) + X[12] + 0x655b59c3) & 0xffffffff;
  A = B + (((sum << 6) & 0xffffffff) | (sum >>> 26));
  sum = (D + (B ^ (A | (~C))) + X[3] + 0x8f0ccc92) & 0xffffffff;
  D = A + (((sum << 10) & 0xffffffff) | (sum >>> 22));
  sum = (C + (A ^ (D | (~B))) + X[10] + 0xffeff47d) & 0xffffffff;
  C = D + (((sum << 15) & 0xffffffff) | (sum >>> 17));
  sum = (B + (D ^ (C | (~A))) + X[1] + 0x85845dd1) & 0xffffffff;
  B = C + (((sum << 21) & 0xffffffff) | (sum >>> 11));
  sum = (A + (C ^ (B | (~D))) + X[8] + 0x6fa87e4f) & 0xffffffff;
  A = B + (((sum << 6) & 0xffffffff) | (sum >>> 26));
  sum = (D + (B ^ (A | (~C))) + X[15] + 0xfe2ce6e0) & 0xffffffff;
  D = A + (((sum << 10) & 0xffffffff) | (sum >>> 22));
  sum = (C + (A ^ (D | (~B))) + X[6] + 0xa3014314) & 0xffffffff;
  C = D + (((sum << 15) & 0xffffffff) | (sum >>> 17));
  sum = (B + (D ^ (C | (~A))) + X[13] + 0x4e0811a1) & 0xffffffff;
  B = C + (((sum << 21) & 0xffffffff) | (sum >>> 11));
  sum = (A + (C ^ (B | (~D))) + X[4] + 0xf7537e82) & 0xffffffff;
  A = B + (((sum << 6) & 0xffffffff) | (sum >>> 26));
  sum = (D + (B ^ (A | (~C))) + X[11] + 0xbd3af235) & 0xffffffff;
  D = A + (((sum << 10) & 0xffffffff) | (sum >>> 22));
  sum = (C + (A ^ (D | (~B))) + X[2] + 0x2ad7d2bb) & 0xffffffff;
  C = D + (((sum << 15) & 0xffffffff) | (sum >>> 17));
  sum = (B + (D ^ (C | (~A))) + X[9] + 0xeb86d391) & 0xffffffff;
  B = C + (((sum << 21) & 0xffffffff) | (sum >>> 11));

  this.chain_[0] = (this.chain_[0] + A) & 0xffffffff;
  this.chain_[1] = (this.chain_[1] + B) & 0xffffffff;
  this.chain_[2] = (this.chain_[2] + C) & 0xffffffff;
  this.chain_[3] = (this.chain_[3] + D) & 0xffffffff;
};


/** @override */
goog.crypt.Md5.prototype.update = function(bytes, opt_length) {
  if (!goog.isDef(opt_length)) {
    opt_length = bytes.length;
  }
  var lengthMinusBlock = opt_length - this.blockSize;

  // Copy some object properties to local variables in order to save on access
  // time from inside the loop (~10% speedup was observed on Chrome 11).
  var block = this.block_;
  var blockLength = this.blockLength_;
  var i = 0;

  // The outer while loop should execute at most twice.
  while (i < opt_length) {
    // When we have no data in the block to top up, we can directly process the
    // input buffer (assuming it contains sufficient data). This gives ~30%
    // speedup on Chrome 14 and ~70% speedup on Firefox 6.0, but requires that
    // the data is provided in large chunks (or in multiples of 64 bytes).
    if (blockLength == 0) {
      while (i <= lengthMinusBlock) {
        this.compress_(bytes, i);
        i += this.blockSize;
      }
    }

    if (goog.isString(bytes)) {
      while (i < opt_length) {
        block[blockLength++] = bytes.charCodeAt(i++);
        if (blockLength == this.blockSize) {
          this.compress_(block);
          blockLength = 0;
          // Jump to the outer loop so we use the full-block optimization.
          break;
        }
      }
    } else {
      while (i < opt_length) {
        block[blockLength++] = bytes[i++];
        if (blockLength == this.blockSize) {
          this.compress_(block);
          blockLength = 0;
          // Jump to the outer loop so we use the full-block optimization.
          break;
        }
      }
    }
  }

  this.blockLength_ = blockLength;
  this.totalLength_ += opt_length;
};


/** @override */
goog.crypt.Md5.prototype.digest = function() {
  // This must accommodate at least 1 padding byte (0x80), 8 bytes of
  // total bitlength, and must end at a 64-byte boundary.
  var pad = new Array((this.blockLength_ < 56 ?
                       this.blockSize :
                       this.blockSize * 2) - this.blockLength_);

  // Add padding: 0x80 0x00*
  pad[0] = 0x80;
  for (var i = 1; i < pad.length - 8; ++i) {
    pad[i] = 0;
  }
  // Add the total number of bits, little endian 64-bit integer.
  var totalBits = this.totalLength_ * 8;
  for (var i = pad.length - 8; i < pad.length; ++i) {
    pad[i] = totalBits & 0xff;
    totalBits /= 0x100; // Don't use bit-shifting here!
  }
  this.update(pad);

  var digest = new Array(16);
  var n = 0;
  for (var i = 0; i < 4; ++i) {
    for (var j = 0; j < 32; j += 8) {
      digest[n++] = (this.chain_[i] >>> j) & 0xff;
    }
  }
  return digest;
};

goog.provide('ol.style.Stroke');

goog.require('goog.crypt');
goog.require('goog.crypt.Md5');
goog.require('ol.color');


/**
 * @classdesc
 * Set stroke style for vector features.
 * Note that the defaults given are the Canvas defaults, which will be used if
 * option is not defined. The `get` functions return whatever was entered in
 * the options; they will not return the default.
 *
 * @constructor
 * @param {olx.style.StrokeOptions=} opt_options Options.
 * @api
 */
ol.style.Stroke = function(opt_options) {

  var options = opt_options || {};

  /**
   * @private
   * @type {ol.Color|string}
   */
  this.color_ = options.color !== undefined ? options.color : null;

  /**
   * @private
   * @type {string|undefined}
   */
  this.lineCap_ = options.lineCap;

  /**
   * @private
   * @type {Array.<number>}
   */
  this.lineDash_ = options.lineDash !== undefined ? options.lineDash : null;

  /**
   * @private
   * @type {string|undefined}
   */
  this.lineJoin_ = options.lineJoin;

  /**
   * @private
   * @type {number|undefined}
   */
  this.miterLimit_ = options.miterLimit;

  /**
   * @private
   * @type {number|undefined}
   */
  this.width_ = options.width;

  /**
   * @private
   * @type {string|undefined}
   */
  this.checksum_ = undefined;
};


/**
 * Get the stroke color.
 * @return {ol.Color|string} Color.
 * @api
 */
ol.style.Stroke.prototype.getColor = function() {
  return this.color_;
};


/**
 * Get the line cap type for the stroke.
 * @return {string|undefined} Line cap.
 * @api
 */
ol.style.Stroke.prototype.getLineCap = function() {
  return this.lineCap_;
};


/**
 * Get the line dash style for the stroke.
 * @return {Array.<number>} Line dash.
 * @api
 */
ol.style.Stroke.prototype.getLineDash = function() {
  return this.lineDash_;
};


/**
 * Get the line join type for the stroke.
 * @return {string|undefined} Line join.
 * @api
 */
ol.style.Stroke.prototype.getLineJoin = function() {
  return this.lineJoin_;
};


/**
 * Get the miter limit for the stroke.
 * @return {number|undefined} Miter limit.
 * @api
 */
ol.style.Stroke.prototype.getMiterLimit = function() {
  return this.miterLimit_;
};


/**
 * Get the stroke width.
 * @return {number|undefined} Width.
 * @api
 */
ol.style.Stroke.prototype.getWidth = function() {
  return this.width_;
};


/**
 * Set the color.
 *
 * @param {ol.Color|string} color Color.
 * @api
 */
ol.style.Stroke.prototype.setColor = function(color) {
  this.color_ = color;
  this.checksum_ = undefined;
};


/**
 * Set the line cap.
 *
 * @param {string|undefined} lineCap Line cap.
 * @api
 */
ol.style.Stroke.prototype.setLineCap = function(lineCap) {
  this.lineCap_ = lineCap;
  this.checksum_ = undefined;
};


/**
 * Set the line dash.
 *
 * @param {Array.<number>} lineDash Line dash.
 * @api
 */
ol.style.Stroke.prototype.setLineDash = function(lineDash) {
  this.lineDash_ = lineDash;
  this.checksum_ = undefined;
};


/**
 * Set the line join.
 *
 * @param {string|undefined} lineJoin Line join.
 * @api
 */
ol.style.Stroke.prototype.setLineJoin = function(lineJoin) {
  this.lineJoin_ = lineJoin;
  this.checksum_ = undefined;
};


/**
 * Set the miter limit.
 *
 * @param {number|undefined} miterLimit Miter limit.
 * @api
 */
ol.style.Stroke.prototype.setMiterLimit = function(miterLimit) {
  this.miterLimit_ = miterLimit;
  this.checksum_ = undefined;
};


/**
 * Set the width.
 *
 * @param {number|undefined} width Width.
 * @api
 */
ol.style.Stroke.prototype.setWidth = function(width) {
  this.width_ = width;
  this.checksum_ = undefined;
};


/**
 * @return {string} The checksum.
 */
ol.style.Stroke.prototype.getChecksum = function() {
  if (this.checksum_ === undefined) {
    var raw = 's' +
        (this.color_ ?
            ol.color.asString(this.color_) : '-') + ',' +
        (this.lineCap_ !== undefined ?
            this.lineCap_.toString() : '-') + ',' +
        (this.lineDash_ ?
            this.lineDash_.toString() : '-') + ',' +
        (this.lineJoin_ !== undefined ?
            this.lineJoin_ : '-') + ',' +
        (this.miterLimit_ !== undefined ?
            this.miterLimit_.toString() : '-') + ',' +
        (this.width_ !== undefined ?
            this.width_.toString() : '-');

    var md5 = new goog.crypt.Md5();
    md5.update(raw);
    this.checksum_ = goog.crypt.byteArrayToString(md5.digest());
  }

  return this.checksum_;
};

goog.provide('ol.style.Circle');

goog.require('goog.asserts');
goog.require('ol');
goog.require('ol.color');
goog.require('ol.has');
goog.require('ol.render.canvas');
goog.require('ol.style.Fill');
goog.require('ol.style.Image');
goog.require('ol.style.ImageState');
goog.require('ol.style.Stroke');


/**
 * @classdesc
 * Set circle style for vector features.
 *
 * @constructor
 * @param {olx.style.CircleOptions=} opt_options Options.
 * @extends {ol.style.Image}
 * @api
 */
ol.style.Circle = function(opt_options) {

  var options = opt_options || {};

  /**
   * @private
   * @type {Array.<string>}
   */
  this.checksums_ = null;

  /**
   * @private
   * @type {HTMLCanvasElement}
   */
  this.canvas_ = null;

  /**
   * @private
   * @type {HTMLCanvasElement}
   */
  this.hitDetectionCanvas_ = null;

  /**
   * @private
   * @type {ol.style.Fill}
   */
  this.fill_ = options.fill !== undefined ? options.fill : null;

  /**
   * @private
   * @type {ol.style.Stroke}
   */
  this.stroke_ = options.stroke !== undefined ? options.stroke : null;

  /**
   * @private
   * @type {number}
   */
  this.radius_ = options.radius;

  /**
   * @private
   * @type {Array.<number>}
   */
  this.origin_ = [0, 0];

  /**
   * @private
   * @type {Array.<number>}
   */
  this.anchor_ = null;

  /**
   * @private
   * @type {ol.Size}
   */
  this.size_ = null;

  /**
   * @private
   * @type {ol.Size}
   */
  this.imageSize_ = null;

  /**
   * @private
   * @type {ol.Size}
   */
  this.hitDetectionImageSize_ = null;

  this.render_(options.atlasManager);

  /**
   * @type {boolean}
   */
  var snapToPixel = options.snapToPixel !== undefined ?
      options.snapToPixel : true;

  goog.base(this, {
    opacity: 1,
    rotateWithView: false,
    rotation: 0,
    scale: 1,
    snapToPixel: snapToPixel
  });

};
goog.inherits(ol.style.Circle, ol.style.Image);


/**
 * @inheritDoc
 */
ol.style.Circle.prototype.getAnchor = function() {
  return this.anchor_;
};


/**
 * Get the fill style for the circle.
 * @return {ol.style.Fill} Fill style.
 * @api
 */
ol.style.Circle.prototype.getFill = function() {
  return this.fill_;
};


/**
 * @inheritDoc
 */
ol.style.Circle.prototype.getHitDetectionImage = function(pixelRatio) {
  return this.hitDetectionCanvas_;
};


/**
 * Get the image used to render the circle.
 * @param {number} pixelRatio Pixel ratio.
 * @return {HTMLCanvasElement} Canvas element.
 * @api
 */
ol.style.Circle.prototype.getImage = function(pixelRatio) {
  return this.canvas_;
};


/**
 * @inheritDoc
 */
ol.style.Circle.prototype.getImageState = function() {
  return ol.style.ImageState.LOADED;
};


/**
 * @inheritDoc
 */
ol.style.Circle.prototype.getImageSize = function() {
  return this.imageSize_;
};


/**
 * @inheritDoc
 */
ol.style.Circle.prototype.getHitDetectionImageSize = function() {
  return this.hitDetectionImageSize_;
};


/**
 * @inheritDoc
 */
ol.style.Circle.prototype.getOrigin = function() {
  return this.origin_;
};


/**
 * Get the circle radius.
 * @return {number} Radius.
 * @api
 */
ol.style.Circle.prototype.getRadius = function() {
  return this.radius_;
};


/**
 * @inheritDoc
 */
ol.style.Circle.prototype.getSize = function() {
  return this.size_;
};


/**
 * Get the stroke style for the circle.
 * @return {ol.style.Stroke} Stroke style.
 * @api
 */
ol.style.Circle.prototype.getStroke = function() {
  return this.stroke_;
};


/**
 * @inheritDoc
 */
ol.style.Circle.prototype.listenImageChange = ol.nullFunction;


/**
 * @inheritDoc
 */
ol.style.Circle.prototype.load = ol.nullFunction;


/**
 * @inheritDoc
 */
ol.style.Circle.prototype.unlistenImageChange = ol.nullFunction;


/**
 * @typedef {{strokeStyle: (string|undefined), strokeWidth: number,
 *   size: number, lineDash: Array.<number>}}
 */
ol.style.Circle.RenderOptions;


/**
 * @private
 * @param {ol.style.AtlasManager|undefined} atlasManager An atlas manager.
 */
ol.style.Circle.prototype.render_ = function(atlasManager) {
  var imageSize;
  var lineDash = null;
  var strokeStyle;
  var strokeWidth = 0;

  if (this.stroke_) {
    strokeStyle = ol.color.asString(this.stroke_.getColor());
    strokeWidth = this.stroke_.getWidth();
    if (strokeWidth === undefined) {
      strokeWidth = ol.render.canvas.defaultLineWidth;
    }
    lineDash = this.stroke_.getLineDash();
    if (!ol.has.CANVAS_LINE_DASH) {
      lineDash = null;
    }
  }


  var size = 2 * (this.radius_ + strokeWidth) + 1;

  /** @type {ol.style.Circle.RenderOptions} */
  var renderOptions = {
    strokeStyle: strokeStyle,
    strokeWidth: strokeWidth,
    size: size,
    lineDash: lineDash
  };

  if (atlasManager === undefined) {
    // no atlas manager is used, create a new canvas
    this.canvas_ = /** @type {HTMLCanvasElement} */
        (document.createElement('CANVAS'));
    this.canvas_.height = size;
    this.canvas_.width = size;

    // canvas.width and height are rounded to the closest integer
    size = this.canvas_.width;
    imageSize = size;

    // draw the circle on the canvas
    var context = /** @type {CanvasRenderingContext2D} */
        (this.canvas_.getContext('2d'));
    this.draw_(renderOptions, context, 0, 0);

    this.createHitDetectionCanvas_(renderOptions);
  } else {
    // an atlas manager is used, add the symbol to an atlas
    size = Math.round(size);

    var hasCustomHitDetectionImage = !this.fill_;
    var renderHitDetectionCallback;
    if (hasCustomHitDetectionImage) {
      // render the hit-detection image into a separate atlas image
      renderHitDetectionCallback =
          this.drawHitDetectionCanvas_.bind(this, renderOptions);
    }

    var id = this.getChecksum();
    var info = atlasManager.add(
        id, size, size, this.draw_.bind(this, renderOptions),
        renderHitDetectionCallback);
    goog.asserts.assert(info, 'circle radius is too large');

    this.canvas_ = info.image;
    this.origin_ = [info.offsetX, info.offsetY];
    imageSize = info.image.width;

    if (hasCustomHitDetectionImage) {
      this.hitDetectionCanvas_ = info.hitImage;
      this.hitDetectionImageSize_ =
          [info.hitImage.width, info.hitImage.height];
    } else {
      this.hitDetectionCanvas_ = this.canvas_;
      this.hitDetectionImageSize_ = [imageSize, imageSize];
    }
  }

  this.anchor_ = [size / 2, size / 2];
  this.size_ = [size, size];
  this.imageSize_ = [imageSize, imageSize];
};


/**
 * @private
 * @param {ol.style.Circle.RenderOptions} renderOptions Render options.
 * @param {CanvasRenderingContext2D} context The rendering context.
 * @param {number} x The origin for the symbol (x).
 * @param {number} y The origin for the symbol (y).
 */
ol.style.Circle.prototype.draw_ = function(renderOptions, context, x, y) {
  // reset transform
  context.setTransform(1, 0, 0, 1, 0, 0);

  // then move to (x, y)
  context.translate(x, y);

  context.beginPath();
  context.arc(
      renderOptions.size / 2, renderOptions.size / 2,
      this.radius_, 0, 2 * Math.PI, true);

  if (this.fill_) {
    context.fillStyle = ol.color.asString(this.fill_.getColor());
    context.fill();
  }
  if (this.stroke_) {
    context.strokeStyle = renderOptions.strokeStyle;
    context.lineWidth = renderOptions.strokeWidth;
    if (renderOptions.lineDash) {
      context.setLineDash(renderOptions.lineDash);
    }
    context.stroke();
  }
  context.closePath();
};


/**
 * @private
 * @param {ol.style.Circle.RenderOptions} renderOptions Render options.
 */
ol.style.Circle.prototype.createHitDetectionCanvas_ = function(renderOptions) {
  this.hitDetectionImageSize_ = [renderOptions.size, renderOptions.size];
  if (this.fill_) {
    this.hitDetectionCanvas_ = this.canvas_;
    return;
  }

  // if no fill style is set, create an extra hit-detection image with a
  // default fill style
  this.hitDetectionCanvas_ = /** @type {HTMLCanvasElement} */
      (document.createElement('CANVAS'));
  var canvas = this.hitDetectionCanvas_;

  canvas.height = renderOptions.size;
  canvas.width = renderOptions.size;

  var context = /** @type {CanvasRenderingContext2D} */
      (canvas.getContext('2d'));
  this.drawHitDetectionCanvas_(renderOptions, context, 0, 0);
};


/**
 * @private
 * @param {ol.style.Circle.RenderOptions} renderOptions Render options.
 * @param {CanvasRenderingContext2D} context The context.
 * @param {number} x The origin for the symbol (x).
 * @param {number} y The origin for the symbol (y).
 */
ol.style.Circle.prototype.drawHitDetectionCanvas_ = function(renderOptions, context, x, y) {
  // reset transform
  context.setTransform(1, 0, 0, 1, 0, 0);

  // then move to (x, y)
  context.translate(x, y);

  context.beginPath();
  context.arc(
      renderOptions.size / 2, renderOptions.size / 2,
      this.radius_, 0, 2 * Math.PI, true);

  context.fillStyle = ol.color.asString(ol.render.canvas.defaultFillStyle);
  context.fill();
  if (this.stroke_) {
    context.strokeStyle = renderOptions.strokeStyle;
    context.lineWidth = renderOptions.strokeWidth;
    if (renderOptions.lineDash) {
      context.setLineDash(renderOptions.lineDash);
    }
    context.stroke();
  }
  context.closePath();
};


/**
 * @return {string} The checksum.
 */
ol.style.Circle.prototype.getChecksum = function() {
  var strokeChecksum = this.stroke_ ?
      this.stroke_.getChecksum() : '-';
  var fillChecksum = this.fill_ ?
      this.fill_.getChecksum() : '-';

  var recalculate = !this.checksums_ ||
      (strokeChecksum != this.checksums_[1] ||
      fillChecksum != this.checksums_[2] ||
      this.radius_ != this.checksums_[3]);

  if (recalculate) {
    var checksum = 'c' + strokeChecksum + fillChecksum +
        (this.radius_ !== undefined ? this.radius_.toString() : '-');
    this.checksums_ = [checksum, strokeChecksum, fillChecksum, this.radius_];
  }

  return this.checksums_[0];
};

goog.provide('ol.style.GeometryFunction');
goog.provide('ol.style.Style');
goog.provide('ol.style.StyleFunction');
goog.provide('ol.style.defaultGeometryFunction');

goog.require('goog.asserts');
goog.require('ol.geom.Geometry');
goog.require('ol.geom.GeometryType');
goog.require('ol.style.Circle');
goog.require('ol.style.Fill');
goog.require('ol.style.Image');
goog.require('ol.style.Stroke');


/**
 * @classdesc
 * Container for vector feature rendering styles. Any changes made to the style
 * or its children through `set*()` methods will not take effect until the
 * feature or layer that uses the style is re-rendered.
 *
 * @constructor
 * @param {olx.style.StyleOptions=} opt_options Style options.
 * @api
 */
ol.style.Style = function(opt_options) {

  var options = opt_options || {};

  /**
   * @private
   * @type {string|ol.geom.Geometry|ol.style.GeometryFunction}
   */
  this.geometry_ = null;

  /**
   * @private
   * @type {!ol.style.GeometryFunction}
   */
  this.geometryFunction_ = ol.style.defaultGeometryFunction;

  if (options.geometry !== undefined) {
    this.setGeometry(options.geometry);
  }

  /**
   * @private
   * @type {ol.style.Fill}
   */
  this.fill_ = options.fill !== undefined ? options.fill : null;

  /**
   * @private
   * @type {ol.style.Image}
   */
  this.image_ = options.image !== undefined ? options.image : null;

  /**
   * @private
   * @type {ol.style.Stroke}
   */
  this.stroke_ = options.stroke !== undefined ? options.stroke : null;

  /**
   * @private
   * @type {ol.style.Text}
   */
  this.text_ = options.text !== undefined ? options.text : null;

  /**
   * @private
   * @type {number|undefined}
   */
  this.zIndex_ = options.zIndex;

};


/**
 * Get the geometry to be rendered.
 * @return {string|ol.geom.Geometry|ol.style.GeometryFunction}
 * Feature property or geometry or function that returns the geometry that will
 * be rendered with this style.
 * @api
 */
ol.style.Style.prototype.getGeometry = function() {
  return this.geometry_;
};


/**
 * Get the function used to generate a geometry for rendering.
 * @return {!ol.style.GeometryFunction} Function that is called with a feature
 * and returns the geometry to render instead of the feature's geometry.
 * @api
 */
ol.style.Style.prototype.getGeometryFunction = function() {
  return this.geometryFunction_;
};


/**
 * Get the fill style.
 * @return {ol.style.Fill} Fill style.
 * @api
 */
ol.style.Style.prototype.getFill = function() {
  return this.fill_;
};


/**
 * Get the image style.
 * @return {ol.style.Image} Image style.
 * @api
 */
ol.style.Style.prototype.getImage = function() {
  return this.image_;
};


/**
 * Get the stroke style.
 * @return {ol.style.Stroke} Stroke style.
 * @api
 */
ol.style.Style.prototype.getStroke = function() {
  return this.stroke_;
};


/**
 * Get the text style.
 * @return {ol.style.Text} Text style.
 * @api
 */
ol.style.Style.prototype.getText = function() {
  return this.text_;
};


/**
 * Get the z-index for the style.
 * @return {number|undefined} ZIndex.
 * @api
 */
ol.style.Style.prototype.getZIndex = function() {
  return this.zIndex_;
};


/**
 * Set a geometry that is rendered instead of the feature's geometry.
 *
 * @param {string|ol.geom.Geometry|ol.style.GeometryFunction} geometry
 *     Feature property or geometry or function returning a geometry to render
 *     for this style.
 * @api
 */
ol.style.Style.prototype.setGeometry = function(geometry) {
  if (goog.isFunction(geometry)) {
    this.geometryFunction_ = geometry;
  } else if (goog.isString(geometry)) {
    this.geometryFunction_ = function(feature) {
      var result = feature.get(geometry);
      if (result) {
        goog.asserts.assertInstanceof(result, ol.geom.Geometry,
            'feature geometry must be an ol.geom.Geometry instance');
      }
      return result;
    };
  } else if (!geometry) {
    this.geometryFunction_ = ol.style.defaultGeometryFunction;
  } else if (geometry !== undefined) {
    goog.asserts.assertInstanceof(geometry, ol.geom.Geometry,
        'geometry must be an ol.geom.Geometry instance');
    this.geometryFunction_ = function() {
      return geometry;
    };
  }
  this.geometry_ = geometry;
};


/**
 * Set the z-index.
 *
 * @param {number|undefined} zIndex ZIndex.
 * @api
 */
ol.style.Style.prototype.setZIndex = function(zIndex) {
  this.zIndex_ = zIndex;
};


/**
 * A function that takes an {@link ol.Feature} and a `{number}` representing
 * the view's resolution. The function should return an array of
 * {@link ol.style.Style}. This way e.g. a vector layer can be styled.
 *
 * @typedef {function((ol.Feature|ol.render.Feature), number):
 *     (ol.style.Style|Array.<ol.style.Style>)}
 * @api
 */
ol.style.StyleFunction;


/**
 * Convert the provided object into a style function.  Functions passed through
 * unchanged.  Arrays of ol.style.Style or single style objects wrapped in a
 * new style function.
 * @param {ol.style.StyleFunction|Array.<ol.style.Style>|ol.style.Style} obj
 *     A style function, a single style, or an array of styles.
 * @return {ol.style.StyleFunction} A style function.
 */
ol.style.createStyleFunction = function(obj) {
  var styleFunction;

  if (goog.isFunction(obj)) {
    styleFunction = obj;
  } else {
    /**
     * @type {Array.<ol.style.Style>}
     */
    var styles;
    if (goog.isArray(obj)) {
      styles = obj;
    } else {
      goog.asserts.assertInstanceof(obj, ol.style.Style,
          'obj geometry must be an ol.style.Style instance');
      styles = [obj];
    }
    styleFunction = function() {
      return styles;
    };
  }
  return styleFunction;
};


/**
 * @type {Array.<ol.style.Style>}
 * @private
 */
ol.style.defaultStyle_ = null;


/**
 * @param {ol.Feature|ol.render.Feature} feature Feature.
 * @param {number} resolution Resolution.
 * @return {Array.<ol.style.Style>} Style.
 */
ol.style.defaultStyleFunction = function(feature, resolution) {
  // We don't use an immediately-invoked function
  // and a closure so we don't get an error at script evaluation time in
  // browsers that do not support Canvas. (ol.style.Circle does
  // canvas.getContext('2d') at construction time, which will cause an.error
  // in such browsers.)
  if (!ol.style.defaultStyle_) {
    var fill = new ol.style.Fill({
      color: 'rgba(255,255,255,0.4)'
    });
    var stroke = new ol.style.Stroke({
      color: '#3399CC',
      width: 1.25
    });
    ol.style.defaultStyle_ = [
      new ol.style.Style({
        image: new ol.style.Circle({
          fill: fill,
          stroke: stroke,
          radius: 5
        }),
        fill: fill,
        stroke: stroke
      })
    ];
  }
  return ol.style.defaultStyle_;
};


/**
 * Default styles for editing features.
 * @return {Object.<ol.geom.GeometryType, Array.<ol.style.Style>>} Styles
 */
ol.style.createDefaultEditingStyles = function() {
  /** @type {Object.<ol.geom.GeometryType, Array.<ol.style.Style>>} */
  var styles = {};
  var white = [255, 255, 255, 1];
  var blue = [0, 153, 255, 1];
  var width = 3;
  styles[ol.geom.GeometryType.POLYGON] = [
    new ol.style.Style({
      fill: new ol.style.Fill({
        color: [255, 255, 255, 0.5]
      })
    })
  ];
  styles[ol.geom.GeometryType.MULTI_POLYGON] =
      styles[ol.geom.GeometryType.POLYGON];

  styles[ol.geom.GeometryType.LINE_STRING] = [
    new ol.style.Style({
      stroke: new ol.style.Stroke({
        color: white,
        width: width + 2
      })
    }),
    new ol.style.Style({
      stroke: new ol.style.Stroke({
        color: blue,
        width: width
      })
    })
  ];
  styles[ol.geom.GeometryType.MULTI_LINE_STRING] =
      styles[ol.geom.GeometryType.LINE_STRING];

  styles[ol.geom.GeometryType.CIRCLE] =
      styles[ol.geom.GeometryType.POLYGON].concat(
          styles[ol.geom.GeometryType.LINE_STRING]
      );


  styles[ol.geom.GeometryType.POINT] = [
    new ol.style.Style({
      image: new ol.style.Circle({
        radius: width * 2,
        fill: new ol.style.Fill({
          color: blue
        }),
        stroke: new ol.style.Stroke({
          color: white,
          width: width / 2
        })
      }),
      zIndex: Infinity
    })
  ];
  styles[ol.geom.GeometryType.MULTI_POINT] =
      styles[ol.geom.GeometryType.POINT];

  styles[ol.geom.GeometryType.GEOMETRY_COLLECTION] =
      styles[ol.geom.GeometryType.POLYGON].concat(
          styles[ol.geom.GeometryType.LINE_STRING],
          styles[ol.geom.GeometryType.POINT]
      );

  return styles;
};


/**
 * A function that takes an {@link ol.Feature} as argument and returns an
 * {@link ol.geom.Geometry} that will be rendered and styled for the feature.
 *
 * @typedef {function((ol.Feature|ol.render.Feature)):
 *     (ol.geom.Geometry|ol.render.Feature|undefined)}
 * @api
 */
ol.style.GeometryFunction;


/**
 * Function that is called with a feature and returns its default geometry.
 * @param {ol.Feature|ol.render.Feature} feature Feature to get the geometry
 *     for.
 * @return {ol.geom.Geometry|ol.render.Feature|undefined} Geometry to render.
 */
ol.style.defaultGeometryFunction = function(feature) {
  goog.asserts.assert(feature, 'feature must not be null');
  return feature.getGeometry();
};

goog.provide('ol.layer.Vector');

goog.require('goog.asserts');
goog.require('goog.object');
goog.require('ol');
goog.require('ol.layer.Layer');
goog.require('ol.style.Style');


/**
 * @enum {string}
 */
ol.layer.VectorProperty = {
  RENDER_ORDER: 'renderOrder'
};


/**
 * @classdesc
 * Vector data that is rendered client-side.
 * Note that any property set in the options is set as a {@link ol.Object}
 * property on the layer object; for example, setting `title: 'My Title'` in the
 * options means that `title` is observable, and has get/set accessors.
 *
 * @constructor
 * @extends {ol.layer.Layer}
 * @fires ol.render.Event
 * @param {olx.layer.VectorOptions=} opt_options Options.
 * @api stable
 */
ol.layer.Vector = function(opt_options) {

  var options = opt_options ?
      opt_options : /** @type {olx.layer.VectorOptions} */ ({});

  goog.asserts.assert(
      options.renderOrder === undefined || !options.renderOrder ||
      goog.isFunction(options.renderOrder),
      'renderOrder must be a comparator function');

  var baseOptions = goog.object.clone(options);

  delete baseOptions.style;
  delete baseOptions.renderBuffer;
  delete baseOptions.updateWhileAnimating;
  delete baseOptions.updateWhileInteracting;
  goog.base(this, /** @type {olx.layer.LayerOptions} */ (baseOptions));

  /**
   * @type {number}
   * @private
   */
  this.renderBuffer_ = options.renderBuffer !== undefined ?
      options.renderBuffer : 100;

  /**
   * User provided style.
   * @type {ol.style.Style|Array.<ol.style.Style>|ol.style.StyleFunction}
   * @private
   */
  this.style_ = null;

  /**
   * Style function for use within the library.
   * @type {ol.style.StyleFunction|undefined}
   * @private
   */
  this.styleFunction_ = undefined;

  this.setStyle(options.style);

  /**
   * @type {boolean}
   * @private
   */
  this.updateWhileAnimating_ = options.updateWhileAnimating !== undefined ?
      options.updateWhileAnimating : false;

  /**
   * @type {boolean}
   * @private
   */
  this.updateWhileInteracting_ = options.updateWhileInteracting !== undefined ?
      options.updateWhileInteracting : false;

};
goog.inherits(ol.layer.Vector, ol.layer.Layer);


/**
 * @return {number|undefined} Render buffer.
 */
ol.layer.Vector.prototype.getRenderBuffer = function() {
  return this.renderBuffer_;
};


/**
 * @return {function(ol.Feature, ol.Feature): number|null|undefined} Render
 *     order.
 */
ol.layer.Vector.prototype.getRenderOrder = function() {
  return /** @type {function(ol.Feature, ol.Feature):number|null|undefined} */ (
      this.get(ol.layer.VectorProperty.RENDER_ORDER));
};


/**
 * Return the associated {@link ol.source.Vector vectorsource} of the layer.
 * @function
 * @return {ol.source.Vector} Source.
 * @api stable
 */
ol.layer.Vector.prototype.getSource;


/**
 * Get the style for features.  This returns whatever was passed to the `style`
 * option at construction or to the `setStyle` method.
 * @return {ol.style.Style|Array.<ol.style.Style>|ol.style.StyleFunction}
 *     Layer style.
 * @api stable
 */
ol.layer.Vector.prototype.getStyle = function() {
  return this.style_;
};


/**
 * Get the style function.
 * @return {ol.style.StyleFunction|undefined} Layer style function.
 * @api stable
 */
ol.layer.Vector.prototype.getStyleFunction = function() {
  return this.styleFunction_;
};


/**
 * @return {boolean} Whether the rendered layer should be updated while
 *     animating.
 */
ol.layer.Vector.prototype.getUpdateWhileAnimating = function() {
  return this.updateWhileAnimating_;
};


/**
 * @return {boolean} Whether the rendered layer should be updated while
 *     interacting.
 */
ol.layer.Vector.prototype.getUpdateWhileInteracting = function() {
  return this.updateWhileInteracting_;
};


/**
 * @param {function(ol.Feature, ol.Feature):number|null|undefined} renderOrder
 *     Render order.
 */
ol.layer.Vector.prototype.setRenderOrder = function(renderOrder) {
  goog.asserts.assert(
      renderOrder === undefined || !renderOrder ||
      goog.isFunction(renderOrder),
      'renderOrder must be a comparator function');
  this.set(ol.layer.VectorProperty.RENDER_ORDER, renderOrder);
};


/**
 * Set the style for features.  This can be a single style object, an array
 * of styles, or a function that takes a feature and resolution and returns
 * an array of styles. If it is `undefined` the default style is used. If
 * it is `null` the layer has no style (a `null` style), so only features
 * that have their own styles will be rendered in the layer. See
 * {@link ol.style} for information on the default style.
 * @param {ol.style.Style|Array.<ol.style.Style>|ol.style.StyleFunction|null|undefined}
 *     style Layer style.
 * @api stable
 */
ol.layer.Vector.prototype.setStyle = function(style) {
  this.style_ = style !== undefined ? style : ol.style.defaultStyleFunction;
  this.styleFunction_ = style === null ?
      undefined : ol.style.createStyleFunction(this.style_);
  this.changed();
};

goog.provide('ol.layer.VectorTile');

goog.require('goog.object');
goog.require('ol.layer.Vector');


/**
 * @enum {string}
 */
ol.layer.VectorTileProperty = {
  PRELOAD: 'preload',
  USE_INTERIM_TILES_ON_ERROR: 'useInterimTilesOnError'
};


/**
 * @classdesc
 * Layer for vector tile data that is rendered client-side.
 * Note that any property set in the options is set as a {@link ol.Object}
 * property on the layer object; for example, setting `title: 'My Title'` in the
 * options means that `title` is observable, and has get/set accessors.
 *
 * @constructor
 * @extends {ol.layer.Vector}
 * @param {olx.layer.VectorTileOptions=} opt_options Options.
 * @api
 */
ol.layer.VectorTile = function(opt_options) {
  var options = opt_options ? opt_options : {};

  var baseOptions = goog.object.clone(options);

  delete baseOptions.preload;
  delete baseOptions.useInterimTilesOnError;
  goog.base(this,  /** @type {olx.layer.VectorOptions} */ (baseOptions));

  this.setPreload(options.preload ? options.preload : 0);
  this.setUseInterimTilesOnError(options.useInterimTilesOnError ?
      options.useInterimTilesOnError : true);

};
goog.inherits(ol.layer.VectorTile, ol.layer.Vector);


/**
 * Return the level as number to which we will preload tiles up to.
 * @return {number} The level to preload tiles up to.
 * @observable
 * @api
 */
ol.layer.VectorTile.prototype.getPreload = function() {
  return /** @type {number} */ (this.get(ol.layer.VectorTileProperty.PRELOAD));
};


/**
 * Return the associated {@link ol.source.VectorTile source} of the layer.
 * @function
 * @return {ol.source.VectorTile} Source.
 * @api
 */
ol.layer.VectorTile.prototype.getSource;


/**
 * Whether we use interim tiles on error.
 * @return {boolean} Use interim tiles on error.
 * @observable
 * @api
 */
ol.layer.VectorTile.prototype.getUseInterimTilesOnError = function() {
  return /** @type {boolean} */ (
      this.get(ol.layer.VectorTileProperty.USE_INTERIM_TILES_ON_ERROR));
};


/**
 * Set the level as number to which we will preload tiles up to.
 * @param {number} preload The level to preload tiles up to.
 * @observable
 * @api
 */
ol.layer.VectorTile.prototype.setPreload = function(preload) {
  this.set(ol.layer.TileProperty.PRELOAD, preload);
};


/**
 * Set whether we use interim tiles on error.
 * @param {boolean} useInterimTilesOnError Use interim tiles on error.
 * @observable
 * @api
 */
ol.layer.VectorTile.prototype.setUseInterimTilesOnError = function(useInterimTilesOnError) {
  this.set(
      ol.layer.TileProperty.USE_INTERIM_TILES_ON_ERROR, useInterimTilesOnError);
};

// FIXME test, especially polygons with holes and multipolygons
// FIXME need to handle large thick features (where pixel size matters)
// FIXME add offset and end to ol.geom.flat.transform.transform2D?

goog.provide('ol.render.canvas.Immediate');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.vec.Mat4');
goog.require('ol.array');
goog.require('ol.color');
goog.require('ol.extent');
goog.require('ol.geom.flat.transform');
goog.require('ol.has');
goog.require('ol.render.VectorContext');
goog.require('ol.render.canvas');
goog.require('ol.vec.Mat4');


/**
 * @classdesc
 * A concrete subclass of {@link ol.render.VectorContext} that implements
 * direct rendering of features and geometries to an HTML5 Canvas context.
 * Instances of this class are created internally by the library and
 * provided to application code as vectorContext member of the
 * {@link ol.render.Event} object associated with postcompose, precompose and
 * render events emitted by layers and maps.
 *
 * @constructor
 * @extends {ol.render.VectorContext}
 * @param {CanvasRenderingContext2D} context Context.
 * @param {number} pixelRatio Pixel ratio.
 * @param {ol.Extent} extent Extent.
 * @param {goog.vec.Mat4.Number} transform Transform.
 * @param {number} viewRotation View rotation.
 * @struct
 */
ol.render.canvas.Immediate = function(context, pixelRatio, extent, transform, viewRotation) {

  /**
   * @private
   * @type {!Object.<string,
   *        Array.<function(ol.render.canvas.Immediate)>>}
   */
  this.callbacksByZIndex_ = {};

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.context_ = context;

  /**
   * @private
   * @type {number}
   */
  this.pixelRatio_ = pixelRatio;

  /**
   * @private
   * @type {ol.Extent}
   */
  this.extent_ = extent;

  /**
   * @private
   * @type {goog.vec.Mat4.Number}
   */
  this.transform_ = transform;

  /**
   * @private
   * @type {number}
   */
  this.viewRotation_ = viewRotation;

  /**
   * @private
   * @type {?ol.render.canvas.FillState}
   */
  this.contextFillState_ = null;

  /**
   * @private
   * @type {?ol.render.canvas.StrokeState}
   */
  this.contextStrokeState_ = null;

  /**
   * @private
   * @type {?ol.render.canvas.TextState}
   */
  this.contextTextState_ = null;

  /**
   * @private
   * @type {?ol.render.canvas.FillState}
   */
  this.fillState_ = null;

  /**
   * @private
   * @type {?ol.render.canvas.StrokeState}
   */
  this.strokeState_ = null;

  /**
   * @private
   * @type {HTMLCanvasElement|HTMLVideoElement|Image}
   */
  this.image_ = null;

  /**
   * @private
   * @type {number}
   */
  this.imageAnchorX_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.imageAnchorY_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.imageHeight_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.imageOpacity_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.imageOriginX_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.imageOriginY_ = 0;

  /**
   * @private
   * @type {boolean}
   */
  this.imageRotateWithView_ = false;

  /**
   * @private
   * @type {number}
   */
  this.imageRotation_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.imageScale_ = 0;

  /**
   * @private
   * @type {boolean}
   */
  this.imageSnapToPixel_ = false;

  /**
   * @private
   * @type {number}
   */
  this.imageWidth_ = 0;

  /**
   * @private
   * @type {string}
   */
  this.text_ = '';

  /**
   * @private
   * @type {number}
   */
  this.textOffsetX_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.textOffsetY_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.textRotation_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.textScale_ = 0;

  /**
   * @private
   * @type {?ol.render.canvas.FillState}
   */
  this.textFillState_ = null;

  /**
   * @private
   * @type {?ol.render.canvas.StrokeState}
   */
  this.textStrokeState_ = null;

  /**
   * @private
   * @type {?ol.render.canvas.TextState}
   */
  this.textState_ = null;

  /**
   * @private
   * @type {Array.<number>}
   */
  this.pixelCoordinates_ = [];

  /**
   * @private
   * @type {!goog.vec.Mat4.Number}
   */
  this.tmpLocalTransform_ = goog.vec.Mat4.createNumber();

};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @private
 */
ol.render.canvas.Immediate.prototype.drawImages_ = function(flatCoordinates, offset, end, stride) {
  if (!this.image_) {
    return;
  }
  goog.asserts.assert(offset === 0, 'offset should be 0');
  goog.asserts.assert(end == flatCoordinates.length,
      'end should be equal to the length of flatCoordinates');
  var pixelCoordinates = ol.geom.flat.transform.transform2D(
      flatCoordinates, offset, end, 2, this.transform_,
      this.pixelCoordinates_);
  var context = this.context_;
  var localTransform = this.tmpLocalTransform_;
  var alpha = context.globalAlpha;
  if (this.imageOpacity_ != 1) {
    context.globalAlpha = alpha * this.imageOpacity_;
  }
  var rotation = this.imageRotation_;
  if (this.imageRotateWithView_) {
    rotation += this.viewRotation_;
  }
  var i, ii;
  for (i = 0, ii = pixelCoordinates.length; i < ii; i += 2) {
    var x = pixelCoordinates[i] - this.imageAnchorX_;
    var y = pixelCoordinates[i + 1] - this.imageAnchorY_;
    if (this.imageSnapToPixel_) {
      x = Math.round(x);
      y = Math.round(y);
    }
    if (rotation !== 0 || this.imageScale_ != 1) {
      var centerX = x + this.imageAnchorX_;
      var centerY = y + this.imageAnchorY_;
      ol.vec.Mat4.makeTransform2D(localTransform,
          centerX, centerY, this.imageScale_, this.imageScale_,
          rotation, -centerX, -centerY);
      context.setTransform(
          goog.vec.Mat4.getElement(localTransform, 0, 0),
          goog.vec.Mat4.getElement(localTransform, 1, 0),
          goog.vec.Mat4.getElement(localTransform, 0, 1),
          goog.vec.Mat4.getElement(localTransform, 1, 1),
          goog.vec.Mat4.getElement(localTransform, 0, 3),
          goog.vec.Mat4.getElement(localTransform, 1, 3));
    }
    context.drawImage(this.image_, this.imageOriginX_, this.imageOriginY_,
        this.imageWidth_, this.imageHeight_, x, y,
        this.imageWidth_, this.imageHeight_);
  }
  if (rotation !== 0 || this.imageScale_ != 1) {
    context.setTransform(1, 0, 0, 1, 0, 0);
  }
  if (this.imageOpacity_ != 1) {
    context.globalAlpha = alpha;
  }
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @private
 */
ol.render.canvas.Immediate.prototype.drawText_ = function(flatCoordinates, offset, end, stride) {
  if (!this.textState_ || this.text_ === '') {
    return;
  }
  if (this.textFillState_) {
    this.setContextFillState_(this.textFillState_);
  }
  if (this.textStrokeState_) {
    this.setContextStrokeState_(this.textStrokeState_);
  }
  this.setContextTextState_(this.textState_);
  goog.asserts.assert(offset === 0, 'offset should be 0');
  goog.asserts.assert(end == flatCoordinates.length,
      'end should be equal to the length of flatCoordinates');
  var pixelCoordinates = ol.geom.flat.transform.transform2D(
      flatCoordinates, offset, end, stride, this.transform_,
      this.pixelCoordinates_);
  var context = this.context_;
  for (; offset < end; offset += stride) {
    var x = pixelCoordinates[offset] + this.textOffsetX_;
    var y = pixelCoordinates[offset + 1] + this.textOffsetY_;
    if (this.textRotation_ !== 0 || this.textScale_ != 1) {
      var localTransform = ol.vec.Mat4.makeTransform2D(this.tmpLocalTransform_,
          x, y, this.textScale_, this.textScale_, this.textRotation_, -x, -y);
      context.setTransform(
          goog.vec.Mat4.getElement(localTransform, 0, 0),
          goog.vec.Mat4.getElement(localTransform, 1, 0),
          goog.vec.Mat4.getElement(localTransform, 0, 1),
          goog.vec.Mat4.getElement(localTransform, 1, 1),
          goog.vec.Mat4.getElement(localTransform, 0, 3),
          goog.vec.Mat4.getElement(localTransform, 1, 3));
    }
    if (this.textStrokeState_) {
      context.strokeText(this.text_, x, y);
    }
    if (this.textFillState_) {
      context.fillText(this.text_, x, y);
    }
  }
  if (this.textRotation_ !== 0 || this.textScale_ != 1) {
    context.setTransform(1, 0, 0, 1, 0, 0);
  }
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {boolean} close Close.
 * @private
 * @return {number} end End.
 */
ol.render.canvas.Immediate.prototype.moveToLineTo_ = function(flatCoordinates, offset, end, stride, close) {
  var context = this.context_;
  var pixelCoordinates = ol.geom.flat.transform.transform2D(
      flatCoordinates, offset, end, stride, this.transform_,
      this.pixelCoordinates_);
  context.moveTo(pixelCoordinates[0], pixelCoordinates[1]);
  var i;
  for (i = 2; i < pixelCoordinates.length; i += 2) {
    context.lineTo(pixelCoordinates[i], pixelCoordinates[i + 1]);
  }
  if (close) {
    context.lineTo(pixelCoordinates[0], pixelCoordinates[1]);
  }
  return end;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<number>} ends Ends.
 * @param {number} stride Stride.
 * @private
 * @return {number} End.
 */
ol.render.canvas.Immediate.prototype.drawRings_ = function(flatCoordinates, offset, ends, stride) {
  var context = this.context_;
  var i, ii;
  for (i = 0, ii = ends.length; i < ii; ++i) {
    offset = this.moveToLineTo_(
        flatCoordinates, offset, ends[i], stride, true);
    context.closePath(); // FIXME is this needed here?
  }
  return offset;
};


/**
 * Register a function to be called for rendering at a given zIndex.  The
 * function will be called asynchronously.  The callback will receive a
 * reference to {@link ol.render.canvas.Immediate} context for drawing.
 *
 * @param {number} zIndex Z index.
 * @param {function(ol.render.canvas.Immediate)} callback Callback.
 * @api
 */
ol.render.canvas.Immediate.prototype.drawAsync = function(zIndex, callback) {
  var zIndexKey = zIndex.toString();
  var callbacks = this.callbacksByZIndex_[zIndexKey];
  if (callbacks !== undefined) {
    callbacks.push(callback);
  } else {
    this.callbacksByZIndex_[zIndexKey] = [callback];
  }
};


/**
 * Render a circle geometry into the canvas.  Rendering is immediate and uses
 * the current fill and stroke styles.
 *
 * @param {ol.geom.Circle} circleGeometry Circle geometry.
 * @api
 */
ol.render.canvas.Immediate.prototype.drawCircleGeometry = function(circleGeometry) {
  if (!ol.extent.intersects(this.extent_, circleGeometry.getExtent())) {
    return;
  }
  if (this.fillState_ || this.strokeState_) {
    if (this.fillState_) {
      this.setContextFillState_(this.fillState_);
    }
    if (this.strokeState_) {
      this.setContextStrokeState_(this.strokeState_);
    }
    var pixelCoordinates = ol.geom.transformSimpleGeometry2D(
        circleGeometry, this.transform_, this.pixelCoordinates_);
    var dx = pixelCoordinates[2] - pixelCoordinates[0];
    var dy = pixelCoordinates[3] - pixelCoordinates[1];
    var radius = Math.sqrt(dx * dx + dy * dy);
    var context = this.context_;
    context.beginPath();
    context.arc(
        pixelCoordinates[0], pixelCoordinates[1], radius, 0, 2 * Math.PI);
    if (this.fillState_) {
      context.fill();
    }
    if (this.strokeState_) {
      context.stroke();
    }
  }
  if (this.text_ !== '') {
    this.drawText_(circleGeometry.getCenter(), 0, 2, 2);
  }
};


/**
 * Render a feature into the canvas.  In order to respect the zIndex of the
 * style this method draws asynchronously and thus *after* calls to
 * drawXxxxGeometry have been finished, effectively drawing the feature
 * *on top* of everything else.  You probably should be using an
 * {@link ol.layer.Vector} instead of calling this method directly.
 *
 * @param {ol.Feature} feature Feature.
 * @param {ol.style.Style} style Style.
 * @api
 */
ol.render.canvas.Immediate.prototype.drawFeature = function(feature, style) {
  var geometry = style.getGeometryFunction()(feature);
  if (!geometry ||
      !ol.extent.intersects(this.extent_, geometry.getExtent())) {
    return;
  }
  var zIndex = style.getZIndex();
  if (zIndex === undefined) {
    zIndex = 0;
  }
  this.drawAsync(zIndex, function(render) {
    render.setFillStrokeStyle(style.getFill(), style.getStroke());
    render.setImageStyle(style.getImage());
    render.setTextStyle(style.getText());
    var renderGeometry =
        ol.render.canvas.Immediate.GEOMETRY_RENDERERS_[geometry.getType()];
    goog.asserts.assert(renderGeometry !== undefined,
        'renderGeometry should be defined');
    renderGeometry.call(render, geometry, null);
  });
};


/**
 * Render a GeometryCollection to the canvas.  Rendering is immediate and
 * uses the current styles appropriate for each geometry in the collection.
 *
 * @param {ol.geom.GeometryCollection} geometryCollectionGeometry Geometry
 *     collection.
 * @param {ol.Feature} feature Feature.
 */
ol.render.canvas.Immediate.prototype.drawGeometryCollectionGeometry = function(geometryCollectionGeometry, feature) {
  var geometries = geometryCollectionGeometry.getGeometriesArray();
  var i, ii;
  for (i = 0, ii = geometries.length; i < ii; ++i) {
    var geometry = geometries[i];
    var geometryRenderer =
        ol.render.canvas.Immediate.GEOMETRY_RENDERERS_[geometry.getType()];
    goog.asserts.assert(geometryRenderer !== undefined,
        'geometryRenderer should be defined');
    geometryRenderer.call(this, geometry, feature);
  }
};


/**
 * Render a Point geometry into the canvas.  Rendering is immediate and uses
 * the current style.
 *
 * @param {ol.geom.Point|ol.render.Feature} pointGeometry Point geometry.
 * @api
 */
ol.render.canvas.Immediate.prototype.drawPointGeometry = function(pointGeometry) {
  var flatCoordinates = pointGeometry.getFlatCoordinates();
  var stride = pointGeometry.getStride();
  if (this.image_) {
    this.drawImages_(flatCoordinates, 0, flatCoordinates.length, stride);
  }
  if (this.text_ !== '') {
    this.drawText_(flatCoordinates, 0, flatCoordinates.length, stride);
  }
};


/**
 * Render a MultiPoint geometry  into the canvas.  Rendering is immediate and
 * uses the current style.
 *
 * @param {ol.geom.MultiPoint|ol.render.Feature} multiPointGeometry MultiPoint
 *     geometry.
 * @api
 */
ol.render.canvas.Immediate.prototype.drawMultiPointGeometry = function(multiPointGeometry) {
  var flatCoordinates = multiPointGeometry.getFlatCoordinates();
  var stride = multiPointGeometry.getStride();
  if (this.image_) {
    this.drawImages_(flatCoordinates, 0, flatCoordinates.length, stride);
  }
  if (this.text_ !== '') {
    this.drawText_(flatCoordinates, 0, flatCoordinates.length, stride);
  }
};


/**
 * Render a LineString into the canvas.  Rendering is immediate and uses
 * the current style.
 *
 * @param {ol.geom.LineString|ol.render.Feature} lineStringGeometry Line
 *     string geometry.
 * @api
 */
ol.render.canvas.Immediate.prototype.drawLineStringGeometry = function(lineStringGeometry) {
  if (!ol.extent.intersects(this.extent_, lineStringGeometry.getExtent())) {
    return;
  }
  if (this.strokeState_) {
    this.setContextStrokeState_(this.strokeState_);
    var context = this.context_;
    var flatCoordinates = lineStringGeometry.getFlatCoordinates();
    context.beginPath();
    this.moveToLineTo_(flatCoordinates, 0, flatCoordinates.length,
        lineStringGeometry.getStride(), false);
    context.stroke();
  }
  if (this.text_ !== '') {
    var flatMidpoint = lineStringGeometry.getFlatMidpoint();
    this.drawText_(flatMidpoint, 0, 2, 2);
  }
};


/**
 * Render a MultiLineString geometry into the canvas.  Rendering is immediate
 * and uses the current style.
 *
 * @param {ol.geom.MultiLineString|ol.render.Feature} multiLineStringGeometry
 *     MultiLineString geometry.
 * @api
 */
ol.render.canvas.Immediate.prototype.drawMultiLineStringGeometry = function(multiLineStringGeometry) {
  var geometryExtent = multiLineStringGeometry.getExtent();
  if (!ol.extent.intersects(this.extent_, geometryExtent)) {
    return;
  }
  if (this.strokeState_) {
    this.setContextStrokeState_(this.strokeState_);
    var context = this.context_;
    var flatCoordinates = multiLineStringGeometry.getFlatCoordinates();
    var offset = 0;
    var ends = multiLineStringGeometry.getEnds();
    var stride = multiLineStringGeometry.getStride();
    context.beginPath();
    var i, ii;
    for (i = 0, ii = ends.length; i < ii; ++i) {
      offset = this.moveToLineTo_(
          flatCoordinates, offset, ends[i], stride, false);
    }
    context.stroke();
  }
  if (this.text_ !== '') {
    var flatMidpoints = multiLineStringGeometry.getFlatMidpoints();
    this.drawText_(flatMidpoints, 0, flatMidpoints.length, 2);
  }
};


/**
 * Render a Polygon geometry into the canvas.  Rendering is immediate and uses
 * the current style.
 *
 * @param {ol.geom.Polygon|ol.render.Feature} polygonGeometry Polygon
 *     geometry.
 * @api
 */
ol.render.canvas.Immediate.prototype.drawPolygonGeometry = function(polygonGeometry) {
  if (!ol.extent.intersects(this.extent_, polygonGeometry.getExtent())) {
    return;
  }
  if (this.strokeState_ || this.fillState_) {
    if (this.fillState_) {
      this.setContextFillState_(this.fillState_);
    }
    if (this.strokeState_) {
      this.setContextStrokeState_(this.strokeState_);
    }
    var context = this.context_;
    context.beginPath();
    this.drawRings_(polygonGeometry.getOrientedFlatCoordinates(),
        0, polygonGeometry.getEnds(), polygonGeometry.getStride());
    if (this.fillState_) {
      context.fill();
    }
    if (this.strokeState_) {
      context.stroke();
    }
  }
  if (this.text_ !== '') {
    var flatInteriorPoint = polygonGeometry.getFlatInteriorPoint();
    this.drawText_(flatInteriorPoint, 0, 2, 2);
  }
};


/**
 * Render MultiPolygon geometry into the canvas.  Rendering is immediate and
 * uses the current style.
 * @param {ol.geom.MultiPolygon} multiPolygonGeometry MultiPolygon geometry.
 * @api
 */
ol.render.canvas.Immediate.prototype.drawMultiPolygonGeometry = function(multiPolygonGeometry) {
  if (!ol.extent.intersects(this.extent_, multiPolygonGeometry.getExtent())) {
    return;
  }
  if (this.strokeState_ || this.fillState_) {
    if (this.fillState_) {
      this.setContextFillState_(this.fillState_);
    }
    if (this.strokeState_) {
      this.setContextStrokeState_(this.strokeState_);
    }
    var context = this.context_;
    var flatCoordinates = multiPolygonGeometry.getOrientedFlatCoordinates();
    var offset = 0;
    var endss = multiPolygonGeometry.getEndss();
    var stride = multiPolygonGeometry.getStride();
    var i, ii;
    for (i = 0, ii = endss.length; i < ii; ++i) {
      var ends = endss[i];
      context.beginPath();
      offset = this.drawRings_(flatCoordinates, offset, ends, stride);
      if (this.fillState_) {
        context.fill();
      }
      if (this.strokeState_) {
        context.stroke();
      }
    }
  }
  if (this.text_ !== '') {
    var flatInteriorPoints = multiPolygonGeometry.getFlatInteriorPoints();
    this.drawText_(flatInteriorPoints, 0, flatInteriorPoints.length, 2);
  }
};


/**
 * @inheritDoc
 */
ol.render.canvas.Immediate.prototype.drawText = goog.abstractMethod;


/**
 * FIXME: empty description for jsdoc
 */
ol.render.canvas.Immediate.prototype.flush = function() {
  /** @type {Array.<number>} */
  var zs = Object.keys(this.callbacksByZIndex_).map(Number);
  zs.sort(ol.array.numberSafeCompareFunction);
  var i, ii, callbacks, j, jj;
  for (i = 0, ii = zs.length; i < ii; ++i) {
    callbacks = this.callbacksByZIndex_[zs[i].toString()];
    for (j = 0, jj = callbacks.length; j < jj; ++j) {
      callbacks[j](this);
    }
  }
};


/**
 * @param {ol.render.canvas.FillState} fillState Fill state.
 * @private
 */
ol.render.canvas.Immediate.prototype.setContextFillState_ = function(fillState) {
  var context = this.context_;
  var contextFillState = this.contextFillState_;
  if (!contextFillState) {
    context.fillStyle = fillState.fillStyle;
    this.contextFillState_ = {
      fillStyle: fillState.fillStyle
    };
  } else {
    if (contextFillState.fillStyle != fillState.fillStyle) {
      contextFillState.fillStyle = context.fillStyle = fillState.fillStyle;
    }
  }
};


/**
 * @param {ol.render.canvas.StrokeState} strokeState Stroke state.
 * @private
 */
ol.render.canvas.Immediate.prototype.setContextStrokeState_ = function(strokeState) {
  var context = this.context_;
  var contextStrokeState = this.contextStrokeState_;
  if (!contextStrokeState) {
    context.lineCap = strokeState.lineCap;
    if (ol.has.CANVAS_LINE_DASH) {
      context.setLineDash(strokeState.lineDash);
    }
    context.lineJoin = strokeState.lineJoin;
    context.lineWidth = strokeState.lineWidth;
    context.miterLimit = strokeState.miterLimit;
    context.strokeStyle = strokeState.strokeStyle;
    this.contextStrokeState_ = {
      lineCap: strokeState.lineCap,
      lineDash: strokeState.lineDash,
      lineJoin: strokeState.lineJoin,
      lineWidth: strokeState.lineWidth,
      miterLimit: strokeState.miterLimit,
      strokeStyle: strokeState.strokeStyle
    };
  } else {
    if (contextStrokeState.lineCap != strokeState.lineCap) {
      contextStrokeState.lineCap = context.lineCap = strokeState.lineCap;
    }
    if (ol.has.CANVAS_LINE_DASH) {
      if (!goog.array.equals(
          contextStrokeState.lineDash, strokeState.lineDash)) {
        context.setLineDash(contextStrokeState.lineDash = strokeState.lineDash);
      }
    }
    if (contextStrokeState.lineJoin != strokeState.lineJoin) {
      contextStrokeState.lineJoin = context.lineJoin = strokeState.lineJoin;
    }
    if (contextStrokeState.lineWidth != strokeState.lineWidth) {
      contextStrokeState.lineWidth = context.lineWidth = strokeState.lineWidth;
    }
    if (contextStrokeState.miterLimit != strokeState.miterLimit) {
      contextStrokeState.miterLimit = context.miterLimit =
          strokeState.miterLimit;
    }
    if (contextStrokeState.strokeStyle != strokeState.strokeStyle) {
      contextStrokeState.strokeStyle = context.strokeStyle =
          strokeState.strokeStyle;
    }
  }
};


/**
 * @param {ol.render.canvas.TextState} textState Text state.
 * @private
 */
ol.render.canvas.Immediate.prototype.setContextTextState_ = function(textState) {
  var context = this.context_;
  var contextTextState = this.contextTextState_;
  if (!contextTextState) {
    context.font = textState.font;
    context.textAlign = textState.textAlign;
    context.textBaseline = textState.textBaseline;
    this.contextTextState_ = {
      font: textState.font,
      textAlign: textState.textAlign,
      textBaseline: textState.textBaseline
    };
  } else {
    if (contextTextState.font != textState.font) {
      contextTextState.font = context.font = textState.font;
    }
    if (contextTextState.textAlign != textState.textAlign) {
      contextTextState.textAlign = context.textAlign = textState.textAlign;
    }
    if (contextTextState.textBaseline != textState.textBaseline) {
      contextTextState.textBaseline = context.textBaseline =
          textState.textBaseline;
    }
  }
};


/**
 * Set the fill and stroke style for subsequent draw operations.  To clear
 * either fill or stroke styles, pass null for the appropriate parameter.
 *
 * @param {ol.style.Fill} fillStyle Fill style.
 * @param {ol.style.Stroke} strokeStyle Stroke style.
 * @api
 */
ol.render.canvas.Immediate.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) {
  if (!fillStyle) {
    this.fillState_ = null;
  } else {
    var fillStyleColor = fillStyle.getColor();
    this.fillState_ = {
      fillStyle: ol.color.asString(fillStyleColor ?
          fillStyleColor : ol.render.canvas.defaultFillStyle)
    };
  }
  if (!strokeStyle) {
    this.strokeState_ = null;
  } else {
    var strokeStyleColor = strokeStyle.getColor();
    var strokeStyleLineCap = strokeStyle.getLineCap();
    var strokeStyleLineDash = strokeStyle.getLineDash();
    var strokeStyleLineJoin = strokeStyle.getLineJoin();
    var strokeStyleWidth = strokeStyle.getWidth();
    var strokeStyleMiterLimit = strokeStyle.getMiterLimit();
    this.strokeState_ = {
      lineCap: strokeStyleLineCap !== undefined ?
          strokeStyleLineCap : ol.render.canvas.defaultLineCap,
      lineDash: strokeStyleLineDash ?
          strokeStyleLineDash : ol.render.canvas.defaultLineDash,
      lineJoin: strokeStyleLineJoin !== undefined ?
          strokeStyleLineJoin : ol.render.canvas.defaultLineJoin,
      lineWidth: this.pixelRatio_ * (strokeStyleWidth !== undefined ?
          strokeStyleWidth : ol.render.canvas.defaultLineWidth),
      miterLimit: strokeStyleMiterLimit !== undefined ?
          strokeStyleMiterLimit : ol.render.canvas.defaultMiterLimit,
      strokeStyle: ol.color.asString(strokeStyleColor ?
          strokeStyleColor : ol.render.canvas.defaultStrokeStyle)
    };
  }
};


/**
 * Set the image style for subsequent draw operations.  Pass null to remove
 * the image style.
 *
 * @param {ol.style.Image} imageStyle Image style.
 * @api
 */
ol.render.canvas.Immediate.prototype.setImageStyle = function(imageStyle) {
  if (!imageStyle) {
    this.image_ = null;
  } else {
    var imageAnchor = imageStyle.getAnchor();
    // FIXME pixel ratio
    var imageImage = imageStyle.getImage(1);
    var imageOrigin = imageStyle.getOrigin();
    var imageSize = imageStyle.getSize();
    goog.asserts.assert(imageAnchor, 'imageAnchor must be truthy');
    goog.asserts.assert(imageImage, 'imageImage must be truthy');
    goog.asserts.assert(imageOrigin, 'imageOrigin must be truthy');
    goog.asserts.assert(imageSize, 'imageSize must be truthy');
    this.imageAnchorX_ = imageAnchor[0];
    this.imageAnchorY_ = imageAnchor[1];
    this.imageHeight_ = imageSize[1];
    this.image_ = imageImage;
    this.imageOpacity_ = imageStyle.getOpacity();
    this.imageOriginX_ = imageOrigin[0];
    this.imageOriginY_ = imageOrigin[1];
    this.imageRotateWithView_ = imageStyle.getRotateWithView();
    this.imageRotation_ = imageStyle.getRotation();
    this.imageScale_ = imageStyle.getScale();
    this.imageSnapToPixel_ = imageStyle.getSnapToPixel();
    this.imageWidth_ = imageSize[0];
  }
};


/**
 * Set the text style for subsequent draw operations.  Pass null to
 * remove the text style.
 *
 * @param {ol.style.Text} textStyle Text style.
 * @api
 */
ol.render.canvas.Immediate.prototype.setTextStyle = function(textStyle) {
  if (!textStyle) {
    this.text_ = '';
  } else {
    var textFillStyle = textStyle.getFill();
    if (!textFillStyle) {
      this.textFillState_ = null;
    } else {
      var textFillStyleColor = textFillStyle.getColor();
      this.textFillState_ = {
        fillStyle: ol.color.asString(textFillStyleColor ?
            textFillStyleColor : ol.render.canvas.defaultFillStyle)
      };
    }
    var textStrokeStyle = textStyle.getStroke();
    if (!textStrokeStyle) {
      this.textStrokeState_ = null;
    } else {
      var textStrokeStyleColor = textStrokeStyle.getColor();
      var textStrokeStyleLineCap = textStrokeStyle.getLineCap();
      var textStrokeStyleLineDash = textStrokeStyle.getLineDash();
      var textStrokeStyleLineJoin = textStrokeStyle.getLineJoin();
      var textStrokeStyleWidth = textStrokeStyle.getWidth();
      var textStrokeStyleMiterLimit = textStrokeStyle.getMiterLimit();
      this.textStrokeState_ = {
        lineCap: textStrokeStyleLineCap !== undefined ?
            textStrokeStyleLineCap : ol.render.canvas.defaultLineCap,
        lineDash: textStrokeStyleLineDash ?
            textStrokeStyleLineDash : ol.render.canvas.defaultLineDash,
        lineJoin: textStrokeStyleLineJoin !== undefined ?
            textStrokeStyleLineJoin : ol.render.canvas.defaultLineJoin,
        lineWidth: textStrokeStyleWidth !== undefined ?
            textStrokeStyleWidth : ol.render.canvas.defaultLineWidth,
        miterLimit: textStrokeStyleMiterLimit !== undefined ?
            textStrokeStyleMiterLimit : ol.render.canvas.defaultMiterLimit,
        strokeStyle: ol.color.asString(textStrokeStyleColor ?
            textStrokeStyleColor : ol.render.canvas.defaultStrokeStyle)
      };
    }
    var textFont = textStyle.getFont();
    var textOffsetX = textStyle.getOffsetX();
    var textOffsetY = textStyle.getOffsetY();
    var textRotation = textStyle.getRotation();
    var textScale = textStyle.getScale();
    var textText = textStyle.getText();
    var textTextAlign = textStyle.getTextAlign();
    var textTextBaseline = textStyle.getTextBaseline();
    this.textState_ = {
      font: textFont !== undefined ?
          textFont : ol.render.canvas.defaultFont,
      textAlign: textTextAlign !== undefined ?
          textTextAlign : ol.render.canvas.defaultTextAlign,
      textBaseline: textTextBaseline !== undefined ?
          textTextBaseline : ol.render.canvas.defaultTextBaseline
    };
    this.text_ = textText !== undefined ? textText : '';
    this.textOffsetX_ =
        textOffsetX !== undefined ? (this.pixelRatio_ * textOffsetX) : 0;
    this.textOffsetY_ =
        textOffsetY !== undefined ? (this.pixelRatio_ * textOffsetY) : 0;
    this.textRotation_ = textRotation !== undefined ? textRotation : 0;
    this.textScale_ = this.pixelRatio_ * (textScale !== undefined ?
        textScale : 1);
  }
};


/**
 * @const
 * @private
 * @type {Object.<ol.geom.GeometryType,
 *     function(this: ol.render.canvas.Immediate,
 *         (ol.geom.Geometry|ol.render.Feature), Object)>}
 */
ol.render.canvas.Immediate.GEOMETRY_RENDERERS_ = {
  'Point': ol.render.canvas.Immediate.prototype.drawPointGeometry,
  'LineString': ol.render.canvas.Immediate.prototype.drawLineStringGeometry,
  'Polygon': ol.render.canvas.Immediate.prototype.drawPolygonGeometry,
  'MultiPoint': ol.render.canvas.Immediate.prototype.drawMultiPointGeometry,
  'MultiLineString':
      ol.render.canvas.Immediate.prototype.drawMultiLineStringGeometry,
  'MultiPolygon': ol.render.canvas.Immediate.prototype.drawMultiPolygonGeometry,
  'GeometryCollection':
      ol.render.canvas.Immediate.prototype.drawGeometryCollectionGeometry,
  'Circle': ol.render.canvas.Immediate.prototype.drawCircleGeometry
};

goog.provide('ol.renderer.canvas.Layer');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.vec.Mat4');
goog.require('ol.dom');
goog.require('ol.extent');
goog.require('ol.layer.Layer');
goog.require('ol.render.Event');
goog.require('ol.render.EventType');
goog.require('ol.render.canvas.Immediate');
goog.require('ol.renderer.Layer');
goog.require('ol.vec.Mat4');


/**
 * @constructor
 * @extends {ol.renderer.Layer}
 * @param {ol.layer.Layer} layer Layer.
 */
ol.renderer.canvas.Layer = function(layer) {

  goog.base(this, layer);

  /**
   * @private
   * @type {!goog.vec.Mat4.Number}
   */
  this.transform_ = goog.vec.Mat4.createNumber();

};
goog.inherits(ol.renderer.canvas.Layer, ol.renderer.Layer);


/**
 * @param {olx.FrameState} frameState Frame state.
 * @param {ol.layer.LayerState} layerState Layer state.
 * @param {CanvasRenderingContext2D} context Context.
 */
ol.renderer.canvas.Layer.prototype.composeFrame = function(frameState, layerState, context) {

  this.dispatchPreComposeEvent(context, frameState);

  var image = this.getImage();
  if (image) {

    // clipped rendering if layer extent is set
    var extent = layerState.extent;
    var clipped = extent !== undefined;
    if (clipped) {
      goog.asserts.assert(extent !== undefined,
          'layerState extent is defined');
      var pixelRatio = frameState.pixelRatio;
      var topLeft = ol.extent.getTopLeft(extent);
      var topRight = ol.extent.getTopRight(extent);
      var bottomRight = ol.extent.getBottomRight(extent);
      var bottomLeft = ol.extent.getBottomLeft(extent);

      ol.vec.Mat4.multVec2(frameState.coordinateToPixelMatrix,
          topLeft, topLeft);
      ol.vec.Mat4.multVec2(frameState.coordinateToPixelMatrix,
          topRight, topRight);
      ol.vec.Mat4.multVec2(frameState.coordinateToPixelMatrix,
          bottomRight, bottomRight);
      ol.vec.Mat4.multVec2(frameState.coordinateToPixelMatrix,
          bottomLeft, bottomLeft);

      context.save();
      context.beginPath();
      context.moveTo(topLeft[0] * pixelRatio, topLeft[1] * pixelRatio);
      context.lineTo(topRight[0] * pixelRatio, topRight[1] * pixelRatio);
      context.lineTo(bottomRight[0] * pixelRatio, bottomRight[1] * pixelRatio);
      context.lineTo(bottomLeft[0] * pixelRatio, bottomLeft[1] * pixelRatio);
      context.clip();
    }

    var imageTransform = this.getImageTransform();
    // for performance reasons, context.save / context.restore is not used
    // to save and restore the transformation matrix and the opacity.
    // see http://jsperf.com/context-save-restore-versus-variable
    var alpha = context.globalAlpha;
    context.globalAlpha = layerState.opacity;

    // for performance reasons, context.setTransform is only used
    // when the view is rotated. see http://jsperf.com/canvas-transform
    if (frameState.viewState.rotation === 0) {
      var dx = goog.vec.Mat4.getElement(imageTransform, 0, 3);
      var dy = goog.vec.Mat4.getElement(imageTransform, 1, 3);
      var dw = image.width * goog.vec.Mat4.getElement(imageTransform, 0, 0);
      var dh = image.height * goog.vec.Mat4.getElement(imageTransform, 1, 1);
      context.drawImage(image, 0, 0, +image.width, +image.height,
          Math.round(dx), Math.round(dy), Math.round(dw), Math.round(dh));
    } else {
      context.setTransform(
          goog.vec.Mat4.getElement(imageTransform, 0, 0),
          goog.vec.Mat4.getElement(imageTransform, 1, 0),
          goog.vec.Mat4.getElement(imageTransform, 0, 1),
          goog.vec.Mat4.getElement(imageTransform, 1, 1),
          goog.vec.Mat4.getElement(imageTransform, 0, 3),
          goog.vec.Mat4.getElement(imageTransform, 1, 3));
      context.drawImage(image, 0, 0);
      context.setTransform(1, 0, 0, 1, 0, 0);
    }
    context.globalAlpha = alpha;

    if (clipped) {
      context.restore();
    }
  }

  this.dispatchPostComposeEvent(context, frameState);

};


/**
 * @param {ol.render.EventType} type Event type.
 * @param {CanvasRenderingContext2D} context Context.
 * @param {olx.FrameState} frameState Frame state.
 * @param {goog.vec.Mat4.Number=} opt_transform Transform.
 * @private
 */
ol.renderer.canvas.Layer.prototype.dispatchComposeEvent_ = function(type, context, frameState, opt_transform) {
  var layer = this.getLayer();
  if (layer.hasListener(type)) {
    var transform = opt_transform !== undefined ?
        opt_transform : this.getTransform(frameState, 0);
    var render = new ol.render.canvas.Immediate(
        context, frameState.pixelRatio, frameState.extent, transform,
        frameState.viewState.rotation);
    var composeEvent = new ol.render.Event(type, layer, render, frameState,
        context, null);
    layer.dispatchEvent(composeEvent);
    render.flush();
  }
};


/**
 * @param {CanvasRenderingContext2D} context Context.
 * @param {olx.FrameState} frameState Frame state.
 * @param {goog.vec.Mat4.Number=} opt_transform Transform.
 * @protected
 */
ol.renderer.canvas.Layer.prototype.dispatchPostComposeEvent = function(context, frameState, opt_transform) {
  this.dispatchComposeEvent_(ol.render.EventType.POSTCOMPOSE, context,
      frameState, opt_transform);
};


/**
 * @param {CanvasRenderingContext2D} context Context.
 * @param {olx.FrameState} frameState Frame state.
 * @param {goog.vec.Mat4.Number=} opt_transform Transform.
 * @protected
 */
ol.renderer.canvas.Layer.prototype.dispatchPreComposeEvent = function(context, frameState, opt_transform) {
  this.dispatchComposeEvent_(ol.render.EventType.PRECOMPOSE, context,
      frameState, opt_transform);
};


/**
 * @param {CanvasRenderingContext2D} context Context.
 * @param {olx.FrameState} frameState Frame state.
 * @param {goog.vec.Mat4.Number=} opt_transform Transform.
 * @protected
 */
ol.renderer.canvas.Layer.prototype.dispatchRenderEvent = function(context, frameState, opt_transform) {
  this.dispatchComposeEvent_(ol.render.EventType.RENDER, context,
      frameState, opt_transform);
};


/**
 * @return {HTMLCanvasElement|HTMLVideoElement|Image} Canvas.
 */
ol.renderer.canvas.Layer.prototype.getImage = goog.abstractMethod;


/**
 * @return {!goog.vec.Mat4.Number} Image transform.
 */
ol.renderer.canvas.Layer.prototype.getImageTransform = goog.abstractMethod;


/**
 * @param {olx.FrameState} frameState Frame state.
 * @param {number} offsetX Offset on the x-axis in view coordinates.
 * @protected
 * @return {!goog.vec.Mat4.Number} Transform.
 */
ol.renderer.canvas.Layer.prototype.getTransform = function(frameState, offsetX) {
  var viewState = frameState.viewState;
  var pixelRatio = frameState.pixelRatio;
  return ol.vec.Mat4.makeTransform2D(this.transform_,
      pixelRatio * frameState.size[0] / 2,
      pixelRatio * frameState.size[1] / 2,
      pixelRatio / viewState.resolution,
      -pixelRatio / viewState.resolution,
      -viewState.rotation,
      -viewState.center[0] + offsetX,
      -viewState.center[1]);
};


/**
 * @param {olx.FrameState} frameState Frame state.
 * @param {ol.layer.LayerState} layerState Layer state.
 * @return {boolean} whether composeFrame should be called.
 */
ol.renderer.canvas.Layer.prototype.prepareFrame = goog.abstractMethod;


/**
 * @param {ol.Pixel} pixelOnMap Pixel.
 * @param {goog.vec.Mat4.Number} imageTransformInv The transformation matrix
 *        to convert from a map pixel to a canvas pixel.
 * @return {ol.Pixel} The pixel.
 * @protected
 */
ol.renderer.canvas.Layer.prototype.getPixelOnCanvas = function(pixelOnMap, imageTransformInv) {
  var pixelOnCanvas = [0, 0];
  ol.vec.Mat4.multVec2(imageTransformInv, pixelOnMap, pixelOnCanvas);
  return pixelOnCanvas;
};


/**
 * @param {ol.Size} size Size.
 * @return {boolean} True when the canvas with the current size does not exceed
 *     the maximum dimensions.
 */
ol.renderer.canvas.Layer.testCanvasSize = (function() {

  /**
   * @type {CanvasRenderingContext2D}
   */
  var context = null;

  /**
   * @type {ImageData}
   */
  var imageData = null;

  return function(size) {
    if (!context) {
      context = ol.dom.createCanvasContext2D(1, 1);
      imageData = context.createImageData(1, 1);
      var data = imageData.data;
      data[0] = 42;
      data[1] = 84;
      data[2] = 126;
      data[3] = 255;
    }
    var canvas = context.canvas;
    var good = size[0] <= canvas.width && size[1] <= canvas.height;
    if (!good) {
      canvas.width = size[0];
      canvas.height = size[1];
      var x = size[0] - 1;
      var y = size[1] - 1;
      context.putImageData(imageData, x, y);
      var result = context.getImageData(x, y, 1, 1);
      good = goog.array.equals(imageData.data, result.data);
    }
    return good;
  };
})();

goog.provide('ol.render.IReplayGroup');

goog.require('ol.render.VectorContext');


/**
 * @enum {string}
 */
ol.render.ReplayType = {
  IMAGE: 'Image',
  LINE_STRING: 'LineString',
  POLYGON: 'Polygon',
  TEXT: 'Text'
};


/**
 * @const
 * @type {Array.<ol.render.ReplayType>}
 */
ol.render.REPLAY_ORDER = [
  ol.render.ReplayType.POLYGON,
  ol.render.ReplayType.LINE_STRING,
  ol.render.ReplayType.IMAGE,
  ol.render.ReplayType.TEXT
];


/**
 * @interface
 */
ol.render.IReplayGroup = function() {
};


/* eslint-disable valid-jsdoc */
// TODO: enable valid-jsdoc for @interface methods when this issue is resolved
// https://github.com/eslint/eslint/issues/4887


/**
 * @param {number|undefined} zIndex Z index.
 * @param {ol.render.ReplayType} replayType Replay type.
 * @return {ol.render.VectorContext} Replay.
 */
ol.render.IReplayGroup.prototype.getReplay = function(zIndex, replayType) {
};


/**
 * @return {boolean} Is empty.
 */
ol.render.IReplayGroup.prototype.isEmpty = function() {
};

// FIXME add option to apply snapToPixel to all coordinates?
// FIXME can eliminate empty set styles and strokes (when all geoms skipped)

goog.provide('ol.render.canvas.ImageReplay');
goog.provide('ol.render.canvas.LineStringReplay');
goog.provide('ol.render.canvas.PolygonReplay');
goog.provide('ol.render.canvas.Replay');
goog.provide('ol.render.canvas.ReplayGroup');
goog.provide('ol.render.canvas.TextReplay');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.object');
goog.require('goog.vec.Mat4');
goog.require('ol');
goog.require('ol.array');
goog.require('ol.color');
goog.require('ol.dom');
goog.require('ol.extent');
goog.require('ol.extent.Relationship');
goog.require('ol.geom.flat.simplify');
goog.require('ol.geom.flat.transform');
goog.require('ol.has');
goog.require('ol.render.IReplayGroup');
goog.require('ol.render.VectorContext');
goog.require('ol.render.canvas');
goog.require('ol.vec.Mat4');


/**
 * @enum {number}
 */
ol.render.canvas.Instruction = {
  BEGIN_GEOMETRY: 0,
  BEGIN_PATH: 1,
  CIRCLE: 2,
  CLOSE_PATH: 3,
  DRAW_IMAGE: 4,
  DRAW_TEXT: 5,
  END_GEOMETRY: 6,
  FILL: 7,
  MOVE_TO_LINE_TO: 8,
  SET_FILL_STYLE: 9,
  SET_STROKE_STYLE: 10,
  SET_TEXT_STYLE: 11,
  STROKE: 12
};


/**
 * @constructor
 * @extends {ol.render.VectorContext}
 * @param {number} tolerance Tolerance.
 * @param {ol.Extent} maxExtent Maximum extent.
 * @param {number} resolution Resolution.
 * @protected
 * @struct
 */
ol.render.canvas.Replay = function(tolerance, maxExtent, resolution) {
  goog.base(this);

  /**
   * @protected
   * @type {number}
   */
  this.tolerance = tolerance;

  /**
   * @protected
   * @const
   * @type {ol.Extent}
   */
  this.maxExtent = maxExtent;

  /**
   * @private
   * @type {ol.Extent}
   */
  this.bufferedMaxExtent_ = null;

  /**
   * @protected
   * @type {number}
   */
  this.maxLineWidth = 0;

  /**
   * @protected
   * @const
   * @type {number}
   */
  this.resolution = resolution;

  /**
   * @private
   * @type {Array.<*>}
   */
  this.beginGeometryInstruction1_ = null;

  /**
   * @private
   * @type {Array.<*>}
   */
  this.beginGeometryInstruction2_ = null;

  /**
   * @protected
   * @type {Array.<*>}
   */
  this.instructions = [];

  /**
   * @protected
   * @type {Array.<number>}
   */
  this.coordinates = [];

  /**
   * @private
   * @type {goog.vec.Mat4.Number}
   */
  this.renderedTransform_ = goog.vec.Mat4.createNumber();

  /**
   * @protected
   * @type {Array.<*>}
   */
  this.hitDetectionInstructions = [];

  /**
   * @private
   * @type {Array.<number>}
   */
  this.pixelCoordinates_ = [];

  /**
   * @private
   * @type {!goog.vec.Mat4.Number}
   */
  this.tmpLocalTransform_ = goog.vec.Mat4.createNumber();

  /**
   * @private
   * @type {!goog.vec.Mat4.Number}
   */
  this.tmpLocalTransformInv_ = goog.vec.Mat4.createNumber();
};
goog.inherits(ol.render.canvas.Replay, ol.render.VectorContext);


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {boolean} close Close.
 * @protected
 * @return {number} My end.
 */
ol.render.canvas.Replay.prototype.appendFlatCoordinates = function(flatCoordinates, offset, end, stride, close) {

  var myEnd = this.coordinates.length;
  var extent = this.getBufferedMaxExtent();
  var lastCoord = [flatCoordinates[offset], flatCoordinates[offset + 1]];
  var nextCoord = [NaN, NaN];
  var skipped = true;

  var i, lastRel, nextRel;
  for (i = offset + stride; i < end; i += stride) {
    nextCoord[0] = flatCoordinates[i];
    nextCoord[1] = flatCoordinates[i + 1];
    nextRel = ol.extent.coordinateRelationship(extent, nextCoord);
    if (nextRel !== lastRel) {
      if (skipped) {
        this.coordinates[myEnd++] = lastCoord[0];
        this.coordinates[myEnd++] = lastCoord[1];
      }
      this.coordinates[myEnd++] = nextCoord[0];
      this.coordinates[myEnd++] = nextCoord[1];
      skipped = false;
    } else if (nextRel === ol.extent.Relationship.INTERSECTING) {
      this.coordinates[myEnd++] = nextCoord[0];
      this.coordinates[myEnd++] = nextCoord[1];
      skipped = false;
    } else {
      skipped = true;
    }
    lastCoord[0] = nextCoord[0];
    lastCoord[1] = nextCoord[1];
    lastRel = nextRel;
  }

  // handle case where there is only one point to append
  if (i === offset + stride) {
    this.coordinates[myEnd++] = lastCoord[0];
    this.coordinates[myEnd++] = lastCoord[1];
  }

  if (close) {
    this.coordinates[myEnd++] = flatCoordinates[offset];
    this.coordinates[myEnd++] = flatCoordinates[offset + 1];
  }
  return myEnd;
};


/**
 * @protected
 * @param {ol.geom.Geometry|ol.render.Feature} geometry Geometry.
 * @param {ol.Feature|ol.render.Feature} feature Feature.
 */
ol.render.canvas.Replay.prototype.beginGeometry = function(geometry, feature) {
  this.beginGeometryInstruction1_ =
      [ol.render.canvas.Instruction.BEGIN_GEOMETRY, feature, 0];
  this.instructions.push(this.beginGeometryInstruction1_);
  this.beginGeometryInstruction2_ =
      [ol.render.canvas.Instruction.BEGIN_GEOMETRY, feature, 0];
  this.hitDetectionInstructions.push(this.beginGeometryInstruction2_);
};


/**
 * @private
 * @param {CanvasRenderingContext2D} context Context.
 * @param {number} pixelRatio Pixel ratio.
 * @param {goog.vec.Mat4.Number} transform Transform.
 * @param {number} viewRotation View rotation.
 * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
 *     to skip.
 * @param {Array.<*>} instructions Instructions array.
 * @param {function((ol.Feature|ol.render.Feature)): T|undefined}
 *     featureCallback Feature callback.
 * @param {ol.Extent=} opt_hitExtent Only check features that intersect this
 *     extent.
 * @return {T|undefined} Callback result.
 * @template T
 */
ol.render.canvas.Replay.prototype.replay_ = function(
    context, pixelRatio, transform, viewRotation, skippedFeaturesHash,
    instructions, featureCallback, opt_hitExtent) {
  /** @type {Array.<number>} */
  var pixelCoordinates;
  if (ol.vec.Mat4.equals2D(transform, this.renderedTransform_)) {
    pixelCoordinates = this.pixelCoordinates_;
  } else {
    pixelCoordinates = ol.geom.flat.transform.transform2D(
        this.coordinates, 0, this.coordinates.length, 2,
        transform, this.pixelCoordinates_);
    goog.vec.Mat4.setFromArray(this.renderedTransform_, transform);
    goog.asserts.assert(pixelCoordinates === this.pixelCoordinates_,
        'pixelCoordinates should be the same as this.pixelCoordinates_');
  }
  var skipFeatures = !goog.object.isEmpty(skippedFeaturesHash);
  var i = 0; // instruction index
  var ii = instructions.length; // end of instructions
  var d = 0; // data index
  var dd; // end of per-instruction data
  var localTransform = this.tmpLocalTransform_;
  var localTransformInv = this.tmpLocalTransformInv_;
  var prevX, prevY, roundX, roundY;
  while (i < ii) {
    var instruction = instructions[i];
    var type = /** @type {ol.render.canvas.Instruction} */ (instruction[0]);
    var feature, fill, stroke, text, x, y;
    switch (type) {
      case ol.render.canvas.Instruction.BEGIN_GEOMETRY:
        feature = /** @type {ol.Feature|ol.render.Feature} */ (instruction[1]);
        if ((skipFeatures &&
            skippedFeaturesHash[goog.getUid(feature).toString()]) ||
            !feature.getGeometry()) {
          i = /** @type {number} */ (instruction[2]);
        } else if (opt_hitExtent !== undefined && !ol.extent.intersects(
            /** @type {Array<number>} */ (opt_hitExtent),
            feature.getGeometry().getExtent())) {
          i = /** @type {number} */ (instruction[2]);
        } else {
          ++i;
        }
        break;
      case ol.render.canvas.Instruction.BEGIN_PATH:
        context.beginPath();
        ++i;
        break;
      case ol.render.canvas.Instruction.CIRCLE:
        goog.asserts.assert(goog.isNumber(instruction[1]),
            'second instruction should be a number');
        d = /** @type {number} */ (instruction[1]);
        var x1 = pixelCoordinates[d];
        var y1 = pixelCoordinates[d + 1];
        var x2 = pixelCoordinates[d + 2];
        var y2 = pixelCoordinates[d + 3];
        var dx = x2 - x1;
        var dy = y2 - y1;
        var r = Math.sqrt(dx * dx + dy * dy);
        context.arc(x1, y1, r, 0, 2 * Math.PI, true);
        ++i;
        break;
      case ol.render.canvas.Instruction.CLOSE_PATH:
        context.closePath();
        ++i;
        break;
      case ol.render.canvas.Instruction.DRAW_IMAGE:
        goog.asserts.assert(goog.isNumber(instruction[1]),
            'second instruction should be a number');
        d = /** @type {number} */ (instruction[1]);
        goog.asserts.assert(goog.isNumber(instruction[2]),
            'third instruction should be a number');
        dd = /** @type {number} */ (instruction[2]);
        var image =  /** @type {HTMLCanvasElement|HTMLVideoElement|Image} */
            (instruction[3]);
        // Remaining arguments in DRAW_IMAGE are in alphabetical order
        var anchorX = /** @type {number} */ (instruction[4]) * pixelRatio;
        var anchorY = /** @type {number} */ (instruction[5]) * pixelRatio;
        var height = /** @type {number} */ (instruction[6]);
        var opacity = /** @type {number} */ (instruction[7]);
        var originX = /** @type {number} */ (instruction[8]);
        var originY = /** @type {number} */ (instruction[9]);
        var rotateWithView = /** @type {boolean} */ (instruction[10]);
        var rotation = /** @type {number} */ (instruction[11]);
        var scale = /** @type {number} */ (instruction[12]);
        var snapToPixel = /** @type {boolean} */ (instruction[13]);
        var width = /** @type {number} */ (instruction[14]);
        if (rotateWithView) {
          rotation += viewRotation;
        }
        for (; d < dd; d += 2) {
          x = pixelCoordinates[d] - anchorX;
          y = pixelCoordinates[d + 1] - anchorY;
          if (snapToPixel) {
            x = Math.round(x);
            y = Math.round(y);
          }
          if (scale != 1 || rotation !== 0) {
            var centerX = x + anchorX;
            var centerY = y + anchorY;
            ol.vec.Mat4.makeTransform2D(
                localTransform, centerX, centerY, scale, scale,
                rotation, -centerX, -centerY);
            context.transform(
                goog.vec.Mat4.getElement(localTransform, 0, 0),
                goog.vec.Mat4.getElement(localTransform, 1, 0),
                goog.vec.Mat4.getElement(localTransform, 0, 1),
                goog.vec.Mat4.getElement(localTransform, 1, 1),
                goog.vec.Mat4.getElement(localTransform, 0, 3),
                goog.vec.Mat4.getElement(localTransform, 1, 3));
          }
          var alpha = context.globalAlpha;
          if (opacity != 1) {
            context.globalAlpha = alpha * opacity;
          }

          var w = (width + originX > image.width) ? image.width - originX : width;
          var h = (height + originY > image.height) ? image.height - originY : height;

          context.drawImage(image, originX, originY, w, h,
              x, y, w * pixelRatio, h * pixelRatio);

          if (opacity != 1) {
            context.globalAlpha = alpha;
          }
          if (scale != 1 || rotation !== 0) {
            goog.vec.Mat4.invert(localTransform, localTransformInv);
            context.transform(
                goog.vec.Mat4.getElement(localTransformInv, 0, 0),
                goog.vec.Mat4.getElement(localTransformInv, 1, 0),
                goog.vec.Mat4.getElement(localTransformInv, 0, 1),
                goog.vec.Mat4.getElement(localTransformInv, 1, 1),
                goog.vec.Mat4.getElement(localTransformInv, 0, 3),
                goog.vec.Mat4.getElement(localTransformInv, 1, 3));
          }
        }
        ++i;
        break;
      case ol.render.canvas.Instruction.DRAW_TEXT:
        goog.asserts.assert(goog.isNumber(instruction[1]),
            '2nd instruction should be a number');
        d = /** @type {number} */ (instruction[1]);
        goog.asserts.assert(goog.isNumber(instruction[2]),
            '3rd instruction should be a number');
        dd = /** @type {number} */ (instruction[2]);
        goog.asserts.assert(goog.isString(instruction[3]),
            '4th instruction should be a string');
        text = /** @type {string} */ (instruction[3]);
        goog.asserts.assert(goog.isNumber(instruction[4]),
            '5th instruction should be a number');
        var offsetX = /** @type {number} */ (instruction[4]) * pixelRatio;
        goog.asserts.assert(goog.isNumber(instruction[5]),
            '6th instruction should be a number');
        var offsetY = /** @type {number} */ (instruction[5]) * pixelRatio;
        goog.asserts.assert(goog.isNumber(instruction[6]),
            '7th instruction should be a number');
        rotation = /** @type {number} */ (instruction[6]);
        goog.asserts.assert(goog.isNumber(instruction[7]),
            '8th instruction should be a number');
        scale = /** @type {number} */ (instruction[7]) * pixelRatio;
        goog.asserts.assert(goog.isBoolean(instruction[8]),
            '9th instruction should be a boolean');
        fill = /** @type {boolean} */ (instruction[8]);
        goog.asserts.assert(goog.isBoolean(instruction[9]),
            '10th instruction should be a boolean');
        stroke = /** @type {boolean} */ (instruction[9]);
        for (; d < dd; d += 2) {
          x = pixelCoordinates[d] + offsetX;
          y = pixelCoordinates[d + 1] + offsetY;
          if (scale != 1 || rotation !== 0) {
            ol.vec.Mat4.makeTransform2D(
                localTransform, x, y, scale, scale, rotation, -x, -y);
            context.transform(
                goog.vec.Mat4.getElement(localTransform, 0, 0),
                goog.vec.Mat4.getElement(localTransform, 1, 0),
                goog.vec.Mat4.getElement(localTransform, 0, 1),
                goog.vec.Mat4.getElement(localTransform, 1, 1),
                goog.vec.Mat4.getElement(localTransform, 0, 3),
                goog.vec.Mat4.getElement(localTransform, 1, 3));
          }

          // Support multiple lines separated by \n
          var lines = text.split('\n');
          var numLines = lines.length;
          var fontSize, lineY;
          if (numLines > 1) {
            // Estimate line height using width of capital M, and add padding
            fontSize = Math.round(context.measureText('M').width * 1.5);
            lineY = y - (((numLines - 1) / 2) * fontSize);
          } else {
            // No need to calculate line height/offset for a single line
            fontSize = 0;
            lineY = y;
          }

          for (var lineIndex = 0; lineIndex < numLines; lineIndex++) {
            var line = lines[lineIndex];
            if (stroke) {
              context.strokeText(line, x, lineY);
            }
            if (fill) {
              context.fillText(line, x, lineY);
            }

            // Move next line down by fontSize px
            lineY = lineY + fontSize;
          }

          if (scale != 1 || rotation !== 0) {
            goog.vec.Mat4.invert(localTransform, localTransformInv);
            context.transform(
                goog.vec.Mat4.getElement(localTransformInv, 0, 0),
                goog.vec.Mat4.getElement(localTransformInv, 1, 0),
                goog.vec.Mat4.getElement(localTransformInv, 0, 1),
                goog.vec.Mat4.getElement(localTransformInv, 1, 1),
                goog.vec.Mat4.getElement(localTransformInv, 0, 3),
                goog.vec.Mat4.getElement(localTransformInv, 1, 3));
          }
        }
        ++i;
        break;
      case ol.render.canvas.Instruction.END_GEOMETRY:
        if (featureCallback !== undefined) {
          feature =
              /** @type {ol.Feature|ol.render.Feature} */ (instruction[1]);
          var result = featureCallback(feature);
          if (result) {
            return result;
          }
        }
        ++i;
        break;
      case ol.render.canvas.Instruction.FILL:
        context.fill();
        ++i;
        break;
      case ol.render.canvas.Instruction.MOVE_TO_LINE_TO:
        goog.asserts.assert(goog.isNumber(instruction[1]),
            '2nd instruction should be a number');
        d = /** @type {number} */ (instruction[1]);
        goog.asserts.assert(goog.isNumber(instruction[2]),
            '3rd instruction should be a number');
        dd = /** @type {number} */ (instruction[2]);
        x = pixelCoordinates[d];
        y = pixelCoordinates[d + 1];
        roundX = (x + 0.5) | 0;
        roundY = (y + 0.5) | 0;
        if (roundX !== prevX || roundY !== prevY) {
          context.moveTo(x, y);
          prevX = roundX;
          prevY = roundY;
        }
        for (d += 2; d < dd; d += 2) {
          x = pixelCoordinates[d];
          y = pixelCoordinates[d + 1];
          roundX = (x + 0.5) | 0;
          roundY = (y + 0.5) | 0;
          if (roundX !== prevX || roundY !== prevY) {
            context.lineTo(x, y);
            prevX = roundX;
            prevY = roundY;
          }
        }
        ++i;
        break;
      case ol.render.canvas.Instruction.SET_FILL_STYLE:
        goog.asserts.assert(goog.isString(instruction[1]),
            '2nd instruction should be a string');
        context.fillStyle = /** @type {string} */ (instruction[1]);
        ++i;
        break;
      case ol.render.canvas.Instruction.SET_STROKE_STYLE:
        goog.asserts.assert(goog.isString(instruction[1]),
            '2nd instruction should be a string');
        goog.asserts.assert(goog.isNumber(instruction[2]),
            '3rd instruction should be a number');
        goog.asserts.assert(goog.isString(instruction[3]),
            '4rd instruction should be a string');
        goog.asserts.assert(goog.isString(instruction[4]),
            '5th instruction should be a string');
        goog.asserts.assert(goog.isNumber(instruction[5]),
            '6th instruction should be a number');
        goog.asserts.assert(instruction[6],
            '7th instruction should not be null');
        var usePixelRatio = instruction[7] !== undefined ?
            instruction[7] : true;
        var lineWidth = /** @type {number} */ (instruction[2]);
        context.strokeStyle = /** @type {string} */ (instruction[1]);
        context.lineWidth = usePixelRatio ? lineWidth * pixelRatio : lineWidth;
        context.lineCap = /** @type {string} */ (instruction[3]);
        context.lineJoin = /** @type {string} */ (instruction[4]);
        context.miterLimit = /** @type {number} */ (instruction[5]);
        if (ol.has.CANVAS_LINE_DASH) {
          context.setLineDash(/** @type {Array.<number>} */ (instruction[6]));
        }
        prevX = NaN;
        prevY = NaN;
        ++i;
        break;
      case ol.render.canvas.Instruction.SET_TEXT_STYLE:
        goog.asserts.assert(goog.isString(instruction[1]),
            '2nd instruction should be a string');
        goog.asserts.assert(goog.isString(instruction[2]),
            '3rd instruction should be a string');
        goog.asserts.assert(goog.isString(instruction[3]),
            '4th instruction should be a string');
        context.font = /** @type {string} */ (instruction[1]);
        context.textAlign = /** @type {string} */ (instruction[2]);
        context.textBaseline = /** @type {string} */ (instruction[3]);
        ++i;
        break;
      case ol.render.canvas.Instruction.STROKE:
        context.stroke();
        ++i;
        break;
      default:
        goog.asserts.fail('Unknown canvas render instruction');
        ++i; // consume the instruction anyway, to avoid an infinite loop
        break;
    }
  }
  // assert that all instructions were consumed
  goog.asserts.assert(i == instructions.length,
      'all instructions should be consumed');
  return undefined;
};


/**
 * @param {CanvasRenderingContext2D} context Context.
 * @param {number} pixelRatio Pixel ratio.
 * @param {goog.vec.Mat4.Number} transform Transform.
 * @param {number} viewRotation View rotation.
 * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
 *     to skip.
 */
ol.render.canvas.Replay.prototype.replay = function(
    context, pixelRatio, transform, viewRotation, skippedFeaturesHash) {
  var instructions = this.instructions;
  this.replay_(context, pixelRatio, transform, viewRotation,
      skippedFeaturesHash, instructions, undefined);
};


/**
 * @param {CanvasRenderingContext2D} context Context.
 * @param {goog.vec.Mat4.Number} transform Transform.
 * @param {number} viewRotation View rotation.
 * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
 *     to skip.
 * @param {function((ol.Feature|ol.render.Feature)): T=} opt_featureCallback
 *     Feature callback.
 * @param {ol.Extent=} opt_hitExtent Only check features that intersect this
 *     extent.
 * @return {T|undefined} Callback result.
 * @template T
 */
ol.render.canvas.Replay.prototype.replayHitDetection = function(
    context, transform, viewRotation, skippedFeaturesHash,
    opt_featureCallback, opt_hitExtent) {
  var instructions = this.hitDetectionInstructions;
  return this.replay_(context, 1, transform, viewRotation,
      skippedFeaturesHash, instructions, opt_featureCallback, opt_hitExtent);
};


/**
 * @private
 */
ol.render.canvas.Replay.prototype.reverseHitDetectionInstructions_ = function() {
  var hitDetectionInstructions = this.hitDetectionInstructions;
  // step 1 - reverse array
  hitDetectionInstructions.reverse();
  // step 2 - reverse instructions within geometry blocks
  var i;
  var n = hitDetectionInstructions.length;
  var instruction;
  var type;
  var begin = -1;
  for (i = 0; i < n; ++i) {
    instruction = hitDetectionInstructions[i];
    type = /** @type {ol.render.canvas.Instruction} */ (instruction[0]);
    if (type == ol.render.canvas.Instruction.END_GEOMETRY) {
      goog.asserts.assert(begin == -1, 'begin should be -1');
      begin = i;
    } else if (type == ol.render.canvas.Instruction.BEGIN_GEOMETRY) {
      instruction[2] = i;
      goog.asserts.assert(begin >= 0,
          'begin should be larger than or equal to 0');
      ol.array.reverseSubArray(this.hitDetectionInstructions, begin, i);
      begin = -1;
    }
  }
};


/**
 * @param {ol.geom.Geometry|ol.render.Feature} geometry Geometry.
 * @param {ol.Feature|ol.render.Feature} feature Feature.
 */
ol.render.canvas.Replay.prototype.endGeometry = function(geometry, feature) {
  goog.asserts.assert(this.beginGeometryInstruction1_,
      'this.beginGeometryInstruction1_ should not be null');
  this.beginGeometryInstruction1_[2] = this.instructions.length;
  this.beginGeometryInstruction1_ = null;
  goog.asserts.assert(this.beginGeometryInstruction2_,
      'this.beginGeometryInstruction2_ should not be null');
  this.beginGeometryInstruction2_[2] = this.hitDetectionInstructions.length;
  this.beginGeometryInstruction2_ = null;
  var endGeometryInstruction =
      [ol.render.canvas.Instruction.END_GEOMETRY, feature];
  this.instructions.push(endGeometryInstruction);
  this.hitDetectionInstructions.push(endGeometryInstruction);
};


/**
 * FIXME empty description for jsdoc
 */
ol.render.canvas.Replay.prototype.finish = ol.nullFunction;


/**
 * Get the buffered rendering extent.  Rendering will be clipped to the extent
 * provided to the constructor.  To account for symbolizers that may intersect
 * this extent, we calculate a buffered extent (e.g. based on stroke width).
 * @return {ol.Extent} The buffered rendering extent.
 * @protected
 */
ol.render.canvas.Replay.prototype.getBufferedMaxExtent = function() {
  return this.maxExtent;
};


/**
 * @constructor
 * @extends {ol.render.canvas.Replay}
 * @param {number} tolerance Tolerance.
 * @param {ol.Extent} maxExtent Maximum extent.
 * @param {number} resolution Resolution.
 * @protected
 * @struct
 */
ol.render.canvas.ImageReplay = function(tolerance, maxExtent, resolution) {
  goog.base(this, tolerance, maxExtent, resolution);

  /**
   * @private
   * @type {HTMLCanvasElement|HTMLVideoElement|Image}
   */
  this.hitDetectionImage_ = null;

  /**
   * @private
   * @type {HTMLCanvasElement|HTMLVideoElement|Image}
   */
  this.image_ = null;

  /**
   * @private
   * @type {number|undefined}
   */
  this.anchorX_ = undefined;

  /**
   * @private
   * @type {number|undefined}
   */
  this.anchorY_ = undefined;

  /**
   * @private
   * @type {number|undefined}
   */
  this.height_ = undefined;

  /**
   * @private
   * @type {number|undefined}
   */
  this.opacity_ = undefined;

  /**
   * @private
   * @type {number|undefined}
   */
  this.originX_ = undefined;

  /**
   * @private
   * @type {number|undefined}
   */
  this.originY_ = undefined;

  /**
   * @private
   * @type {boolean|undefined}
   */
  this.rotateWithView_ = undefined;

  /**
   * @private
   * @type {number|undefined}
   */
  this.rotation_ = undefined;

  /**
   * @private
   * @type {number|undefined}
   */
  this.scale_ = undefined;

  /**
   * @private
   * @type {boolean|undefined}
   */
  this.snapToPixel_ = undefined;

  /**
   * @private
   * @type {number|undefined}
   */
  this.width_ = undefined;

};
goog.inherits(ol.render.canvas.ImageReplay, ol.render.canvas.Replay);


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @private
 * @return {number} My end.
 */
ol.render.canvas.ImageReplay.prototype.drawCoordinates_ = function(flatCoordinates, offset, end, stride) {
  return this.appendFlatCoordinates(
      flatCoordinates, offset, end, stride, false);
};


/**
 * @inheritDoc
 */
ol.render.canvas.ImageReplay.prototype.drawPointGeometry = function(pointGeometry, feature) {
  if (!this.image_) {
    return;
  }
  goog.asserts.assert(this.anchorX_ !== undefined,
      'this.anchorX_ should be defined');
  goog.asserts.assert(this.anchorY_ !== undefined,
      'this.anchorY_ should be defined');
  goog.asserts.assert(this.height_ !== undefined,
      'this.height_ should be defined');
  goog.asserts.assert(this.opacity_ !== undefined,
      'this.opacity_ should be defined');
  goog.asserts.assert(this.originX_ !== undefined,
      'this.originX_ should be defined');
  goog.asserts.assert(this.originY_ !== undefined,
      'this.originY_ should be defined');
  goog.asserts.assert(this.rotateWithView_ !== undefined,
      'this.rotateWithView_ should be defined');
  goog.asserts.assert(this.rotation_ !== undefined,
      'this.rotation_ should be defined');
  goog.asserts.assert(this.scale_ !== undefined,
      'this.scale_ should be defined');
  goog.asserts.assert(this.width_ !== undefined,
      'this.width_ should be defined');
  this.beginGeometry(pointGeometry, feature);
  var flatCoordinates = pointGeometry.getFlatCoordinates();
  var stride = pointGeometry.getStride();
  var myBegin = this.coordinates.length;
  var myEnd = this.drawCoordinates_(
      flatCoordinates, 0, flatCoordinates.length, stride);
  this.instructions.push([
    ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd, this.image_,
    // Remaining arguments to DRAW_IMAGE are in alphabetical order
    this.anchorX_, this.anchorY_, this.height_, this.opacity_,
    this.originX_, this.originY_, this.rotateWithView_, this.rotation_,
    this.scale_, this.snapToPixel_, this.width_
  ]);
  this.hitDetectionInstructions.push([
    ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd,
    this.hitDetectionImage_,
    // Remaining arguments to DRAW_IMAGE are in alphabetical order
    this.anchorX_, this.anchorY_, this.height_, this.opacity_,
    this.originX_, this.originY_, this.rotateWithView_, this.rotation_,
    this.scale_, this.snapToPixel_, this.width_
  ]);
  this.endGeometry(pointGeometry, feature);
};


/**
 * @inheritDoc
 */
ol.render.canvas.ImageReplay.prototype.drawMultiPointGeometry = function(multiPointGeometry, feature) {
  if (!this.image_) {
    return;
  }
  goog.asserts.assert(this.anchorX_ !== undefined,
      'this.anchorX_ should be defined');
  goog.asserts.assert(this.anchorY_ !== undefined,
      'this.anchorY_ should be defined');
  goog.asserts.assert(this.height_ !== undefined,
      'this.height_ should be defined');
  goog.asserts.assert(this.opacity_ !== undefined,
      'this.opacity_ should be defined');
  goog.asserts.assert(this.originX_ !== undefined,
      'this.originX_ should be defined');
  goog.asserts.assert(this.originY_ !== undefined,
      'this.originY_ should be defined');
  goog.asserts.assert(this.rotateWithView_ !== undefined,
      'this.rotateWithView_ should be defined');
  goog.asserts.assert(this.rotation_ !== undefined,
      'this.rotation_ should be defined');
  goog.asserts.assert(this.scale_ !== undefined,
      'this.scale_ should be defined');
  goog.asserts.assert(this.width_ !== undefined,
      'this.width_ should be defined');
  this.beginGeometry(multiPointGeometry, feature);
  var flatCoordinates = multiPointGeometry.getFlatCoordinates();
  var stride = multiPointGeometry.getStride();
  var myBegin = this.coordinates.length;
  var myEnd = this.drawCoordinates_(
      flatCoordinates, 0, flatCoordinates.length, stride);
  this.instructions.push([
    ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd, this.image_,
    // Remaining arguments to DRAW_IMAGE are in alphabetical order
    this.anchorX_, this.anchorY_, this.height_, this.opacity_,
    this.originX_, this.originY_, this.rotateWithView_, this.rotation_,
    this.scale_, this.snapToPixel_, this.width_
  ]);
  this.hitDetectionInstructions.push([
    ol.render.canvas.Instruction.DRAW_IMAGE, myBegin, myEnd,
    this.hitDetectionImage_,
    // Remaining arguments to DRAW_IMAGE are in alphabetical order
    this.anchorX_, this.anchorY_, this.height_, this.opacity_,
    this.originX_, this.originY_, this.rotateWithView_, this.rotation_,
    this.scale_, this.snapToPixel_, this.width_
  ]);
  this.endGeometry(multiPointGeometry, feature);
};


/**
 * @inheritDoc
 */
ol.render.canvas.ImageReplay.prototype.finish = function() {
  this.reverseHitDetectionInstructions_();
  // FIXME this doesn't really protect us against further calls to draw*Geometry
  this.anchorX_ = undefined;
  this.anchorY_ = undefined;
  this.hitDetectionImage_ = null;
  this.image_ = null;
  this.height_ = undefined;
  this.scale_ = undefined;
  this.opacity_ = undefined;
  this.originX_ = undefined;
  this.originY_ = undefined;
  this.rotateWithView_ = undefined;
  this.rotation_ = undefined;
  this.snapToPixel_ = undefined;
  this.width_ = undefined;
};


/**
 * @inheritDoc
 */
ol.render.canvas.ImageReplay.prototype.setImageStyle = function(imageStyle) {
  goog.asserts.assert(imageStyle, 'imageStyle should not be null');
  var anchor = imageStyle.getAnchor();
  goog.asserts.assert(anchor, 'anchor should not be null');
  var size = imageStyle.getSize();
  goog.asserts.assert(size, 'size should not be null');
  var hitDetectionImage = imageStyle.getHitDetectionImage(1);
  goog.asserts.assert(hitDetectionImage,
      'hitDetectionImage should not be null');
  var image = imageStyle.getImage(1);
  goog.asserts.assert(image, 'image should not be null');
  var origin = imageStyle.getOrigin();
  goog.asserts.assert(origin, 'origin should not be null');
  this.anchorX_ = anchor[0];
  this.anchorY_ = anchor[1];
  this.hitDetectionImage_ = hitDetectionImage;
  this.image_ = image;
  this.height_ = size[1];
  this.opacity_ = imageStyle.getOpacity();
  this.originX_ = origin[0];
  this.originY_ = origin[1];
  this.rotateWithView_ = imageStyle.getRotateWithView();
  this.rotation_ = imageStyle.getRotation();
  this.scale_ = imageStyle.getScale();
  this.snapToPixel_ = imageStyle.getSnapToPixel();
  this.width_ = size[0];
};


/**
 * @constructor
 * @extends {ol.render.canvas.Replay}
 * @param {number} tolerance Tolerance.
 * @param {ol.Extent} maxExtent Maximum extent.
 * @param {number} resolution Resolution.
 * @protected
 * @struct
 */
ol.render.canvas.LineStringReplay = function(tolerance, maxExtent, resolution) {

  goog.base(this, tolerance, maxExtent, resolution);

  /**
   * @private
   * @type {{currentStrokeStyle: (string|undefined),
   *         currentLineCap: (string|undefined),
   *         currentLineDash: Array.<number>,
   *         currentLineJoin: (string|undefined),
   *         currentLineWidth: (number|undefined),
   *         currentMiterLimit: (number|undefined),
   *         lastStroke: number,
   *         strokeStyle: (string|undefined),
   *         lineCap: (string|undefined),
   *         lineDash: Array.<number>,
   *         lineJoin: (string|undefined),
   *         lineWidth: (number|undefined),
   *         miterLimit: (number|undefined)}|null}
   */
  this.state_ = {
    currentStrokeStyle: undefined,
    currentLineCap: undefined,
    currentLineDash: null,
    currentLineJoin: undefined,
    currentLineWidth: undefined,
    currentMiterLimit: undefined,
    lastStroke: 0,
    strokeStyle: undefined,
    lineCap: undefined,
    lineDash: null,
    lineJoin: undefined,
    lineWidth: undefined,
    miterLimit: undefined
  };

};
goog.inherits(ol.render.canvas.LineStringReplay, ol.render.canvas.Replay);


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @private
 * @return {number} end.
 */
ol.render.canvas.LineStringReplay.prototype.drawFlatCoordinates_ = function(flatCoordinates, offset, end, stride) {
  var myBegin = this.coordinates.length;
  var myEnd = this.appendFlatCoordinates(
      flatCoordinates, offset, end, stride, false);
  var moveToLineToInstruction =
      [ol.render.canvas.Instruction.MOVE_TO_LINE_TO, myBegin, myEnd];
  this.instructions.push(moveToLineToInstruction);
  this.hitDetectionInstructions.push(moveToLineToInstruction);
  return end;
};


/**
 * @inheritDoc
 */
ol.render.canvas.LineStringReplay.prototype.getBufferedMaxExtent = function() {
  if (!this.bufferedMaxExtent_) {
    this.bufferedMaxExtent_ = ol.extent.clone(this.maxExtent);
    if (this.maxLineWidth > 0) {
      var width = this.resolution * (this.maxLineWidth + 1) / 2;
      ol.extent.buffer(this.bufferedMaxExtent_, width, this.bufferedMaxExtent_);
    }
  }
  return this.bufferedMaxExtent_;
};


/**
 * @private
 */
ol.render.canvas.LineStringReplay.prototype.setStrokeStyle_ = function() {
  var state = this.state_;
  var strokeStyle = state.strokeStyle;
  var lineCap = state.lineCap;
  var lineDash = state.lineDash;
  var lineJoin = state.lineJoin;
  var lineWidth = state.lineWidth;
  var miterLimit = state.miterLimit;
  goog.asserts.assert(strokeStyle !== undefined,
      'strokeStyle should be defined');
  goog.asserts.assert(lineCap !== undefined, 'lineCap should be defined');
  goog.asserts.assert(lineDash, 'lineDash should not be null');
  goog.asserts.assert(lineJoin !== undefined, 'lineJoin should be defined');
  goog.asserts.assert(lineWidth !== undefined, 'lineWidth should be defined');
  goog.asserts.assert(miterLimit !== undefined, 'miterLimit should be defined');
  if (state.currentStrokeStyle != strokeStyle ||
      state.currentLineCap != lineCap ||
      !goog.array.equals(state.currentLineDash, lineDash) ||
      state.currentLineJoin != lineJoin ||
      state.currentLineWidth != lineWidth ||
      state.currentMiterLimit != miterLimit) {
    if (state.lastStroke != this.coordinates.length) {
      this.instructions.push(
          [ol.render.canvas.Instruction.STROKE]);
      state.lastStroke = this.coordinates.length;
    }
    this.instructions.push(
        [ol.render.canvas.Instruction.SET_STROKE_STYLE,
         strokeStyle, lineWidth, lineCap, lineJoin, miterLimit, lineDash],
        [ol.render.canvas.Instruction.BEGIN_PATH]);
    state.currentStrokeStyle = strokeStyle;
    state.currentLineCap = lineCap;
    state.currentLineDash = lineDash;
    state.currentLineJoin = lineJoin;
    state.currentLineWidth = lineWidth;
    state.currentMiterLimit = miterLimit;
  }
};


/**
 * @inheritDoc
 */
ol.render.canvas.LineStringReplay.prototype.drawLineStringGeometry = function(lineStringGeometry, feature) {
  var state = this.state_;
  goog.asserts.assert(state, 'state should not be null');
  var strokeStyle = state.strokeStyle;
  var lineWidth = state.lineWidth;
  if (strokeStyle === undefined || lineWidth === undefined) {
    return;
  }
  this.setStrokeStyle_();
  this.beginGeometry(lineStringGeometry, feature);
  this.hitDetectionInstructions.push(
      [ol.render.canvas.Instruction.SET_STROKE_STYLE,
       state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin,
       state.miterLimit, state.lineDash],
      [ol.render.canvas.Instruction.BEGIN_PATH]);
  var flatCoordinates = lineStringGeometry.getFlatCoordinates();
  var stride = lineStringGeometry.getStride();
  this.drawFlatCoordinates_(
      flatCoordinates, 0, flatCoordinates.length, stride);
  this.hitDetectionInstructions.push([ol.render.canvas.Instruction.STROKE]);
  this.endGeometry(lineStringGeometry, feature);
};


/**
 * @inheritDoc
 */
ol.render.canvas.LineStringReplay.prototype.drawMultiLineStringGeometry = function(multiLineStringGeometry, feature) {
  var state = this.state_;
  goog.asserts.assert(state, 'state should not be null');
  var strokeStyle = state.strokeStyle;
  var lineWidth = state.lineWidth;
  if (strokeStyle === undefined || lineWidth === undefined) {
    return;
  }
  this.setStrokeStyle_();
  this.beginGeometry(multiLineStringGeometry, feature);
  this.hitDetectionInstructions.push(
      [ol.render.canvas.Instruction.SET_STROKE_STYLE,
       state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin,
       state.miterLimit, state.lineDash],
      [ol.render.canvas.Instruction.BEGIN_PATH]);
  var ends = multiLineStringGeometry.getEnds();
  var flatCoordinates = multiLineStringGeometry.getFlatCoordinates();
  var stride = multiLineStringGeometry.getStride();
  var offset = 0;
  var i, ii;
  for (i = 0, ii = ends.length; i < ii; ++i) {
    offset = this.drawFlatCoordinates_(
        flatCoordinates, offset, ends[i], stride);
  }
  this.hitDetectionInstructions.push([ol.render.canvas.Instruction.STROKE]);
  this.endGeometry(multiLineStringGeometry, feature);
};


/**
 * @inheritDoc
 */
ol.render.canvas.LineStringReplay.prototype.finish = function() {
  var state = this.state_;
  goog.asserts.assert(state, 'state should not be null');
  if (state.lastStroke != this.coordinates.length) {
    this.instructions.push([ol.render.canvas.Instruction.STROKE]);
  }
  this.reverseHitDetectionInstructions_();
  this.state_ = null;
};


/**
 * @inheritDoc
 */
ol.render.canvas.LineStringReplay.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) {
  goog.asserts.assert(this.state_, 'this.state_ should not be null');
  goog.asserts.assert(!fillStyle, 'fillStyle should be null');
  goog.asserts.assert(strokeStyle, 'strokeStyle should not be null');
  var strokeStyleColor = strokeStyle.getColor();
  this.state_.strokeStyle = ol.color.asString(strokeStyleColor ?
      strokeStyleColor : ol.render.canvas.defaultStrokeStyle);
  var strokeStyleLineCap = strokeStyle.getLineCap();
  this.state_.lineCap = strokeStyleLineCap !== undefined ?
      strokeStyleLineCap : ol.render.canvas.defaultLineCap;
  var strokeStyleLineDash = strokeStyle.getLineDash();
  this.state_.lineDash = strokeStyleLineDash ?
      strokeStyleLineDash : ol.render.canvas.defaultLineDash;
  var strokeStyleLineJoin = strokeStyle.getLineJoin();
  this.state_.lineJoin = strokeStyleLineJoin !== undefined ?
      strokeStyleLineJoin : ol.render.canvas.defaultLineJoin;
  var strokeStyleWidth = strokeStyle.getWidth();
  this.state_.lineWidth = strokeStyleWidth !== undefined ?
      strokeStyleWidth : ol.render.canvas.defaultLineWidth;
  var strokeStyleMiterLimit = strokeStyle.getMiterLimit();
  this.state_.miterLimit = strokeStyleMiterLimit !== undefined ?
      strokeStyleMiterLimit : ol.render.canvas.defaultMiterLimit;

  if (this.state_.lineWidth > this.maxLineWidth) {
    this.maxLineWidth = this.state_.lineWidth;
    // invalidate the buffered max extent cache
    this.bufferedMaxExtent_ = null;
  }
};


/**
 * @constructor
 * @extends {ol.render.canvas.Replay}
 * @param {number} tolerance Tolerance.
 * @param {ol.Extent} maxExtent Maximum extent.
 * @param {number} resolution Resolution.
 * @protected
 * @struct
 */
ol.render.canvas.PolygonReplay = function(tolerance, maxExtent, resolution) {

  goog.base(this, tolerance, maxExtent, resolution);

  /**
   * @private
   * @type {{currentFillStyle: (string|undefined),
   *         currentStrokeStyle: (string|undefined),
   *         currentLineCap: (string|undefined),
   *         currentLineDash: Array.<number>,
   *         currentLineJoin: (string|undefined),
   *         currentLineWidth: (number|undefined),
   *         currentMiterLimit: (number|undefined),
   *         fillStyle: (string|undefined),
   *         strokeStyle: (string|undefined),
   *         lineCap: (string|undefined),
   *         lineDash: Array.<number>,
   *         lineJoin: (string|undefined),
   *         lineWidth: (number|undefined),
   *         miterLimit: (number|undefined)}|null}
   */
  this.state_ = {
    currentFillStyle: undefined,
    currentStrokeStyle: undefined,
    currentLineCap: undefined,
    currentLineDash: null,
    currentLineJoin: undefined,
    currentLineWidth: undefined,
    currentMiterLimit: undefined,
    fillStyle: undefined,
    strokeStyle: undefined,
    lineCap: undefined,
    lineDash: null,
    lineJoin: undefined,
    lineWidth: undefined,
    miterLimit: undefined
  };

};
goog.inherits(ol.render.canvas.PolygonReplay, ol.render.canvas.Replay);


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<number>} ends Ends.
 * @param {number} stride Stride.
 * @private
 * @return {number} End.
 */
ol.render.canvas.PolygonReplay.prototype.drawFlatCoordinatess_ = function(flatCoordinates, offset, ends, stride) {
  var state = this.state_;
  var beginPathInstruction = [ol.render.canvas.Instruction.BEGIN_PATH];
  this.instructions.push(beginPathInstruction);
  this.hitDetectionInstructions.push(beginPathInstruction);
  var i, ii;
  for (i = 0, ii = ends.length; i < ii; ++i) {
    var end = ends[i];
    var myBegin = this.coordinates.length;
    var myEnd = this.appendFlatCoordinates(
        flatCoordinates, offset, end, stride, true);
    var moveToLineToInstruction =
        [ol.render.canvas.Instruction.MOVE_TO_LINE_TO, myBegin, myEnd];
    var closePathInstruction = [ol.render.canvas.Instruction.CLOSE_PATH];
    this.instructions.push(moveToLineToInstruction, closePathInstruction);
    this.hitDetectionInstructions.push(moveToLineToInstruction,
        closePathInstruction);
    offset = end;
  }
  // FIXME is it quicker to fill and stroke each polygon individually,
  // FIXME or all polygons together?
  var fillInstruction = [ol.render.canvas.Instruction.FILL];
  this.hitDetectionInstructions.push(fillInstruction);
  if (state.fillStyle !== undefined) {
    this.instructions.push(fillInstruction);
  }
  if (state.strokeStyle !== undefined) {
    goog.asserts.assert(state.lineWidth !== undefined,
        'state.lineWidth should be defined');
    var strokeInstruction = [ol.render.canvas.Instruction.STROKE];
    this.instructions.push(strokeInstruction);
    this.hitDetectionInstructions.push(strokeInstruction);
  }
  return offset;
};


/**
 * @inheritDoc
 */
ol.render.canvas.PolygonReplay.prototype.drawCircleGeometry = function(circleGeometry, feature) {
  var state = this.state_;
  goog.asserts.assert(state, 'state should not be null');
  var fillStyle = state.fillStyle;
  var strokeStyle = state.strokeStyle;
  if (fillStyle === undefined && strokeStyle === undefined) {
    return;
  }
  if (strokeStyle !== undefined) {
    goog.asserts.assert(state.lineWidth !== undefined,
        'state.lineWidth should be defined');
  }
  this.setFillStrokeStyles_();
  this.beginGeometry(circleGeometry, feature);
  // always fill the circle for hit detection
  this.hitDetectionInstructions.push(
      [ol.render.canvas.Instruction.SET_FILL_STYLE,
       ol.color.asString(ol.render.canvas.defaultFillStyle)]);
  if (state.strokeStyle !== undefined) {
    this.hitDetectionInstructions.push(
        [ol.render.canvas.Instruction.SET_STROKE_STYLE,
         state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin,
         state.miterLimit, state.lineDash]);
  }
  var flatCoordinates = circleGeometry.getFlatCoordinates();
  var stride = circleGeometry.getStride();
  var myBegin = this.coordinates.length;
  this.appendFlatCoordinates(
      flatCoordinates, 0, flatCoordinates.length, stride, false);
  var beginPathInstruction = [ol.render.canvas.Instruction.BEGIN_PATH];
  var circleInstruction = [ol.render.canvas.Instruction.CIRCLE, myBegin];
  this.instructions.push(beginPathInstruction, circleInstruction);
  this.hitDetectionInstructions.push(beginPathInstruction, circleInstruction);
  var fillInstruction = [ol.render.canvas.Instruction.FILL];
  this.hitDetectionInstructions.push(fillInstruction);
  if (state.fillStyle !== undefined) {
    this.instructions.push(fillInstruction);
  }
  if (state.strokeStyle !== undefined) {
    goog.asserts.assert(state.lineWidth !== undefined,
        'state.lineWidth should be defined');
    var strokeInstruction = [ol.render.canvas.Instruction.STROKE];
    this.instructions.push(strokeInstruction);
    this.hitDetectionInstructions.push(strokeInstruction);
  }
  this.endGeometry(circleGeometry, feature);
};


/**
 * @inheritDoc
 */
ol.render.canvas.PolygonReplay.prototype.drawPolygonGeometry = function(polygonGeometry, feature) {
  var state = this.state_;
  goog.asserts.assert(state, 'state should not be null');
  var fillStyle = state.fillStyle;
  var strokeStyle = state.strokeStyle;
  if (fillStyle === undefined && strokeStyle === undefined) {
    return;
  }
  if (strokeStyle !== undefined) {
    goog.asserts.assert(state.lineWidth !== undefined,
        'state.lineWidth should be defined');
  }
  this.setFillStrokeStyles_();
  this.beginGeometry(polygonGeometry, feature);
  // always fill the polygon for hit detection
  this.hitDetectionInstructions.push(
      [ol.render.canvas.Instruction.SET_FILL_STYLE,
       ol.color.asString(ol.render.canvas.defaultFillStyle)]);
  if (state.strokeStyle !== undefined) {
    this.hitDetectionInstructions.push(
        [ol.render.canvas.Instruction.SET_STROKE_STYLE,
         state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin,
         state.miterLimit, state.lineDash]);
  }
  var ends = polygonGeometry.getEnds();
  var flatCoordinates = polygonGeometry.getOrientedFlatCoordinates();
  var stride = polygonGeometry.getStride();
  this.drawFlatCoordinatess_(flatCoordinates, 0, ends, stride);
  this.endGeometry(polygonGeometry, feature);
};


/**
 * @inheritDoc
 */
ol.render.canvas.PolygonReplay.prototype.drawMultiPolygonGeometry = function(multiPolygonGeometry, feature) {
  var state = this.state_;
  goog.asserts.assert(state, 'state should not be null');
  var fillStyle = state.fillStyle;
  var strokeStyle = state.strokeStyle;
  if (fillStyle === undefined && strokeStyle === undefined) {
    return;
  }
  if (strokeStyle !== undefined) {
    goog.asserts.assert(state.lineWidth !== undefined,
        'state.lineWidth should be defined');
  }
  this.setFillStrokeStyles_();
  this.beginGeometry(multiPolygonGeometry, feature);
  // always fill the multi-polygon for hit detection
  this.hitDetectionInstructions.push(
      [ol.render.canvas.Instruction.SET_FILL_STYLE,
       ol.color.asString(ol.render.canvas.defaultFillStyle)]);
  if (state.strokeStyle !== undefined) {
    this.hitDetectionInstructions.push(
        [ol.render.canvas.Instruction.SET_STROKE_STYLE,
         state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin,
         state.miterLimit, state.lineDash]);
  }
  var endss = multiPolygonGeometry.getEndss();
  var flatCoordinates = multiPolygonGeometry.getOrientedFlatCoordinates();
  var stride = multiPolygonGeometry.getStride();
  var offset = 0;
  var i, ii;
  for (i = 0, ii = endss.length; i < ii; ++i) {
    offset = this.drawFlatCoordinatess_(
        flatCoordinates, offset, endss[i], stride);
  }
  this.endGeometry(multiPolygonGeometry, feature);
};


/**
 * @inheritDoc
 */
ol.render.canvas.PolygonReplay.prototype.finish = function() {
  goog.asserts.assert(this.state_, 'this.state_ should not be null');
  this.reverseHitDetectionInstructions_();
  this.state_ = null;
  // We want to preserve topology when drawing polygons.  Polygons are
  // simplified using quantization and point elimination. However, we might
  // have received a mix of quantized and non-quantized geometries, so ensure
  // that all are quantized by quantizing all coordinates in the batch.
  var tolerance = this.tolerance;
  if (tolerance !== 0) {
    var coordinates = this.coordinates;
    var i, ii;
    for (i = 0, ii = coordinates.length; i < ii; ++i) {
      coordinates[i] = ol.geom.flat.simplify.snap(coordinates[i], tolerance);
    }
  }
};


/**
 * @inheritDoc
 */
ol.render.canvas.PolygonReplay.prototype.getBufferedMaxExtent = function() {
  if (!this.bufferedMaxExtent_) {
    this.bufferedMaxExtent_ = ol.extent.clone(this.maxExtent);
    if (this.maxLineWidth > 0) {
      var width = this.resolution * (this.maxLineWidth + 1) / 2;
      ol.extent.buffer(this.bufferedMaxExtent_, width, this.bufferedMaxExtent_);
    }
  }
  return this.bufferedMaxExtent_;
};


/**
 * @inheritDoc
 */
ol.render.canvas.PolygonReplay.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) {
  goog.asserts.assert(this.state_, 'this.state_ should not be null');
  goog.asserts.assert(fillStyle || strokeStyle,
      'fillStyle or strokeStyle should not be null');
  var state = this.state_;
  if (fillStyle) {
    var fillStyleColor = fillStyle.getColor();
    state.fillStyle = ol.color.asString(fillStyleColor ?
        fillStyleColor : ol.render.canvas.defaultFillStyle);
  } else {
    state.fillStyle = undefined;
  }
  if (strokeStyle) {
    var strokeStyleColor = strokeStyle.getColor();
    state.strokeStyle = ol.color.asString(strokeStyleColor ?
        strokeStyleColor : ol.render.canvas.defaultStrokeStyle);
    var strokeStyleLineCap = strokeStyle.getLineCap();
    state.lineCap = strokeStyleLineCap !== undefined ?
        strokeStyleLineCap : ol.render.canvas.defaultLineCap;
    var strokeStyleLineDash = strokeStyle.getLineDash();
    state.lineDash = strokeStyleLineDash ?
        strokeStyleLineDash.slice() : ol.render.canvas.defaultLineDash;
    var strokeStyleLineJoin = strokeStyle.getLineJoin();
    state.lineJoin = strokeStyleLineJoin !== undefined ?
        strokeStyleLineJoin : ol.render.canvas.defaultLineJoin;
    var strokeStyleWidth = strokeStyle.getWidth();
    state.lineWidth = strokeStyleWidth !== undefined ?
        strokeStyleWidth : ol.render.canvas.defaultLineWidth;
    var strokeStyleMiterLimit = strokeStyle.getMiterLimit();
    state.miterLimit = strokeStyleMiterLimit !== undefined ?
        strokeStyleMiterLimit : ol.render.canvas.defaultMiterLimit;

    if (state.lineWidth > this.maxLineWidth) {
      this.maxLineWidth = state.lineWidth;
      // invalidate the buffered max extent cache
      this.bufferedMaxExtent_ = null;
    }
  } else {
    state.strokeStyle = undefined;
    state.lineCap = undefined;
    state.lineDash = null;
    state.lineJoin = undefined;
    state.lineWidth = undefined;
    state.miterLimit = undefined;
  }
};


/**
 * @private
 */
ol.render.canvas.PolygonReplay.prototype.setFillStrokeStyles_ = function() {
  var state = this.state_;
  var fillStyle = state.fillStyle;
  var strokeStyle = state.strokeStyle;
  var lineCap = state.lineCap;
  var lineDash = state.lineDash;
  var lineJoin = state.lineJoin;
  var lineWidth = state.lineWidth;
  var miterLimit = state.miterLimit;
  if (fillStyle !== undefined && state.currentFillStyle != fillStyle) {
    this.instructions.push(
        [ol.render.canvas.Instruction.SET_FILL_STYLE, fillStyle]);
    state.currentFillStyle = state.fillStyle;
  }
  if (strokeStyle !== undefined) {
    goog.asserts.assert(lineCap !== undefined, 'lineCap should be defined');
    goog.asserts.assert(lineDash, 'lineDash should not be null');
    goog.asserts.assert(lineJoin !== undefined, 'lineJoin should be defined');
    goog.asserts.assert(lineWidth !== undefined, 'lineWidth should be defined');
    goog.asserts.assert(miterLimit !== undefined,
        'miterLimit should be defined');
    if (state.currentStrokeStyle != strokeStyle ||
        state.currentLineCap != lineCap ||
        state.currentLineDash != lineDash ||
        state.currentLineJoin != lineJoin ||
        state.currentLineWidth != lineWidth ||
        state.currentMiterLimit != miterLimit) {
      this.instructions.push(
          [ol.render.canvas.Instruction.SET_STROKE_STYLE,
           strokeStyle, lineWidth, lineCap, lineJoin, miterLimit, lineDash]);
      state.currentStrokeStyle = strokeStyle;
      state.currentLineCap = lineCap;
      state.currentLineDash = lineDash;
      state.currentLineJoin = lineJoin;
      state.currentLineWidth = lineWidth;
      state.currentMiterLimit = miterLimit;
    }
  }
};


/**
 * @constructor
 * @extends {ol.render.canvas.Replay}
 * @param {number} tolerance Tolerance.
 * @param {ol.Extent} maxExtent Maximum extent.
 * @param {number} resolution Resolution.
 * @protected
 * @struct
 */
ol.render.canvas.TextReplay = function(tolerance, maxExtent, resolution) {

  goog.base(this, tolerance, maxExtent, resolution);

  /**
   * @private
   * @type {?ol.render.canvas.FillState}
   */
  this.replayFillState_ = null;

  /**
   * @private
   * @type {?ol.render.canvas.StrokeState}
   */
  this.replayStrokeState_ = null;

  /**
   * @private
   * @type {?ol.render.canvas.TextState}
   */
  this.replayTextState_ = null;

  /**
   * @private
   * @type {string}
   */
  this.text_ = '';

  /**
   * @private
   * @type {number}
   */
  this.textOffsetX_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.textOffsetY_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.textRotation_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.textScale_ = 0;

  /**
   * @private
   * @type {?ol.render.canvas.FillState}
   */
  this.textFillState_ = null;

  /**
   * @private
   * @type {?ol.render.canvas.StrokeState}
   */
  this.textStrokeState_ = null;

  /**
   * @private
   * @type {?ol.render.canvas.TextState}
   */
  this.textState_ = null;

};
goog.inherits(ol.render.canvas.TextReplay, ol.render.canvas.Replay);


/**
 * @inheritDoc
 */
ol.render.canvas.TextReplay.prototype.drawText = function(flatCoordinates, offset, end, stride, geometry, feature) {
  if (this.text_ === '' || !this.textState_ ||
      (!this.textFillState_ && !this.textStrokeState_)) {
    return;
  }
  if (this.textFillState_) {
    this.setReplayFillState_(this.textFillState_);
  }
  if (this.textStrokeState_) {
    this.setReplayStrokeState_(this.textStrokeState_);
  }
  this.setReplayTextState_(this.textState_);
  this.beginGeometry(geometry, feature);
  var myBegin = this.coordinates.length;
  var myEnd =
      this.appendFlatCoordinates(flatCoordinates, offset, end, stride, false);
  var fill = !!this.textFillState_;
  var stroke = !!this.textStrokeState_;
  var drawTextInstruction = [
    ol.render.canvas.Instruction.DRAW_TEXT, myBegin, myEnd, this.text_,
    this.textOffsetX_, this.textOffsetY_, this.textRotation_, this.textScale_,
    fill, stroke];
  this.instructions.push(drawTextInstruction);
  this.hitDetectionInstructions.push(drawTextInstruction);
  this.endGeometry(geometry, feature);
};


/**
 * @param {ol.render.canvas.FillState} fillState Fill state.
 * @private
 */
ol.render.canvas.TextReplay.prototype.setReplayFillState_ = function(fillState) {
  var replayFillState = this.replayFillState_;
  if (replayFillState &&
      replayFillState.fillStyle == fillState.fillStyle) {
    return;
  }
  var setFillStyleInstruction =
      [ol.render.canvas.Instruction.SET_FILL_STYLE, fillState.fillStyle];
  this.instructions.push(setFillStyleInstruction);
  this.hitDetectionInstructions.push(setFillStyleInstruction);
  if (!replayFillState) {
    this.replayFillState_ = {
      fillStyle: fillState.fillStyle
    };
  } else {
    replayFillState.fillStyle = fillState.fillStyle;
  }
};


/**
 * @param {ol.render.canvas.StrokeState} strokeState Stroke state.
 * @private
 */
ol.render.canvas.TextReplay.prototype.setReplayStrokeState_ = function(strokeState) {
  var replayStrokeState = this.replayStrokeState_;
  if (replayStrokeState &&
      replayStrokeState.lineCap == strokeState.lineCap &&
      replayStrokeState.lineDash == strokeState.lineDash &&
      replayStrokeState.lineJoin == strokeState.lineJoin &&
      replayStrokeState.lineWidth == strokeState.lineWidth &&
      replayStrokeState.miterLimit == strokeState.miterLimit &&
      replayStrokeState.strokeStyle == strokeState.strokeStyle) {
    return;
  }
  var setStrokeStyleInstruction = [
    ol.render.canvas.Instruction.SET_STROKE_STYLE, strokeState.strokeStyle,
    strokeState.lineWidth, strokeState.lineCap, strokeState.lineJoin,
    strokeState.miterLimit, strokeState.lineDash, false
  ];
  this.instructions.push(setStrokeStyleInstruction);
  this.hitDetectionInstructions.push(setStrokeStyleInstruction);
  if (!replayStrokeState) {
    this.replayStrokeState_ = {
      lineCap: strokeState.lineCap,
      lineDash: strokeState.lineDash,
      lineJoin: strokeState.lineJoin,
      lineWidth: strokeState.lineWidth,
      miterLimit: strokeState.miterLimit,
      strokeStyle: strokeState.strokeStyle
    };
  } else {
    replayStrokeState.lineCap = strokeState.lineCap;
    replayStrokeState.lineDash = strokeState.lineDash;
    replayStrokeState.lineJoin = strokeState.lineJoin;
    replayStrokeState.lineWidth = strokeState.lineWidth;
    replayStrokeState.miterLimit = strokeState.miterLimit;
    replayStrokeState.strokeStyle = strokeState.strokeStyle;
  }
};


/**
 * @param {ol.render.canvas.TextState} textState Text state.
 * @private
 */
ol.render.canvas.TextReplay.prototype.setReplayTextState_ = function(textState) {
  var replayTextState = this.replayTextState_;
  if (replayTextState &&
      replayTextState.font == textState.font &&
      replayTextState.textAlign == textState.textAlign &&
      replayTextState.textBaseline == textState.textBaseline) {
    return;
  }
  var setTextStyleInstruction = [ol.render.canvas.Instruction.SET_TEXT_STYLE,
    textState.font, textState.textAlign, textState.textBaseline];
  this.instructions.push(setTextStyleInstruction);
  this.hitDetectionInstructions.push(setTextStyleInstruction);
  if (!replayTextState) {
    this.replayTextState_ = {
      font: textState.font,
      textAlign: textState.textAlign,
      textBaseline: textState.textBaseline
    };
  } else {
    replayTextState.font = textState.font;
    replayTextState.textAlign = textState.textAlign;
    replayTextState.textBaseline = textState.textBaseline;
  }
};


/**
 * @inheritDoc
 */
ol.render.canvas.TextReplay.prototype.setTextStyle = function(textStyle) {
  if (!textStyle) {
    this.text_ = '';
  } else {
    var textFillStyle = textStyle.getFill();
    if (!textFillStyle) {
      this.textFillState_ = null;
    } else {
      var textFillStyleColor = textFillStyle.getColor();
      var fillStyle = ol.color.asString(textFillStyleColor ?
          textFillStyleColor : ol.render.canvas.defaultFillStyle);
      if (!this.textFillState_) {
        this.textFillState_ = {
          fillStyle: fillStyle
        };
      } else {
        var textFillState = this.textFillState_;
        textFillState.fillStyle = fillStyle;
      }
    }
    var textStrokeStyle = textStyle.getStroke();
    if (!textStrokeStyle) {
      this.textStrokeState_ = null;
    } else {
      var textStrokeStyleColor = textStrokeStyle.getColor();
      var textStrokeStyleLineCap = textStrokeStyle.getLineCap();
      var textStrokeStyleLineDash = textStrokeStyle.getLineDash();
      var textStrokeStyleLineJoin = textStrokeStyle.getLineJoin();
      var textStrokeStyleWidth = textStrokeStyle.getWidth();
      var textStrokeStyleMiterLimit = textStrokeStyle.getMiterLimit();
      var lineCap = textStrokeStyleLineCap !== undefined ?
          textStrokeStyleLineCap : ol.render.canvas.defaultLineCap;
      var lineDash = textStrokeStyleLineDash ?
          textStrokeStyleLineDash.slice() : ol.render.canvas.defaultLineDash;
      var lineJoin = textStrokeStyleLineJoin !== undefined ?
          textStrokeStyleLineJoin : ol.render.canvas.defaultLineJoin;
      var lineWidth = textStrokeStyleWidth !== undefined ?
          textStrokeStyleWidth : ol.render.canvas.defaultLineWidth;
      var miterLimit = textStrokeStyleMiterLimit !== undefined ?
          textStrokeStyleMiterLimit : ol.render.canvas.defaultMiterLimit;
      var strokeStyle = ol.color.asString(textStrokeStyleColor ?
          textStrokeStyleColor : ol.render.canvas.defaultStrokeStyle);
      if (!this.textStrokeState_) {
        this.textStrokeState_ = {
          lineCap: lineCap,
          lineDash: lineDash,
          lineJoin: lineJoin,
          lineWidth: lineWidth,
          miterLimit: miterLimit,
          strokeStyle: strokeStyle
        };
      } else {
        var textStrokeState = this.textStrokeState_;
        textStrokeState.lineCap = lineCap;
        textStrokeState.lineDash = lineDash;
        textStrokeState.lineJoin = lineJoin;
        textStrokeState.lineWidth = lineWidth;
        textStrokeState.miterLimit = miterLimit;
        textStrokeState.strokeStyle = strokeStyle;
      }
    }
    var textFont = textStyle.getFont();
    var textOffsetX = textStyle.getOffsetX();
    var textOffsetY = textStyle.getOffsetY();
    var textRotation = textStyle.getRotation();
    var textScale = textStyle.getScale();
    var textText = textStyle.getText();
    var textTextAlign = textStyle.getTextAlign();
    var textTextBaseline = textStyle.getTextBaseline();
    var font = textFont !== undefined ?
        textFont : ol.render.canvas.defaultFont;
    var textAlign = textTextAlign !== undefined ?
        textTextAlign : ol.render.canvas.defaultTextAlign;
    var textBaseline = textTextBaseline !== undefined ?
        textTextBaseline : ol.render.canvas.defaultTextBaseline;
    if (!this.textState_) {
      this.textState_ = {
        font: font,
        textAlign: textAlign,
        textBaseline: textBaseline
      };
    } else {
      var textState = this.textState_;
      textState.font = font;
      textState.textAlign = textAlign;
      textState.textBaseline = textBaseline;
    }
    this.text_ = textText !== undefined ? textText : '';
    this.textOffsetX_ = textOffsetX !== undefined ? textOffsetX : 0;
    this.textOffsetY_ = textOffsetY !== undefined ? textOffsetY : 0;
    this.textRotation_ = textRotation !== undefined ? textRotation : 0;
    this.textScale_ = textScale !== undefined ? textScale : 1;
  }
};


/**
 * @constructor
 * @implements {ol.render.IReplayGroup}
 * @param {number} tolerance Tolerance.
 * @param {ol.Extent} maxExtent Max extent.
 * @param {number} resolution Resolution.
 * @param {number=} opt_renderBuffer Optional rendering buffer.
 * @struct
 */
ol.render.canvas.ReplayGroup = function(
    tolerance, maxExtent, resolution, opt_renderBuffer) {

  /**
   * @private
   * @type {number}
   */
  this.tolerance_ = tolerance;

  /**
   * @private
   * @type {ol.Extent}
   */
  this.maxExtent_ = maxExtent;

  /**
   * @private
   * @type {number}
   */
  this.resolution_ = resolution;

  /**
   * @private
   * @type {number|undefined}
   */
  this.renderBuffer_ = opt_renderBuffer;

  /**
   * @private
   * @type {!Object.<string,
   *        Object.<ol.render.ReplayType, ol.render.canvas.Replay>>}
   */
  this.replaysByZIndex_ = {};

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.hitDetectionContext_ = ol.dom.createCanvasContext2D(1, 1);

  /**
   * @private
   * @type {!goog.vec.Mat4.Number}
   */
  this.hitDetectionTransform_ = goog.vec.Mat4.createNumber();

};


/**
 * FIXME empty description for jsdoc
 */
ol.render.canvas.ReplayGroup.prototype.finish = function() {
  var zKey;
  for (zKey in this.replaysByZIndex_) {
    var replays = this.replaysByZIndex_[zKey];
    var replayKey;
    for (replayKey in replays) {
      replays[replayKey].finish();
    }
  }
};


/**
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {number} resolution Resolution.
 * @param {number} rotation Rotation.
 * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
 *     to skip.
 * @param {function((ol.Feature|ol.render.Feature)): T} callback Feature
 *     callback.
 * @return {T|undefined} Callback result.
 * @template T
 */
ol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function(
    coordinate, resolution, rotation, skippedFeaturesHash, callback) {

  var transform = this.hitDetectionTransform_;
  ol.vec.Mat4.makeTransform2D(transform, 0.5, 0.5,
      1 / resolution, -1 / resolution, -rotation,
      -coordinate[0], -coordinate[1]);

  var context = this.hitDetectionContext_;
  context.clearRect(0, 0, 1, 1);

  /**
   * @type {ol.Extent}
   */
  var hitExtent;
  if (this.renderBuffer_ !== undefined) {
    hitExtent = ol.extent.createEmpty();
    ol.extent.extendCoordinate(hitExtent, coordinate);
    ol.extent.buffer(hitExtent, resolution * this.renderBuffer_, hitExtent);
  }

  return this.replayHitDetection_(context, transform, rotation,
      skippedFeaturesHash,
      /**
       * @param {ol.Feature|ol.render.Feature} feature Feature.
       * @return {?} Callback result.
       */
      function(feature) {
        var imageData = context.getImageData(0, 0, 1, 1).data;
        if (imageData[3] > 0) {
          var result = callback(feature);
          if (result) {
            return result;
          }
          context.clearRect(0, 0, 1, 1);
        }
      }, hitExtent);
};


/**
 * @inheritDoc
 */
ol.render.canvas.ReplayGroup.prototype.getReplay = function(zIndex, replayType) {
  var zIndexKey = zIndex !== undefined ? zIndex.toString() : '0';
  var replays = this.replaysByZIndex_[zIndexKey];
  if (replays === undefined) {
    replays = {};
    this.replaysByZIndex_[zIndexKey] = replays;
  }
  var replay = replays[replayType];
  if (replay === undefined) {
    var Constructor = ol.render.canvas.BATCH_CONSTRUCTORS_[replayType];
    goog.asserts.assert(Constructor !== undefined,
        replayType +
        ' constructor missing from ol.render.canvas.BATCH_CONSTRUCTORS_');
    replay = new Constructor(this.tolerance_, this.maxExtent_,
        this.resolution_);
    replays[replayType] = replay;
  }
  return replay;
};


/**
 * @inheritDoc
 */
ol.render.canvas.ReplayGroup.prototype.isEmpty = function() {
  return goog.object.isEmpty(this.replaysByZIndex_);
};


/**
 * @param {CanvasRenderingContext2D} context Context.
 * @param {number} pixelRatio Pixel ratio.
 * @param {goog.vec.Mat4.Number} transform Transform.
 * @param {number} viewRotation View rotation.
 * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
 *     to skip.
 * @param {boolean=} opt_clip Clip at `maxExtent`. Default is true.
 */
ol.render.canvas.ReplayGroup.prototype.replay = function(context, pixelRatio,
    transform, viewRotation, skippedFeaturesHash, opt_clip) {

  /** @type {Array.<number>} */
  var zs = Object.keys(this.replaysByZIndex_).map(Number);
  zs.sort(ol.array.numberSafeCompareFunction);

  if (opt_clip !== false) {
    // setup clipping so that the parts of over-simplified geometries are not
    // visible outside the current extent when panning
    var maxExtent = this.maxExtent_;
    var minX = maxExtent[0];
    var minY = maxExtent[1];
    var maxX = maxExtent[2];
    var maxY = maxExtent[3];
    var flatClipCoords = [minX, minY, minX, maxY, maxX, maxY, maxX, minY];
    ol.geom.flat.transform.transform2D(
        flatClipCoords, 0, 8, 2, transform, flatClipCoords);
    context.save();
    context.beginPath();
    context.moveTo(flatClipCoords[0], flatClipCoords[1]);
    context.lineTo(flatClipCoords[2], flatClipCoords[3]);
    context.lineTo(flatClipCoords[4], flatClipCoords[5]);
    context.lineTo(flatClipCoords[6], flatClipCoords[7]);
    context.closePath();
    context.clip();
  }

  var i, ii, j, jj, replays, replay;
  for (i = 0, ii = zs.length; i < ii; ++i) {
    replays = this.replaysByZIndex_[zs[i].toString()];
    for (j = 0, jj = ol.render.REPLAY_ORDER.length; j < jj; ++j) {
      replay = replays[ol.render.REPLAY_ORDER[j]];
      if (replay !== undefined) {
        replay.replay(context, pixelRatio, transform, viewRotation,
            skippedFeaturesHash);
      }
    }
  }

  context.restore();
};


/**
 * @private
 * @param {CanvasRenderingContext2D} context Context.
 * @param {goog.vec.Mat4.Number} transform Transform.
 * @param {number} viewRotation View rotation.
 * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
 *     to skip.
 * @param {function((ol.Feature|ol.render.Feature)): T} featureCallback
 *     Feature callback.
 * @param {ol.Extent=} opt_hitExtent Only check features that intersect this
 *     extent.
 * @return {T|undefined} Callback result.
 * @template T
 */
ol.render.canvas.ReplayGroup.prototype.replayHitDetection_ = function(
    context, transform, viewRotation, skippedFeaturesHash,
    featureCallback, opt_hitExtent) {
  /** @type {Array.<number>} */
  var zs = Object.keys(this.replaysByZIndex_).map(Number);
  zs.sort(function(a, b) {
    return b - a;
  });

  var i, ii, j, replays, replay, result;
  for (i = 0, ii = zs.length; i < ii; ++i) {
    replays = this.replaysByZIndex_[zs[i].toString()];
    for (j = ol.render.REPLAY_ORDER.length - 1; j >= 0; --j) {
      replay = replays[ol.render.REPLAY_ORDER[j]];
      if (replay !== undefined) {
        result = replay.replayHitDetection(context, transform, viewRotation,
            skippedFeaturesHash, featureCallback, opt_hitExtent);
        if (result) {
          return result;
        }
      }
    }
  }
  return undefined;
};


/**
 * @const
 * @private
 * @type {Object.<ol.render.ReplayType,
 *                function(new: ol.render.canvas.Replay, number, ol.Extent,
 *                number)>}
 */
ol.render.canvas.BATCH_CONSTRUCTORS_ = {
  'Image': ol.render.canvas.ImageReplay,
  'LineString': ol.render.canvas.LineStringReplay,
  'Polygon': ol.render.canvas.PolygonReplay,
  'Text': ol.render.canvas.TextReplay
};

goog.provide('ol.render.Feature');


goog.require('goog.asserts');
goog.require('ol.extent');
goog.require('ol.geom.GeometryType');


/**
 * Lightweight, read-only, {@link ol.Feature} and {@link ol.geom.Geometry} like
 * structure, optimized for rendering and styling. Geometry access through the
 * API is limited to getting the type and extent of the geometry.
 *
 * @constructor
 * @param {ol.geom.GeometryType} type Geometry type.
 * @param {Array.<number>} flatCoordinates Flat coordinates. These always need
 *     to be right-handed for polygons.
 * @param {Array.<number>|Array.<Array.<number>>} ends Ends or Endss.
 * @param {Object.<string, *>} properties Properties.
 */
ol.render.Feature = function(type, flatCoordinates, ends, properties) {

  /**
   * @private
   * @type {ol.Extent|undefined}
   */
  this.extent_;

  goog.asserts.assert(type === ol.geom.GeometryType.POINT ||
      type === ol.geom.GeometryType.MULTI_POINT ||
      type === ol.geom.GeometryType.LINE_STRING ||
      type === ol.geom.GeometryType.MULTI_LINE_STRING ||
      type === ol.geom.GeometryType.POLYGON,
      'Need a Point, MultiPoint, LineString, MultiLineString or Polygon type');

  /**
   * @private
   * @type {ol.geom.GeometryType}
   */
  this.type_ = type;

  /**
   * @private
   * @type {Array.<number>}
   */
  this.flatCoordinates_ = flatCoordinates;

  /**
   * @private
   * @type {Array.<number>|Array.<Array.<number>>}
   */
  this.ends_ = ends;

  /**
   * @private
   * @type {Object.<string, *>}
   */
  this.properties_ = properties;

};


/**
 * Get a feature property by its key.
 * @param {string} key Key
 * @return {*} Value for the requested key.
 * @api
 */
ol.render.Feature.prototype.get = function(key) {
  return this.properties_[key];
};


/**
 * @return {Array.<number>|Array.<Array.<number>>} Ends or endss.
 */
ol.render.Feature.prototype.getEnds = function() {
  return this.ends_;
};


/**
 * Get the extent of this feature's geometry.
 * @return {ol.Extent} Extent.
 * @api
 */
ol.render.Feature.prototype.getExtent = function() {
  if (!this.extent_) {
    this.extent_ = this.type_ === ol.geom.GeometryType.POINT ?
        ol.extent.createOrUpdateFromCoordinate(this.flatCoordinates_) :
        ol.extent.createOrUpdateFromFlatCoordinates(
            this.flatCoordinates_, 0, this.flatCoordinates_.length, 2);

  }
  return this.extent_;
};


/**
 * @return {Array.<number>} Flat coordinates.
 */
ol.render.Feature.prototype.getOrientedFlatCoordinates = function() {
  return this.flatCoordinates_;
};


/**
 * @return {Array.<number>} Flat coordinates.
 */
ol.render.Feature.prototype.getFlatCoordinates =
    ol.render.Feature.prototype.getOrientedFlatCoordinates;


/**
 * Get the feature for working with its geometry.
 * @return {ol.render.Feature} Feature.
 * @api
 */
ol.render.Feature.prototype.getGeometry = function() {
  return this;
};


/**
 * Get the feature properties.
 * @return {Object.<string, *>} Feature properties.
 * @api
 */
ol.render.Feature.prototype.getProperties = function() {
  return this.properties_;
};


/**
 * Get the feature for working with its geometry.
 * @return {ol.render.Feature} Feature.
 */
ol.render.Feature.prototype.getSimplifiedGeometry =
    ol.render.Feature.prototype.getGeometry;


/**
 * @return {number} Stride.
 */
ol.render.Feature.prototype.getStride = function() {
  return 2;
};


/**
 * @return {undefined}
 */
ol.render.Feature.prototype.getStyleFunction = ol.nullFunction;


/**
 * Get the type of this feature's geometry.
 * @return {ol.geom.GeometryType} Geometry type.
 * @api
 */
ol.render.Feature.prototype.getType = function() {
  return this.type_;
};

goog.provide('ol.renderer.vector');

goog.require('goog.asserts');
goog.require('ol.render.Feature');
goog.require('ol.render.IReplayGroup');
goog.require('ol.style.ImageState');
goog.require('ol.style.Style');


/**
 * @param {ol.Feature|ol.render.Feature} feature1 Feature 1.
 * @param {ol.Feature|ol.render.Feature} feature2 Feature 2.
 * @return {number} Order.
 */
ol.renderer.vector.defaultOrder = function(feature1, feature2) {
  return goog.getUid(feature1) - goog.getUid(feature2);
};


/**
 * @param {number} resolution Resolution.
 * @param {number} pixelRatio Pixel ratio.
 * @return {number} Squared pixel tolerance.
 */
ol.renderer.vector.getSquaredTolerance = function(resolution, pixelRatio) {
  var tolerance = ol.renderer.vector.getTolerance(resolution, pixelRatio);
  return tolerance * tolerance;
};


/**
 * @param {number} resolution Resolution.
 * @param {number} pixelRatio Pixel ratio.
 * @return {number} Pixel tolerance.
 */
ol.renderer.vector.getTolerance = function(resolution, pixelRatio) {
  return ol.SIMPLIFY_TOLERANCE * resolution / pixelRatio;
};


/**
 * @param {ol.render.IReplayGroup} replayGroup Replay group.
 * @param {ol.geom.Circle} geometry Geometry.
 * @param {ol.style.Style} style Style.
 * @param {ol.Feature} feature Feature.
 * @private
 */
ol.renderer.vector.renderCircleGeometry_ = function(replayGroup, geometry, style, feature) {
  var fillStyle = style.getFill();
  var strokeStyle = style.getStroke();
  if (fillStyle || strokeStyle) {
    var polygonReplay = replayGroup.getReplay(
        style.getZIndex(), ol.render.ReplayType.POLYGON);
    polygonReplay.setFillStrokeStyle(fillStyle, strokeStyle);
    polygonReplay.drawCircleGeometry(geometry, feature);
  }
  var textStyle = style.getText();
  if (textStyle) {
    var textReplay = replayGroup.getReplay(
        style.getZIndex(), ol.render.ReplayType.TEXT);
    textReplay.setTextStyle(textStyle);
    textReplay.drawText(geometry.getCenter(), 0, 2, 2, geometry, feature);
  }
};


/**
 * @param {ol.render.IReplayGroup} replayGroup Replay group.
 * @param {ol.Feature|ol.render.Feature} feature Feature.
 * @param {ol.style.Style} style Style.
 * @param {number} squaredTolerance Squared tolerance.
 * @param {function(this: T, goog.events.Event)} listener Listener function.
 * @param {T} thisArg Value to use as `this` when executing `listener`.
 * @return {boolean} `true` if style is loading.
 * @template T
 */
ol.renderer.vector.renderFeature = function(
    replayGroup, feature, style, squaredTolerance, listener, thisArg) {
  var loading = false;
  var imageStyle, imageState;
  imageStyle = style.getImage();
  if (imageStyle) {
    imageState = imageStyle.getImageState();
    if (imageState == ol.style.ImageState.LOADED ||
        imageState == ol.style.ImageState.ERROR) {
      imageStyle.unlistenImageChange(listener, thisArg);
    } else {
      if (imageState == ol.style.ImageState.IDLE) {
        imageStyle.load();
      }
      imageState = imageStyle.getImageState();
      goog.asserts.assert(imageState == ol.style.ImageState.LOADING,
          'imageState should be LOADING');
      imageStyle.listenImageChange(listener, thisArg);
      loading = true;
    }
  }
  ol.renderer.vector.renderFeature_(replayGroup, feature, style,
      squaredTolerance);
  return loading;
};


/**
 * @param {ol.render.IReplayGroup} replayGroup Replay group.
 * @param {ol.Feature|ol.render.Feature} feature Feature.
 * @param {ol.style.Style} style Style.
 * @param {number} squaredTolerance Squared tolerance.
 * @private
 */
ol.renderer.vector.renderFeature_ = function(
    replayGroup, feature, style, squaredTolerance) {
  var geometry = style.getGeometryFunction()(feature);
  if (!geometry) {
    return;
  }
  var simplifiedGeometry = geometry.getSimplifiedGeometry(squaredTolerance);
  var geometryRenderer =
      ol.renderer.vector.GEOMETRY_RENDERERS_[simplifiedGeometry.getType()];
  goog.asserts.assert(geometryRenderer !== undefined,
      'geometryRenderer should be defined');
  geometryRenderer(replayGroup, simplifiedGeometry, style, feature);
};


/**
 * @param {ol.render.IReplayGroup} replayGroup Replay group.
 * @param {ol.geom.GeometryCollection} geometry Geometry.
 * @param {ol.style.Style} style Style.
 * @param {ol.Feature} feature Feature.
 * @private
 */
ol.renderer.vector.renderGeometryCollectionGeometry_ = function(replayGroup, geometry, style, feature) {
  var geometries = geometry.getGeometriesArray();
  var i, ii;
  for (i = 0, ii = geometries.length; i < ii; ++i) {
    var geometryRenderer =
        ol.renderer.vector.GEOMETRY_RENDERERS_[geometries[i].getType()];
    goog.asserts.assert(geometryRenderer !== undefined,
        'geometryRenderer should be defined');
    geometryRenderer(replayGroup, geometries[i], style, feature);
  }
};


/**
 * @param {ol.render.IReplayGroup} replayGroup Replay group.
 * @param {ol.geom.LineString|ol.render.Feature} geometry Geometry.
 * @param {ol.style.Style} style Style.
 * @param {ol.Feature|ol.render.Feature} feature Feature.
 * @private
 */
ol.renderer.vector.renderLineStringGeometry_ = function(replayGroup, geometry, style, feature) {
  var strokeStyle = style.getStroke();
  if (strokeStyle) {
    var lineStringReplay = replayGroup.getReplay(
        style.getZIndex(), ol.render.ReplayType.LINE_STRING);
    lineStringReplay.setFillStrokeStyle(null, strokeStyle);
    lineStringReplay.drawLineStringGeometry(geometry, feature);
  }
  var textStyle = style.getText();
  if (textStyle) {
    var textReplay = replayGroup.getReplay(
        style.getZIndex(), ol.render.ReplayType.TEXT);
    textReplay.setTextStyle(textStyle);
    textReplay.drawText(geometry.getFlatMidpoint(), 0, 2, 2, geometry, feature);
  }
};


/**
 * @param {ol.render.IReplayGroup} replayGroup Replay group.
 * @param {ol.geom.MultiLineString|ol.render.Feature} geometry Geometry.
 * @param {ol.style.Style} style Style.
 * @param {ol.Feature|ol.render.Feature} feature Feature.
 * @private
 */
ol.renderer.vector.renderMultiLineStringGeometry_ = function(replayGroup, geometry, style, feature) {
  var strokeStyle = style.getStroke();
  if (strokeStyle) {
    var lineStringReplay = replayGroup.getReplay(
        style.getZIndex(), ol.render.ReplayType.LINE_STRING);
    lineStringReplay.setFillStrokeStyle(null, strokeStyle);
    lineStringReplay.drawMultiLineStringGeometry(geometry, feature);
  }
  var textStyle = style.getText();
  if (textStyle) {
    var textReplay = replayGroup.getReplay(
        style.getZIndex(), ol.render.ReplayType.TEXT);
    textReplay.setTextStyle(textStyle);
    var flatMidpointCoordinates = geometry.getFlatMidpoints();
    textReplay.drawText(flatMidpointCoordinates, 0,
        flatMidpointCoordinates.length, 2, geometry, feature);
  }
};


/**
 * @param {ol.render.IReplayGroup} replayGroup Replay group.
 * @param {ol.geom.MultiPolygon} geometry Geometry.
 * @param {ol.style.Style} style Style.
 * @param {ol.Feature} feature Feature.
 * @private
 */
ol.renderer.vector.renderMultiPolygonGeometry_ = function(replayGroup, geometry, style, feature) {
  var fillStyle = style.getFill();
  var strokeStyle = style.getStroke();
  if (strokeStyle || fillStyle) {
    var polygonReplay = replayGroup.getReplay(
        style.getZIndex(), ol.render.ReplayType.POLYGON);
    polygonReplay.setFillStrokeStyle(fillStyle, strokeStyle);
    polygonReplay.drawMultiPolygonGeometry(geometry, feature);
  }
  var textStyle = style.getText();
  if (textStyle) {
    var textReplay = replayGroup.getReplay(
        style.getZIndex(), ol.render.ReplayType.TEXT);
    textReplay.setTextStyle(textStyle);
    var flatInteriorPointCoordinates = geometry.getFlatInteriorPoints();
    textReplay.drawText(flatInteriorPointCoordinates, 0,
        flatInteriorPointCoordinates.length, 2, geometry, feature);
  }
};


/**
 * @param {ol.render.IReplayGroup} replayGroup Replay group.
 * @param {ol.geom.Point|ol.render.Feature} geometry Geometry.
 * @param {ol.style.Style} style Style.
 * @param {ol.Feature|ol.render.Feature} feature Feature.
 * @private
 */
ol.renderer.vector.renderPointGeometry_ = function(replayGroup, geometry, style, feature) {
  var imageStyle = style.getImage();
  if (imageStyle) {
    if (imageStyle.getImageState() != ol.style.ImageState.LOADED) {
      return;
    }
    var imageReplay = replayGroup.getReplay(
        style.getZIndex(), ol.render.ReplayType.IMAGE);
    imageReplay.setImageStyle(imageStyle);
    imageReplay.drawPointGeometry(geometry, feature);
  }
  var textStyle = style.getText();
  if (textStyle) {
    var textReplay = replayGroup.getReplay(
        style.getZIndex(), ol.render.ReplayType.TEXT);
    textReplay.setTextStyle(textStyle);
    textReplay.drawText(geometry.getFlatCoordinates(), 0, 2, 2, geometry,
        feature);
  }
};


/**
 * @param {ol.render.IReplayGroup} replayGroup Replay group.
 * @param {ol.geom.MultiPoint|ol.render.Feature} geometry Geometry.
 * @param {ol.style.Style} style Style.
 * @param {ol.Feature|ol.render.Feature} feature Feature.
 * @private
 */
ol.renderer.vector.renderMultiPointGeometry_ = function(replayGroup, geometry, style, feature) {
  var imageStyle = style.getImage();
  if (imageStyle) {
    if (imageStyle.getImageState() != ol.style.ImageState.LOADED) {
      return;
    }
    var imageReplay = replayGroup.getReplay(
        style.getZIndex(), ol.render.ReplayType.IMAGE);
    imageReplay.setImageStyle(imageStyle);
    imageReplay.drawMultiPointGeometry(geometry, feature);
  }
  var textStyle = style.getText();
  if (textStyle) {
    var textReplay = replayGroup.getReplay(
        style.getZIndex(), ol.render.ReplayType.TEXT);
    textReplay.setTextStyle(textStyle);
    var flatCoordinates = geometry.getFlatCoordinates();
    textReplay.drawText(flatCoordinates, 0, flatCoordinates.length,
        geometry.getStride(), geometry, feature);
  }
};


/**
 * @param {ol.render.IReplayGroup} replayGroup Replay group.
 * @param {ol.geom.Polygon|ol.render.Feature} geometry Geometry.
 * @param {ol.style.Style} style Style.
 * @param {ol.Feature|ol.render.Feature} feature Feature.
 * @private
 */
ol.renderer.vector.renderPolygonGeometry_ = function(replayGroup, geometry, style, feature) {
  var fillStyle = style.getFill();
  var strokeStyle = style.getStroke();
  if (fillStyle || strokeStyle) {
    var polygonReplay = replayGroup.getReplay(
        style.getZIndex(), ol.render.ReplayType.POLYGON);
    polygonReplay.setFillStrokeStyle(fillStyle, strokeStyle);
    polygonReplay.drawPolygonGeometry(geometry, feature);
  }
  var textStyle = style.getText();
  if (textStyle) {
    var textReplay = replayGroup.getReplay(
        style.getZIndex(), ol.render.ReplayType.TEXT);
    textReplay.setTextStyle(textStyle);
    textReplay.drawText(
        geometry.getFlatInteriorPoint(), 0, 2, 2, geometry, feature);
  }
};


/**
 * @const
 * @private
 * @type {Object.<ol.geom.GeometryType,
 *                function(ol.render.IReplayGroup, ol.geom.Geometry,
 *                         ol.style.Style, Object)>}
 */
ol.renderer.vector.GEOMETRY_RENDERERS_ = {
  'Point': ol.renderer.vector.renderPointGeometry_,
  'LineString': ol.renderer.vector.renderLineStringGeometry_,
  'Polygon': ol.renderer.vector.renderPolygonGeometry_,
  'MultiPoint': ol.renderer.vector.renderMultiPointGeometry_,
  'MultiLineString': ol.renderer.vector.renderMultiLineStringGeometry_,
  'MultiPolygon': ol.renderer.vector.renderMultiPolygonGeometry_,
  'GeometryCollection': ol.renderer.vector.renderGeometryCollectionGeometry_,
  'Circle': ol.renderer.vector.renderCircleGeometry_
};

goog.provide('ol.ImageCanvas');

goog.require('goog.asserts');
goog.require('ol.ImageBase');
goog.require('ol.ImageState');


/**
 * @constructor
 * @extends {ol.ImageBase}
 * @param {ol.Extent} extent Extent.
 * @param {number} resolution Resolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {Array.<ol.Attribution>} attributions Attributions.
 * @param {HTMLCanvasElement} canvas Canvas.
 * @param {ol.ImageCanvasLoader=} opt_loader Optional loader function to
 *     support asynchronous canvas drawing.
 */
ol.ImageCanvas = function(extent, resolution, pixelRatio, attributions,
    canvas, opt_loader) {

  /**
   * Optional canvas loader function.
   * @type {?ol.ImageCanvasLoader}
   * @private
   */
  this.loader_ = opt_loader !== undefined ? opt_loader : null;

  var state = opt_loader !== undefined ?
      ol.ImageState.IDLE : ol.ImageState.LOADED;

  goog.base(this, extent, resolution, pixelRatio, state, attributions);

  /**
   * @private
   * @type {HTMLCanvasElement}
   */
  this.canvas_ = canvas;

  /**
   * @private
   * @type {Error}
   */
  this.error_ = null;

};
goog.inherits(ol.ImageCanvas, ol.ImageBase);


/**
 * Get any error associated with asynchronous rendering.
 * @return {Error} Any error that occurred during rendering.
 */
ol.ImageCanvas.prototype.getError = function() {
  return this.error_;
};


/**
 * Handle async drawing complete.
 * @param {Error} err Any error during drawing.
 * @private
 */
ol.ImageCanvas.prototype.handleLoad_ = function(err) {
  if (err) {
    this.error_ = err;
    this.state = ol.ImageState.ERROR;
  } else {
    this.state = ol.ImageState.LOADED;
  }
  this.changed();
};


/**
 * Trigger drawing on canvas.
 */
ol.ImageCanvas.prototype.load = function() {
  if (this.state == ol.ImageState.IDLE) {
    goog.asserts.assert(this.loader_, 'this.loader_ must be set');
    this.state = ol.ImageState.LOADING;
    this.changed();
    this.loader_(this.handleLoad_.bind(this));
  }
};


/**
 * @inheritDoc
 */
ol.ImageCanvas.prototype.getImage = function(opt_context) {
  return this.canvas_;
};


/**
 * A function that is called to trigger asynchronous canvas drawing.  It is
 * called with a "done" callback that should be called when drawing is done.
 * If any error occurs during drawing, the "done" callback should be called with
 * that error.
 *
 * @typedef {function(function(Error))}
 */
ol.ImageCanvasLoader;

goog.provide('ol.reproj');

goog.require('goog.labs.userAgent.browser');
goog.require('goog.labs.userAgent.platform');
goog.require('goog.math');
goog.require('ol.dom');
goog.require('ol.extent');
goog.require('ol.math');
goog.require('ol.proj');


/**
 * We need to employ more sophisticated solution
 * if the web browser antialiases clipping edges on canvas.
 *
 * Currently only Chrome does not antialias the edges, but this is probably
 * going to be "fixed" in the future: http://crbug.com/424291
 *
 * @type {boolean}
 * @private
 */
ol.reproj.browserAntialiasesClip_ = !goog.labs.userAgent.browser.isChrome() ||
                                    goog.labs.userAgent.platform.isIos();


/**
 * Calculates ideal resolution to use from the source in order to achieve
 * pixel mapping as close as possible to 1:1 during reprojection.
 * The resolution is calculated regardless of what resolutions
 * are actually available in the dataset (TileGrid, Image, ...).
 *
 * @param {ol.proj.Projection} sourceProj Source projection.
 * @param {ol.proj.Projection} targetProj Target projection.
 * @param {ol.Coordinate} targetCenter Target center.
 * @param {number} targetResolution Target resolution.
 * @return {number} The best resolution to use. Can be +-Infinity, NaN or 0.
 */
ol.reproj.calculateSourceResolution = function(sourceProj, targetProj,
    targetCenter, targetResolution) {

  var sourceCenter = ol.proj.transform(targetCenter, targetProj, sourceProj);

  // calculate the ideal resolution of the source data
  var sourceResolution =
      targetProj.getPointResolution(targetResolution, targetCenter);

  var targetMetersPerUnit = targetProj.getMetersPerUnit();
  if (targetMetersPerUnit !== undefined) {
    sourceResolution *= targetMetersPerUnit;
  }
  var sourceMetersPerUnit = sourceProj.getMetersPerUnit();
  if (sourceMetersPerUnit !== undefined) {
    sourceResolution /= sourceMetersPerUnit;
  }

  // Based on the projection properties, the point resolution at the specified
  // coordinates may be slightly different. We need to reverse-compensate this
  // in order to achieve optimal results.

  var compensationFactor =
      sourceProj.getPointResolution(sourceResolution, sourceCenter) /
      sourceResolution;

  if (goog.math.isFiniteNumber(compensationFactor) && compensationFactor > 0) {
    sourceResolution /= compensationFactor;
  }

  return sourceResolution;
};


/**
 * Enlarge the clipping triangle point by 1 pixel to ensure the edges overlap
 * in order to mask gaps caused by antialiasing.
 *
 * @param {number} centroidX Centroid of the triangle (x coordinate in pixels).
 * @param {number} centroidY Centroid of the triangle (y coordinate in pixels).
 * @param {number} x X coordinate of the point (in pixels).
 * @param {number} y Y coordinate of the point (in pixels).
 * @return {ol.Coordinate} New point 1 px farther from the centroid.
 * @private
 */
ol.reproj.enlargeClipPoint_ = function(centroidX, centroidY, x, y) {
  var dX = x - centroidX, dY = y - centroidY;
  var distance = Math.sqrt(dX * dX + dY * dY);
  return [Math.round(x + dX / distance), Math.round(y + dY / distance)];
};


/**
 * Renders the source data into new canvas based on the triangulation.
 *
 * @param {number} width Width of the canvas.
 * @param {number} height Height of the canvas.
 * @param {number} pixelRatio Pixel ratio.
 * @param {number} sourceResolution Source resolution.
 * @param {ol.Extent} sourceExtent Extent of the data source.
 * @param {number} targetResolution Target resolution.
 * @param {ol.Extent} targetExtent Target extent.
 * @param {ol.reproj.Triangulation} triangulation Calculated triangulation.
 * @param {Array.<{extent: ol.Extent,
 *                 image: (HTMLCanvasElement|Image|HTMLVideoElement)}>} sources
 *             Array of sources.
 * @param {boolean=} opt_renderEdges Render reprojection edges.
 * @return {HTMLCanvasElement} Canvas with reprojected data.
 */
ol.reproj.render = function(width, height, pixelRatio,
    sourceResolution, sourceExtent, targetResolution, targetExtent,
    triangulation, sources, opt_renderEdges) {

  var context = ol.dom.createCanvasContext2D(Math.round(pixelRatio * width),
                                             Math.round(pixelRatio * height));

  if (sources.length === 0) {
    return context.canvas;
  }

  context.scale(pixelRatio, pixelRatio);

  var sourceDataExtent = ol.extent.createEmpty();
  sources.forEach(function(src, i, arr) {
    ol.extent.extend(sourceDataExtent, src.extent);
  });

  var canvasWidthInUnits = ol.extent.getWidth(sourceDataExtent);
  var canvasHeightInUnits = ol.extent.getHeight(sourceDataExtent);
  var stitchContext = ol.dom.createCanvasContext2D(
      Math.round(pixelRatio * canvasWidthInUnits / sourceResolution),
      Math.round(pixelRatio * canvasHeightInUnits / sourceResolution));

  stitchContext.scale(pixelRatio / sourceResolution,
                      pixelRatio / sourceResolution);
  stitchContext.translate(-sourceDataExtent[0], sourceDataExtent[3]);

  sources.forEach(function(src, i, arr) {
    var xPos = src.extent[0];
    var yPos = -src.extent[3];
    var srcWidth = ol.extent.getWidth(src.extent);
    var srcHeight = ol.extent.getHeight(src.extent);

    stitchContext.drawImage(src.image, xPos, yPos, srcWidth, srcHeight);
  });

  var targetTopLeft = ol.extent.getTopLeft(targetExtent);

  triangulation.getTriangles().forEach(function(triangle, i, arr) {
    /* Calculate affine transform (src -> dst)
     * Resulting matrix can be used to transform coordinate
     * from `sourceProjection` to destination pixels.
     *
     * To optimize number of context calls and increase numerical stability,
     * we also do the following operations:
     * trans(-topLeftExtentCorner), scale(1 / targetResolution), scale(1, -1)
     * here before solving the linear system so [ui, vi] are pixel coordinates.
     *
     * Src points: xi, yi
     * Dst points: ui, vi
     * Affine coefficients: aij
     *
     * | x0 y0 1  0  0 0 |   |a00|   |u0|
     * | x1 y1 1  0  0 0 |   |a01|   |u1|
     * | x2 y2 1  0  0 0 | x |a02| = |u2|
     * |  0  0 0 x0 y0 1 |   |a10|   |v0|
     * |  0  0 0 x1 y1 1 |   |a11|   |v1|
     * |  0  0 0 x2 y2 1 |   |a12|   |v2|
     */
    var source = triangle.source, target = triangle.target;
    var x0 = source[0][0], y0 = source[0][1],
        x1 = source[1][0], y1 = source[1][1],
        x2 = source[2][0], y2 = source[2][1];
    var u0 = (target[0][0] - targetTopLeft[0]) / targetResolution,
        v0 = -(target[0][1] - targetTopLeft[1]) / targetResolution;
    var u1 = (target[1][0] - targetTopLeft[0]) / targetResolution,
        v1 = -(target[1][1] - targetTopLeft[1]) / targetResolution;
    var u2 = (target[2][0] - targetTopLeft[0]) / targetResolution,
        v2 = -(target[2][1] - targetTopLeft[1]) / targetResolution;

    // Shift all the source points to improve numerical stability
    // of all the subsequent calculations. The [x0, y0] is used here.
    // This is also used to simplify the linear system.
    var sourceNumericalShiftX = x0, sourceNumericalShiftY = y0;
    x0 = 0;
    y0 = 0;
    x1 -= sourceNumericalShiftX;
    y1 -= sourceNumericalShiftY;
    x2 -= sourceNumericalShiftX;
    y2 -= sourceNumericalShiftY;

    var augmentedMatrix = [
      [x1, y1, 0, 0, u1 - u0],
      [x2, y2, 0, 0, u2 - u0],
      [0, 0, x1, y1, v1 - v0],
      [0, 0, x2, y2, v2 - v0]
    ];
    var affineCoefs = ol.math.solveLinearSystem(augmentedMatrix);
    if (!affineCoefs) {
      return;
    }

    context.save();
    context.beginPath();
    if (ol.reproj.browserAntialiasesClip_) {
      var centroidX = (u0 + u1 + u2) / 3, centroidY = (v0 + v1 + v2) / 3;
      var p0 = ol.reproj.enlargeClipPoint_(centroidX, centroidY, u0, v0);
      var p1 = ol.reproj.enlargeClipPoint_(centroidX, centroidY, u1, v1);
      var p2 = ol.reproj.enlargeClipPoint_(centroidX, centroidY, u2, v2);

      context.moveTo(p0[0], p0[1]);
      context.lineTo(p1[0], p1[1]);
      context.lineTo(p2[0], p2[1]);
    } else {
      context.moveTo(u0, v0);
      context.lineTo(u1, v1);
      context.lineTo(u2, v2);
    }
    context.closePath();
    context.clip();

    context.transform(
        affineCoefs[0], affineCoefs[2], affineCoefs[1], affineCoefs[3], u0, v0);

    context.translate(sourceDataExtent[0] - sourceNumericalShiftX,
                      sourceDataExtent[3] - sourceNumericalShiftY);

    context.scale(sourceResolution / pixelRatio,
                  -sourceResolution / pixelRatio);

    context.drawImage(stitchContext.canvas, 0, 0);
    context.restore();
  });

  if (opt_renderEdges) {
    context.save();

    context.strokeStyle = 'black';
    context.lineWidth = 1;

    triangulation.getTriangles().forEach(function(triangle, i, arr) {
      var target = triangle.target;
      var u0 = (target[0][0] - targetTopLeft[0]) / targetResolution,
          v0 = -(target[0][1] - targetTopLeft[1]) / targetResolution;
      var u1 = (target[1][0] - targetTopLeft[0]) / targetResolution,
          v1 = -(target[1][1] - targetTopLeft[1]) / targetResolution;
      var u2 = (target[2][0] - targetTopLeft[0]) / targetResolution,
          v2 = -(target[2][1] - targetTopLeft[1]) / targetResolution;

      context.beginPath();
      context.moveTo(u0, v0);
      context.lineTo(u1, v1);
      context.lineTo(u2, v2);
      context.closePath();
      context.stroke();
    });

    context.restore();
  }
  return context.canvas;
};

goog.provide('ol.reproj.Triangulation');

goog.require('goog.asserts');
goog.require('goog.math');
goog.require('ol.extent');
goog.require('ol.proj');


/**
 * Single triangle; consists of 3 source points and 3 target points.
 *
 * @typedef {{source: Array.<ol.Coordinate>,
 *            target: Array.<ol.Coordinate>}}
 */
ol.reproj.Triangle;


/**
 * @classdesc
 * Class containing triangulation of the given target extent.
 * Used for determining source data and the reprojection itself.
 *
 * @param {ol.proj.Projection} sourceProj Source projection.
 * @param {ol.proj.Projection} targetProj Target projection.
 * @param {ol.Extent} targetExtent Target extent to triangulate.
 * @param {ol.Extent} maxSourceExtent Maximal source extent that can be used.
 * @param {number} errorThreshold Acceptable error (in source units).
 * @constructor
 */
ol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent,
    maxSourceExtent, errorThreshold) {

  /**
   * @type {ol.proj.Projection}
   * @private
   */
  this.sourceProj_ = sourceProj;

  /**
   * @type {ol.proj.Projection}
   * @private
   */
  this.targetProj_ = targetProj;

  /** @type {!Object.<string, ol.Coordinate>} */
  var transformInvCache = {};
  var transformInv = ol.proj.getTransform(this.targetProj_, this.sourceProj_);

  /**
   * @param {ol.Coordinate} c A coordinate.
   * @return {ol.Coordinate} Transformed coordinate.
   * @private
   */
  this.transformInv_ = function(c) {
    var key = c[0] + '/' + c[1];
    if (!transformInvCache[key]) {
      transformInvCache[key] = transformInv(c);
    }
    return transformInvCache[key];
  };

  /**
   * @type {ol.Extent}
   * @private
   */
  this.maxSourceExtent_ = maxSourceExtent;

  /**
   * @type {number}
   * @private
   */
  this.errorThresholdSquared_ = errorThreshold * errorThreshold;

  /**
   * @type {Array.<ol.reproj.Triangle>}
   * @private
   */
  this.triangles_ = [];

  /**
   * Indicates that the triangulation crosses edge of the source projection.
   * @type {boolean}
   * @private
   */
  this.wrapsXInSource_ = false;

  /**
   * @type {boolean}
   * @private
   */
  this.canWrapXInSource_ = this.sourceProj_.canWrapX() &&
      !!maxSourceExtent &&
      !!this.sourceProj_.getExtent() &&
      (ol.extent.getWidth(maxSourceExtent) ==
       ol.extent.getWidth(this.sourceProj_.getExtent()));

  /**
   * @type {?number}
   * @private
   */
  this.sourceWorldWidth_ = this.sourceProj_.getExtent() ?
      ol.extent.getWidth(this.sourceProj_.getExtent()) : null;

  /**
   * @type {?number}
   * @private
   */
  this.targetWorldWidth_ = this.targetProj_.getExtent() ?
      ol.extent.getWidth(this.targetProj_.getExtent()) : null;

  var destinationTopLeft = ol.extent.getTopLeft(targetExtent);
  var destinationTopRight = ol.extent.getTopRight(targetExtent);
  var destinationBottomRight = ol.extent.getBottomRight(targetExtent);
  var destinationBottomLeft = ol.extent.getBottomLeft(targetExtent);
  var sourceTopLeft = this.transformInv_(destinationTopLeft);
  var sourceTopRight = this.transformInv_(destinationTopRight);
  var sourceBottomRight = this.transformInv_(destinationBottomRight);
  var sourceBottomLeft = this.transformInv_(destinationBottomLeft);

  this.addQuad_(
      destinationTopLeft, destinationTopRight,
      destinationBottomRight, destinationBottomLeft,
      sourceTopLeft, sourceTopRight, sourceBottomRight, sourceBottomLeft,
      ol.RASTER_REPROJECTION_MAX_SUBDIVISION);

  if (this.wrapsXInSource_) {
    // Fix coordinates (ol.proj returns wrapped coordinates, "unwrap" here).
    // This significantly simplifies the rest of the reprojection process.

    goog.asserts.assert(this.sourceWorldWidth_ !== null);
    var leftBound = Infinity;
    this.triangles_.forEach(function(triangle, i, arr) {
      leftBound = Math.min(leftBound,
          triangle.source[0][0], triangle.source[1][0], triangle.source[2][0]);
    });

    // Shift triangles to be as close to `leftBound` as possible
    // (if the distance is more than `worldWidth / 2` it can be closer.
    this.triangles_.forEach(function(triangle) {
      if (Math.max(triangle.source[0][0], triangle.source[1][0],
          triangle.source[2][0]) - leftBound > this.sourceWorldWidth_ / 2) {
        var newTriangle = [[triangle.source[0][0], triangle.source[0][1]],
                           [triangle.source[1][0], triangle.source[1][1]],
                           [triangle.source[2][0], triangle.source[2][1]]];
        if ((newTriangle[0][0] - leftBound) > this.sourceWorldWidth_ / 2) {
          newTriangle[0][0] -= this.sourceWorldWidth_;
        }
        if ((newTriangle[1][0] - leftBound) > this.sourceWorldWidth_ / 2) {
          newTriangle[1][0] -= this.sourceWorldWidth_;
        }
        if ((newTriangle[2][0] - leftBound) > this.sourceWorldWidth_ / 2) {
          newTriangle[2][0] -= this.sourceWorldWidth_;
        }

        // Rarely (if the extent contains both the dateline and prime meridian)
        // the shift can in turn break some triangles.
        // Detect this here and don't shift in such cases.
        var minX = Math.min(
            newTriangle[0][0], newTriangle[1][0], newTriangle[2][0]);
        var maxX = Math.max(
            newTriangle[0][0], newTriangle[1][0], newTriangle[2][0]);
        if ((maxX - minX) < this.sourceWorldWidth_ / 2) {
          triangle.source = newTriangle;
        }
      }
    }, this);
  }

  transformInvCache = {};
};


/**
 * Adds triangle to the triangulation.
 * @param {ol.Coordinate} a The target a coordinate.
 * @param {ol.Coordinate} b The target b coordinate.
 * @param {ol.Coordinate} c The target c coordinate.
 * @param {ol.Coordinate} aSrc The source a coordinate.
 * @param {ol.Coordinate} bSrc The source b coordinate.
 * @param {ol.Coordinate} cSrc The source c coordinate.
 * @private
 */
ol.reproj.Triangulation.prototype.addTriangle_ = function(a, b, c,
    aSrc, bSrc, cSrc) {
  this.triangles_.push({
    source: [aSrc, bSrc, cSrc],
    target: [a, b, c]
  });
};


/**
 * Adds quad (points in clock-wise order) to the triangulation
 * (and reprojects the vertices) if valid.
 * Performs quad subdivision if needed to increase precision.
 *
 * @param {ol.Coordinate} a The target a coordinate.
 * @param {ol.Coordinate} b The target b coordinate.
 * @param {ol.Coordinate} c The target c coordinate.
 * @param {ol.Coordinate} d The target d coordinate.
 * @param {ol.Coordinate} aSrc The source a coordinate.
 * @param {ol.Coordinate} bSrc The source b coordinate.
 * @param {ol.Coordinate} cSrc The source c coordinate.
 * @param {ol.Coordinate} dSrc The source d coordinate.
 * @param {number} maxSubdivision Maximal allowed subdivision of the quad.
 * @private
 */
ol.reproj.Triangulation.prototype.addQuad_ = function(a, b, c, d,
    aSrc, bSrc, cSrc, dSrc, maxSubdivision) {

  var sourceQuadExtent = ol.extent.boundingExtent([aSrc, bSrc, cSrc, dSrc]);
  var sourceCoverageX = this.sourceWorldWidth_ ?
      ol.extent.getWidth(sourceQuadExtent) / this.sourceWorldWidth_ : null;

  // when the quad is wrapped in the source projection
  // it covers most of the projection extent, but not fully
  var wrapsX = this.sourceProj_.canWrapX() &&
               sourceCoverageX > 0.5 && sourceCoverageX < 1;

  var needsSubdivision = false;

  if (maxSubdivision > 0) {
    if (this.targetProj_.isGlobal() && this.targetWorldWidth_) {
      var targetQuadExtent = ol.extent.boundingExtent([a, b, c, d]);
      var targetCoverageX =
          ol.extent.getWidth(targetQuadExtent) / this.targetWorldWidth_;
      needsSubdivision |=
          targetCoverageX > ol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH;
    }
    if (!wrapsX && this.sourceProj_.isGlobal() && sourceCoverageX) {
      needsSubdivision |=
          sourceCoverageX > ol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH;
    }
  }

  if (!needsSubdivision && this.maxSourceExtent_) {
    if (!ol.extent.intersects(sourceQuadExtent, this.maxSourceExtent_)) {
      // whole quad outside source projection extent -> ignore
      return;
    }
  }

  if (!needsSubdivision) {
    if (!isFinite(aSrc[0]) || !isFinite(aSrc[1]) ||
        !isFinite(bSrc[0]) || !isFinite(bSrc[1]) ||
        !isFinite(cSrc[0]) || !isFinite(cSrc[1]) ||
        !isFinite(dSrc[0]) || !isFinite(dSrc[1])) {
      if (maxSubdivision > 0) {
        needsSubdivision = true;
      } else {
        return;
      }
    }
  }

  if (maxSubdivision > 0) {
    if (!needsSubdivision) {
      var center = [(a[0] + c[0]) / 2, (a[1] + c[1]) / 2];
      var centerSrc = this.transformInv_(center);

      var dx;
      if (wrapsX) {
        goog.asserts.assert(this.sourceWorldWidth_);
        var centerSrcEstimX =
            (goog.math.modulo(aSrc[0], this.sourceWorldWidth_) +
             goog.math.modulo(cSrc[0], this.sourceWorldWidth_)) / 2;
        dx = centerSrcEstimX -
            goog.math.modulo(centerSrc[0], this.sourceWorldWidth_);
      } else {
        dx = (aSrc[0] + cSrc[0]) / 2 - centerSrc[0];
      }
      var dy = (aSrc[1] + cSrc[1]) / 2 - centerSrc[1];
      var centerSrcErrorSquared = dx * dx + dy * dy;
      needsSubdivision = centerSrcErrorSquared > this.errorThresholdSquared_;
    }
    if (needsSubdivision) {
      if (Math.abs(a[0] - c[0]) <= Math.abs(a[1] - c[1])) {
        // split horizontally (top & bottom)
        var bc = [(b[0] + c[0]) / 2, (b[1] + c[1]) / 2];
        var bcSrc = this.transformInv_(bc);
        var da = [(d[0] + a[0]) / 2, (d[1] + a[1]) / 2];
        var daSrc = this.transformInv_(da);

        this.addQuad_(
            a, b, bc, da, aSrc, bSrc, bcSrc, daSrc, maxSubdivision - 1);
        this.addQuad_(
            da, bc, c, d, daSrc, bcSrc, cSrc, dSrc, maxSubdivision - 1);
      } else {
        // split vertically (left & right)
        var ab = [(a[0] + b[0]) / 2, (a[1] + b[1]) / 2];
        var abSrc = this.transformInv_(ab);
        var cd = [(c[0] + d[0]) / 2, (c[1] + d[1]) / 2];
        var cdSrc = this.transformInv_(cd);

        this.addQuad_(
            a, ab, cd, d, aSrc, abSrc, cdSrc, dSrc, maxSubdivision - 1);
        this.addQuad_(
            ab, b, c, cd, abSrc, bSrc, cSrc, cdSrc, maxSubdivision - 1);
      }
      return;
    }
  }

  if (wrapsX) {
    if (!this.canWrapXInSource_) {
      return;
    }
    this.wrapsXInSource_ = true;
  }

  this.addTriangle_(a, c, d, aSrc, cSrc, dSrc);
  this.addTriangle_(a, b, c, aSrc, bSrc, cSrc);
};


/**
 * Calculates extent of the 'source' coordinates from all the triangles.
 *
 * @return {ol.Extent} Calculated extent.
 */
ol.reproj.Triangulation.prototype.calculateSourceExtent = function() {
  var extent = ol.extent.createEmpty();

  this.triangles_.forEach(function(triangle, i, arr) {
    var src = triangle.source;
    ol.extent.extendCoordinate(extent, src[0]);
    ol.extent.extendCoordinate(extent, src[1]);
    ol.extent.extendCoordinate(extent, src[2]);
  });

  return extent;
};


/**
 * @return {Array.<ol.reproj.Triangle>} Array of the calculated triangles.
 */
ol.reproj.Triangulation.prototype.getTriangles = function() {
  return this.triangles_;
};

goog.provide('ol.reproj.Image');
goog.provide('ol.reproj.ImageFunctionType');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('ol.ImageBase');
goog.require('ol.ImageState');
goog.require('ol.extent');
goog.require('ol.proj');
goog.require('ol.reproj');
goog.require('ol.reproj.Triangulation');


/**
 * @typedef {function(ol.Extent, number, number) : ol.ImageBase}
 */
ol.reproj.ImageFunctionType;


/**
 * @classdesc
 * Class encapsulating single reprojected image.
 * See {@link ol.source.Image}.
 *
 * @constructor
 * @extends {ol.ImageBase}
 * @param {ol.proj.Projection} sourceProj Source projection (of the data).
 * @param {ol.proj.Projection} targetProj Target projection.
 * @param {ol.Extent} targetExtent Target extent.
 * @param {number} targetResolution Target resolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {ol.reproj.ImageFunctionType} getImageFunction
 *     Function returning source images (extent, resolution, pixelRatio).
 */
ol.reproj.Image = function(sourceProj, targetProj,
    targetExtent, targetResolution, pixelRatio, getImageFunction) {

  /**
   * @private
   * @type {ol.proj.Projection}
   */
  this.targetProj_ = targetProj;

  /**
   * @private
   * @type {ol.Extent}
   */
  this.maxSourceExtent_ = sourceProj.getExtent();
  var maxTargetExtent = targetProj.getExtent();

  var limitedTargetExtent = maxTargetExtent ?
      ol.extent.getIntersection(targetExtent, maxTargetExtent) : targetExtent;

  var targetCenter = ol.extent.getCenter(limitedTargetExtent);
  var sourceResolution = ol.reproj.calculateSourceResolution(
      sourceProj, targetProj, targetCenter, targetResolution);

  var errorThresholdInPixels = ol.DEFAULT_RASTER_REPROJECTION_ERROR_THRESHOLD;

  /**
   * @private
   * @type {!ol.reproj.Triangulation}
   */
  this.triangulation_ = new ol.reproj.Triangulation(
      sourceProj, targetProj, limitedTargetExtent, this.maxSourceExtent_,
      sourceResolution * errorThresholdInPixels);

  /**
   * @private
   * @type {number}
   */
  this.targetResolution_ = targetResolution;

  /**
   * @private
   * @type {ol.Extent}
   */
  this.targetExtent_ = targetExtent;

  var sourceExtent = this.triangulation_.calculateSourceExtent();

  /**
   * @private
   * @type {ol.ImageBase}
   */
  this.sourceImage_ =
      getImageFunction(sourceExtent, sourceResolution, pixelRatio);

  /**
   * @private
   * @type {number}
   */
  this.sourcePixelRatio_ =
      this.sourceImage_ ? this.sourceImage_.getPixelRatio() : 1;

  /**
   * @private
   * @type {HTMLCanvasElement}
   */
  this.canvas_ = null;

  /**
   * @private
   * @type {goog.events.Key}
   */
  this.sourceListenerKey_ = null;


  var state = ol.ImageState.LOADED;
  var attributions = [];

  if (this.sourceImage_) {
    state = ol.ImageState.IDLE;
    attributions = this.sourceImage_.getAttributions();
  }

  goog.base(this, targetExtent, targetResolution, this.sourcePixelRatio_,
            state, attributions);
};
goog.inherits(ol.reproj.Image, ol.ImageBase);


/**
 * @inheritDoc
 */
ol.reproj.Image.prototype.disposeInternal = function() {
  if (this.state == ol.ImageState.LOADING) {
    this.unlistenSource_();
  }
  goog.base(this, 'disposeInternal');
};


/**
 * @inheritDoc
 */
ol.reproj.Image.prototype.getImage = function(opt_context) {
  return this.canvas_;
};


/**
 * @return {ol.proj.Projection} Projection.
 */
ol.reproj.Image.prototype.getProjection = function() {
  return this.targetProj_;
};


/**
 * @private
 */
ol.reproj.Image.prototype.reproject_ = function() {
  var sourceState = this.sourceImage_.getState();
  if (sourceState == ol.ImageState.LOADED) {
    var width = ol.extent.getWidth(this.targetExtent_) / this.targetResolution_;
    var height =
        ol.extent.getHeight(this.targetExtent_) / this.targetResolution_;

    this.canvas_ = ol.reproj.render(width, height, this.sourcePixelRatio_,
        this.sourceImage_.getResolution(), this.maxSourceExtent_,
        this.targetResolution_, this.targetExtent_, this.triangulation_, [{
          extent: this.sourceImage_.getExtent(),
          image: this.sourceImage_.getImage()
        }]);
  }
  this.state = sourceState;
  this.changed();
};


/**
 * @inheritDoc
 */
ol.reproj.Image.prototype.load = function() {
  if (this.state == ol.ImageState.IDLE) {
    this.state = ol.ImageState.LOADING;
    this.changed();

    var sourceState = this.sourceImage_.getState();
    if (sourceState == ol.ImageState.LOADED ||
        sourceState == ol.ImageState.ERROR) {
      this.reproject_();
    } else {
      this.sourceListenerKey_ = this.sourceImage_.listen(
          goog.events.EventType.CHANGE, function(e) {
            var sourceState = this.sourceImage_.getState();
            if (sourceState == ol.ImageState.LOADED ||
                sourceState == ol.ImageState.ERROR) {
              this.unlistenSource_();
              this.reproject_();
            }
          }, false, this);
      this.sourceImage_.load();
    }
  }
};


/**
 * @private
 */
ol.reproj.Image.prototype.unlistenSource_ = function() {
  goog.asserts.assert(this.sourceListenerKey_,
      'this.sourceListenerKey_ should not be null');
  goog.events.unlistenByKey(this.sourceListenerKey_);
  this.sourceListenerKey_ = null;
};

goog.provide('ol.source.Image');
goog.provide('ol.source.ImageEvent');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.events.Event');
goog.require('ol.Attribution');
goog.require('ol.ImageState');
goog.require('ol.array');
goog.require('ol.extent');
goog.require('ol.proj');
goog.require('ol.reproj.Image');
goog.require('ol.source.Source');


/**
 * @typedef {{attributions: (Array.<ol.Attribution>|undefined),
 *            extent: (null|ol.Extent|undefined),
 *            logo: (string|olx.LogoOptions|undefined),
 *            projection: ol.proj.ProjectionLike,
 *            resolutions: (Array.<number>|undefined),
 *            state: (ol.source.State|undefined)}}
 */
ol.source.ImageOptions;


/**
 * @classdesc
 * Abstract base class; normally only used for creating subclasses and not
 * instantiated in apps.
 * Base class for sources providing a single image.
 *
 * @constructor
 * @extends {ol.source.Source}
 * @param {ol.source.ImageOptions} options Single image source options.
 * @api
 */
ol.source.Image = function(options) {

  goog.base(this, {
    attributions: options.attributions,
    extent: options.extent,
    logo: options.logo,
    projection: options.projection,
    state: options.state
  });

  /**
   * @private
   * @type {Array.<number>}
   */
  this.resolutions_ = options.resolutions !== undefined ?
      options.resolutions : null;
  goog.asserts.assert(!this.resolutions_ ||
      goog.array.isSorted(this.resolutions_,
          function(a, b) {
            return b - a;
          }, true), 'resolutions must be null or sorted in descending order');


  /**
   * @private
   * @type {ol.reproj.Image}
   */
  this.reprojectedImage_ = null;


  /**
   * @private
   * @type {number}
   */
  this.reprojectedRevision_ = 0;

};
goog.inherits(ol.source.Image, ol.source.Source);


/**
 * @return {Array.<number>} Resolutions.
 */
ol.source.Image.prototype.getResolutions = function() {
  return this.resolutions_;
};


/**
 * @protected
 * @param {number} resolution Resolution.
 * @return {number} Resolution.
 */
ol.source.Image.prototype.findNearestResolution = function(resolution) {
  if (this.resolutions_) {
    var idx = ol.array.linearFindNearest(this.resolutions_, resolution, 0);
    resolution = this.resolutions_[idx];
  }
  return resolution;
};


/**
 * @param {ol.Extent} extent Extent.
 * @param {number} resolution Resolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {ol.proj.Projection} projection Projection.
 * @return {ol.ImageBase} Single image.
 */
ol.source.Image.prototype.getImage = function(extent, resolution, pixelRatio, projection) {
  var sourceProjection = this.getProjection();
  if (!ol.ENABLE_RASTER_REPROJECTION ||
      !sourceProjection ||
      !projection ||
      ol.proj.equivalent(sourceProjection, projection)) {
    if (sourceProjection) {
      projection = sourceProjection;
    }
    return this.getImageInternal(extent, resolution, pixelRatio, projection);
  } else {
    if (this.reprojectedImage_) {
      if (this.reprojectedRevision_ == this.getRevision() &&
          ol.proj.equivalent(
              this.reprojectedImage_.getProjection(), projection) &&
          this.reprojectedImage_.getResolution() == resolution &&
          this.reprojectedImage_.getPixelRatio() == pixelRatio &&
          ol.extent.equals(this.reprojectedImage_.getExtent(), extent)) {
        return this.reprojectedImage_;
      }
      this.reprojectedImage_.dispose();
      this.reprojectedImage_ = null;
    }

    this.reprojectedImage_ = new ol.reproj.Image(
        sourceProjection, projection, extent, resolution, pixelRatio,
        function(extent, resolution, pixelRatio) {
          return this.getImageInternal(extent, resolution,
              pixelRatio, sourceProjection);
        }.bind(this));
    this.reprojectedRevision_ = this.getRevision();

    return this.reprojectedImage_;
  }
};


/**
 * @param {ol.Extent} extent Extent.
 * @param {number} resolution Resolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {ol.proj.Projection} projection Projection.
 * @return {ol.ImageBase} Single image.
 * @protected
 */
ol.source.Image.prototype.getImageInternal = goog.abstractMethod;


/**
 * Handle image change events.
 * @param {goog.events.Event} event Event.
 * @protected
 */
ol.source.Image.prototype.handleImageChange = function(event) {
  var image = /** @type {ol.Image} */ (event.target);
  switch (image.getState()) {
    case ol.ImageState.LOADING:
      this.dispatchEvent(
          new ol.source.ImageEvent(ol.source.ImageEventType.IMAGELOADSTART,
              image));
      break;
    case ol.ImageState.LOADED:
      this.dispatchEvent(
          new ol.source.ImageEvent(ol.source.ImageEventType.IMAGELOADEND,
              image));
      break;
    case ol.ImageState.ERROR:
      this.dispatchEvent(
          new ol.source.ImageEvent(ol.source.ImageEventType.IMAGELOADERROR,
              image));
      break;
    default:
      // pass
  }
};


/**
 * Default image load function for image sources that use ol.Image image
 * instances.
 * @param {ol.Image} image Image.
 * @param {string} src Source.
 */
ol.source.Image.defaultImageLoadFunction = function(image, src) {
  image.getImage().src = src;
};


/**
 * @classdesc
 * Events emitted by {@link ol.source.Image} instances are instances of this
 * type.
 *
 * @constructor
 * @extends {goog.events.Event}
 * @implements {oli.source.ImageEvent}
 * @param {string} type Type.
 * @param {ol.Image} image The image.
 */
ol.source.ImageEvent = function(type, image) {

  goog.base(this, type);

  /**
   * The image related to the event.
   * @type {ol.Image}
   * @api
   */
  this.image = image;

};
goog.inherits(ol.source.ImageEvent, goog.events.Event);


/**
 * @enum {string}
 */
ol.source.ImageEventType = {

  /**
   * Triggered when an image starts loading.
   * @event ol.source.ImageEvent#imageloadstart
   * @api
   */
  IMAGELOADSTART: 'imageloadstart',

  /**
   * Triggered when an image finishes loading.
   * @event ol.source.ImageEvent#imageloadend
   * @api
   */
  IMAGELOADEND: 'imageloadend',

  /**
   * Triggered if image loading results in an error.
   * @event ol.source.ImageEvent#imageloaderror
   * @api
   */
  IMAGELOADERROR: 'imageloaderror'

};

goog.provide('ol.source.ImageCanvas');

goog.require('ol.CanvasFunctionType');
goog.require('ol.ImageCanvas');
goog.require('ol.extent');
goog.require('ol.source.Image');


/**
 * @classdesc
 * Base class for image sources where a canvas element is the image.
 *
 * @constructor
 * @extends {ol.source.Image}
 * @param {olx.source.ImageCanvasOptions} options Constructor options.
 * @api
 */
ol.source.ImageCanvas = function(options) {

  goog.base(this, {
    attributions: options.attributions,
    logo: options.logo,
    projection: options.projection,
    resolutions: options.resolutions,
    state: options.state !== undefined ?
        /** @type {ol.source.State} */ (options.state) : undefined
  });

  /**
   * @private
   * @type {ol.CanvasFunctionType}
   */
  this.canvasFunction_ = options.canvasFunction;

  /**
   * @private
   * @type {ol.ImageCanvas}
   */
  this.canvas_ = null;

  /**
   * @private
   * @type {number}
   */
  this.renderedRevision_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.ratio_ = options.ratio !== undefined ?
      options.ratio : 1.5;

};
goog.inherits(ol.source.ImageCanvas, ol.source.Image);


/**
 * @inheritDoc
 */
ol.source.ImageCanvas.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) {
  resolution = this.findNearestResolution(resolution);

  var canvas = this.canvas_;
  if (canvas &&
      this.renderedRevision_ == this.getRevision() &&
      canvas.getResolution() == resolution &&
      canvas.getPixelRatio() == pixelRatio &&
      ol.extent.containsExtent(canvas.getExtent(), extent)) {
    return canvas;
  }

  extent = extent.slice();
  ol.extent.scaleFromCenter(extent, this.ratio_);
  var width = ol.extent.getWidth(extent) / resolution;
  var height = ol.extent.getHeight(extent) / resolution;
  var size = [width * pixelRatio, height * pixelRatio];

  var canvasElement = this.canvasFunction_(
      extent, resolution, pixelRatio, size, projection);
  if (canvasElement) {
    canvas = new ol.ImageCanvas(extent, resolution, pixelRatio,
        this.getAttributions(), canvasElement);
  }
  this.canvas_ = canvas;
  this.renderedRevision_ = this.getRevision();

  return canvas;
};

goog.provide('ol.Feature');
goog.provide('ol.FeatureStyleFunction');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('ol');
goog.require('ol.Object');
goog.require('ol.geom.Geometry');
goog.require('ol.style.Style');


/**
 * @classdesc
 * A vector object for geographic features with a geometry and other
 * attribute properties, similar to the features in vector file formats like
 * GeoJSON.
 *
 * Features can be styled individually with `setStyle`; otherwise they use the
 * style of their vector layer.
 *
 * Note that attribute properties are set as {@link ol.Object} properties on
 * the feature object, so they are observable, and have get/set accessors.
 *
 * Typically, a feature has a single geometry property. You can set the
 * geometry using the `setGeometry` method and get it with `getGeometry`.
 * It is possible to store more than one geometry on a feature using attribute
 * properties. By default, the geometry used for rendering is identified by
 * the property name `geometry`. If you want to use another geometry property
 * for rendering, use the `setGeometryName` method to change the attribute
 * property associated with the geometry for the feature.  For example:
 *
 * ```js
 * var feature = new ol.Feature({
 *   geometry: new ol.geom.Polygon(polyCoords),
 *   labelPoint: new ol.geom.Point(labelCoords),
 *   name: 'My Polygon'
 * });
 *
 * // get the polygon geometry
 * var poly = feature.getGeometry();
 *
 * // Render the feature as a point using the coordinates from labelPoint
 * feature.setGeometryName('labelPoint');
 *
 * // get the point geometry
 * var point = feature.getGeometry();
 * ```
 *
 * @constructor
 * @extends {ol.Object}
 * @param {ol.geom.Geometry|Object.<string, *>=} opt_geometryOrProperties
 *     You may pass a Geometry object directly, or an object literal
 *     containing properties.  If you pass an object literal, you may
 *     include a Geometry associated with a `geometry` key.
 * @api stable
 */
ol.Feature = function(opt_geometryOrProperties) {

  goog.base(this);

  /**
   * @private
   * @type {number|string|undefined}
   */
  this.id_ = undefined;

  /**
   * @type {string}
   * @private
   */
  this.geometryName_ = 'geometry';

  /**
   * User provided style.
   * @private
   * @type {ol.style.Style|Array.<ol.style.Style>|
   *     ol.FeatureStyleFunction}
   */
  this.style_ = null;

  /**
   * @private
   * @type {ol.FeatureStyleFunction|undefined}
   */
  this.styleFunction_ = undefined;

  /**
   * @private
   * @type {goog.events.Key}
   */
  this.geometryChangeKey_ = null;

  goog.events.listen(
      this, ol.Object.getChangeEventType(this.geometryName_),
      this.handleGeometryChanged_, false, this);

  if (opt_geometryOrProperties !== undefined) {
    if (opt_geometryOrProperties instanceof ol.geom.Geometry ||
        !opt_geometryOrProperties) {
      var geometry = /** @type {ol.geom.Geometry} */ (opt_geometryOrProperties);
      this.setGeometry(geometry);
    } else {
      goog.asserts.assert(goog.isObject(opt_geometryOrProperties),
          'opt_geometryOrProperties should be an Object');
      var properties = /** @type {Object.<string, *>} */
          (opt_geometryOrProperties);
      this.setProperties(properties);
    }
  }
};
goog.inherits(ol.Feature, ol.Object);


/**
 * Clone this feature. If the original feature has a geometry it
 * is also cloned. The feature id is not set in the clone.
 * @return {ol.Feature} The clone.
 * @api stable
 */
ol.Feature.prototype.clone = function() {
  var clone = new ol.Feature(this.getProperties());
  clone.setGeometryName(this.getGeometryName());
  var geometry = this.getGeometry();
  if (geometry) {
    clone.setGeometry(geometry.clone());
  }
  var style = this.getStyle();
  if (style) {
    clone.setStyle(style);
  }
  return clone;
};


/**
 * Get the feature's default geometry.  A feature may have any number of named
 * geometries.  The "default" geometry (the one that is rendered by default) is
 * set when calling {@link ol.Feature#setGeometry}.
 * @return {ol.geom.Geometry|undefined} The default geometry for the feature.
 * @api stable
 * @observable
 */
ol.Feature.prototype.getGeometry = function() {
  return /** @type {ol.geom.Geometry|undefined} */ (
      this.get(this.geometryName_));
};


/**
 * Get the feature identifier.  This is a stable identifier for the feature and
 * is either set when reading data from a remote source or set explicitly by
 * calling {@link ol.Feature#setId}.
 * @return {number|string|undefined} Id.
 * @api stable
 * @observable
 */
ol.Feature.prototype.getId = function() {
  return this.id_;
};


/**
 * Get the name of the feature's default geometry.  By default, the default
 * geometry is named `geometry`.
 * @return {string} Get the property name associated with the default geometry
 *     for this feature.
 * @api stable
 */
ol.Feature.prototype.getGeometryName = function() {
  return this.geometryName_;
};


/**
 * Get the feature's style.  This return for this method depends on what was
 * provided to the {@link ol.Feature#setStyle} method.
 * @return {ol.style.Style|Array.<ol.style.Style>|
 *     ol.FeatureStyleFunction} The feature style.
 * @api stable
 * @observable
 */
ol.Feature.prototype.getStyle = function() {
  return this.style_;
};


/**
 * Get the feature's style function.
 * @return {ol.FeatureStyleFunction|undefined} Return a function
 * representing the current style of this feature.
 * @api stable
 */
ol.Feature.prototype.getStyleFunction = function() {
  return this.styleFunction_;
};


/**
 * @private
 */
ol.Feature.prototype.handleGeometryChange_ = function() {
  this.changed();
};


/**
 * @private
 */
ol.Feature.prototype.handleGeometryChanged_ = function() {
  if (this.geometryChangeKey_) {
    goog.events.unlistenByKey(this.geometryChangeKey_);
    this.geometryChangeKey_ = null;
  }
  var geometry = this.getGeometry();
  if (geometry) {
    this.geometryChangeKey_ = goog.events.listen(geometry,
        goog.events.EventType.CHANGE, this.handleGeometryChange_, false, this);
  }
  this.changed();
};


/**
 * Set the default geometry for the feature.  This will update the property
 * with the name returned by {@link ol.Feature#getGeometryName}.
 * @param {ol.geom.Geometry|undefined} geometry The new geometry.
 * @api stable
 * @observable
 */
ol.Feature.prototype.setGeometry = function(geometry) {
  this.set(this.geometryName_, geometry);
};


/**
 * Set the style for the feature.  This can be a single style object, an array
 * of styles, or a function that takes a resolution and returns an array of
 * styles. If it is `null` the feature has no style (a `null` style).
 * @param {ol.style.Style|Array.<ol.style.Style>|
 *     ol.FeatureStyleFunction} style Style for this feature.
 * @api stable
 * @observable
 */
ol.Feature.prototype.setStyle = function(style) {
  this.style_ = style;
  this.styleFunction_ = !style ?
      undefined : ol.Feature.createStyleFunction(style);
  this.changed();
};


/**
 * Set the feature id.  The feature id is considered stable and may be used when
 * requesting features or comparing identifiers returned from a remote source.
 * The feature id can be used with the {@link ol.source.Vector#getFeatureById}
 * method.
 * @param {number|string|undefined} id The feature id.
 * @api stable
 * @observable
 */
ol.Feature.prototype.setId = function(id) {
  this.id_ = id;
  this.changed();
};


/**
 * Set the property name to be used when getting the feature's default geometry.
 * When calling {@link ol.Feature#getGeometry}, the value of the property with
 * this name will be returned.
 * @param {string} name The property name of the default geometry.
 * @api stable
 */
ol.Feature.prototype.setGeometryName = function(name) {
  goog.events.unlisten(
      this, ol.Object.getChangeEventType(this.geometryName_),
      this.handleGeometryChanged_, false, this);
  this.geometryName_ = name;
  goog.events.listen(
      this, ol.Object.getChangeEventType(this.geometryName_),
      this.handleGeometryChanged_, false, this);
  this.handleGeometryChanged_();
};


/**
 * A function that returns an array of {@link ol.style.Style styles} given a
 * resolution. The `this` keyword inside the function references the
 * {@link ol.Feature} to be styled.
 *
 * @typedef {function(this: ol.Feature, number):
 *     (ol.style.Style|Array.<ol.style.Style>)}
 * @api stable
 */
ol.FeatureStyleFunction;


/**
 * Convert the provided object into a feature style function.  Functions passed
 * through unchanged.  Arrays of ol.style.Style or single style objects wrapped
 * in a new feature style function.
 * @param {ol.FeatureStyleFunction|!Array.<ol.style.Style>|!ol.style.Style} obj
 *     A feature style function, a single style, or an array of styles.
 * @return {ol.FeatureStyleFunction} A style function.
 */
ol.Feature.createStyleFunction = function(obj) {
  var styleFunction;

  if (goog.isFunction(obj)) {
    styleFunction = obj;
  } else {
    /**
     * @type {Array.<ol.style.Style>}
     */
    var styles;
    if (goog.isArray(obj)) {
      styles = obj;
    } else {
      goog.asserts.assertInstanceof(obj, ol.style.Style,
          'obj should be an ol.style.Style');
      styles = [obj];
    }
    styleFunction = function() {
      return styles;
    };
  }
  return styleFunction;
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Common events for the network classes.
 */


goog.provide('goog.net.EventType');


/**
 * Event names for network events
 * @enum {string}
 */
goog.net.EventType = {
  COMPLETE: 'complete',
  SUCCESS: 'success',
  ERROR: 'error',
  ABORT: 'abort',
  READY: 'ready',
  READY_STATE_CHANGE: 'readystatechange',
  TIMEOUT: 'timeout',
  INCREMENTAL_DATA: 'incrementaldata',
  PROGRESS: 'progress'
};

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

goog.provide('goog.Thenable');



/**
 * Provides a more strict interface for Thenables in terms of
 * http://promisesaplus.com for interop with {@see goog.Promise}.
 *
 * @interface
 * @extends {IThenable<TYPE>}
 * @template TYPE
 */
goog.Thenable = function() {};


/**
 * Adds callbacks that will operate on the result of the Thenable, returning a
 * new child Promise.
 *
 * If the Thenable is fulfilled, the {@code onFulfilled} callback will be
 * invoked with the fulfillment value as argument, and the child Promise will
 * be fulfilled with the return value of the callback. If the callback throws
 * an exception, the child Promise will be rejected with the thrown value
 * instead.
 *
 * If the Thenable is rejected, the {@code onRejected} callback will be invoked
 * with the rejection reason as argument, and the child Promise will be rejected
 * with the return value of the callback or thrown value.
 *
 * @param {?(function(this:THIS, TYPE): VALUE)=} opt_onFulfilled A
 *     function that will be invoked with the fulfillment value if the Promise
 *     is fullfilled.
 * @param {?(function(this:THIS, *): *)=} opt_onRejected A function that will
 *     be invoked with the rejection reason if the Promise is rejected.
 * @param {THIS=} opt_context An optional context object that will be the
 *     execution context for the callbacks. By default, functions are executed
 *     with the default this.
 *
 * @return {RESULT} A new Promise that will receive the result
 *     of the fulfillment or rejection callback.
 * @template VALUE
 * @template THIS
 *
 * When a Promise (or thenable) is returned from the fulfilled callback,
 * the result is the payload of that promise, not the promise itself.
 *
 * @template RESULT := type('goog.Promise',
 *     cond(isUnknown(VALUE), unknown(),
 *       mapunion(VALUE, (V) =>
 *         cond(isTemplatized(V) && sub(rawTypeOf(V), 'IThenable'),
 *           templateTypeOf(V, 0),
 *           cond(sub(V, 'Thenable'),
 *              unknown(),
 *              V)))))
 *  =:
 *
 */
goog.Thenable.prototype.then = function(opt_onFulfilled, opt_onRejected,
    opt_context) {};


/**
 * An expando property to indicate that an object implements
 * {@code goog.Thenable}.
 *
 * {@see addImplementation}.
 *
 * @const
 */
goog.Thenable.IMPLEMENTED_BY_PROP = '$goog_Thenable';


/**
 * Marks a given class (constructor) as an implementation of Thenable, so
 * that we can query that fact at runtime. The class must have already
 * implemented the interface.
 * Exports a 'then' method on the constructor prototype, so that the objects
 * also implement the extern {@see goog.Thenable} interface for interop with
 * other Promise implementations.
 * @param {function(new:goog.Thenable,...?)} ctor The class constructor. The
 *     corresponding class must have already implemented the interface.
 */
goog.Thenable.addImplementation = function(ctor) {
  goog.exportProperty(ctor.prototype, 'then', ctor.prototype.then);
  if (COMPILED) {
    ctor.prototype[goog.Thenable.IMPLEMENTED_BY_PROP] = true;
  } else {
    // Avoids dictionary access in uncompiled mode.
    ctor.prototype.$goog_Thenable = true;
  }
};


/**
 * @param {*} object
 * @return {boolean} Whether a given instance implements {@code goog.Thenable}.
 *     The class/superclass of the instance must call {@code addImplementation}.
 */
goog.Thenable.isImplementedBy = function(object) {
  if (!object) {
    return false;
  }
  try {
    if (COMPILED) {
      return !!object[goog.Thenable.IMPLEMENTED_BY_PROP];
    }
    return !!object.$goog_Thenable;
  } catch (e) {
    // Property access seems to be forbidden.
    return false;
  }
};

// Copyright 2015 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Simple freelist.
 *
 * An anterative to goog.structs.SimplePool, it imposes the requirement that the
 * objects in the list contain a "next" property that can be used to maintain
 * the pool.
 */

goog.provide('goog.async.FreeList');


/**
 * @template ITEM
 */
goog.async.FreeList = goog.defineClass(null, {
  /**
   * @param {function():ITEM} create
   * @param {function(ITEM):void} reset
   * @param {number} limit
   */
  constructor: function(create, reset, limit) {
    /** @const {number} */
    this.limit_ = limit;
    /** @const {function()} */
    this.create_ = create;
    /** @const {function(ITEM):void} */
    this.reset_ = reset;

    /** @type {number} */
    this.occupants_ = 0;
    /** @type {ITEM} */
    this.head_ = null;
  },

  /**
   * @return {ITEM}
   */
  get: function() {
    var item;
    if (this.occupants_ > 0) {
      this.occupants_--;
      item = this.head_;
      this.head_ = item.next;
      item.next = null;
    } else {
      item = this.create_();
    }
    return item;
  },

  /**
   * @param {ITEM} item An item available for possible future reuse.
   */
  put: function(item) {
    this.reset_(item);
    if (this.occupants_ < this.limit_) {
      this.occupants_++;
      item.next = this.head_;
      this.head_ = item;
    }
  },

  /**
   * Visible for testing.
   * @package
   * @return {number}
   */
  occupants: function() {
    return this.occupants_;
  }
});




// Copyright 2015 The Closure Library Authors. All Rights Reserved.
//
// 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.

goog.provide('goog.async.WorkItem');
goog.provide('goog.async.WorkQueue');

goog.require('goog.asserts');
goog.require('goog.async.FreeList');


// TODO(johnlenz): generalize the WorkQueue if this is used by more
// than goog.async.run.



/**
 * A low GC workqueue. The key elements of this design:
 *   - avoids the need for goog.bind or equivalent by carrying scope
 *   - avoids the need for array reallocation by using a linked list
 *   - minimizes work entry objects allocation by recycling objects
 * @constructor
 * @final
 * @struct
 */
goog.async.WorkQueue = function() {
  this.workHead_ = null;
  this.workTail_ = null;
};


/** @define {number} The maximum number of entries to keep for recycling. */
goog.define('goog.async.WorkQueue.DEFAULT_MAX_UNUSED', 100);


/** @const @private {goog.async.FreeList<goog.async.WorkItem>} */
goog.async.WorkQueue.freelist_ = new goog.async.FreeList(
    function() {return new goog.async.WorkItem(); },
    function(item) {item.reset()},
    goog.async.WorkQueue.DEFAULT_MAX_UNUSED);


/**
 * @param {function()} fn
 * @param {Object|null|undefined} scope
 */
goog.async.WorkQueue.prototype.add = function(fn, scope) {
  var item = this.getUnusedItem_();
  item.set(fn, scope);

  if (this.workTail_) {
    this.workTail_.next = item;
    this.workTail_ = item;
  } else {
    goog.asserts.assert(!this.workHead_);
    this.workHead_ = item;
    this.workTail_ = item;
  }
};


/**
 * @return {goog.async.WorkItem}
 */
goog.async.WorkQueue.prototype.remove = function() {
  var item = null;

  if (this.workHead_) {
    item = this.workHead_;
    this.workHead_ = this.workHead_.next;
    if (!this.workHead_) {
      this.workTail_ = null;
    }
    item.next = null;
  }
  return item;
};


/**
 * @param {goog.async.WorkItem} item
 */
goog.async.WorkQueue.prototype.returnUnused = function(item) {
  goog.async.WorkQueue.freelist_.put(item);
};


/**
 * @return {goog.async.WorkItem}
 * @private
 */
goog.async.WorkQueue.prototype.getUnusedItem_ = function() {
  return goog.async.WorkQueue.freelist_.get();
};



/**
 * @constructor
 * @final
 * @struct
 */
goog.async.WorkItem = function() {
  /** @type {?function()} */
  this.fn = null;
  /** @type {Object|null|undefined} */
  this.scope = null;
  /** @type {?goog.async.WorkItem} */
  this.next = null;
};


/**
 * @param {function()} fn
 * @param {Object|null|undefined} scope
 */
goog.async.WorkItem.prototype.set = function(fn, scope) {
  this.fn = fn;
  this.scope = scope;
  this.next = null;
};


/** Reset the work item so they don't prevent GC before reuse */
goog.async.WorkItem.prototype.reset = function() {
  this.fn = null;
  this.scope = null;
  this.next = null;
};

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Simple notifiers for the Closure testing framework.
 *
 * @author johnlenz@google.com (John Lenz)
 */

goog.provide('goog.testing.watchers');


/** @private {!Array<function()>} */
goog.testing.watchers.resetWatchers_ = [];


/**
 * Fires clock reset watching functions.
 */
goog.testing.watchers.signalClockReset = function() {
  var watchers = goog.testing.watchers.resetWatchers_;
  for (var i = 0; i < watchers.length; i++) {
    goog.testing.watchers.resetWatchers_[i]();
  }
};


/**
 * Enqueues a function to be called when the clock used for setTimeout is reset.
 * @param {function()} fn
 */
goog.testing.watchers.watchClockReset = function(fn) {
  goog.testing.watchers.resetWatchers_.push(fn);
};


// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

goog.provide('goog.async.run');

goog.require('goog.async.WorkQueue');
goog.require('goog.async.nextTick');
goog.require('goog.async.throwException');
goog.require('goog.testing.watchers');


/**
 * Fires the provided callback just before the current callstack unwinds, or as
 * soon as possible after the current JS execution context.
 * @param {function(this:THIS)} callback
 * @param {THIS=} opt_context Object to use as the "this value" when calling
 *     the provided function.
 * @template THIS
 */
goog.async.run = function(callback, opt_context) {
  if (!goog.async.run.schedule_) {
    goog.async.run.initializeRunner_();
  }
  if (!goog.async.run.workQueueScheduled_) {
    // Nothing is currently scheduled, schedule it now.
    goog.async.run.schedule_();
    goog.async.run.workQueueScheduled_ = true;
  }

  goog.async.run.workQueue_.add(callback, opt_context);
};


/**
 * Initializes the function to use to process the work queue.
 * @private
 */
goog.async.run.initializeRunner_ = function() {
  // If native Promises are available in the browser, just schedule the callback
  // on a fulfilled promise, which is specified to be async, but as fast as
  // possible.
  if (goog.global.Promise && goog.global.Promise.resolve) {
    var promise = goog.global.Promise.resolve(undefined);
    goog.async.run.schedule_ = function() {
      promise.then(goog.async.run.processWorkQueue);
    };
  } else {
    goog.async.run.schedule_ = function() {
      goog.async.nextTick(goog.async.run.processWorkQueue);
    };
  }
};


/**
 * Forces goog.async.run to use nextTick instead of Promise.
 *
 * This should only be done in unit tests. It's useful because MockClock
 * replaces nextTick, but not the browser Promise implementation, so it allows
 * Promise-based code to be tested with MockClock.
 *
 * However, we also want to run promises if the MockClock is no longer in
 * control so we schedule a backup "setTimeout" to the unmocked timeout if
 * provided.
 *
 * @param {function(function())=} opt_realSetTimeout
 */
goog.async.run.forceNextTick = function(opt_realSetTimeout) {
  goog.async.run.schedule_ = function() {
    goog.async.nextTick(goog.async.run.processWorkQueue);
    if (opt_realSetTimeout) {
      opt_realSetTimeout(goog.async.run.processWorkQueue);
    }
  };
};


/**
 * The function used to schedule work asynchronousely.
 * @private {function()}
 */
goog.async.run.schedule_;


/** @private {boolean} */
goog.async.run.workQueueScheduled_ = false;


/** @private {!goog.async.WorkQueue} */
goog.async.run.workQueue_ = new goog.async.WorkQueue();


if (goog.DEBUG) {
  /**
   * Reset the work queue.
   * @private
   */
  goog.async.run.resetQueue_ = function() {
    goog.async.run.workQueueScheduled_ = false;
    goog.async.run.workQueue_ = new goog.async.WorkQueue();
  };

  // If there is a clock implemenation in use for testing
  // and it is reset, reset the queue.
  goog.testing.watchers.watchClockReset(goog.async.run.resetQueue_);
}


/**
 * Run any pending goog.async.run work items. This function is not intended
 * for general use, but for use by entry point handlers to run items ahead of
 * goog.async.nextTick.
 */
goog.async.run.processWorkQueue = function() {
  // NOTE: additional work queue items may be added while processing.
  var item = null;
  while (item = goog.async.run.workQueue_.remove()) {
    try {
      item.fn.call(item.scope);
    } catch (e) {
      goog.async.throwException(e);
    }
    goog.async.run.workQueue_.returnUnused(item);
  }

  // There are no more work items, allow processing to be scheduled again.
  goog.async.run.workQueueScheduled_ = false;
};

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

goog.provide('goog.promise.Resolver');



/**
 * Resolver interface for promises. The resolver is a convenience interface that
 * bundles the promise and its associated resolve and reject functions together,
 * for cases where the resolver needs to be persisted internally.
 *
 * @interface
 * @template TYPE
 */
goog.promise.Resolver = function() {};


/**
 * The promise that created this resolver.
 * @type {!goog.Promise<TYPE>}
 */
goog.promise.Resolver.prototype.promise;


/**
 * Resolves this resolver with the specified value.
 * @type {function((TYPE|goog.Promise<TYPE>|Thenable)=)}
 */
goog.promise.Resolver.prototype.resolve;


/**
 * Rejects this resolver with the specified reason.
 * @type {function(*=): void}
 */
goog.promise.Resolver.prototype.reject;

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

goog.provide('goog.Promise');

goog.require('goog.Thenable');
goog.require('goog.asserts');
goog.require('goog.async.FreeList');
goog.require('goog.async.run');
goog.require('goog.async.throwException');
goog.require('goog.debug.Error');
goog.require('goog.promise.Resolver');



/**
 * Promises provide a result that may be resolved asynchronously. A Promise may
 * be resolved by being fulfilled with a fulfillment value, rejected with a
 * rejection reason, or blocked by another Promise. A Promise is said to be
 * settled if it is either fulfilled or rejected. Once settled, the Promise
 * result is immutable.
 *
 * Promises may represent results of any type, including undefined. Rejection
 * reasons are typically Errors, but may also be of any type. Closure Promises
 * allow for optional type annotations that enforce that fulfillment values are
 * of the appropriate types at compile time.
 *
 * The result of a Promise is accessible by calling {@code then} and registering
 * {@code onFulfilled} and {@code onRejected} callbacks. Once the Promise
 * is settled, the relevant callbacks are invoked with the fulfillment value or
 * rejection reason as argument. Callbacks are always invoked in the order they
 * were registered, even when additional {@code then} calls are made from inside
 * another callback. A callback is always run asynchronously sometime after the
 * scope containing the registering {@code then} invocation has returned.
 *
 * If a Promise is resolved with another Promise, the first Promise will block
 * until the second is settled, and then assumes the same result as the second
 * Promise. This allows Promises to depend on the results of other Promises,
 * linking together multiple asynchronous operations.
 *
 * This implementation is compatible with the Promises/A+ specification and
 * passes that specification's conformance test suite. A Closure Promise may be
 * resolved with a Promise instance (or sufficiently compatible Promise-like
 * object) created by other Promise implementations. From the specification,
 * Promise-like objects are known as "Thenables".
 *
 * @see http://promisesaplus.com/
 *
 * @param {function(
 *             this:RESOLVER_CONTEXT,
 *             function((TYPE|IThenable<TYPE>|Thenable)=),
 *             function(*=)): void} resolver
 *     Initialization function that is invoked immediately with {@code resolve}
 *     and {@code reject} functions as arguments. The Promise is resolved or
 *     rejected with the first argument passed to either function.
 * @param {RESOLVER_CONTEXT=} opt_context An optional context for executing the
 *     resolver function. If unspecified, the resolver function will be executed
 *     in the default scope.
 * @constructor
 * @struct
 * @final
 * @implements {goog.Thenable<TYPE>}
 * @template TYPE,RESOLVER_CONTEXT
 */
goog.Promise = function(resolver, opt_context) {
  /**
   * The internal state of this Promise. Either PENDING, FULFILLED, REJECTED, or
   * BLOCKED.
   * @private {goog.Promise.State_}
   */
  this.state_ = goog.Promise.State_.PENDING;

  /**
   * The settled result of the Promise. Immutable once set with either a
   * fulfillment value or rejection reason.
   * @private {*}
   */
  this.result_ = undefined;

  /**
   * For Promises created by calling {@code then()}, the originating parent.
   * @private {goog.Promise}
   */
  this.parent_ = null;

  /**
   * The linked list of {@code onFulfilled} and {@code onRejected} callbacks
   * added to this Promise by calls to {@code then()}.
   * @private {?goog.Promise.CallbackEntry_}
   */
  this.callbackEntries_ = null;

  /**
   * The tail of the linked list of {@code onFulfilled} and {@code onRejected}
   * callbacks added to this Promise by calls to {@code then()}.
   * @private {?goog.Promise.CallbackEntry_}
   */
  this.callbackEntriesTail_ = null;

  /**
   * Whether the Promise is in the queue of Promises to execute.
   * @private {boolean}
   */
  this.executing_ = false;

  if (goog.Promise.UNHANDLED_REJECTION_DELAY > 0) {
    /**
     * A timeout ID used when the {@code UNHANDLED_REJECTION_DELAY} is greater
     * than 0 milliseconds. The ID is set when the Promise is rejected, and
     * cleared only if an {@code onRejected} callback is invoked for the
     * Promise (or one of its descendants) before the delay is exceeded.
     *
     * If the rejection is not handled before the timeout completes, the
     * rejection reason is passed to the unhandled rejection handler.
     * @private {number}
     */
    this.unhandledRejectionId_ = 0;
  } else if (goog.Promise.UNHANDLED_REJECTION_DELAY == 0) {
    /**
     * When the {@code UNHANDLED_REJECTION_DELAY} is set to 0 milliseconds, a
     * boolean that is set if the Promise is rejected, and reset to false if an
     * {@code onRejected} callback is invoked for the Promise (or one of its
     * descendants). If the rejection is not handled before the next timestep,
     * the rejection reason is passed to the unhandled rejection handler.
     * @private {boolean}
     */
    this.hadUnhandledRejection_ = false;
  }

  if (goog.Promise.LONG_STACK_TRACES) {
    /**
     * A list of stack trace frames pointing to the locations where this Promise
     * was created or had callbacks added to it. Saved to add additional context
     * to stack traces when an exception is thrown.
     * @private {!Array<string>}
     */
    this.stack_ = [];
    this.addStackTrace_(new Error('created'));

    /**
     * Index of the most recently executed stack frame entry.
     * @private {number}
     */
    this.currentStep_ = 0;
  }

  // As an optimization, we can skip this if resolver is goog.nullFunction.
  // This value is passed internally when creating a promise which will be
  // resolved through a more optimized path.
  if (resolver != goog.nullFunction) {
    try {
      var self = this;
      resolver.call(
          opt_context,
          function(value) {
            self.resolve_(goog.Promise.State_.FULFILLED, value);
          },
          function(reason) {
            if (goog.DEBUG &&
                !(reason instanceof goog.Promise.CancellationError)) {
              try {
                // Promise was rejected. Step up one call frame to see why.
                if (reason instanceof Error) {
                  throw reason;
                } else {
                  throw new Error('Promise rejected.');
                }
              } catch (e) {
                // Only thrown so browser dev tools can catch rejections of
                // promises when the option to break on caught exceptions is
                // activated.
              }
            }
            self.resolve_(goog.Promise.State_.REJECTED, reason);
          });
    } catch (e) {
      this.resolve_(goog.Promise.State_.REJECTED, e);
    }
  }
};


/**
 * @define {boolean} Whether traces of {@code then} calls should be included in
 * exceptions thrown
 */
goog.define('goog.Promise.LONG_STACK_TRACES', false);


/**
 * @define {number} The delay in milliseconds before a rejected Promise's reason
 * is passed to the rejection handler. By default, the rejection handler
 * rethrows the rejection reason so that it appears in the developer console or
 * {@code window.onerror} handler.
 *
 * Rejections are rethrown as quickly as possible by default. A negative value
 * disables rejection handling entirely.
 */
goog.define('goog.Promise.UNHANDLED_REJECTION_DELAY', 0);


/**
 * The possible internal states for a Promise. These states are not directly
 * observable to external callers.
 * @enum {number}
 * @private
 */
goog.Promise.State_ = {
  /** The Promise is waiting for resolution. */
  PENDING: 0,

  /** The Promise is blocked waiting for the result of another Thenable. */
  BLOCKED: 1,

  /** The Promise has been resolved with a fulfillment value. */
  FULFILLED: 2,

  /** The Promise has been resolved with a rejection reason. */
  REJECTED: 3
};



/**
 * Entries in the callback chain. Each call to {@code then},
 * {@code thenCatch}, or {@code thenAlways} creates an entry containing the
 * functions that may be invoked once the Promise is settled.
 *
 * @private @final @struct @constructor
 */
goog.Promise.CallbackEntry_ = function() {
  /** @type {?goog.Promise} */
  this.child = null;
  /** @type {Function} */
  this.onFulfilled = null;
  /** @type {Function} */
  this.onRejected = null;
  /** @type {?} */
  this.context = null;
  /** @type {?goog.Promise.CallbackEntry_} */
  this.next = null;

  /**
   * A boolean value to indicate this is a "thenAlways" callback entry.
   * Unlike a normal "then/thenVoid" a "thenAlways doesn't participate
   * in "cancel" considerations but is simply an observer and requires
   * special handling.
   * @type {boolean}
   */
  this.always = false;
};


/** clear the object prior to reuse */
goog.Promise.CallbackEntry_.prototype.reset = function() {
  this.child = null;
  this.onFulfilled = null;
  this.onRejected = null;
  this.context = null;
  this.always = false;
};


/**
 * @define {number} The number of currently unused objects to keep around for
 *    reuse.
 */
goog.define('goog.Promise.DEFAULT_MAX_UNUSED', 100);


/** @const @private {goog.async.FreeList<!goog.Promise.CallbackEntry_>} */
goog.Promise.freelist_ = new goog.async.FreeList(
    function() {
      return new goog.Promise.CallbackEntry_();
    },
    function(item) {
      item.reset();
    },
    goog.Promise.DEFAULT_MAX_UNUSED);


/**
 * @param {Function} onFulfilled
 * @param {Function} onRejected
 * @param {?} context
 * @return {!goog.Promise.CallbackEntry_}
 * @private
 */
goog.Promise.getCallbackEntry_ = function(onFulfilled, onRejected, context) {
  var entry = goog.Promise.freelist_.get();
  entry.onFulfilled = onFulfilled;
  entry.onRejected = onRejected;
  entry.context = context;
  return entry;
};


/**
 * @param {!goog.Promise.CallbackEntry_} entry
 * @private
 */
goog.Promise.returnEntry_ = function(entry) {
  goog.Promise.freelist_.put(entry);
};


// NOTE: this is the same template expression as is used for
// goog.IThenable.prototype.then


/**
 * @param {VALUE=} opt_value
 * @return {RESULT} A new Promise that is immediately resolved
 *     with the given value. If the input value is already a goog.Promise, it
 *     will be returned immediately without creating a new instance.
 * @template VALUE
 * @template RESULT := type('goog.Promise',
 *     cond(isUnknown(VALUE), unknown(),
 *       mapunion(VALUE, (V) =>
 *         cond(isTemplatized(V) && sub(rawTypeOf(V), 'IThenable'),
 *           templateTypeOf(V, 0),
 *           cond(sub(V, 'Thenable'),
 *              unknown(),
 *              V)))))
 * =:
 */
goog.Promise.resolve = function(opt_value) {
  if (opt_value instanceof goog.Promise) {
    // Avoid creating a new object if we already have a promise object
    // of the correct type.
    return opt_value;
  }

  // Passing goog.nullFunction will cause the constructor to take an optimized
  // path that skips calling the resolver function.
  var promise = new goog.Promise(goog.nullFunction);
  promise.resolve_(goog.Promise.State_.FULFILLED, opt_value);
  return promise;
};


/**
 * @param {*=} opt_reason
 * @return {!goog.Promise} A new Promise that is immediately rejected with the
 *     given reason.
 */
goog.Promise.reject = function(opt_reason) {
  return new goog.Promise(function(resolve, reject) {
    reject(opt_reason);
  });
};


/**
 * This is identical to
 * {@code goog.Promise.resolve(value).then(onFulfilled, onRejected)}, but it
 * avoids creating an unnecessary wrapper Promise when {@code value} is already
 * thenable.
 *
 * @param {?(goog.Thenable<TYPE>|Thenable|TYPE)} value
 * @param {function(TYPE): ?} onFulfilled
 * @param {function(*): *} onRejected
 * @template TYPE
 * @private
 */
goog.Promise.resolveThen_ = function(value, onFulfilled, onRejected) {
  var isThenable = goog.Promise.maybeThen_(
      value, onFulfilled, onRejected, null);
  if (!isThenable) {
    goog.async.run(goog.partial(onFulfilled, value));
  }
};


/**
 * @param {!Array<?(goog.Promise<TYPE>|goog.Thenable<TYPE>|Thenable|*)>}
 *     promises
 * @return {!goog.Promise<TYPE>} A Promise that receives the result of the
 *     first Promise (or Promise-like) input to settle immediately after it
 *     settles.
 * @template TYPE
 */
goog.Promise.race = function(promises) {
  return new goog.Promise(function(resolve, reject) {
    if (!promises.length) {
      resolve(undefined);
    }
    for (var i = 0, promise; i < promises.length; i++) {
      promise = promises[i];
      goog.Promise.resolveThen_(promise, resolve, reject);
    }
  });
};


/**
 * @param {!Array<?(goog.Promise<TYPE>|goog.Thenable<TYPE>|Thenable|*)>}
 *     promises
 * @return {!goog.Promise<!Array<TYPE>>} A Promise that receives a list of
 *     every fulfilled value once every input Promise (or Promise-like) is
 *     successfully fulfilled, or is rejected with the first rejection reason
 *     immediately after it is rejected.
 * @template TYPE
 */
goog.Promise.all = function(promises) {
  return new goog.Promise(function(resolve, reject) {
    var toFulfill = promises.length;
    var values = [];

    if (!toFulfill) {
      resolve(values);
      return;
    }

    var onFulfill = function(index, value) {
      toFulfill--;
      values[index] = value;
      if (toFulfill == 0) {
        resolve(values);
      }
    };

    var onReject = function(reason) {
      reject(reason);
    };

    for (var i = 0, promise; i < promises.length; i++) {
      promise = promises[i];
      goog.Promise.resolveThen_(
          promise, goog.partial(onFulfill, i), onReject);
    }
  });
};


/**
 * @param {!Array<?(goog.Promise<TYPE>|goog.Thenable<TYPE>|Thenable|*)>}
 *     promises
 * @return {!goog.Promise<!Array<{
 *     fulfilled: boolean,
 *     value: (TYPE|undefined),
 *     reason: (*|undefined)}>>} A Promise that resolves with a list of
 *         result objects once all input Promises (or Promise-like) have
 *         settled. Each result object contains a 'fulfilled' boolean indicating
 *         whether an input Promise was fulfilled or rejected. For fulfilled
 *         Promises, the resulting value is stored in the 'value' field. For
 *         rejected Promises, the rejection reason is stored in the 'reason'
 *         field.
 * @template TYPE
 */
goog.Promise.allSettled = function(promises) {
  return new goog.Promise(function(resolve, reject) {
    var toSettle = promises.length;
    var results = [];

    if (!toSettle) {
      resolve(results);
      return;
    }

    var onSettled = function(index, fulfilled, result) {
      toSettle--;
      results[index] = fulfilled ?
          {fulfilled: true, value: result} :
          {fulfilled: false, reason: result};
      if (toSettle == 0) {
        resolve(results);
      }
    };

    for (var i = 0, promise; i < promises.length; i++) {
      promise = promises[i];
      goog.Promise.resolveThen_(promise,
          goog.partial(onSettled, i, true /* fulfilled */),
          goog.partial(onSettled, i, false /* fulfilled */));
    }
  });
};


/**
 * @param {!Array<?(goog.Promise<TYPE>|goog.Thenable<TYPE>|Thenable|*)>}
 *     promises
 * @return {!goog.Promise<TYPE>} A Promise that receives the value of the first
 *     input to be fulfilled, or is rejected with a list of every rejection
 *     reason if all inputs are rejected.
 * @template TYPE
 */
goog.Promise.firstFulfilled = function(promises) {
  return new goog.Promise(function(resolve, reject) {
    var toReject = promises.length;
    var reasons = [];

    if (!toReject) {
      resolve(undefined);
      return;
    }

    var onFulfill = function(value) {
      resolve(value);
    };

    var onReject = function(index, reason) {
      toReject--;
      reasons[index] = reason;
      if (toReject == 0) {
        reject(reasons);
      }
    };

    for (var i = 0, promise; i < promises.length; i++) {
      promise = promises[i];
      goog.Promise.resolveThen_(
          promise, onFulfill, goog.partial(onReject, i));
    }
  });
};


/**
 * @return {!goog.promise.Resolver<TYPE>} Resolver wrapping the promise and its
 *     resolve / reject functions. Resolving or rejecting the resolver
 *     resolves or rejects the promise.
 * @template TYPE
 */
goog.Promise.withResolver = function() {
  var resolve, reject;
  var promise = new goog.Promise(function(rs, rj) {
    resolve = rs;
    reject = rj;
  });
  return new goog.Promise.Resolver_(promise, resolve, reject);
};


/**
 * Adds callbacks that will operate on the result of the Promise, returning a
 * new child Promise.
 *
 * If the Promise is fulfilled, the {@code onFulfilled} callback will be invoked
 * with the fulfillment value as argument, and the child Promise will be
 * fulfilled with the return value of the callback. If the callback throws an
 * exception, the child Promise will be rejected with the thrown value instead.
 *
 * If the Promise is rejected, the {@code onRejected} callback will be invoked
 * with the rejection reason as argument, and the child Promise will be resolved
 * with the return value or rejected with the thrown value of the callback.
 *
 * @override
 */
goog.Promise.prototype.then = function(
    opt_onFulfilled, opt_onRejected, opt_context) {

  if (opt_onFulfilled != null) {
    goog.asserts.assertFunction(opt_onFulfilled,
        'opt_onFulfilled should be a function.');
  }
  if (opt_onRejected != null) {
    goog.asserts.assertFunction(opt_onRejected,
        'opt_onRejected should be a function. Did you pass opt_context ' +
        'as the second argument instead of the third?');
  }

  if (goog.Promise.LONG_STACK_TRACES) {
    this.addStackTrace_(new Error('then'));
  }

  return this.addChildPromise_(
      goog.isFunction(opt_onFulfilled) ? opt_onFulfilled : null,
      goog.isFunction(opt_onRejected) ? opt_onRejected : null,
      opt_context);
};
goog.Thenable.addImplementation(goog.Promise);


/**
 * Adds callbacks that will operate on the result of the Promise without
 * returning a child Promise (unlike "then").
 *
 * If the Promise is fulfilled, the {@code onFulfilled} callback will be invoked
 * with the fulfillment value as argument.
 *
 * If the Promise is rejected, the {@code onRejected} callback will be invoked
 * with the rejection reason as argument.
 *
 * @param {?(function(this:THIS, TYPE):?)=} opt_onFulfilled A
 *     function that will be invoked with the fulfillment value if the Promise
 *     is fulfilled.
 * @param {?(function(this:THIS, *): *)=} opt_onRejected A function that will
 *     be invoked with the rejection reason if the Promise is rejected.
 * @param {THIS=} opt_context An optional context object that will be the
 *     execution context for the callbacks. By default, functions are executed
 *     with the default this.
 * @package
 * @template THIS
 */
goog.Promise.prototype.thenVoid = function(
    opt_onFulfilled, opt_onRejected, opt_context) {

  if (opt_onFulfilled != null) {
    goog.asserts.assertFunction(opt_onFulfilled,
        'opt_onFulfilled should be a function.');
  }
  if (opt_onRejected != null) {
    goog.asserts.assertFunction(opt_onRejected,
        'opt_onRejected should be a function. Did you pass opt_context ' +
        'as the second argument instead of the third?');
  }

  if (goog.Promise.LONG_STACK_TRACES) {
    this.addStackTrace_(new Error('then'));
  }

  // Note: no default rejection handler is provided here as we need to
  // distinguish unhandled rejections.
  this.addCallbackEntry_(goog.Promise.getCallbackEntry_(
      opt_onFulfilled || goog.nullFunction,
      opt_onRejected || null,
      opt_context));
};


/**
 * Adds a callback that will be invoked when the Promise is settled (fulfilled
 * or rejected). The callback receives no argument, and no new child Promise is
 * created. This is useful for ensuring that cleanup takes place after certain
 * asynchronous operations. Callbacks added with {@code thenAlways} will be
 * executed in the same order with other calls to {@code then},
 * {@code thenAlways}, or {@code thenCatch}.
 *
 * Since it does not produce a new child Promise, cancellation propagation is
 * not prevented by adding callbacks with {@code thenAlways}. A Promise that has
 * a cleanup handler added with {@code thenAlways} will be canceled if all of
 * its children created by {@code then} (or {@code thenCatch}) are canceled.
 * Additionally, since any rejections are not passed to the callback, it does
 * not stop the unhandled rejection handler from running.
 *
 * @param {function(this:THIS): void} onSettled A function that will be invoked
 *     when the Promise is settled (fulfilled or rejected).
 * @param {THIS=} opt_context An optional context object that will be the
 *     execution context for the callbacks. By default, functions are executed
 *     in the global scope.
 * @return {!goog.Promise<TYPE>} This Promise, for chaining additional calls.
 * @template THIS
 */
goog.Promise.prototype.thenAlways = function(onSettled, opt_context) {
  if (goog.Promise.LONG_STACK_TRACES) {
    this.addStackTrace_(new Error('thenAlways'));
  }

  var entry = goog.Promise.getCallbackEntry_(onSettled, onSettled, opt_context);
  entry.always = true;
  this.addCallbackEntry_(entry);
  return this;
};


/**
 * Adds a callback that will be invoked only if the Promise is rejected. This
 * is equivalent to {@code then(null, onRejected)}.
 *
 * @param {!function(this:THIS, *): *} onRejected A function that will be
 *     invoked with the rejection reason if the Promise is rejected.
 * @param {THIS=} opt_context An optional context object that will be the
 *     execution context for the callbacks. By default, functions are executed
 *     in the global scope.
 * @return {!goog.Promise} A new Promise that will receive the result of the
 *     callback.
 * @template THIS
 */
goog.Promise.prototype.thenCatch = function(onRejected, opt_context) {
  if (goog.Promise.LONG_STACK_TRACES) {
    this.addStackTrace_(new Error('thenCatch'));
  }
  return this.addChildPromise_(null, onRejected, opt_context);
};


/**
 * Cancels the Promise if it is still pending by rejecting it with a cancel
 * Error. No action is performed if the Promise is already resolved.
 *
 * All child Promises of the canceled Promise will be rejected with the same
 * cancel error, as with normal Promise rejection. If the Promise to be canceled
 * is the only child of a pending Promise, the parent Promise will also be
 * canceled. Cancellation may propagate upward through multiple generations.
 *
 * @param {string=} opt_message An optional debugging message for describing the
 *     cancellation reason.
 */
goog.Promise.prototype.cancel = function(opt_message) {
  if (this.state_ == goog.Promise.State_.PENDING) {
    goog.async.run(function() {
      var err = new goog.Promise.CancellationError(opt_message);
      this.cancelInternal_(err);
    }, this);
  }
};


/**
 * Cancels this Promise with the given error.
 *
 * @param {!Error} err The cancellation error.
 * @private
 */
goog.Promise.prototype.cancelInternal_ = function(err) {
  if (this.state_ == goog.Promise.State_.PENDING) {
    if (this.parent_) {
      // Cancel the Promise and remove it from the parent's child list.
      this.parent_.cancelChild_(this, err);
      this.parent_ = null;
    } else {
      this.resolve_(goog.Promise.State_.REJECTED, err);
    }
  }
};


/**
 * Cancels a child Promise from the list of callback entries. If the Promise has
 * not already been resolved, reject it with a cancel error. If there are no
 * other children in the list of callback entries, propagate the cancellation
 * by canceling this Promise as well.
 *
 * @param {!goog.Promise} childPromise The Promise to cancel.
 * @param {!Error} err The cancel error to use for rejecting the Promise.
 * @private
 */
goog.Promise.prototype.cancelChild_ = function(childPromise, err) {
  if (!this.callbackEntries_) {
    return;
  }
  var childCount = 0;
  var childEntry = null;
  var beforeChildEntry = null;

  // Find the callback entry for the childPromise, and count whether there are
  // additional child Promises.
  for (var entry = this.callbackEntries_; entry; entry = entry.next) {
    if (!entry.always) {
      childCount++;
      if (entry.child == childPromise) {
        childEntry = entry;
      }
      if (childEntry && childCount > 1) {
        break;
      }
    }
    if (!childEntry) {
      beforeChildEntry = entry;
    }
  }

  // Can a child entry be missing?

  // If the child Promise was the only child, cancel this Promise as well.
  // Otherwise, reject only the child Promise with the cancel error.
  if (childEntry) {
    if (this.state_ == goog.Promise.State_.PENDING && childCount == 1) {
      this.cancelInternal_(err);
    } else {
      if (beforeChildEntry) {
        this.removeEntryAfter_(beforeChildEntry);
      } else {
        this.popEntry_();
      }

      this.executeCallback_(
          childEntry, goog.Promise.State_.REJECTED, err);
    }
  }
};


/**
 * Adds a callback entry to the current Promise, and schedules callback
 * execution if the Promise has already been settled.
 *
 * @param {goog.Promise.CallbackEntry_} callbackEntry Record containing
 *     {@code onFulfilled} and {@code onRejected} callbacks to execute after
 *     the Promise is settled.
 * @private
 */
goog.Promise.prototype.addCallbackEntry_ = function(callbackEntry) {
  if (!this.hasEntry_() &&
      (this.state_ == goog.Promise.State_.FULFILLED ||
       this.state_ == goog.Promise.State_.REJECTED)) {
    this.scheduleCallbacks_();
  }
  this.queueEntry_(callbackEntry);
};


/**
 * Creates a child Promise and adds it to the callback entry list. The result of
 * the child Promise is determined by the state of the parent Promise and the
 * result of the {@code onFulfilled} or {@code onRejected} callbacks as
 * specified in the Promise resolution procedure.
 *
 * @see http://promisesaplus.com/#the__method
 *
 * @param {?function(this:THIS, TYPE):
 *          (RESULT|goog.Promise<RESULT>|Thenable)} onFulfilled A callback that
 *     will be invoked if the Promise is fullfilled, or null.
 * @param {?function(this:THIS, *): *} onRejected A callback that will be
 *     invoked if the Promise is rejected, or null.
 * @param {THIS=} opt_context An optional execution context for the callbacks.
 *     in the default calling context.
 * @return {!goog.Promise} The child Promise.
 * @template RESULT,THIS
 * @private
 */
goog.Promise.prototype.addChildPromise_ = function(
    onFulfilled, onRejected, opt_context) {

  /** @type {goog.Promise.CallbackEntry_} */
  var callbackEntry = goog.Promise.getCallbackEntry_(null, null, null);

  callbackEntry.child = new goog.Promise(function(resolve, reject) {
    // Invoke onFulfilled, or resolve with the parent's value if absent.
    callbackEntry.onFulfilled = onFulfilled ? function(value) {
      try {
        var result = onFulfilled.call(opt_context, value);
        resolve(result);
      } catch (err) {
        reject(err);
      }
    } : resolve;

    // Invoke onRejected, or reject with the parent's reason if absent.
    callbackEntry.onRejected = onRejected ? function(reason) {
      try {
        var result = onRejected.call(opt_context, reason);
        if (!goog.isDef(result) &&
            reason instanceof goog.Promise.CancellationError) {
          // Propagate cancellation to children if no other result is returned.
          reject(reason);
        } else {
          resolve(result);
        }
      } catch (err) {
        reject(err);
      }
    } : reject;
  });

  callbackEntry.child.parent_ = this;
  this.addCallbackEntry_(callbackEntry);
  return callbackEntry.child;
};


/**
 * Unblocks the Promise and fulfills it with the given value.
 *
 * @param {TYPE} value
 * @private
 */
goog.Promise.prototype.unblockAndFulfill_ = function(value) {
  goog.asserts.assert(this.state_ == goog.Promise.State_.BLOCKED);
  this.state_ = goog.Promise.State_.PENDING;
  this.resolve_(goog.Promise.State_.FULFILLED, value);
};


/**
 * Unblocks the Promise and rejects it with the given rejection reason.
 *
 * @param {*} reason
 * @private
 */
goog.Promise.prototype.unblockAndReject_ = function(reason) {
  goog.asserts.assert(this.state_ == goog.Promise.State_.BLOCKED);
  this.state_ = goog.Promise.State_.PENDING;
  this.resolve_(goog.Promise.State_.REJECTED, reason);
};


/**
 * Attempts to resolve a Promise with a given resolution state and value. This
 * is a no-op if the given Promise has already been resolved.
 *
 * If the given result is a Thenable (such as another Promise), the Promise will
 * be settled with the same state and result as the Thenable once it is itself
 * settled.
 *
 * If the given result is not a Thenable, the Promise will be settled (fulfilled
 * or rejected) with that result based on the given state.
 *
 * @see http://promisesaplus.com/#the_promise_resolution_procedure
 *
 * @param {goog.Promise.State_} state
 * @param {*} x The result to apply to the Promise.
 * @private
 */
goog.Promise.prototype.resolve_ = function(state, x) {
  if (this.state_ != goog.Promise.State_.PENDING) {
    return;
  }

  if (this == x) {
    state = goog.Promise.State_.REJECTED;
    x = new TypeError('Promise cannot resolve to itself');
  }

  this.state_ = goog.Promise.State_.BLOCKED;
  var isThenable = goog.Promise.maybeThen_(
      x, this.unblockAndFulfill_, this.unblockAndReject_, this);
  if (isThenable) {
    return;
  }

  this.result_ = x;
  this.state_ = state;
  // Since we can no longer be canceled, remove link to parent, so that the
  // child promise does not keep the parent promise alive.
  this.parent_ = null;
  this.scheduleCallbacks_();

  if (state == goog.Promise.State_.REJECTED &&
      !(x instanceof goog.Promise.CancellationError)) {
    goog.Promise.addUnhandledRejection_(this, x);
  }
};


/**
 * Invokes the "then" method of an input value if that value is a Thenable. This
 * is a no-op if the value is not thenable.
 *
 * @param {*} value A potentially thenable value.
 * @param {!Function} onFulfilled
 * @param {!Function} onRejected
 * @param {*} context
 * @return {boolean} Whether the input value was thenable.
 * @private
 */
goog.Promise.maybeThen_ = function(value, onFulfilled, onRejected, context) {
  if (value instanceof goog.Promise) {
    value.thenVoid(onFulfilled, onRejected, context);
    return true;
  } else if (goog.Thenable.isImplementedBy(value)) {
    value = /** @type {!goog.Thenable} */ (value);
    value.then(onFulfilled, onRejected, context);
    return true;
  } else if (goog.isObject(value)) {
    try {
      var then = value['then'];
      if (goog.isFunction(then)) {
        goog.Promise.tryThen_(
            value, then, onFulfilled, onRejected, context);
        return true;
      }
    } catch (e) {
      onRejected.call(context, e);
      return true;
    }
  }

  return false;
};


/**
 * Attempts to call the {@code then} method on an object in the hopes that it is
 * a Promise-compatible instance. This allows interoperation between different
 * Promise implementations, however a non-compliant object may cause a Promise
 * to hang indefinitely. If the {@code then} method throws an exception, the
 * dependent Promise will be rejected with the thrown value.
 *
 * @see http://promisesaplus.com/#point-70
 *
 * @param {Thenable} thenable An object with a {@code then} method that may be
 *     compatible with the Promise/A+ specification.
 * @param {!Function} then The {@code then} method of the Thenable object.
 * @param {!Function} onFulfilled
 * @param {!Function} onRejected
 * @param {*} context
 * @private
 */
goog.Promise.tryThen_ = function(
    thenable, then, onFulfilled, onRejected, context) {

  var called = false;
  var resolve = function(value) {
    if (!called) {
      called = true;
      onFulfilled.call(context, value);
    }
  };

  var reject = function(reason) {
    if (!called) {
      called = true;
      onRejected.call(context, reason);
    }
  };

  try {
    then.call(thenable, resolve, reject);
  } catch (e) {
    reject(e);
  }
};


/**
 * Executes the pending callbacks of a settled Promise after a timeout.
 *
 * Section 2.2.4 of the Promises/A+ specification requires that Promise
 * callbacks must only be invoked from a call stack that only contains Promise
 * implementation code, which we accomplish by invoking callback execution after
 * a timeout. If {@code startExecution_} is called multiple times for the same
 * Promise, the callback chain will be evaluated only once. Additional callbacks
 * may be added during the evaluation phase, and will be executed in the same
 * event loop.
 *
 * All Promises added to the waiting list during the same browser event loop
 * will be executed in one batch to avoid using a separate timeout per Promise.
 *
 * @private
 */
goog.Promise.prototype.scheduleCallbacks_ = function() {
  if (!this.executing_) {
    this.executing_ = true;
    goog.async.run(this.executeCallbacks_, this);
  }
};


/**
 * @return {boolean} Whether there are any pending callbacks queued.
 * @private
 */
goog.Promise.prototype.hasEntry_ = function() {
  return !!this.callbackEntries_;
};


/**
 * @param {goog.Promise.CallbackEntry_} entry
 * @private
 */
goog.Promise.prototype.queueEntry_ = function(entry) {
  goog.asserts.assert(entry.onFulfilled != null);

  if (this.callbackEntriesTail_) {
    this.callbackEntriesTail_.next = entry;
    this.callbackEntriesTail_ = entry;
  } else {
    // It the work queue was empty set the head too.
    this.callbackEntries_ = entry;
    this.callbackEntriesTail_ = entry;
  }
};


/**
 * @return {goog.Promise.CallbackEntry_} entry
 * @private
 */
goog.Promise.prototype.popEntry_ = function() {
  var entry = null;
  if (this.callbackEntries_) {
    entry = this.callbackEntries_;
    this.callbackEntries_ = entry.next;
    entry.next = null;
  }
  // It the work queue is empty clear the tail too.
  if (!this.callbackEntries_) {
    this.callbackEntriesTail_ = null;
  }

  if (entry != null) {
    goog.asserts.assert(entry.onFulfilled != null);
  }
  return entry;
};


/**
 * @param {goog.Promise.CallbackEntry_} previous
 * @private
 */
goog.Promise.prototype.removeEntryAfter_ = function(previous) {
  goog.asserts.assert(this.callbackEntries_);
  goog.asserts.assert(previous != null);
  // If the last entry is being removed, update the tail
  if (previous.next == this.callbackEntriesTail_) {
    this.callbackEntriesTail_ = previous;
  }

  previous.next = previous.next.next;
};


/**
 * Executes all pending callbacks for this Promise.
 *
 * @private
 */
goog.Promise.prototype.executeCallbacks_ = function() {
  var entry = null;
  while (entry = this.popEntry_()) {
    if (goog.Promise.LONG_STACK_TRACES) {
      this.currentStep_++;
    }
    this.executeCallback_(entry, this.state_, this.result_);
  }
  this.executing_ = false;
};


/**
 * Executes a pending callback for this Promise. Invokes an {@code onFulfilled}
 * or {@code onRejected} callback based on the settled state of the Promise.
 *
 * @param {!goog.Promise.CallbackEntry_} callbackEntry An entry containing the
 *     onFulfilled and/or onRejected callbacks for this step.
 * @param {goog.Promise.State_} state The resolution status of the Promise,
 *     either FULFILLED or REJECTED.
 * @param {*} result The settled result of the Promise.
 * @private
 */
goog.Promise.prototype.executeCallback_ = function(
    callbackEntry, state, result) {
  // Cancel an unhandled rejection if the then/thenVoid call had an onRejected.
  if (state == goog.Promise.State_.REJECTED &&
      callbackEntry.onRejected && !callbackEntry.always) {
    this.removeUnhandledRejection_();
  }

  if (callbackEntry.child) {
    // When the parent is settled, the child no longer needs to hold on to it,
    // as the parent can no longer be canceled.
    callbackEntry.child.parent_ = null;
    goog.Promise.invokeCallback_(callbackEntry, state, result);
  } else {
    // Callbacks created with thenAlways or thenVoid do not have the rejection
    // handling code normally set up in the child Promise.
    try {
      callbackEntry.always ?
          callbackEntry.onFulfilled.call(callbackEntry.context) :
          goog.Promise.invokeCallback_(callbackEntry, state, result);
    } catch (err) {
      goog.Promise.handleRejection_.call(null, err);
    }
  }
  goog.Promise.returnEntry_(callbackEntry);
};


/**
 * Executes the onFulfilled or onRejected callback for a callbackEntry.
 *
 * @param {!goog.Promise.CallbackEntry_} callbackEntry
 * @param {goog.Promise.State_} state
 * @param {*} result
 * @private
 */
goog.Promise.invokeCallback_ = function(callbackEntry, state, result) {
  if (state == goog.Promise.State_.FULFILLED) {
    callbackEntry.onFulfilled.call(callbackEntry.context, result);
  } else if (callbackEntry.onRejected) {
    callbackEntry.onRejected.call(callbackEntry.context, result);
  }
};


/**
 * Records a stack trace entry for functions that call {@code then} or the
 * Promise constructor. May be disabled by unsetting {@code LONG_STACK_TRACES}.
 *
 * @param {!Error} err An Error object created by the calling function for
 *     providing a stack trace.
 * @private
 */
goog.Promise.prototype.addStackTrace_ = function(err) {
  if (goog.Promise.LONG_STACK_TRACES && goog.isString(err.stack)) {
    // Extract the third line of the stack trace, which is the entry for the
    // user function that called into Promise code.
    var trace = err.stack.split('\n', 4)[3];
    var message = err.message;

    // Pad the message to align the traces.
    message += Array(11 - message.length).join(' ');
    this.stack_.push(message + trace);
  }
};


/**
 * Adds extra stack trace information to an exception for the list of
 * asynchronous {@code then} calls that have been run for this Promise. Stack
 * trace information is recorded in {@see #addStackTrace_}, and appended to
 * rethrown errors when {@code LONG_STACK_TRACES} is enabled.
 *
 * @param {*} err An unhandled exception captured during callback execution.
 * @private
 */
goog.Promise.prototype.appendLongStack_ = function(err) {
  if (goog.Promise.LONG_STACK_TRACES &&
      err && goog.isString(err.stack) && this.stack_.length) {
    var longTrace = ['Promise trace:'];

    for (var promise = this; promise; promise = promise.parent_) {
      for (var i = this.currentStep_; i >= 0; i--) {
        longTrace.push(promise.stack_[i]);
      }
      longTrace.push('Value: ' +
          '[' + (promise.state_ == goog.Promise.State_.REJECTED ?
              'REJECTED' : 'FULFILLED') + '] ' +
          '<' + String(promise.result_) + '>');
    }
    err.stack += '\n\n' + longTrace.join('\n');
  }
};


/**
 * Marks this rejected Promise as having being handled. Also marks any parent
 * Promises in the rejected state as handled. The rejection handler will no
 * longer be invoked for this Promise (if it has not been called already).
 *
 * @private
 */
goog.Promise.prototype.removeUnhandledRejection_ = function() {
  if (goog.Promise.UNHANDLED_REJECTION_DELAY > 0) {
    for (var p = this; p && p.unhandledRejectionId_; p = p.parent_) {
      goog.global.clearTimeout(p.unhandledRejectionId_);
      p.unhandledRejectionId_ = 0;
    }
  } else if (goog.Promise.UNHANDLED_REJECTION_DELAY == 0) {
    for (var p = this; p && p.hadUnhandledRejection_; p = p.parent_) {
      p.hadUnhandledRejection_ = false;
    }
  }
};


/**
 * Marks this rejected Promise as unhandled. If no {@code onRejected} callback
 * is called for this Promise before the {@code UNHANDLED_REJECTION_DELAY}
 * expires, the reason will be passed to the unhandled rejection handler. The
 * handler typically rethrows the rejection reason so that it becomes visible in
 * the developer console.
 *
 * @param {!goog.Promise} promise The rejected Promise.
 * @param {*} reason The Promise rejection reason.
 * @private
 */
goog.Promise.addUnhandledRejection_ = function(promise, reason) {
  if (goog.Promise.UNHANDLED_REJECTION_DELAY > 0) {
    promise.unhandledRejectionId_ = goog.global.setTimeout(function() {
      promise.appendLongStack_(reason);
      goog.Promise.handleRejection_.call(null, reason);
    }, goog.Promise.UNHANDLED_REJECTION_DELAY);

  } else if (goog.Promise.UNHANDLED_REJECTION_DELAY == 0) {
    promise.hadUnhandledRejection_ = true;
    goog.async.run(function() {
      if (promise.hadUnhandledRejection_) {
        promise.appendLongStack_(reason);
        goog.Promise.handleRejection_.call(null, reason);
      }
    });
  }
};


/**
 * A method that is invoked with the rejection reasons for Promises that are
 * rejected but have no {@code onRejected} callbacks registered yet.
 * @type {function(*)}
 * @private
 */
goog.Promise.handleRejection_ = goog.async.throwException;


/**
 * Sets a handler that will be called with reasons from unhandled rejected
 * Promises. If the rejected Promise (or one of its descendants) has an
 * {@code onRejected} callback registered, the rejection will be considered
 * handled, and the rejection handler will not be called.
 *
 * By default, unhandled rejections are rethrown so that the error may be
 * captured by the developer console or a {@code window.onerror} handler.
 *
 * @param {function(*)} handler A function that will be called with reasons from
 *     rejected Promises. Defaults to {@code goog.async.throwException}.
 */
goog.Promise.setUnhandledRejectionHandler = function(handler) {
  goog.Promise.handleRejection_ = handler;
};



/**
 * Error used as a rejection reason for canceled Promises.
 *
 * @param {string=} opt_message
 * @constructor
 * @extends {goog.debug.Error}
 * @final
 */
goog.Promise.CancellationError = function(opt_message) {
  goog.Promise.CancellationError.base(this, 'constructor', opt_message);
};
goog.inherits(goog.Promise.CancellationError, goog.debug.Error);


/** @override */
goog.Promise.CancellationError.prototype.name = 'cancel';



/**
 * Internal implementation of the resolver interface.
 *
 * @param {!goog.Promise<TYPE>} promise
 * @param {function((TYPE|goog.Promise<TYPE>|Thenable)=)} resolve
 * @param {function(*=): void} reject
 * @implements {goog.promise.Resolver<TYPE>}
 * @final @struct
 * @constructor
 * @private
 * @template TYPE
 */
goog.Promise.Resolver_ = function(promise, resolve, reject) {
  /** @const */
  this.promise = promise;

  /** @const */
  this.resolve = resolve;

  /** @const */
  this.reject = reject;
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview A timer class to which other classes and objects can
 * listen on.  This is only an abstraction above setInterval.
 *
 * @see ../demos/timers.html
 */

goog.provide('goog.Timer');

goog.require('goog.Promise');
goog.require('goog.events.EventTarget');



/**
 * Class for handling timing events.
 *
 * @param {number=} opt_interval Number of ms between ticks (Default: 1ms).
 * @param {Object=} opt_timerObject  An object that has setTimeout, setInterval,
 *     clearTimeout and clearInterval (eg Window).
 * @constructor
 * @extends {goog.events.EventTarget}
 */
goog.Timer = function(opt_interval, opt_timerObject) {
  goog.events.EventTarget.call(this);

  /**
   * Number of ms between ticks
   * @type {number}
   * @private
   */
  this.interval_ = opt_interval || 1;

  /**
   * An object that implements setTimeout, setInterval, clearTimeout and
   * clearInterval. We default to the window object. Changing this on
   * goog.Timer.prototype changes the object for all timer instances which can
   * be useful if your environment has some other implementation of timers than
   * the window object.
   * @type {Object}
   * @private
   */
  this.timerObject_ = opt_timerObject || goog.Timer.defaultTimerObject;

  /**
   * Cached tick_ bound to the object for later use in the timer.
   * @type {Function}
   * @private
   */
  this.boundTick_ = goog.bind(this.tick_, this);

  /**
   * Firefox browser often fires the timer event sooner
   * (sometimes MUCH sooner) than the requested timeout. So we
   * compare the time to when the event was last fired, and
   * reschedule if appropriate. See also goog.Timer.intervalScale
   * @type {number}
   * @private
   */
  this.last_ = goog.now();
};
goog.inherits(goog.Timer, goog.events.EventTarget);


/**
 * Maximum timeout value.
 *
 * Timeout values too big to fit into a signed 32-bit integer may cause
 * overflow in FF, Safari, and Chrome, resulting in the timeout being
 * scheduled immediately.  It makes more sense simply not to schedule these
 * timeouts, since 24.8 days is beyond a reasonable expectation for the
 * browser to stay open.
 *
 * @type {number}
 * @private
 */
goog.Timer.MAX_TIMEOUT_ = 2147483647;


/**
 * A timer ID that cannot be returned by any known implmentation of
 * Window.setTimeout.  Passing this value to window.clearTimeout should
 * therefore be a no-op.
 *
 * @const {number}
 * @private
 */
goog.Timer.INVALID_TIMEOUT_ID_ = -1;


/**
 * Whether this timer is enabled
 * @type {boolean}
 */
goog.Timer.prototype.enabled = false;


/**
 * An object that implements setTimout, setInterval, clearTimeout and
 * clearInterval. We default to the global object. Changing
 * goog.Timer.defaultTimerObject changes the object for all timer instances
 * which can be useful if your environment has some other implementation of
 * timers you'd like to use.
 * @type {Object}
 */
goog.Timer.defaultTimerObject = goog.global;


/**
 * A variable that controls the timer error correction. If the
 * timer is called before the requested interval times
 * intervalScale, which often happens on mozilla, the timer is
 * rescheduled. See also this.last_
 * @type {number}
 */
goog.Timer.intervalScale = 0.8;


/**
 * Variable for storing the result of setInterval
 * @type {?number}
 * @private
 */
goog.Timer.prototype.timer_ = null;


/**
 * Gets the interval of the timer.
 * @return {number} interval Number of ms between ticks.
 */
goog.Timer.prototype.getInterval = function() {
  return this.interval_;
};


/**
 * Sets the interval of the timer.
 * @param {number} interval Number of ms between ticks.
 */
goog.Timer.prototype.setInterval = function(interval) {
  this.interval_ = interval;
  if (this.timer_ && this.enabled) {
    // Stop and then start the timer to reset the interval.
    this.stop();
    this.start();
  } else if (this.timer_) {
    this.stop();
  }
};


/**
 * Callback for the setTimeout used by the timer
 * @private
 */
goog.Timer.prototype.tick_ = function() {
  if (this.enabled) {
    var elapsed = goog.now() - this.last_;
    if (elapsed > 0 &&
        elapsed < this.interval_ * goog.Timer.intervalScale) {
      this.timer_ = this.timerObject_.setTimeout(this.boundTick_,
          this.interval_ - elapsed);
      return;
    }

    // Prevents setInterval from registering a duplicate timeout when called
    // in the timer event handler.
    if (this.timer_) {
      this.timerObject_.clearTimeout(this.timer_);
      this.timer_ = null;
    }

    this.dispatchTick();
    // The timer could be stopped in the timer event handler.
    if (this.enabled) {
      this.timer_ = this.timerObject_.setTimeout(this.boundTick_,
          this.interval_);
      this.last_ = goog.now();
    }
  }
};


/**
 * Dispatches the TICK event. This is its own method so subclasses can override.
 */
goog.Timer.prototype.dispatchTick = function() {
  this.dispatchEvent(goog.Timer.TICK);
};


/**
 * Starts the timer.
 */
goog.Timer.prototype.start = function() {
  this.enabled = true;

  // If there is no interval already registered, start it now
  if (!this.timer_) {
    // IMPORTANT!
    // window.setInterval in FireFox has a bug - it fires based on
    // absolute time, rather than on relative time. What this means
    // is that if a computer is sleeping/hibernating for 24 hours
    // and the timer interval was configured to fire every 1000ms,
    // then after the PC wakes up the timer will fire, in rapid
    // succession, 3600*24 times.
    // This bug is described here and is already fixed, but it will
    // take time to propagate, so for now I am switching this over
    // to setTimeout logic.
    //     https://bugzilla.mozilla.org/show_bug.cgi?id=376643
    //
    this.timer_ = this.timerObject_.setTimeout(this.boundTick_,
        this.interval_);
    this.last_ = goog.now();
  }
};


/**
 * Stops the timer.
 */
goog.Timer.prototype.stop = function() {
  this.enabled = false;
  if (this.timer_) {
    this.timerObject_.clearTimeout(this.timer_);
    this.timer_ = null;
  }
};


/** @override */
goog.Timer.prototype.disposeInternal = function() {
  goog.Timer.superClass_.disposeInternal.call(this);
  this.stop();
  delete this.timerObject_;
};


/**
 * Constant for the timer's event type
 * @type {string}
 */
goog.Timer.TICK = 'tick';


/**
 * Calls the given function once, after the optional pause.
 *
 * The function is always called asynchronously, even if the delay is 0. This
 * is a common trick to schedule a function to run after a batch of browser
 * event processing.
 *
 * @param {function(this:SCOPE)|{handleEvent:function()}|null} listener Function
 *     or object that has a handleEvent method.
 * @param {number=} opt_delay Milliseconds to wait; default is 0.
 * @param {SCOPE=} opt_handler Object in whose scope to call the listener.
 * @return {number} A handle to the timer ID.
 * @template SCOPE
 */
goog.Timer.callOnce = function(listener, opt_delay, opt_handler) {
  if (goog.isFunction(listener)) {
    if (opt_handler) {
      listener = goog.bind(listener, opt_handler);
    }
  } else if (listener && typeof listener.handleEvent == 'function') {
    // using typeof to prevent strict js warning
    listener = goog.bind(listener.handleEvent, listener);
  } else {
    throw Error('Invalid listener argument');
  }

  if (opt_delay > goog.Timer.MAX_TIMEOUT_) {
    // Timeouts greater than MAX_INT return immediately due to integer
    // overflow in many browsers.  Since MAX_INT is 24.8 days, just don't
    // schedule anything at all.
    return goog.Timer.INVALID_TIMEOUT_ID_;
  } else {
    return goog.Timer.defaultTimerObject.setTimeout(
        listener, opt_delay || 0);
  }
};


/**
 * Clears a timeout initiated by callOnce
 * @param {?number} timerId a timer ID.
 */
goog.Timer.clear = function(timerId) {
  goog.Timer.defaultTimerObject.clearTimeout(timerId);
};


/**
 * @param {number} delay Milliseconds to wait.
 * @param {(RESULT|goog.Thenable<RESULT>|Thenable)=} opt_result The value
 *     with which the promise will be resolved.
 * @return {!goog.Promise<RESULT>} A promise that will be resolved after
 *     the specified delay, unless it is canceled first.
 * @template RESULT
 */
goog.Timer.promise = function(delay, opt_result) {
  var timerKey = null;
  return new goog.Promise(function(resolve, reject) {
    timerKey = goog.Timer.callOnce(function() {
      resolve(opt_result);
    }, delay);
    if (timerKey == goog.Timer.INVALID_TIMEOUT_ID_) {
      reject(new Error('Failed to schedule timer.'));
    }
  }).thenCatch(function(error) {
    // Clear the timer. The most likely reason is "cancel" signal.
    goog.Timer.clear(timerKey);
    throw error;
  });
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview JSON utility functions.
 * @author arv@google.com (Erik Arvidsson)
 */


goog.provide('goog.json');
goog.provide('goog.json.Replacer');
goog.provide('goog.json.Reviver');
goog.provide('goog.json.Serializer');


/**
 * @define {boolean} If true, use the native JSON parsing API.
 * NOTE(ruilopes): EXPERIMENTAL, handle with care.  Setting this to true might
 * break your code.  The default {@code goog.json.parse} implementation is able
 * to handle invalid JSON, such as JSPB.
 */
goog.define('goog.json.USE_NATIVE_JSON', false);


/**
 * Tests if a string is an invalid JSON string. This only ensures that we are
 * not using any invalid characters
 * @param {string} s The string to test.
 * @return {boolean} True if the input is a valid JSON string.
 */
goog.json.isValid = function(s) {
  // All empty whitespace is not valid.
  if (/^\s*$/.test(s)) {
    return false;
  }

  // This is taken from http://www.json.org/json2.js which is released to the
  // public domain.
  // Changes: We dissallow \u2028 Line separator and \u2029 Paragraph separator
  // inside strings.  We also treat \u2028 and \u2029 as whitespace which they
  // are in the RFC but IE and Safari does not match \s to these so we need to
  // include them in the reg exps in all places where whitespace is allowed.
  // We allowed \x7f inside strings because some tools don't escape it,
  // e.g. http://www.json.org/java/org/json/JSONObject.java

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

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

  // Don't make these static since they have the global flag.
  var backslashesRe = /\\["\\\/bfnrtu]/g;
  var simpleValuesRe =
      /"[^"\\\n\r\u2028\u2029\x00-\x08\x0a-\x1f]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g;
  var openBracketsRe = /(?:^|:|,)(?:[\s\u2028\u2029]*\[)+/g;
  var remainderRe = /^[\],:{}\s\u2028\u2029]*$/;

  return remainderRe.test(s.replace(backslashesRe, '@').
      replace(simpleValuesRe, ']').
      replace(openBracketsRe, ''));
};


/**
 * Parses a JSON string and returns the result. This throws an exception if
 * the string is an invalid JSON string.
 *
 * Note that this is very slow on large strings. If you trust the source of
 * the string then you should use unsafeParse instead.
 *
 * @param {*} s The JSON string to parse.
 * @throws Error if s is invalid JSON.
 * @return {Object} The object generated from the JSON string, or null.
 */
goog.json.parse = goog.json.USE_NATIVE_JSON ?
    /** @type {function(*):Object} */ (goog.global['JSON']['parse']) :
    function(s) {
      var o = String(s);
      if (goog.json.isValid(o)) {
        /** @preserveTry */
        try {
          return /** @type {Object} */ (eval('(' + o + ')'));
        } catch (ex) {
        }
      }
      throw Error('Invalid JSON string: ' + o);
    };


/**
 * Parses a JSON string and returns the result. This uses eval so it is open
 * to security issues and it should only be used if you trust the source.
 *
 * @param {string} s The JSON string to parse.
 * @return {Object} The object generated from the JSON string.
 */
goog.json.unsafeParse = goog.json.USE_NATIVE_JSON ?
    /** @type {function(string):Object} */ (goog.global['JSON']['parse']) :
    function(s) {
      return /** @type {Object} */ (eval('(' + s + ')'));
    };


/**
 * JSON replacer, as defined in Section 15.12.3 of the ES5 spec.
 * @see http://ecma-international.org/ecma-262/5.1/#sec-15.12.3
 *
 * TODO(nicksantos): Array should also be a valid replacer.
 *
 * @typedef {function(this:Object, string, *): *}
 */
goog.json.Replacer;


/**
 * JSON reviver, as defined in Section 15.12.2 of the ES5 spec.
 * @see http://ecma-international.org/ecma-262/5.1/#sec-15.12.3
 *
 * @typedef {function(this:Object, string, *): *}
 */
goog.json.Reviver;


/**
 * Serializes an object or a value to a JSON string.
 *
 * @param {*} object The object to serialize.
 * @param {?goog.json.Replacer=} opt_replacer A replacer function
 *     called for each (key, value) pair that determines how the value
 *     should be serialized. By defult, this just returns the value
 *     and allows default serialization to kick in.
 * @throws Error if there are loops in the object graph.
 * @return {string} A JSON string representation of the input.
 */
goog.json.serialize = goog.json.USE_NATIVE_JSON ?
    /** @type {function(*, ?goog.json.Replacer=):string} */
    (goog.global['JSON']['stringify']) :
    function(object, opt_replacer) {
      // NOTE(nicksantos): Currently, we never use JSON.stringify.
      //
      // The last time I evaluated this, JSON.stringify had subtle bugs and
      // behavior differences on all browsers, and the performance win was not
      // large enough to justify all the issues. This may change in the future
      // as browser implementations get better.
      //
      // assertSerialize in json_test contains if branches for the cases
      // that fail.
      return new goog.json.Serializer(opt_replacer).serialize(object);
    };



/**
 * Class that is used to serialize JSON objects to a string.
 * @param {?goog.json.Replacer=} opt_replacer Replacer.
 * @constructor
 */
goog.json.Serializer = function(opt_replacer) {
  /**
   * @type {goog.json.Replacer|null|undefined}
   * @private
   */
  this.replacer_ = opt_replacer;
};


/**
 * Serializes an object or a value to a JSON string.
 *
 * @param {*} object The object to serialize.
 * @throws Error if there are loops in the object graph.
 * @return {string} A JSON string representation of the input.
 */
goog.json.Serializer.prototype.serialize = function(object) {
  var sb = [];
  this.serializeInternal(object, sb);
  return sb.join('');
};


/**
 * Serializes a generic value to a JSON string
 * @protected
 * @param {*} object The object to serialize.
 * @param {Array<string>} sb Array used as a string builder.
 * @throws Error if there are loops in the object graph.
 */
goog.json.Serializer.prototype.serializeInternal = function(object, sb) {
  if (object == null) {
    // undefined == null so this branch covers undefined as well as null
    sb.push('null');
    return;
  }

  if (typeof object == 'object') {
    if (goog.isArray(object)) {
      this.serializeArray(object, sb);
      return;
    } else if (object instanceof String ||
               object instanceof Number ||
               object instanceof Boolean) {
      object = object.valueOf();
      // Fall through to switch below.
    } else {
      this.serializeObject_(/** @type {Object} */ (object), sb);
      return;
    }
  }

  switch (typeof object) {
    case 'string':
      this.serializeString_(object, sb);
      break;
    case 'number':
      this.serializeNumber_(object, sb);
      break;
    case 'boolean':
      sb.push(object);
      break;
    case 'function':
      sb.push('null');
      break;
    default:
      throw Error('Unknown type: ' + typeof object);
  }
};


/**
 * Character mappings used internally for goog.string.quote
 * @private
 * @type {!Object}
 */
goog.json.Serializer.charToJsonCharCache_ = {
  '\"': '\\"',
  '\\': '\\\\',
  '/': '\\/',
  '\b': '\\b',
  '\f': '\\f',
  '\n': '\\n',
  '\r': '\\r',
  '\t': '\\t',

  '\x0B': '\\u000b' // '\v' is not supported in JScript
};


/**
 * Regular expression used to match characters that need to be replaced.
 * The S60 browser has a bug where unicode characters are not matched by
 * regular expressions. The condition below detects such behaviour and
 * adjusts the regular expression accordingly.
 * @private
 * @type {!RegExp}
 */
goog.json.Serializer.charsToReplace_ = /\uffff/.test('\uffff') ?
    /[\\\"\x00-\x1f\x7f-\uffff]/g : /[\\\"\x00-\x1f\x7f-\xff]/g;


/**
 * Serializes a string to a JSON string
 * @private
 * @param {string} s The string to serialize.
 * @param {Array<string>} sb Array used as a string builder.
 */
goog.json.Serializer.prototype.serializeString_ = function(s, sb) {
  // The official JSON implementation does not work with international
  // characters.
  sb.push('"', s.replace(goog.json.Serializer.charsToReplace_, function(c) {
    // caching the result improves performance by a factor 2-3
    var rv = goog.json.Serializer.charToJsonCharCache_[c];
    if (!rv) {
      rv = '\\u' + (c.charCodeAt(0) | 0x10000).toString(16).substr(1);
      goog.json.Serializer.charToJsonCharCache_[c] = rv;
    }
    return rv;
  }), '"');
};


/**
 * Serializes a number to a JSON string
 * @private
 * @param {number} n The number to serialize.
 * @param {Array<string>} sb Array used as a string builder.
 */
goog.json.Serializer.prototype.serializeNumber_ = function(n, sb) {
  sb.push(isFinite(n) && !isNaN(n) ? n : 'null');
};


/**
 * Serializes an array to a JSON string
 * @param {Array<string>} arr The array to serialize.
 * @param {Array<string>} sb Array used as a string builder.
 * @protected
 */
goog.json.Serializer.prototype.serializeArray = function(arr, sb) {
  var l = arr.length;
  sb.push('[');
  var sep = '';
  for (var i = 0; i < l; i++) {
    sb.push(sep);

    var value = arr[i];
    this.serializeInternal(
        this.replacer_ ? this.replacer_.call(arr, String(i), value) : value,
        sb);

    sep = ',';
  }
  sb.push(']');
};


/**
 * Serializes an object to a JSON string
 * @private
 * @param {Object} obj The object to serialize.
 * @param {Array<string>} sb Array used as a string builder.
 */
goog.json.Serializer.prototype.serializeObject_ = function(obj, sb) {
  sb.push('{');
  var sep = '';
  for (var key in obj) {
    if (Object.prototype.hasOwnProperty.call(obj, key)) {
      var value = obj[key];
      // Skip functions.
      if (typeof value != 'function') {
        sb.push(sep);
        this.serializeString_(key, sb);
        sb.push(':');

        this.serializeInternal(
            this.replacer_ ? this.replacer_.call(obj, key, value) : value,
            sb);

        sep = ',';
      }
    }
  }
  sb.push('}');
};

// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Error codes shared between goog.net.IframeIo and
 * goog.net.XhrIo.
 */

goog.provide('goog.net.ErrorCode');


/**
 * Error codes
 * @enum {number}
 */
goog.net.ErrorCode = {

  /**
   * There is no error condition.
   */
  NO_ERROR: 0,

  /**
   * The most common error from iframeio, unfortunately, is that the browser
   * responded with an error page that is classed as a different domain. The
   * situations, are when a browser error page  is shown -- 404, access denied,
   * DNS failure, connection reset etc.)
   *
   */
  ACCESS_DENIED: 1,

  /**
   * Currently the only case where file not found will be caused is when the
   * code is running on the local file system and a non-IE browser makes a
   * request to a file that doesn't exist.
   */
  FILE_NOT_FOUND: 2,

  /**
   * If Firefox shows a browser error page, such as a connection reset by
   * server or access denied, then it will fail silently without the error or
   * load handlers firing.
   */
  FF_SILENT_ERROR: 3,

  /**
   * Custom error provided by the client through the error check hook.
   */
  CUSTOM_ERROR: 4,

  /**
   * Exception was thrown while processing the request.
   */
  EXCEPTION: 5,

  /**
   * The Http response returned a non-successful http status code.
   */
  HTTP_ERROR: 6,

  /**
   * The request was aborted.
   */
  ABORT: 7,

  /**
   * The request timed out.
   */
  TIMEOUT: 8,

  /**
   * The resource is not available offline.
   */
  OFFLINE: 9
};


/**
 * Returns a friendly error message for an error code. These messages are for
 * debugging and are not localized.
 * @param {goog.net.ErrorCode} errorCode An error code.
 * @return {string} A message for debugging.
 */
goog.net.ErrorCode.getDebugMessage = function(errorCode) {
  switch (errorCode) {
    case goog.net.ErrorCode.NO_ERROR:
      return 'No Error';

    case goog.net.ErrorCode.ACCESS_DENIED:
      return 'Access denied to content document';

    case goog.net.ErrorCode.FILE_NOT_FOUND:
      return 'File not found';

    case goog.net.ErrorCode.FF_SILENT_ERROR:
      return 'Firefox silently errored';

    case goog.net.ErrorCode.CUSTOM_ERROR:
      return 'Application custom error';

    case goog.net.ErrorCode.EXCEPTION:
      return 'An exception occurred';

    case goog.net.ErrorCode.HTTP_ERROR:
      return 'Http response at 400 or 500 level';

    case goog.net.ErrorCode.ABORT:
      return 'Request was aborted';

    case goog.net.ErrorCode.TIMEOUT:
      return 'Request timed out';

    case goog.net.ErrorCode.OFFLINE:
      return 'The resource is not available offline';

    default:
      return 'Unrecognized error code';
  }
};

// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Constants for HTTP status codes.
 */

goog.provide('goog.net.HttpStatus');


/**
 * HTTP Status Codes defined in RFC 2616 and RFC 6585.
 * @see http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html
 * @see http://tools.ietf.org/html/rfc6585
 * @enum {number}
 */
goog.net.HttpStatus = {
  // Informational 1xx
  CONTINUE: 100,
  SWITCHING_PROTOCOLS: 101,

  // Successful 2xx
  OK: 200,
  CREATED: 201,
  ACCEPTED: 202,
  NON_AUTHORITATIVE_INFORMATION: 203,
  NO_CONTENT: 204,
  RESET_CONTENT: 205,
  PARTIAL_CONTENT: 206,

  // Redirection 3xx
  MULTIPLE_CHOICES: 300,
  MOVED_PERMANENTLY: 301,
  FOUND: 302,
  SEE_OTHER: 303,
  NOT_MODIFIED: 304,
  USE_PROXY: 305,
  TEMPORARY_REDIRECT: 307,

  // Client Error 4xx
  BAD_REQUEST: 400,
  UNAUTHORIZED: 401,
  PAYMENT_REQUIRED: 402,
  FORBIDDEN: 403,
  NOT_FOUND: 404,
  METHOD_NOT_ALLOWED: 405,
  NOT_ACCEPTABLE: 406,
  PROXY_AUTHENTICATION_REQUIRED: 407,
  REQUEST_TIMEOUT: 408,
  CONFLICT: 409,
  GONE: 410,
  LENGTH_REQUIRED: 411,
  PRECONDITION_FAILED: 412,
  REQUEST_ENTITY_TOO_LARGE: 413,
  REQUEST_URI_TOO_LONG: 414,
  UNSUPPORTED_MEDIA_TYPE: 415,
  REQUEST_RANGE_NOT_SATISFIABLE: 416,
  EXPECTATION_FAILED: 417,
  PRECONDITION_REQUIRED: 428,
  TOO_MANY_REQUESTS: 429,
  REQUEST_HEADER_FIELDS_TOO_LARGE: 431,

  // Server Error 5xx
  INTERNAL_SERVER_ERROR: 500,
  NOT_IMPLEMENTED: 501,
  BAD_GATEWAY: 502,
  SERVICE_UNAVAILABLE: 503,
  GATEWAY_TIMEOUT: 504,
  HTTP_VERSION_NOT_SUPPORTED: 505,
  NETWORK_AUTHENTICATION_REQUIRED: 511,

  /*
   * IE returns this code for 204 due to its use of URLMon, which returns this
   * code for 'Operation Aborted'. The status text is 'Unknown', the response
   * headers are ''. Known to occur on IE 6 on XP through IE9 on Win7.
   */
  QUIRK_IE_NO_CONTENT: 1223
};


/**
 * Returns whether the given status should be considered successful.
 *
 * Successful codes are OK (200), CREATED (201), ACCEPTED (202),
 * NO CONTENT (204), PARTIAL CONTENT (206), NOT MODIFIED (304),
 * and IE's no content code (1223).
 *
 * @param {number} status The status code to test.
 * @return {boolean} Whether the status code should be considered successful.
 */
goog.net.HttpStatus.isSuccess = function(status) {
  switch (status) {
    case goog.net.HttpStatus.OK:
    case goog.net.HttpStatus.CREATED:
    case goog.net.HttpStatus.ACCEPTED:
    case goog.net.HttpStatus.NO_CONTENT:
    case goog.net.HttpStatus.PARTIAL_CONTENT:
    case goog.net.HttpStatus.NOT_MODIFIED:
    case goog.net.HttpStatus.QUIRK_IE_NO_CONTENT:
      return true;

    default:
      return false;
  }
};

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

goog.provide('goog.net.XhrLike');



/**
 * Interface for the common parts of XMLHttpRequest.
 *
 * Mostly copied from externs/w3c_xml.js.
 *
 * @interface
 * @see http://www.w3.org/TR/XMLHttpRequest/
 */
goog.net.XhrLike = function() {};


/**
 * Typedef that refers to either native or custom-implemented XHR objects.
 * @typedef {!goog.net.XhrLike|!XMLHttpRequest}
 */
goog.net.XhrLike.OrNative;


/**
 * @type {function()|null|undefined}
 * @see http://www.w3.org/TR/XMLHttpRequest/#handler-xhr-onreadystatechange
 */
goog.net.XhrLike.prototype.onreadystatechange;


/**
 * @type {string}
 * @see http://www.w3.org/TR/XMLHttpRequest/#the-responsetext-attribute
 */
goog.net.XhrLike.prototype.responseText;


/**
 * @type {Document}
 * @see http://www.w3.org/TR/XMLHttpRequest/#the-responsexml-attribute
 */
goog.net.XhrLike.prototype.responseXML;


/**
 * @type {number}
 * @see http://www.w3.org/TR/XMLHttpRequest/#readystate
 */
goog.net.XhrLike.prototype.readyState;


/**
 * @type {number}
 * @see http://www.w3.org/TR/XMLHttpRequest/#status
 */
goog.net.XhrLike.prototype.status;


/**
 * @type {string}
 * @see http://www.w3.org/TR/XMLHttpRequest/#statustext
 */
goog.net.XhrLike.prototype.statusText;


/**
 * @param {string} method
 * @param {string} url
 * @param {?boolean=} opt_async
 * @param {?string=} opt_user
 * @param {?string=} opt_password
 * @see http://www.w3.org/TR/XMLHttpRequest/#the-open()-method
 */
goog.net.XhrLike.prototype.open = function(method, url, opt_async, opt_user,
    opt_password) {};


/**
 * @param {ArrayBuffer|ArrayBufferView|Blob|Document|FormData|string=} opt_data
 * @see http://www.w3.org/TR/XMLHttpRequest/#the-send()-method
 */
goog.net.XhrLike.prototype.send = function(opt_data) {};


/**
 * @see http://www.w3.org/TR/XMLHttpRequest/#the-abort()-method
 */
goog.net.XhrLike.prototype.abort = function() {};


/**
 * @param {string} header
 * @param {string} value
 * @see http://www.w3.org/TR/XMLHttpRequest/#the-setrequestheader()-method
 */
goog.net.XhrLike.prototype.setRequestHeader = function(header, value) {};


/**
 * @param {string} header
 * @return {string}
 * @see http://www.w3.org/TR/XMLHttpRequest/#the-getresponseheader()-method
 */
goog.net.XhrLike.prototype.getResponseHeader = function(header) {};


/**
 * @return {string}
 * @see http://www.w3.org/TR/XMLHttpRequest/#the-getallresponseheaders()-method
 */
goog.net.XhrLike.prototype.getAllResponseHeaders = function() {};

// Copyright 2010 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Interface for a factory for creating XMLHttpRequest objects
 * and metadata about them.
 * @author dbk@google.com (David Barrett-Kahn)
 */

goog.provide('goog.net.XmlHttpFactory');

/** @suppress {extraRequire} Typedef. */
goog.require('goog.net.XhrLike');



/**
 * Abstract base class for an XmlHttpRequest factory.
 * @constructor
 */
goog.net.XmlHttpFactory = function() {
};


/**
 * Cache of options - we only actually call internalGetOptions once.
 * @type {Object}
 * @private
 */
goog.net.XmlHttpFactory.prototype.cachedOptions_ = null;


/**
 * @return {!goog.net.XhrLike.OrNative} A new XhrLike instance.
 */
goog.net.XmlHttpFactory.prototype.createInstance = goog.abstractMethod;


/**
 * @return {Object} Options describing how xhr objects obtained from this
 *     factory should be used.
 */
goog.net.XmlHttpFactory.prototype.getOptions = function() {
  return this.cachedOptions_ ||
      (this.cachedOptions_ = this.internalGetOptions());
};


/**
 * Override this method in subclasses to preserve the caching offered by
 * getOptions().
 * @return {Object} Options describing how xhr objects obtained from this
 *     factory should be used.
 * @protected
 */
goog.net.XmlHttpFactory.prototype.internalGetOptions = goog.abstractMethod;

// Copyright 2010 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Implementation of XmlHttpFactory which allows construction from
 * simple factory methods.
 * @author dbk@google.com (David Barrett-Kahn)
 */

goog.provide('goog.net.WrapperXmlHttpFactory');

/** @suppress {extraRequire} Typedef. */
goog.require('goog.net.XhrLike');
goog.require('goog.net.XmlHttpFactory');



/**
 * An xhr factory subclass which can be constructed using two factory methods.
 * This exists partly to allow the preservation of goog.net.XmlHttp.setFactory()
 * with an unchanged signature.
 * @param {function():!goog.net.XhrLike.OrNative} xhrFactory
 *     A function which returns a new XHR object.
 * @param {function():!Object} optionsFactory A function which returns the
 *     options associated with xhr objects from this factory.
 * @extends {goog.net.XmlHttpFactory}
 * @constructor
 * @final
 */
goog.net.WrapperXmlHttpFactory = function(xhrFactory, optionsFactory) {
  goog.net.XmlHttpFactory.call(this);

  /**
   * XHR factory method.
   * @type {function() : !goog.net.XhrLike.OrNative}
   * @private
   */
  this.xhrFactory_ = xhrFactory;

  /**
   * Options factory method.
   * @type {function() : !Object}
   * @private
   */
  this.optionsFactory_ = optionsFactory;
};
goog.inherits(goog.net.WrapperXmlHttpFactory, goog.net.XmlHttpFactory);


/** @override */
goog.net.WrapperXmlHttpFactory.prototype.createInstance = function() {
  return this.xhrFactory_();
};


/** @override */
goog.net.WrapperXmlHttpFactory.prototype.getOptions = function() {
  return this.optionsFactory_();
};


// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Low level handling of XMLHttpRequest.
 * @author arv@google.com (Erik Arvidsson)
 * @author dbk@google.com (David Barrett-Kahn)
 */

goog.provide('goog.net.DefaultXmlHttpFactory');
goog.provide('goog.net.XmlHttp');
goog.provide('goog.net.XmlHttp.OptionType');
goog.provide('goog.net.XmlHttp.ReadyState');
goog.provide('goog.net.XmlHttpDefines');

goog.require('goog.asserts');
goog.require('goog.net.WrapperXmlHttpFactory');
goog.require('goog.net.XmlHttpFactory');


/**
 * Static class for creating XMLHttpRequest objects.
 * @return {!goog.net.XhrLike.OrNative} A new XMLHttpRequest object.
 */
goog.net.XmlHttp = function() {
  return goog.net.XmlHttp.factory_.createInstance();
};


/**
 * @define {boolean} Whether to assume XMLHttpRequest exists. Setting this to
 *     true bypasses the ActiveX probing code.
 * NOTE(ruilopes): Due to the way JSCompiler works, this define *will not* strip
 * out the ActiveX probing code from binaries.  To achieve this, use
 * {@code goog.net.XmlHttpDefines.ASSUME_NATIVE_XHR} instead.
 * TODO(ruilopes): Collapse both defines.
 */
goog.define('goog.net.XmlHttp.ASSUME_NATIVE_XHR', false);


/** @const */
goog.net.XmlHttpDefines = {};


/**
 * @define {boolean} Whether to assume XMLHttpRequest exists. Setting this to
 *     true eliminates the ActiveX probing code.
 */
goog.define('goog.net.XmlHttpDefines.ASSUME_NATIVE_XHR', false);


/**
 * Gets the options to use with the XMLHttpRequest objects obtained using
 * the static methods.
 * @return {Object} The options.
 */
goog.net.XmlHttp.getOptions = function() {
  return goog.net.XmlHttp.factory_.getOptions();
};


/**
 * Type of options that an XmlHttp object can have.
 * @enum {number}
 */
goog.net.XmlHttp.OptionType = {
  /**
   * Whether a goog.nullFunction should be used to clear the onreadystatechange
   * handler instead of null.
   */
  USE_NULL_FUNCTION: 0,

  /**
   * NOTE(user): In IE if send() errors on a *local* request the readystate
   * is still changed to COMPLETE.  We need to ignore it and allow the
   * try/catch around send() to pick up the error.
   */
  LOCAL_REQUEST_ERROR: 1
};


/**
 * Status constants for XMLHTTP, matches:
 * http://msdn.microsoft.com/library/default.asp?url=/library/
 *   en-us/xmlsdk/html/0e6a34e4-f90c-489d-acff-cb44242fafc6.asp
 * @enum {number}
 */
goog.net.XmlHttp.ReadyState = {
  /**
   * Constant for when xmlhttprequest.readyState is uninitialized
   */
  UNINITIALIZED: 0,

  /**
   * Constant for when xmlhttprequest.readyState is loading.
   */
  LOADING: 1,

  /**
   * Constant for when xmlhttprequest.readyState is loaded.
   */
  LOADED: 2,

  /**
   * Constant for when xmlhttprequest.readyState is in an interactive state.
   */
  INTERACTIVE: 3,

  /**
   * Constant for when xmlhttprequest.readyState is completed
   */
  COMPLETE: 4
};


/**
 * The global factory instance for creating XMLHttpRequest objects.
 * @type {goog.net.XmlHttpFactory}
 * @private
 */
goog.net.XmlHttp.factory_;


/**
 * Sets the factories for creating XMLHttpRequest objects and their options.
 * @param {Function} factory The factory for XMLHttpRequest objects.
 * @param {Function} optionsFactory The factory for options.
 * @deprecated Use setGlobalFactory instead.
 */
goog.net.XmlHttp.setFactory = function(factory, optionsFactory) {
  goog.net.XmlHttp.setGlobalFactory(new goog.net.WrapperXmlHttpFactory(
      goog.asserts.assert(factory),
      goog.asserts.assert(optionsFactory)));
};


/**
 * Sets the global factory object.
 * @param {!goog.net.XmlHttpFactory} factory New global factory object.
 */
goog.net.XmlHttp.setGlobalFactory = function(factory) {
  goog.net.XmlHttp.factory_ = factory;
};



/**
 * Default factory to use when creating xhr objects.  You probably shouldn't be
 * instantiating this directly, but rather using it via goog.net.XmlHttp.
 * @extends {goog.net.XmlHttpFactory}
 * @constructor
 */
goog.net.DefaultXmlHttpFactory = function() {
  goog.net.XmlHttpFactory.call(this);
};
goog.inherits(goog.net.DefaultXmlHttpFactory, goog.net.XmlHttpFactory);


/** @override */
goog.net.DefaultXmlHttpFactory.prototype.createInstance = function() {
  var progId = this.getProgId_();
  if (progId) {
    return new ActiveXObject(progId);
  } else {
    return new XMLHttpRequest();
  }
};


/** @override */
goog.net.DefaultXmlHttpFactory.prototype.internalGetOptions = function() {
  var progId = this.getProgId_();
  var options = {};
  if (progId) {
    options[goog.net.XmlHttp.OptionType.USE_NULL_FUNCTION] = true;
    options[goog.net.XmlHttp.OptionType.LOCAL_REQUEST_ERROR] = true;
  }
  return options;
};


/**
 * The ActiveX PROG ID string to use to create xhr's in IE. Lazily initialized.
 * @type {string|undefined}
 * @private
 */
goog.net.DefaultXmlHttpFactory.prototype.ieProgId_;


/**
 * Initialize the private state used by other functions.
 * @return {string} The ActiveX PROG ID string to use to create xhr's in IE.
 * @private
 */
goog.net.DefaultXmlHttpFactory.prototype.getProgId_ = function() {
  if (goog.net.XmlHttp.ASSUME_NATIVE_XHR ||
      goog.net.XmlHttpDefines.ASSUME_NATIVE_XHR) {
    return '';
  }

  // The following blog post describes what PROG IDs to use to create the
  // XMLHTTP object in Internet Explorer:
  // http://blogs.msdn.com/xmlteam/archive/2006/10/23/using-the-right-version-of-msxml-in-internet-explorer.aspx
  // However we do not (yet) fully trust that this will be OK for old versions
  // of IE on Win9x so we therefore keep the last 2.
  if (!this.ieProgId_ && typeof XMLHttpRequest == 'undefined' &&
      typeof ActiveXObject != 'undefined') {
    // Candidate Active X types.
    var ACTIVE_X_IDENTS = ['MSXML2.XMLHTTP.6.0', 'MSXML2.XMLHTTP.3.0',
                           'MSXML2.XMLHTTP', 'Microsoft.XMLHTTP'];
    for (var i = 0; i < ACTIVE_X_IDENTS.length; i++) {
      var candidate = ACTIVE_X_IDENTS[i];
      /** @preserveTry */
      try {
        new ActiveXObject(candidate);
        // NOTE(user): cannot assign progid and return candidate in one line
        // because JSCompiler complaings: BUG 658126
        this.ieProgId_ = candidate;
        return candidate;
      } catch (e) {
        // do nothing; try next choice
      }
    }

    // couldn't find any matches
    throw Error('Could not create ActiveXObject. ActiveX might be disabled,' +
                ' or MSXML might not be installed');
  }

  return /** @type {string} */ (this.ieProgId_);
};


//Set the global factory to an instance of the default factory.
goog.net.XmlHttp.setGlobalFactory(new goog.net.DefaultXmlHttpFactory());

// Copyright 2008 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Simple utilities for dealing with URI strings.
 *
 * This is intended to be a lightweight alternative to constructing goog.Uri
 * objects.  Whereas goog.Uri adds several kilobytes to the binary regardless
 * of how much of its functionality you use, this is designed to be a set of
 * mostly-independent utilities so that the compiler includes only what is
 * necessary for the task.  Estimated savings of porting is 5k pre-gzip and
 * 1.5k post-gzip.  To ensure the savings remain, future developers should
 * avoid adding new functionality to existing functions, but instead create
 * new ones and factor out shared code.
 *
 * Many of these utilities have limited functionality, tailored to common
 * cases.  The query parameter utilities assume that the parameter keys are
 * already encoded, since most keys are compile-time alphanumeric strings.  The
 * query parameter mutation utilities also do not tolerate fragment identifiers.
 *
 * By design, these functions can be slower than goog.Uri equivalents.
 * Repeated calls to some of functions may be quadratic in behavior for IE,
 * although the effect is somewhat limited given the 2kb limit.
 *
 * One advantage of the limited functionality here is that this approach is
 * less sensitive to differences in URI encodings than goog.Uri, since these
 * functions operate on strings directly, rather than decoding them and
 * then re-encoding.
 *
 * Uses features of RFC 3986 for parsing/formatting URIs:
 *   http://www.ietf.org/rfc/rfc3986.txt
 *
 * @author gboyer@google.com (Garrett Boyer) - The "lightened" design.
 */

goog.provide('goog.uri.utils');
goog.provide('goog.uri.utils.ComponentIndex');
goog.provide('goog.uri.utils.QueryArray');
goog.provide('goog.uri.utils.QueryValue');
goog.provide('goog.uri.utils.StandardQueryParam');

goog.require('goog.asserts');
goog.require('goog.string');


/**
 * Character codes inlined to avoid object allocations due to charCode.
 * @enum {number}
 * @private
 */
goog.uri.utils.CharCode_ = {
  AMPERSAND: 38,
  EQUAL: 61,
  HASH: 35,
  QUESTION: 63
};


/**
 * Builds a URI string from already-encoded parts.
 *
 * No encoding is performed.  Any component may be omitted as either null or
 * undefined.
 *
 * @param {?string=} opt_scheme The scheme such as 'http'.
 * @param {?string=} opt_userInfo The user name before the '@'.
 * @param {?string=} opt_domain The domain such as 'www.google.com', already
 *     URI-encoded.
 * @param {(string|number|null)=} opt_port The port number.
 * @param {?string=} opt_path The path, already URI-encoded.  If it is not
 *     empty, it must begin with a slash.
 * @param {?string=} opt_queryData The URI-encoded query data.
 * @param {?string=} opt_fragment The URI-encoded fragment identifier.
 * @return {string} The fully combined URI.
 */
goog.uri.utils.buildFromEncodedParts = function(opt_scheme, opt_userInfo,
    opt_domain, opt_port, opt_path, opt_queryData, opt_fragment) {
  var out = '';

  if (opt_scheme) {
    out += opt_scheme + ':';
  }

  if (opt_domain) {
    out += '//';

    if (opt_userInfo) {
      out += opt_userInfo + '@';
    }

    out += opt_domain;

    if (opt_port) {
      out += ':' + opt_port;
    }
  }

  if (opt_path) {
    out += opt_path;
  }

  if (opt_queryData) {
    out += '?' + opt_queryData;
  }

  if (opt_fragment) {
    out += '#' + opt_fragment;
  }

  return out;
};


/**
 * A regular expression for breaking a URI into its component parts.
 *
 * {@link http://www.ietf.org/rfc/rfc3986.txt} says in Appendix B
 * As the "first-match-wins" algorithm is identical to the "greedy"
 * disambiguation method used by POSIX regular expressions, it is natural and
 * commonplace to use a regular expression for parsing the potential five
 * components of a URI reference.
 *
 * The following line is the regular expression for breaking-down a
 * well-formed URI reference into its components.
 *
 * <pre>
 * ^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?
 *  12            3  4          5       6  7        8 9
 * </pre>
 *
 * The numbers in the second line above are only to assist readability; they
 * indicate the reference points for each subexpression (i.e., each paired
 * parenthesis). We refer to the value matched for subexpression <n> as $<n>.
 * For example, matching the above expression to
 * <pre>
 *     http://www.ics.uci.edu/pub/ietf/uri/#Related
 * </pre>
 * results in the following subexpression matches:
 * <pre>
 *    $1 = http:
 *    $2 = http
 *    $3 = //www.ics.uci.edu
 *    $4 = www.ics.uci.edu
 *    $5 = /pub/ietf/uri/
 *    $6 = <undefined>
 *    $7 = <undefined>
 *    $8 = #Related
 *    $9 = Related
 * </pre>
 * where <undefined> indicates that the component is not present, as is the
 * case for the query component in the above example. Therefore, we can
 * determine the value of the five components as
 * <pre>
 *    scheme    = $2
 *    authority = $4
 *    path      = $5
 *    query     = $7
 *    fragment  = $9
 * </pre>
 *
 * The regular expression has been modified slightly to expose the
 * userInfo, domain, and port separately from the authority.
 * The modified version yields
 * <pre>
 *    $1 = http              scheme
 *    $2 = <undefined>       userInfo -\
 *    $3 = www.ics.uci.edu   domain     | authority
 *    $4 = <undefined>       port     -/
 *    $5 = /pub/ietf/uri/    path
 *    $6 = <undefined>       query without ?
 *    $7 = Related           fragment without #
 * </pre>
 * @type {!RegExp}
 * @private
 */
goog.uri.utils.splitRe_ = new RegExp(
    '^' +
    '(?:' +
        '([^:/?#.]+)' +                  // scheme - ignore special characters
                                         // used by other URL parts such as :,
                                         // ?, /, #, and .
    ':)?' +
    '(?://' +
        '(?:([^/?#]*)@)?' +              // userInfo
        '([^/#?]*?)' +                   // domain
        '(?::([0-9]+))?' +               // port
        '(?=[/#?]|$)' +                  // authority-terminating character
    ')?' +
    '([^?#]+)?' +                        // path
    '(?:\\?([^#]*))?' +                  // query
    '(?:#(.*))?' +                       // fragment
    '$');


/**
 * The index of each URI component in the return value of goog.uri.utils.split.
 * @enum {number}
 */
goog.uri.utils.ComponentIndex = {
  SCHEME: 1,
  USER_INFO: 2,
  DOMAIN: 3,
  PORT: 4,
  PATH: 5,
  QUERY_DATA: 6,
  FRAGMENT: 7
};


/**
 * Splits a URI into its component parts.
 *
 * Each component can be accessed via the component indices; for example:
 * <pre>
 * goog.uri.utils.split(someStr)[goog.uri.utils.CompontentIndex.QUERY_DATA];
 * </pre>
 *
 * @param {string} uri The URI string to examine.
 * @return {!Array<string|undefined>} Each component still URI-encoded.
 *     Each component that is present will contain the encoded value, whereas
 *     components that are not present will be undefined or empty, depending
 *     on the browser's regular expression implementation.  Never null, since
 *     arbitrary strings may still look like path names.
 */
goog.uri.utils.split = function(uri) {
  // See @return comment -- never null.
  return /** @type {!Array<string|undefined>} */ (
      uri.match(goog.uri.utils.splitRe_));
};


/**
 * @param {?string} uri A possibly null string.
 * @param {boolean=} opt_preserveReserved If true, percent-encoding of RFC-3986
 *     reserved characters will not be removed.
 * @return {?string} The string URI-decoded, or null if uri is null.
 * @private
 */
goog.uri.utils.decodeIfPossible_ = function(uri, opt_preserveReserved) {
  if (!uri) {
    return uri;
  }

  return opt_preserveReserved ? decodeURI(uri) : decodeURIComponent(uri);
};


/**
 * Gets a URI component by index.
 *
 * It is preferred to use the getPathEncoded() variety of functions ahead,
 * since they are more readable.
 *
 * @param {goog.uri.utils.ComponentIndex} componentIndex The component index.
 * @param {string} uri The URI to examine.
 * @return {?string} The still-encoded component, or null if the component
 *     is not present.
 * @private
 */
goog.uri.utils.getComponentByIndex_ = function(componentIndex, uri) {
  // Convert undefined, null, and empty string into null.
  return goog.uri.utils.split(uri)[componentIndex] || null;
};


/**
 * @param {string} uri The URI to examine.
 * @return {?string} The protocol or scheme, or null if none.  Does not
 *     include trailing colons or slashes.
 */
goog.uri.utils.getScheme = function(uri) {
  return goog.uri.utils.getComponentByIndex_(
      goog.uri.utils.ComponentIndex.SCHEME, uri);
};


/**
 * Gets the effective scheme for the URL.  If the URL is relative then the
 * scheme is derived from the page's location.
 * @param {string} uri The URI to examine.
 * @return {string} The protocol or scheme, always lower case.
 */
goog.uri.utils.getEffectiveScheme = function(uri) {
  var scheme = goog.uri.utils.getScheme(uri);
  if (!scheme && goog.global.self && goog.global.self.location) {
    var protocol = goog.global.self.location.protocol;
    scheme = protocol.substr(0, protocol.length - 1);
  }
  // NOTE: When called from a web worker in Firefox 3.5, location maybe null.
  // All other browsers with web workers support self.location from the worker.
  return scheme ? scheme.toLowerCase() : '';
};


/**
 * @param {string} uri The URI to examine.
 * @return {?string} The user name still encoded, or null if none.
 */
goog.uri.utils.getUserInfoEncoded = function(uri) {
  return goog.uri.utils.getComponentByIndex_(
      goog.uri.utils.ComponentIndex.USER_INFO, uri);
};


/**
 * @param {string} uri The URI to examine.
 * @return {?string} The decoded user info, or null if none.
 */
goog.uri.utils.getUserInfo = function(uri) {
  return goog.uri.utils.decodeIfPossible_(
      goog.uri.utils.getUserInfoEncoded(uri));
};


/**
 * @param {string} uri The URI to examine.
 * @return {?string} The domain name still encoded, or null if none.
 */
goog.uri.utils.getDomainEncoded = function(uri) {
  return goog.uri.utils.getComponentByIndex_(
      goog.uri.utils.ComponentIndex.DOMAIN, uri);
};


/**
 * @param {string} uri The URI to examine.
 * @return {?string} The decoded domain, or null if none.
 */
goog.uri.utils.getDomain = function(uri) {
  return goog.uri.utils.decodeIfPossible_(
      goog.uri.utils.getDomainEncoded(uri), true /* opt_preserveReserved */);
};


/**
 * @param {string} uri The URI to examine.
 * @return {?number} The port number, or null if none.
 */
goog.uri.utils.getPort = function(uri) {
  // Coerce to a number.  If the result of getComponentByIndex_ is null or
  // non-numeric, the number coersion yields NaN.  This will then return
  // null for all non-numeric cases (though also zero, which isn't a relevant
  // port number).
  return Number(goog.uri.utils.getComponentByIndex_(
      goog.uri.utils.ComponentIndex.PORT, uri)) || null;
};


/**
 * @param {string} uri The URI to examine.
 * @return {?string} The path still encoded, or null if none. Includes the
 *     leading slash, if any.
 */
goog.uri.utils.getPathEncoded = function(uri) {
  return goog.uri.utils.getComponentByIndex_(
      goog.uri.utils.ComponentIndex.PATH, uri);
};


/**
 * @param {string} uri The URI to examine.
 * @return {?string} The decoded path, or null if none.  Includes the leading
 *     slash, if any.
 */
goog.uri.utils.getPath = function(uri) {
  return goog.uri.utils.decodeIfPossible_(
      goog.uri.utils.getPathEncoded(uri), true /* opt_preserveReserved */);
};


/**
 * @param {string} uri The URI to examine.
 * @return {?string} The query data still encoded, or null if none.  Does not
 *     include the question mark itself.
 */
goog.uri.utils.getQueryData = function(uri) {
  return goog.uri.utils.getComponentByIndex_(
      goog.uri.utils.ComponentIndex.QUERY_DATA, uri);
};


/**
 * @param {string} uri The URI to examine.
 * @return {?string} The fragment identifier, or null if none.  Does not
 *     include the hash mark itself.
 */
goog.uri.utils.getFragmentEncoded = function(uri) {
  // The hash mark may not appear in any other part of the URL.
  var hashIndex = uri.indexOf('#');
  return hashIndex < 0 ? null : uri.substr(hashIndex + 1);
};


/**
 * @param {string} uri The URI to examine.
 * @param {?string} fragment The encoded fragment identifier, or null if none.
 *     Does not include the hash mark itself.
 * @return {string} The URI with the fragment set.
 */
goog.uri.utils.setFragmentEncoded = function(uri, fragment) {
  return goog.uri.utils.removeFragment(uri) + (fragment ? '#' + fragment : '');
};


/**
 * @param {string} uri The URI to examine.
 * @return {?string} The decoded fragment identifier, or null if none.  Does
 *     not include the hash mark.
 */
goog.uri.utils.getFragment = function(uri) {
  return goog.uri.utils.decodeIfPossible_(
      goog.uri.utils.getFragmentEncoded(uri));
};


/**
 * Extracts everything up to the port of the URI.
 * @param {string} uri The URI string.
 * @return {string} Everything up to and including the port.
 */
goog.uri.utils.getHost = function(uri) {
  var pieces = goog.uri.utils.split(uri);
  return goog.uri.utils.buildFromEncodedParts(
      pieces[goog.uri.utils.ComponentIndex.SCHEME],
      pieces[goog.uri.utils.ComponentIndex.USER_INFO],
      pieces[goog.uri.utils.ComponentIndex.DOMAIN],
      pieces[goog.uri.utils.ComponentIndex.PORT]);
};


/**
 * Extracts the path of the URL and everything after.
 * @param {string} uri The URI string.
 * @return {string} The URI, starting at the path and including the query
 *     parameters and fragment identifier.
 */
goog.uri.utils.getPathAndAfter = function(uri) {
  var pieces = goog.uri.utils.split(uri);
  return goog.uri.utils.buildFromEncodedParts(null, null, null, null,
      pieces[goog.uri.utils.ComponentIndex.PATH],
      pieces[goog.uri.utils.ComponentIndex.QUERY_DATA],
      pieces[goog.uri.utils.ComponentIndex.FRAGMENT]);
};


/**
 * Gets the URI with the fragment identifier removed.
 * @param {string} uri The URI to examine.
 * @return {string} Everything preceding the hash mark.
 */
goog.uri.utils.removeFragment = function(uri) {
  // The hash mark may not appear in any other part of the URL.
  var hashIndex = uri.indexOf('#');
  return hashIndex < 0 ? uri : uri.substr(0, hashIndex);
};


/**
 * Ensures that two URI's have the exact same domain, scheme, and port.
 *
 * Unlike the version in goog.Uri, this checks protocol, and therefore is
 * suitable for checking against the browser's same-origin policy.
 *
 * @param {string} uri1 The first URI.
 * @param {string} uri2 The second URI.
 * @return {boolean} Whether they have the same scheme, domain and port.
 */
goog.uri.utils.haveSameDomain = function(uri1, uri2) {
  var pieces1 = goog.uri.utils.split(uri1);
  var pieces2 = goog.uri.utils.split(uri2);
  return pieces1[goog.uri.utils.ComponentIndex.DOMAIN] ==
             pieces2[goog.uri.utils.ComponentIndex.DOMAIN] &&
         pieces1[goog.uri.utils.ComponentIndex.SCHEME] ==
             pieces2[goog.uri.utils.ComponentIndex.SCHEME] &&
         pieces1[goog.uri.utils.ComponentIndex.PORT] ==
             pieces2[goog.uri.utils.ComponentIndex.PORT];
};


/**
 * Asserts that there are no fragment or query identifiers, only in uncompiled
 * mode.
 * @param {string} uri The URI to examine.
 * @private
 */
goog.uri.utils.assertNoFragmentsOrQueries_ = function(uri) {
  // NOTE: would use goog.asserts here, but jscompiler doesn't know that
  // indexOf has no side effects.
  if (goog.DEBUG && (uri.indexOf('#') >= 0 || uri.indexOf('?') >= 0)) {
    throw Error('goog.uri.utils: Fragment or query identifiers are not ' +
        'supported: [' + uri + ']');
  }
};


/**
 * Supported query parameter values by the parameter serializing utilities.
 *
 * If a value is null or undefined, the key-value pair is skipped, as an easy
 * way to omit parameters conditionally.  Non-array parameters are converted
 * to a string and URI encoded.  Array values are expanded into multiple
 * &key=value pairs, with each element stringized and URI-encoded.
 *
 * @typedef {*}
 */
goog.uri.utils.QueryValue;


/**
 * An array representing a set of query parameters with alternating keys
 * and values.
 *
 * Keys are assumed to be URI encoded already and live at even indices.  See
 * goog.uri.utils.QueryValue for details on how parameter values are encoded.
 *
 * Example:
 * <pre>
 * var data = [
 *   // Simple param: ?name=BobBarker
 *   'name', 'BobBarker',
 *   // Conditional param -- may be omitted entirely.
 *   'specialDietaryNeeds', hasDietaryNeeds() ? getDietaryNeeds() : null,
 *   // Multi-valued param: &house=LosAngeles&house=NewYork&house=null
 *   'house', ['LosAngeles', 'NewYork', null]
 * ];
 * </pre>
 *
 * @typedef {!Array<string|goog.uri.utils.QueryValue>}
 */
goog.uri.utils.QueryArray;


/**
 * Parses encoded query parameters and calls callback function for every
 * parameter found in the string.
 *
 * Missing value of parameter (e.g. “…&key&…”) is treated as if the value was an
 * empty string.  Keys may be empty strings (e.g. “…&=value&…”) which also means
 * that “…&=&…” and “…&&…” will result in an empty key and value.
 *
 * @param {string} encodedQuery Encoded query string excluding question mark at
 *     the beginning.
 * @param {function(string, string)} callback Function called for every
 *     parameter found in query string.  The first argument (name) will not be
 *     urldecoded (so the function is consistent with buildQueryData), but the
 *     second will.  If the parameter has no value (i.e. “=” was not present)
 *     the second argument (value) will be an empty string.
 */
goog.uri.utils.parseQueryData = function(encodedQuery, callback) {
  if (!encodedQuery) {
    return;
  }
  var pairs = encodedQuery.split('&');
  for (var i = 0; i < pairs.length; i++) {
    var indexOfEquals = pairs[i].indexOf('=');
    var name = null;
    var value = null;
    if (indexOfEquals >= 0) {
      name = pairs[i].substring(0, indexOfEquals);
      value = pairs[i].substring(indexOfEquals + 1);
    } else {
      name = pairs[i];
    }
    callback(name, value ? goog.string.urlDecode(value) : '');
  }
};


/**
 * Appends a URI and query data in a string buffer with special preconditions.
 *
 * Internal implementation utility, performing very few object allocations.
 *
 * @param {!Array<string|undefined>} buffer A string buffer.  The first element
 *     must be the base URI, and may have a fragment identifier.  If the array
 *     contains more than one element, the second element must be an ampersand,
 *     and may be overwritten, depending on the base URI.  Undefined elements
 *     are treated as empty-string.
 * @return {string} The concatenated URI and query data.
 * @private
 */
goog.uri.utils.appendQueryData_ = function(buffer) {
  if (buffer[1]) {
    // At least one query parameter was added.  We need to check the
    // punctuation mark, which is currently an ampersand, and also make sure
    // there aren't any interfering fragment identifiers.
    var baseUri = /** @type {string} */ (buffer[0]);
    var hashIndex = baseUri.indexOf('#');
    if (hashIndex >= 0) {
      // Move the fragment off the base part of the URI into the end.
      buffer.push(baseUri.substr(hashIndex));
      buffer[0] = baseUri = baseUri.substr(0, hashIndex);
    }
    var questionIndex = baseUri.indexOf('?');
    if (questionIndex < 0) {
      // No question mark, so we need a question mark instead of an ampersand.
      buffer[1] = '?';
    } else if (questionIndex == baseUri.length - 1) {
      // Question mark is the very last character of the existing URI, so don't
      // append an additional delimiter.
      buffer[1] = undefined;
    }
  }

  return buffer.join('');
};


/**
 * Appends key=value pairs to an array, supporting multi-valued objects.
 * @param {string} key The key prefix.
 * @param {goog.uri.utils.QueryValue} value The value to serialize.
 * @param {!Array<string>} pairs The array to which the 'key=value' strings
 *     should be appended.
 * @private
 */
goog.uri.utils.appendKeyValuePairs_ = function(key, value, pairs) {
  if (goog.isArray(value)) {
    // Convince the compiler it's an array.
    goog.asserts.assertArray(value);
    for (var j = 0; j < value.length; j++) {
      // Convert to string explicitly, to short circuit the null and array
      // logic in this function -- this ensures that null and undefined get
      // written as literal 'null' and 'undefined', and arrays don't get
      // expanded out but instead encoded in the default way.
      goog.uri.utils.appendKeyValuePairs_(key, String(value[j]), pairs);
    }
  } else if (value != null) {
    // Skip a top-level null or undefined entirely.
    pairs.push('&', key,
        // Check for empty string. Zero gets encoded into the url as literal
        // strings.  For empty string, skip the equal sign, to be consistent
        // with UriBuilder.java.
        value === '' ? '' : '=',
        goog.string.urlEncode(value));
  }
};


/**
 * Builds a buffer of query data from a sequence of alternating keys and values.
 *
 * @param {!Array<string|undefined>} buffer A string buffer to append to.  The
 *     first element appended will be an '&', and may be replaced by the caller.
 * @param {!goog.uri.utils.QueryArray|!Arguments} keysAndValues An array with
 *     alternating keys and values -- see the typedef.
 * @param {number=} opt_startIndex A start offset into the arary, defaults to 0.
 * @return {!Array<string|undefined>} The buffer argument.
 * @private
 */
goog.uri.utils.buildQueryDataBuffer_ = function(
    buffer, keysAndValues, opt_startIndex) {
  goog.asserts.assert(Math.max(keysAndValues.length - (opt_startIndex || 0),
      0) % 2 == 0, 'goog.uri.utils: Key/value lists must be even in length.');

  for (var i = opt_startIndex || 0; i < keysAndValues.length; i += 2) {
    goog.uri.utils.appendKeyValuePairs_(
        keysAndValues[i], keysAndValues[i + 1], buffer);
  }

  return buffer;
};


/**
 * Builds a query data string from a sequence of alternating keys and values.
 * Currently generates "&key&" for empty args.
 *
 * @param {goog.uri.utils.QueryArray} keysAndValues Alternating keys and
 *     values.  See the typedef.
 * @param {number=} opt_startIndex A start offset into the arary, defaults to 0.
 * @return {string} The encoded query string, in the form 'a=1&b=2'.
 */
goog.uri.utils.buildQueryData = function(keysAndValues, opt_startIndex) {
  var buffer = goog.uri.utils.buildQueryDataBuffer_(
      [], keysAndValues, opt_startIndex);
  buffer[0] = ''; // Remove the leading ampersand.
  return buffer.join('');
};


/**
 * Builds a buffer of query data from a map.
 *
 * @param {!Array<string|undefined>} buffer A string buffer to append to.  The
 *     first element appended will be an '&', and may be replaced by the caller.
 * @param {!Object<string, goog.uri.utils.QueryValue>} map An object where keys
 *     are URI-encoded parameter keys, and the values conform to the contract
 *     specified in the goog.uri.utils.QueryValue typedef.
 * @return {!Array<string|undefined>} The buffer argument.
 * @private
 */
goog.uri.utils.buildQueryDataBufferFromMap_ = function(buffer, map) {
  for (var key in map) {
    goog.uri.utils.appendKeyValuePairs_(key, map[key], buffer);
  }

  return buffer;
};


/**
 * Builds a query data string from a map.
 * Currently generates "&key&" for empty args.
 *
 * @param {!Object<string, goog.uri.utils.QueryValue>} map An object where keys
 *     are URI-encoded parameter keys, and the values are arbitrary types
 *     or arrays. Keys with a null value are dropped.
 * @return {string} The encoded query string, in the form 'a=1&b=2'.
 */
goog.uri.utils.buildQueryDataFromMap = function(map) {
  var buffer = goog.uri.utils.buildQueryDataBufferFromMap_([], map);
  buffer[0] = '';
  return buffer.join('');
};


/**
 * Appends URI parameters to an existing URI.
 *
 * The variable arguments may contain alternating keys and values.  Keys are
 * assumed to be already URI encoded.  The values should not be URI-encoded,
 * and will instead be encoded by this function.
 * <pre>
 * appendParams('http://www.foo.com?existing=true',
 *     'key1', 'value1',
 *     'key2', 'value?willBeEncoded',
 *     'key3', ['valueA', 'valueB', 'valueC'],
 *     'key4', null);
 * result: 'http://www.foo.com?existing=true&' +
 *     'key1=value1&' +
 *     'key2=value%3FwillBeEncoded&' +
 *     'key3=valueA&key3=valueB&key3=valueC'
 * </pre>
 *
 * A single call to this function will not exhibit quadratic behavior in IE,
 * whereas multiple repeated calls may, although the effect is limited by
 * fact that URL's generally can't exceed 2kb.
 *
 * @param {string} uri The original URI, which may already have query data.
 * @param {...(goog.uri.utils.QueryArray|string|goog.uri.utils.QueryValue)} var_args
 *     An array or argument list conforming to goog.uri.utils.QueryArray.
 * @return {string} The URI with all query parameters added.
 */
goog.uri.utils.appendParams = function(uri, var_args) {
  return goog.uri.utils.appendQueryData_(
      arguments.length == 2 ?
      goog.uri.utils.buildQueryDataBuffer_([uri], arguments[1], 0) :
      goog.uri.utils.buildQueryDataBuffer_([uri], arguments, 1));
};


/**
 * Appends query parameters from a map.
 *
 * @param {string} uri The original URI, which may already have query data.
 * @param {!Object<goog.uri.utils.QueryValue>} map An object where keys are
 *     URI-encoded parameter keys, and the values are arbitrary types or arrays.
 *     Keys with a null value are dropped.
 * @return {string} The new parameters.
 */
goog.uri.utils.appendParamsFromMap = function(uri, map) {
  return goog.uri.utils.appendQueryData_(
      goog.uri.utils.buildQueryDataBufferFromMap_([uri], map));
};


/**
 * Appends a single URI parameter.
 *
 * Repeated calls to this can exhibit quadratic behavior in IE6 due to the
 * way string append works, though it should be limited given the 2kb limit.
 *
 * @param {string} uri The original URI, which may already have query data.
 * @param {string} key The key, which must already be URI encoded.
 * @param {*=} opt_value The value, which will be stringized and encoded
 *     (assumed not already to be encoded).  If omitted, undefined, or null, the
 *     key will be added as a valueless parameter.
 * @return {string} The URI with the query parameter added.
 */
goog.uri.utils.appendParam = function(uri, key, opt_value) {
  var paramArr = [uri, '&', key];
  if (goog.isDefAndNotNull(opt_value)) {
    paramArr.push('=', goog.string.urlEncode(opt_value));
  }
  return goog.uri.utils.appendQueryData_(paramArr);
};


/**
 * Finds the next instance of a query parameter with the specified name.
 *
 * Does not instantiate any objects.
 *
 * @param {string} uri The URI to search.  May contain a fragment identifier
 *     if opt_hashIndex is specified.
 * @param {number} startIndex The index to begin searching for the key at.  A
 *     match may be found even if this is one character after the ampersand.
 * @param {string} keyEncoded The URI-encoded key.
 * @param {number} hashOrEndIndex Index to stop looking at.  If a hash
 *     mark is present, it should be its index, otherwise it should be the
 *     length of the string.
 * @return {number} The position of the first character in the key's name,
 *     immediately after either a question mark or a dot.
 * @private
 */
goog.uri.utils.findParam_ = function(
    uri, startIndex, keyEncoded, hashOrEndIndex) {
  var index = startIndex;
  var keyLength = keyEncoded.length;

  // Search for the key itself and post-filter for surronuding punctuation,
  // rather than expensively building a regexp.
  while ((index = uri.indexOf(keyEncoded, index)) >= 0 &&
      index < hashOrEndIndex) {
    var precedingChar = uri.charCodeAt(index - 1);
    // Ensure that the preceding character is '&' or '?'.
    if (precedingChar == goog.uri.utils.CharCode_.AMPERSAND ||
        precedingChar == goog.uri.utils.CharCode_.QUESTION) {
      // Ensure the following character is '&', '=', '#', or NaN
      // (end of string).
      var followingChar = uri.charCodeAt(index + keyLength);
      if (!followingChar ||
          followingChar == goog.uri.utils.CharCode_.EQUAL ||
          followingChar == goog.uri.utils.CharCode_.AMPERSAND ||
          followingChar == goog.uri.utils.CharCode_.HASH) {
        return index;
      }
    }
    index += keyLength + 1;
  }

  return -1;
};


/**
 * Regular expression for finding a hash mark or end of string.
 * @type {RegExp}
 * @private
 */
goog.uri.utils.hashOrEndRe_ = /#|$/;


/**
 * Determines if the URI contains a specific key.
 *
 * Performs no object instantiations.
 *
 * @param {string} uri The URI to process.  May contain a fragment
 *     identifier.
 * @param {string} keyEncoded The URI-encoded key.  Case-sensitive.
 * @return {boolean} Whether the key is present.
 */
goog.uri.utils.hasParam = function(uri, keyEncoded) {
  return goog.uri.utils.findParam_(uri, 0, keyEncoded,
      uri.search(goog.uri.utils.hashOrEndRe_)) >= 0;
};


/**
 * Gets the first value of a query parameter.
 * @param {string} uri The URI to process.  May contain a fragment.
 * @param {string} keyEncoded The URI-encoded key.  Case-sensitive.
 * @return {?string} The first value of the parameter (URI-decoded), or null
 *     if the parameter is not found.
 */
goog.uri.utils.getParamValue = function(uri, keyEncoded) {
  var hashOrEndIndex = uri.search(goog.uri.utils.hashOrEndRe_);
  var foundIndex = goog.uri.utils.findParam_(
      uri, 0, keyEncoded, hashOrEndIndex);

  if (foundIndex < 0) {
    return null;
  } else {
    var endPosition = uri.indexOf('&', foundIndex);
    if (endPosition < 0 || endPosition > hashOrEndIndex) {
      endPosition = hashOrEndIndex;
    }
    // Progress forth to the end of the "key=" or "key&" substring.
    foundIndex += keyEncoded.length + 1;
    // Use substr, because it (unlike substring) will return empty string
    // if foundIndex > endPosition.
    return goog.string.urlDecode(
        uri.substr(foundIndex, endPosition - foundIndex));
  }
};


/**
 * Gets all values of a query parameter.
 * @param {string} uri The URI to process.  May contain a fragment.
 * @param {string} keyEncoded The URI-encoded key.  Case-sensitive.
 * @return {!Array<string>} All URI-decoded values with the given key.
 *     If the key is not found, this will have length 0, but never be null.
 */
goog.uri.utils.getParamValues = function(uri, keyEncoded) {
  var hashOrEndIndex = uri.search(goog.uri.utils.hashOrEndRe_);
  var position = 0;
  var foundIndex;
  var result = [];

  while ((foundIndex = goog.uri.utils.findParam_(
      uri, position, keyEncoded, hashOrEndIndex)) >= 0) {
    // Find where this parameter ends, either the '&' or the end of the
    // query parameters.
    position = uri.indexOf('&', foundIndex);
    if (position < 0 || position > hashOrEndIndex) {
      position = hashOrEndIndex;
    }

    // Progress forth to the end of the "key=" or "key&" substring.
    foundIndex += keyEncoded.length + 1;
    // Use substr, because it (unlike substring) will return empty string
    // if foundIndex > position.
    result.push(goog.string.urlDecode(uri.substr(
        foundIndex, position - foundIndex)));
  }

  return result;
};


/**
 * Regexp to find trailing question marks and ampersands.
 * @type {RegExp}
 * @private
 */
goog.uri.utils.trailingQueryPunctuationRe_ = /[?&]($|#)/;


/**
 * Removes all instances of a query parameter.
 * @param {string} uri The URI to process.  Must not contain a fragment.
 * @param {string} keyEncoded The URI-encoded key.
 * @return {string} The URI with all instances of the parameter removed.
 */
goog.uri.utils.removeParam = function(uri, keyEncoded) {
  var hashOrEndIndex = uri.search(goog.uri.utils.hashOrEndRe_);
  var position = 0;
  var foundIndex;
  var buffer = [];

  // Look for a query parameter.
  while ((foundIndex = goog.uri.utils.findParam_(
      uri, position, keyEncoded, hashOrEndIndex)) >= 0) {
    // Get the portion of the query string up to, but not including, the ?
    // or & starting the parameter.
    buffer.push(uri.substring(position, foundIndex));
    // Progress to immediately after the '&'.  If not found, go to the end.
    // Avoid including the hash mark.
    position = Math.min((uri.indexOf('&', foundIndex) + 1) || hashOrEndIndex,
        hashOrEndIndex);
  }

  // Append everything that is remaining.
  buffer.push(uri.substr(position));

  // Join the buffer, and remove trailing punctuation that remains.
  return buffer.join('').replace(
      goog.uri.utils.trailingQueryPunctuationRe_, '$1');
};


/**
 * Replaces all existing definitions of a parameter with a single definition.
 *
 * Repeated calls to this can exhibit quadratic behavior due to the need to
 * find existing instances and reconstruct the string, though it should be
 * limited given the 2kb limit.  Consider using appendParams to append multiple
 * parameters in bulk.
 *
 * @param {string} uri The original URI, which may already have query data.
 * @param {string} keyEncoded The key, which must already be URI encoded.
 * @param {*} value The value, which will be stringized and encoded (assumed
 *     not already to be encoded).
 * @return {string} The URI with the query parameter added.
 */
goog.uri.utils.setParam = function(uri, keyEncoded, value) {
  return goog.uri.utils.appendParam(
      goog.uri.utils.removeParam(uri, keyEncoded), keyEncoded, value);
};


/**
 * Generates a URI path using a given URI and a path with checks to
 * prevent consecutive "//". The baseUri passed in must not contain
 * query or fragment identifiers. The path to append may not contain query or
 * fragment identifiers.
 *
 * @param {string} baseUri URI to use as the base.
 * @param {string} path Path to append.
 * @return {string} Updated URI.
 */
goog.uri.utils.appendPath = function(baseUri, path) {
  goog.uri.utils.assertNoFragmentsOrQueries_(baseUri);

  // Remove any trailing '/'
  if (goog.string.endsWith(baseUri, '/')) {
    baseUri = baseUri.substr(0, baseUri.length - 1);
  }
  // Remove any leading '/'
  if (goog.string.startsWith(path, '/')) {
    path = path.substr(1);
  }
  return goog.string.buildString(baseUri, '/', path);
};


/**
 * Replaces the path.
 * @param {string} uri URI to use as the base.
 * @param {string} path New path.
 * @return {string} Updated URI.
 */
goog.uri.utils.setPath = function(uri, path) {
  // Add any missing '/'.
  if (!goog.string.startsWith(path, '/')) {
    path = '/' + path;
  }
  var parts = goog.uri.utils.split(uri);
  return goog.uri.utils.buildFromEncodedParts(
      parts[goog.uri.utils.ComponentIndex.SCHEME],
      parts[goog.uri.utils.ComponentIndex.USER_INFO],
      parts[goog.uri.utils.ComponentIndex.DOMAIN],
      parts[goog.uri.utils.ComponentIndex.PORT],
      path,
      parts[goog.uri.utils.ComponentIndex.QUERY_DATA],
      parts[goog.uri.utils.ComponentIndex.FRAGMENT]);
};


/**
 * Standard supported query parameters.
 * @enum {string}
 */
goog.uri.utils.StandardQueryParam = {

  /** Unused parameter for unique-ifying. */
  RANDOM: 'zx'
};


/**
 * Sets the zx parameter of a URI to a random value.
 * @param {string} uri Any URI.
 * @return {string} That URI with the "zx" parameter added or replaced to
 *     contain a random string.
 */
goog.uri.utils.makeUnique = function(uri) {
  return goog.uri.utils.setParam(uri,
      goog.uri.utils.StandardQueryParam.RANDOM, goog.string.getRandomString());
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Wrapper class for handling XmlHttpRequests.
 *
 * One off requests can be sent through goog.net.XhrIo.send() or an
 * instance can be created to send multiple requests.  Each request uses its
 * own XmlHttpRequest object and handles clearing of the event callback to
 * ensure no leaks.
 *
 * XhrIo is event based, it dispatches events on success, failure, finishing,
 * ready-state change, or progress.
 *
 * The ready-state or timeout event fires first, followed by
 * a generic completed event. Then the abort, error, or success event
 * is fired as appropriate. Progress events are fired as they are
 * received. Lastly, the ready event will fire to indicate that the
 * object may be used to make another request.
 *
 * The error event may also be called before completed and
 * ready-state-change if the XmlHttpRequest.open() or .send() methods throw.
 *
 * This class does not support multiple requests, queuing, or prioritization.
 *
 * When progress events are supported by the browser, and progress is
 * enabled via .setProgressEventsEnabled(true), the
 * goog.net.EventType.PROGRESS event will be the re-dispatched browser
 * progress event.
 *
 * Tested = IE6, FF1.5, Safari, Opera 8.5
 *
 * TODO(user): Error cases aren't playing nicely in Safari.
 *
 */


goog.provide('goog.net.XhrIo');
goog.provide('goog.net.XhrIo.ResponseType');

goog.require('goog.Timer');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.debug.entryPointRegistry');
goog.require('goog.events.EventTarget');
goog.require('goog.json');
goog.require('goog.log');
goog.require('goog.net.ErrorCode');
goog.require('goog.net.EventType');
goog.require('goog.net.HttpStatus');
goog.require('goog.net.XmlHttp');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.structs');
goog.require('goog.structs.Map');
goog.require('goog.uri.utils');
goog.require('goog.userAgent');

goog.forwardDeclare('goog.Uri');



/**
 * Basic class for handling XMLHttpRequests.
 * @param {goog.net.XmlHttpFactory=} opt_xmlHttpFactory Factory to use when
 *     creating XMLHttpRequest objects.
 * @constructor
 * @extends {goog.events.EventTarget}
 */
goog.net.XhrIo = function(opt_xmlHttpFactory) {
  goog.net.XhrIo.base(this, 'constructor');

  /**
   * Map of default headers to add to every request, use:
   * XhrIo.headers.set(name, value)
   * @type {!goog.structs.Map}
   */
  this.headers = new goog.structs.Map();

  /**
   * Optional XmlHttpFactory
   * @private {goog.net.XmlHttpFactory}
   */
  this.xmlHttpFactory_ = opt_xmlHttpFactory || null;

  /**
   * Whether XMLHttpRequest is active.  A request is active from the time send()
   * is called until onReadyStateChange() is complete, or error() or abort()
   * is called.
   * @private {boolean}
   */
  this.active_ = false;

  /**
   * The XMLHttpRequest object that is being used for the transfer.
   * @private {?goog.net.XhrLike.OrNative}
   */
  this.xhr_ = null;

  /**
   * The options to use with the current XMLHttpRequest object.
   * @private {Object}
   */
  this.xhrOptions_ = null;

  /**
   * Last URL that was requested.
   * @private {string|goog.Uri}
   */
  this.lastUri_ = '';

  /**
   * Method for the last request.
   * @private {string}
   */
  this.lastMethod_ = '';

  /**
   * Last error code.
   * @private {!goog.net.ErrorCode}
   */
  this.lastErrorCode_ = goog.net.ErrorCode.NO_ERROR;

  /**
   * Last error message.
   * @private {Error|string}
   */
  this.lastError_ = '';

  /**
   * Used to ensure that we don't dispatch an multiple ERROR events. This can
   * happen in IE when it does a synchronous load and one error is handled in
   * the ready statte change and one is handled due to send() throwing an
   * exception.
   * @private {boolean}
   */
  this.errorDispatched_ = false;

  /**
   * Used to make sure we don't fire the complete event from inside a send call.
   * @private {boolean}
   */
  this.inSend_ = false;

  /**
   * Used in determining if a call to {@link #onReadyStateChange_} is from
   * within a call to this.xhr_.open.
   * @private {boolean}
   */
  this.inOpen_ = false;

  /**
   * Used in determining if a call to {@link #onReadyStateChange_} is from
   * within a call to this.xhr_.abort.
   * @private {boolean}
   */
  this.inAbort_ = false;

  /**
   * Number of milliseconds after which an incomplete request will be aborted
   * and a {@link goog.net.EventType.TIMEOUT} event raised; 0 means no timeout
   * is set.
   * @private {number}
   */
  this.timeoutInterval_ = 0;

  /**
   * Timer to track request timeout.
   * @private {?number}
   */
  this.timeoutId_ = null;

  /**
   * The requested type for the response. The empty string means use the default
   * XHR behavior.
   * @private {goog.net.XhrIo.ResponseType}
   */
  this.responseType_ = goog.net.XhrIo.ResponseType.DEFAULT;

  /**
   * Whether a "credentialed" request is to be sent (one that is aware of
   * cookies and authentication). This is applicable only for cross-domain
   * requests and more recent browsers that support this part of the HTTP Access
   * Control standard.
   *
   * @see http://www.w3.org/TR/XMLHttpRequest/#the-withcredentials-attribute
   *
   * @private {boolean}
   */
  this.withCredentials_ = false;

  /**
   * Whether progress events are enabled for this request. This is
   * disabled by default because setting a progress event handler
   * causes pre-flight OPTIONS requests to be sent for CORS requests,
   * even in cases where a pre-flight request would not otherwise be
   * sent.
   *
   * @see http://xhr.spec.whatwg.org/#security-considerations
   *
   * Note that this can cause problems for Firefox 22 and below, as an
   * older "LSProgressEvent" will be dispatched by the browser. That
   * progress event is no longer supported, and can lead to failures,
   * including throwing exceptions.
   *
   * @see http://bugzilla.mozilla.org/show_bug.cgi?id=845631
   * @see b/23469793
   *
   * @private {boolean}
   */
  this.progressEventsEnabled_ = false;

  /**
   * True if we can use XMLHttpRequest's timeout directly.
   * @private {boolean}
   */
  this.useXhr2Timeout_ = false;
};
goog.inherits(goog.net.XhrIo, goog.events.EventTarget);


/**
 * Response types that may be requested for XMLHttpRequests.
 * @enum {string}
 * @see http://www.w3.org/TR/XMLHttpRequest/#the-responsetype-attribute
 */
goog.net.XhrIo.ResponseType = {
  DEFAULT: '',
  TEXT: 'text',
  DOCUMENT: 'document',
  // Not supported as of Chrome 10.0.612.1 dev
  BLOB: 'blob',
  ARRAY_BUFFER: 'arraybuffer'
};


/**
 * A reference to the XhrIo logger
 * @private {goog.debug.Logger}
 * @const
 */
goog.net.XhrIo.prototype.logger_ =
    goog.log.getLogger('goog.net.XhrIo');


/**
 * The Content-Type HTTP header name
 * @type {string}
 */
goog.net.XhrIo.CONTENT_TYPE_HEADER = 'Content-Type';


/**
 * The pattern matching the 'http' and 'https' URI schemes
 * @type {!RegExp}
 */
goog.net.XhrIo.HTTP_SCHEME_PATTERN = /^https?$/i;


/**
 * The methods that typically come along with form data.  We set different
 * headers depending on whether the HTTP action is one of these.
 */
goog.net.XhrIo.METHODS_WITH_FORM_DATA = ['POST', 'PUT'];


/**
 * The Content-Type HTTP header value for a url-encoded form
 * @type {string}
 */
goog.net.XhrIo.FORM_CONTENT_TYPE =
    'application/x-www-form-urlencoded;charset=utf-8';


/**
 * The XMLHttpRequest Level two timeout delay ms property name.
 *
 * @see http://www.w3.org/TR/XMLHttpRequest/#the-timeout-attribute
 *
 * @private {string}
 * @const
 */
goog.net.XhrIo.XHR2_TIMEOUT_ = 'timeout';


/**
 * The XMLHttpRequest Level two ontimeout handler property name.
 *
 * @see http://www.w3.org/TR/XMLHttpRequest/#the-timeout-attribute
 *
 * @private {string}
 * @const
 */
goog.net.XhrIo.XHR2_ON_TIMEOUT_ = 'ontimeout';


/**
 * All non-disposed instances of goog.net.XhrIo created
 * by {@link goog.net.XhrIo.send} are in this Array.
 * @see goog.net.XhrIo.cleanup
 * @private {!Array<!goog.net.XhrIo>}
 */
goog.net.XhrIo.sendInstances_ = [];


/**
 * Static send that creates a short lived instance of XhrIo to send the
 * request.
 * @see goog.net.XhrIo.cleanup
 * @param {string|goog.Uri} url Uri to make request to.
 * @param {?function(this:goog.net.XhrIo, ?)=} opt_callback Callback function
 *     for when request is complete.
 * @param {string=} opt_method Send method, default: GET.
 * @param {ArrayBuffer|ArrayBufferView|Blob|Document|FormData|string=}
 *     opt_content Body data.
 * @param {Object|goog.structs.Map=} opt_headers Map of headers to add to the
 *     request.
 * @param {number=} opt_timeoutInterval Number of milliseconds after which an
 *     incomplete request will be aborted; 0 means no timeout is set.
 * @param {boolean=} opt_withCredentials Whether to send credentials with the
 *     request. Default to false. See {@link goog.net.XhrIo#setWithCredentials}.
 * @return {!goog.net.XhrIo} The sent XhrIo.
 */
goog.net.XhrIo.send = function(url, opt_callback, opt_method, opt_content,
                               opt_headers, opt_timeoutInterval,
                               opt_withCredentials) {
  var x = new goog.net.XhrIo();
  goog.net.XhrIo.sendInstances_.push(x);
  if (opt_callback) {
    x.listen(goog.net.EventType.COMPLETE, opt_callback);
  }
  x.listenOnce(goog.net.EventType.READY, x.cleanupSend_);
  if (opt_timeoutInterval) {
    x.setTimeoutInterval(opt_timeoutInterval);
  }
  if (opt_withCredentials) {
    x.setWithCredentials(opt_withCredentials);
  }
  x.send(url, opt_method, opt_content, opt_headers);
  return x;
};


/**
 * Disposes all non-disposed instances of goog.net.XhrIo created by
 * {@link goog.net.XhrIo.send}.
 * {@link goog.net.XhrIo.send} cleans up the goog.net.XhrIo instance
 * it creates when the request completes or fails.  However, if
 * the request never completes, then the goog.net.XhrIo is not disposed.
 * This can occur if the window is unloaded before the request completes.
 * We could have {@link goog.net.XhrIo.send} return the goog.net.XhrIo
 * it creates and make the client of {@link goog.net.XhrIo.send} be
 * responsible for disposing it in this case.  However, this makes things
 * significantly more complicated for the client, and the whole point
 * of {@link goog.net.XhrIo.send} is that it's simple and easy to use.
 * Clients of {@link goog.net.XhrIo.send} should call
 * {@link goog.net.XhrIo.cleanup} when doing final
 * cleanup on window unload.
 */
goog.net.XhrIo.cleanup = function() {
  var instances = goog.net.XhrIo.sendInstances_;
  while (instances.length) {
    instances.pop().dispose();
  }
};


/**
 * Installs exception protection for all entry point introduced by
 * goog.net.XhrIo instances which are not protected by
 * {@link goog.debug.ErrorHandler#protectWindowSetTimeout},
 * {@link goog.debug.ErrorHandler#protectWindowSetInterval}, or
 * {@link goog.events.protectBrowserEventEntryPoint}.
 *
 * @param {goog.debug.ErrorHandler} errorHandler Error handler with which to
 *     protect the entry point(s).
 */
goog.net.XhrIo.protectEntryPoints = function(errorHandler) {
  goog.net.XhrIo.prototype.onReadyStateChangeEntryPoint_ =
      errorHandler.protectEntryPoint(
          goog.net.XhrIo.prototype.onReadyStateChangeEntryPoint_);
};


/**
 * Disposes of the specified goog.net.XhrIo created by
 * {@link goog.net.XhrIo.send} and removes it from
 * {@link goog.net.XhrIo.pendingStaticSendInstances_}.
 * @private
 */
goog.net.XhrIo.prototype.cleanupSend_ = function() {
  this.dispose();
  goog.array.remove(goog.net.XhrIo.sendInstances_, this);
};


/**
 * Returns the number of milliseconds after which an incomplete request will be
 * aborted, or 0 if no timeout is set.
 * @return {number} Timeout interval in milliseconds.
 */
goog.net.XhrIo.prototype.getTimeoutInterval = function() {
  return this.timeoutInterval_;
};


/**
 * Sets the number of milliseconds after which an incomplete request will be
 * aborted and a {@link goog.net.EventType.TIMEOUT} event raised; 0 means no
 * timeout is set.
 * @param {number} ms Timeout interval in milliseconds; 0 means none.
 */
goog.net.XhrIo.prototype.setTimeoutInterval = function(ms) {
  this.timeoutInterval_ = Math.max(0, ms);
};


/**
 * Sets the desired type for the response. At time of writing, this is only
 * supported in very recent versions of WebKit (10.0.612.1 dev and later).
 *
 * If this is used, the response may only be accessed via {@link #getResponse}.
 *
 * @param {goog.net.XhrIo.ResponseType} type The desired type for the response.
 */
goog.net.XhrIo.prototype.setResponseType = function(type) {
  this.responseType_ = type;
};


/**
 * Gets the desired type for the response.
 * @return {goog.net.XhrIo.ResponseType} The desired type for the response.
 */
goog.net.XhrIo.prototype.getResponseType = function() {
  return this.responseType_;
};


/**
 * Sets whether a "credentialed" request that is aware of cookie and
 * authentication information should be made. This option is only supported by
 * browsers that support HTTP Access Control. As of this writing, this option
 * is not supported in IE.
 *
 * @param {boolean} withCredentials Whether this should be a "credentialed"
 *     request.
 */
goog.net.XhrIo.prototype.setWithCredentials = function(withCredentials) {
  this.withCredentials_ = withCredentials;
};


/**
 * Gets whether a "credentialed" request is to be sent.
 * @return {boolean} The desired type for the response.
 */
goog.net.XhrIo.prototype.getWithCredentials = function() {
  return this.withCredentials_;
};


/**
 * Sets whether progress events are enabled for this request. Note
 * that progress events require pre-flight OPTIONS request handling
 * for CORS requests, and may cause trouble with older browsers. See
 * progressEventsEnabled_ for details.
 * @param {boolean} enabled Whether progress events should be enabled.
 */
goog.net.XhrIo.prototype.setProgressEventsEnabled = function(enabled) {
  this.progressEventsEnabled_ = enabled;
};


/**
 * Gets whether progress events are enabled.
 * @return {boolean} Whether progress events are enabled for this request.
 */
goog.net.XhrIo.prototype.getProgressEventsEnabled = function() {
  return this.progressEventsEnabled_;
};


/**
 * Instance send that actually uses XMLHttpRequest to make a server call.
 * @param {string|goog.Uri} url Uri to make request to.
 * @param {string=} opt_method Send method, default: GET.
 * @param {ArrayBuffer|ArrayBufferView|Blob|Document|FormData|string=}
 *     opt_content Body data.
 * @param {Object|goog.structs.Map=} opt_headers Map of headers to add to the
 *     request.
 */
goog.net.XhrIo.prototype.send = function(url, opt_method, opt_content,
                                         opt_headers) {
  if (this.xhr_) {
    throw Error('[goog.net.XhrIo] Object is active with another request=' +
        this.lastUri_ + '; newUri=' + url);
  }

  var method = opt_method ? opt_method.toUpperCase() : 'GET';

  this.lastUri_ = url;
  this.lastError_ = '';
  this.lastErrorCode_ = goog.net.ErrorCode.NO_ERROR;
  this.lastMethod_ = method;
  this.errorDispatched_ = false;
  this.active_ = true;

  // Use the factory to create the XHR object and options
  this.xhr_ = this.createXhr();
  this.xhrOptions_ = this.xmlHttpFactory_ ?
      this.xmlHttpFactory_.getOptions() : goog.net.XmlHttp.getOptions();

  // Set up the onreadystatechange callback
  this.xhr_.onreadystatechange = goog.bind(this.onReadyStateChange_, this);

  // Set up upload/download progress events, if progress events are supported.
  if (this.getProgressEventsEnabled() && 'onprogress' in this.xhr_) {
    this.xhr_.onprogress = goog.bind(this.onProgressHandler_, this);
    if (this.xhr_.upload) {
      this.xhr_.upload.onprogress = goog.bind(this.onProgressHandler_, this);
    }
  }

  /**
   * Try to open the XMLHttpRequest (always async), if an error occurs here it
   * is generally permission denied
   * @preserveTry
   */
  try {
    goog.log.fine(this.logger_, this.formatMsg_('Opening Xhr'));
    this.inOpen_ = true;
    this.xhr_.open(method, String(url), true);  // Always async!
    this.inOpen_ = false;
  } catch (err) {
    goog.log.fine(this.logger_,
        this.formatMsg_('Error opening Xhr: ' + err.message));
    this.error_(goog.net.ErrorCode.EXCEPTION, err);
    return;
  }

  // We can't use null since this won't allow requests with form data to have a
  // content length specified which will cause some proxies to return a 411
  // error.
  var content = opt_content || '';

  var headers = this.headers.clone();

  // Add headers specific to this request
  if (opt_headers) {
    goog.structs.forEach(opt_headers, function(value, key) {
      headers.set(key, value);
    });
  }

  // Find whether a content type header is set, ignoring case.
  // HTTP header names are case-insensitive.  See:
  // http://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
  var contentTypeKey = goog.array.find(headers.getKeys(),
      goog.net.XhrIo.isContentTypeHeader_);

  var contentIsFormData = (goog.global['FormData'] &&
      (content instanceof goog.global['FormData']));
  if (goog.array.contains(goog.net.XhrIo.METHODS_WITH_FORM_DATA, method) &&
      !contentTypeKey && !contentIsFormData) {
    // For requests typically with form data, default to the url-encoded form
    // content type unless this is a FormData request.  For FormData,
    // the browser will automatically add a multipart/form-data content type
    // with an appropriate multipart boundary.
    headers.set(goog.net.XhrIo.CONTENT_TYPE_HEADER,
                goog.net.XhrIo.FORM_CONTENT_TYPE);
  }

  // Add the headers to the Xhr object
  headers.forEach(function(value, key) {
    this.xhr_.setRequestHeader(key, value);
  }, this);

  if (this.responseType_) {
    this.xhr_.responseType = this.responseType_;
  }

  if (goog.object.containsKey(this.xhr_, 'withCredentials')) {
    this.xhr_.withCredentials = this.withCredentials_;
  }

  /**
   * Try to send the request, or other wise report an error (404 not found).
   * @preserveTry
   */
  try {
    this.cleanUpTimeoutTimer_(); // Paranoid, should never be running.
    if (this.timeoutInterval_ > 0) {
      this.useXhr2Timeout_ = goog.net.XhrIo.shouldUseXhr2Timeout_(this.xhr_);
      goog.log.fine(this.logger_, this.formatMsg_('Will abort after ' +
          this.timeoutInterval_ + 'ms if incomplete, xhr2 ' +
          this.useXhr2Timeout_));
      if (this.useXhr2Timeout_) {
        this.xhr_[goog.net.XhrIo.XHR2_TIMEOUT_] = this.timeoutInterval_;
        this.xhr_[goog.net.XhrIo.XHR2_ON_TIMEOUT_] =
            goog.bind(this.timeout_, this);
      } else {
        this.timeoutId_ = goog.Timer.callOnce(this.timeout_,
            this.timeoutInterval_, this);
      }
    }
    goog.log.fine(this.logger_, this.formatMsg_('Sending request'));
    this.inSend_ = true;
    this.xhr_.send(content);
    this.inSend_ = false;

  } catch (err) {
    goog.log.fine(this.logger_, this.formatMsg_('Send error: ' + err.message));
    this.error_(goog.net.ErrorCode.EXCEPTION, err);
  }
};


/**
 * Determines if the argument is an XMLHttpRequest that supports the level 2
 * timeout value and event.
 *
 * Currently, FF 21.0 OS X has the fields but won't actually call the timeout
 * handler.  Perhaps the confusion in the bug referenced below hasn't
 * entirely been resolved.
 *
 * @see http://www.w3.org/TR/XMLHttpRequest/#the-timeout-attribute
 * @see https://bugzilla.mozilla.org/show_bug.cgi?id=525816
 *
 * @param {!goog.net.XhrLike.OrNative} xhr The request.
 * @return {boolean} True if the request supports level 2 timeout.
 * @private
 */
goog.net.XhrIo.shouldUseXhr2Timeout_ = function(xhr) {
  return goog.userAgent.IE &&
      goog.userAgent.isVersionOrHigher(9) &&
      goog.isNumber(xhr[goog.net.XhrIo.XHR2_TIMEOUT_]) &&
      goog.isDef(xhr[goog.net.XhrIo.XHR2_ON_TIMEOUT_]);
};


/**
 * @param {string} header An HTTP header key.
 * @return {boolean} Whether the key is a content type header (ignoring
 *     case.
 * @private
 */
goog.net.XhrIo.isContentTypeHeader_ = function(header) {
  return goog.string.caseInsensitiveEquals(
      goog.net.XhrIo.CONTENT_TYPE_HEADER, header);
};


/**
 * Creates a new XHR object.
 * @return {!goog.net.XhrLike.OrNative} The newly created XHR object.
 * @protected
 */
goog.net.XhrIo.prototype.createXhr = function() {
  return this.xmlHttpFactory_ ?
      this.xmlHttpFactory_.createInstance() : goog.net.XmlHttp();
};


/**
 * The request didn't complete after {@link goog.net.XhrIo#timeoutInterval_}
 * milliseconds; raises a {@link goog.net.EventType.TIMEOUT} event and aborts
 * the request.
 * @private
 */
goog.net.XhrIo.prototype.timeout_ = function() {
  if (typeof goog == 'undefined') {
    // If goog is undefined then the callback has occurred as the application
    // is unloading and will error.  Thus we let it silently fail.
  } else if (this.xhr_) {
    this.lastError_ = 'Timed out after ' + this.timeoutInterval_ +
                      'ms, aborting';
    this.lastErrorCode_ = goog.net.ErrorCode.TIMEOUT;
    goog.log.fine(this.logger_, this.formatMsg_(this.lastError_));
    this.dispatchEvent(goog.net.EventType.TIMEOUT);
    this.abort(goog.net.ErrorCode.TIMEOUT);
  }
};


/**
 * Something errorred, so inactivate, fire error callback and clean up
 * @param {goog.net.ErrorCode} errorCode The error code.
 * @param {Error} err The error object.
 * @private
 */
goog.net.XhrIo.prototype.error_ = function(errorCode, err) {
  this.active_ = false;
  if (this.xhr_) {
    this.inAbort_ = true;
    this.xhr_.abort();  // Ensures XHR isn't hung (FF)
    this.inAbort_ = false;
  }
  this.lastError_ = err;
  this.lastErrorCode_ = errorCode;
  this.dispatchErrors_();
  this.cleanUpXhr_();
};


/**
 * Dispatches COMPLETE and ERROR in case of an error. This ensures that we do
 * not dispatch multiple error events.
 * @private
 */
goog.net.XhrIo.prototype.dispatchErrors_ = function() {
  if (!this.errorDispatched_) {
    this.errorDispatched_ = true;
    this.dispatchEvent(goog.net.EventType.COMPLETE);
    this.dispatchEvent(goog.net.EventType.ERROR);
  }
};


/**
 * Abort the current XMLHttpRequest
 * @param {goog.net.ErrorCode=} opt_failureCode Optional error code to use -
 *     defaults to ABORT.
 */
goog.net.XhrIo.prototype.abort = function(opt_failureCode) {
  if (this.xhr_ && this.active_) {
    goog.log.fine(this.logger_, this.formatMsg_('Aborting'));
    this.active_ = false;
    this.inAbort_ = true;
    this.xhr_.abort();
    this.inAbort_ = false;
    this.lastErrorCode_ = opt_failureCode || goog.net.ErrorCode.ABORT;
    this.dispatchEvent(goog.net.EventType.COMPLETE);
    this.dispatchEvent(goog.net.EventType.ABORT);
    this.cleanUpXhr_();
  }
};


/**
 * Nullifies all callbacks to reduce risks of leaks.
 * @override
 * @protected
 */
goog.net.XhrIo.prototype.disposeInternal = function() {
  if (this.xhr_) {
    // We explicitly do not call xhr_.abort() unless active_ is still true.
    // This is to avoid unnecessarily aborting a successful request when
    // dispose() is called in a callback triggered by a complete response, but
    // in which browser cleanup has not yet finished.
    // (See http://b/issue?id=1684217.)
    if (this.active_) {
      this.active_ = false;
      this.inAbort_ = true;
      this.xhr_.abort();
      this.inAbort_ = false;
    }
    this.cleanUpXhr_(true);
  }

  goog.net.XhrIo.base(this, 'disposeInternal');
};


/**
 * Internal handler for the XHR object's readystatechange event.  This method
 * checks the status and the readystate and fires the correct callbacks.
 * If the request has ended, the handlers are cleaned up and the XHR object is
 * nullified.
 * @private
 */
goog.net.XhrIo.prototype.onReadyStateChange_ = function() {
  if (this.isDisposed()) {
    // This method is the target of an untracked goog.Timer.callOnce().
    return;
  }
  if (!this.inOpen_ && !this.inSend_ && !this.inAbort_) {
    // Were not being called from within a call to this.xhr_.send
    // this.xhr_.abort, or this.xhr_.open, so this is an entry point
    this.onReadyStateChangeEntryPoint_();
  } else {
    this.onReadyStateChangeHelper_();
  }
};


/**
 * Used to protect the onreadystatechange handler entry point.  Necessary
 * as {#onReadyStateChange_} maybe called from within send or abort, this
 * method is only called when {#onReadyStateChange_} is called as an
 * entry point.
 * {@see #protectEntryPoints}
 * @private
 */
goog.net.XhrIo.prototype.onReadyStateChangeEntryPoint_ = function() {
  this.onReadyStateChangeHelper_();
};


/**
 * Helper for {@link #onReadyStateChange_}.  This is used so that
 * entry point calls to {@link #onReadyStateChange_} can be routed through
 * {@link #onReadyStateChangeEntryPoint_}.
 * @private
 */
goog.net.XhrIo.prototype.onReadyStateChangeHelper_ = function() {
  if (!this.active_) {
    // can get called inside abort call
    return;
  }

  if (typeof goog == 'undefined') {
    // NOTE(user): If goog is undefined then the callback has occurred as the
    // application is unloading and will error.  Thus we let it silently fail.

  } else if (
      this.xhrOptions_[goog.net.XmlHttp.OptionType.LOCAL_REQUEST_ERROR] &&
      this.getReadyState() == goog.net.XmlHttp.ReadyState.COMPLETE &&
      this.getStatus() == 2) {
    // NOTE(user): In IE if send() errors on a *local* request the readystate
    // is still changed to COMPLETE.  We need to ignore it and allow the
    // try/catch around send() to pick up the error.
    goog.log.fine(this.logger_, this.formatMsg_(
        'Local request error detected and ignored'));

  } else {

    // In IE when the response has been cached we sometimes get the callback
    // from inside the send call and this usually breaks code that assumes that
    // XhrIo is asynchronous.  If that is the case we delay the callback
    // using a timer.
    if (this.inSend_ &&
        this.getReadyState() == goog.net.XmlHttp.ReadyState.COMPLETE) {
      goog.Timer.callOnce(this.onReadyStateChange_, 0, this);
      return;
    }

    this.dispatchEvent(goog.net.EventType.READY_STATE_CHANGE);

    // readyState indicates the transfer has finished
    if (this.isComplete()) {
      goog.log.fine(this.logger_, this.formatMsg_('Request complete'));

      this.active_ = false;

      try {
        // Call the specific callbacks for success or failure. Only call the
        // success if the status is 200 (HTTP_OK) or 304 (HTTP_CACHED)
        if (this.isSuccess()) {
          this.dispatchEvent(goog.net.EventType.COMPLETE);
          this.dispatchEvent(goog.net.EventType.SUCCESS);
        } else {
          this.lastErrorCode_ = goog.net.ErrorCode.HTTP_ERROR;
          this.lastError_ =
              this.getStatusText() + ' [' + this.getStatus() + ']';
          this.dispatchErrors_();
        }
      } finally {
        this.cleanUpXhr_();
      }
    }
  }
};


/**
 * Internal handler for the XHR object's onprogress event.
 * @param {!ProgressEvent} e XHR progress event.
 * @private
 */
goog.net.XhrIo.prototype.onProgressHandler_ = function(e) {
  goog.asserts.assert(e.type === goog.net.EventType.PROGRESS,
      'goog.net.EventType.PROGRESS is of the same type as raw XHR progress.');
  // Redispatch the progress event.
  this.dispatchEvent(e);
};


/**
 * Remove the listener to protect against leaks, and nullify the XMLHttpRequest
 * object.
 * @param {boolean=} opt_fromDispose If this is from the dispose (don't want to
 *     fire any events).
 * @private
 */
goog.net.XhrIo.prototype.cleanUpXhr_ = function(opt_fromDispose) {
  if (this.xhr_) {
    // Cancel any pending timeout event handler.
    this.cleanUpTimeoutTimer_();

    // Save reference so we can mark it as closed after the READY event.  The
    // READY event may trigger another request, thus we must nullify this.xhr_
    var xhr = this.xhr_;
    var clearedOnReadyStateChange =
        this.xhrOptions_[goog.net.XmlHttp.OptionType.USE_NULL_FUNCTION] ?
            goog.nullFunction : null;
    this.xhr_ = null;
    this.xhrOptions_ = null;

    if (!opt_fromDispose) {
      this.dispatchEvent(goog.net.EventType.READY);
    }

    try {
      // NOTE(user): Not nullifying in FireFox can still leak if the callbacks
      // are defined in the same scope as the instance of XhrIo. But, IE doesn't
      // allow you to set the onreadystatechange to NULL so nullFunction is
      // used.
      xhr.onreadystatechange = clearedOnReadyStateChange;
    } catch (e) {
      // This seems to occur with a Gears HTTP request. Delayed the setting of
      // this onreadystatechange until after READY is sent out and catching the
      // error to see if we can track down the problem.
      goog.log.error(this.logger_,
          'Problem encountered resetting onreadystatechange: ' + e.message);
    }
  }
};


/**
 * Make sure the timeout timer isn't running.
 * @private
 */
goog.net.XhrIo.prototype.cleanUpTimeoutTimer_ = function() {
  if (this.xhr_ && this.useXhr2Timeout_) {
    this.xhr_[goog.net.XhrIo.XHR2_ON_TIMEOUT_] = null;
  }
  if (goog.isNumber(this.timeoutId_)) {
    goog.Timer.clear(this.timeoutId_);
    this.timeoutId_ = null;
  }
};


/**
 * @return {boolean} Whether there is an active request.
 */
goog.net.XhrIo.prototype.isActive = function() {
  return !!this.xhr_;
};


/**
 * @return {boolean} Whether the request has completed.
 */
goog.net.XhrIo.prototype.isComplete = function() {
  return this.getReadyState() == goog.net.XmlHttp.ReadyState.COMPLETE;
};


/**
 * @return {boolean} Whether the request completed with a success.
 */
goog.net.XhrIo.prototype.isSuccess = function() {
  var status = this.getStatus();
  // A zero status code is considered successful for local files.
  return goog.net.HttpStatus.isSuccess(status) ||
      status === 0 && !this.isLastUriEffectiveSchemeHttp_();
};


/**
 * @return {boolean} whether the effective scheme of the last URI that was
 *     fetched was 'http' or 'https'.
 * @private
 */
goog.net.XhrIo.prototype.isLastUriEffectiveSchemeHttp_ = function() {
  var scheme = goog.uri.utils.getEffectiveScheme(String(this.lastUri_));
  return goog.net.XhrIo.HTTP_SCHEME_PATTERN.test(scheme);
};


/**
 * Get the readystate from the Xhr object
 * Will only return correct result when called from the context of a callback
 * @return {goog.net.XmlHttp.ReadyState} goog.net.XmlHttp.ReadyState.*.
 */
goog.net.XhrIo.prototype.getReadyState = function() {
  return this.xhr_ ?
      /** @type {goog.net.XmlHttp.ReadyState} */ (this.xhr_.readyState) :
      goog.net.XmlHttp.ReadyState.UNINITIALIZED;
};


/**
 * Get the status from the Xhr object
 * Will only return correct result when called from the context of a callback
 * @return {number} Http status.
 */
goog.net.XhrIo.prototype.getStatus = function() {
  /**
   * IE doesn't like you checking status until the readystate is greater than 2
   * (i.e. it is receiving or complete).  The try/catch is used for when the
   * page is unloading and an ERROR_NOT_AVAILABLE may occur when accessing xhr_.
   * @preserveTry
   */
  try {
    return this.getReadyState() > goog.net.XmlHttp.ReadyState.LOADED ?
        this.xhr_.status : -1;
  } catch (e) {
    return -1;
  }
};


/**
 * Get the status text from the Xhr object
 * Will only return correct result when called from the context of a callback
 * @return {string} Status text.
 */
goog.net.XhrIo.prototype.getStatusText = function() {
  /**
   * IE doesn't like you checking status until the readystate is greater than 2
   * (i.e. it is recieving or complete).  The try/catch is used for when the
   * page is unloading and an ERROR_NOT_AVAILABLE may occur when accessing xhr_.
   * @preserveTry
   */
  try {
    return this.getReadyState() > goog.net.XmlHttp.ReadyState.LOADED ?
        this.xhr_.statusText : '';
  } catch (e) {
    goog.log.fine(this.logger_, 'Can not get status: ' + e.message);
    return '';
  }
};


/**
 * Get the last Uri that was requested
 * @return {string} Last Uri.
 */
goog.net.XhrIo.prototype.getLastUri = function() {
  return String(this.lastUri_);
};


/**
 * Get the response text from the Xhr object
 * Will only return correct result when called from the context of a callback.
 * @return {string} Result from the server, or '' if no result available.
 */
goog.net.XhrIo.prototype.getResponseText = function() {
  /** @preserveTry */
  try {
    return this.xhr_ ? this.xhr_.responseText : '';
  } catch (e) {
    // http://www.w3.org/TR/XMLHttpRequest/#the-responsetext-attribute
    // states that responseText should return '' (and responseXML null)
    // when the state is not LOADING or DONE. Instead, IE can
    // throw unexpected exceptions, for example when a request is aborted
    // or no data is available yet.
    goog.log.fine(this.logger_, 'Can not get responseText: ' + e.message);
    return '';
  }
};


/**
 * Get the response body from the Xhr object. This property is only available
 * in IE since version 7 according to MSDN:
 * http://msdn.microsoft.com/en-us/library/ie/ms534368(v=vs.85).aspx
 * Will only return correct result when called from the context of a callback.
 *
 * One option is to construct a VBArray from the returned object and convert
 * it to a JavaScript array using the toArray method:
 * {@code (new window['VBArray'](xhrIo.getResponseBody())).toArray()}
 * This will result in an array of numbers in the range of [0..255]
 *
 * Another option is to use the VBScript CStr method to convert it into a
 * string as outlined in http://stackoverflow.com/questions/1919972
 *
 * @return {Object} Binary result from the server or null if not available.
 */
goog.net.XhrIo.prototype.getResponseBody = function() {
  /** @preserveTry */
  try {
    if (this.xhr_ && 'responseBody' in this.xhr_) {
      return this.xhr_['responseBody'];
    }
  } catch (e) {
    // IE can throw unexpected exceptions, for example when a request is aborted
    // or no data is yet available.
    goog.log.fine(this.logger_, 'Can not get responseBody: ' + e.message);
  }
  return null;
};


/**
 * Get the response XML from the Xhr object
 * Will only return correct result when called from the context of a callback.
 * @return {Document} The DOM Document representing the XML file, or null
 * if no result available.
 */
goog.net.XhrIo.prototype.getResponseXml = function() {
  /** @preserveTry */
  try {
    return this.xhr_ ? this.xhr_.responseXML : null;
  } catch (e) {
    goog.log.fine(this.logger_, 'Can not get responseXML: ' + e.message);
    return null;
  }
};


/**
 * Get the response and evaluates it as JSON from the Xhr object
 * Will only return correct result when called from the context of a callback
 * @param {string=} opt_xssiPrefix Optional XSSI prefix string to use for
 *     stripping of the response before parsing. This needs to be set only if
 *     your backend server prepends the same prefix string to the JSON response.
 * @return {Object|undefined} JavaScript object.
 */
goog.net.XhrIo.prototype.getResponseJson = function(opt_xssiPrefix) {
  if (!this.xhr_) {
    return undefined;
  }

  var responseText = this.xhr_.responseText;
  if (opt_xssiPrefix && responseText.indexOf(opt_xssiPrefix) == 0) {
    responseText = responseText.substring(opt_xssiPrefix.length);
  }

  return goog.json.parse(responseText);
};


/**
 * Get the response as the type specificed by {@link #setResponseType}. At time
 * of writing, this is only directly supported in very recent versions of WebKit
 * (10.0.612.1 dev and later). If the field is not supported directly, we will
 * try to emulate it.
 *
 * Emulating the response means following the rules laid out at
 * http://www.w3.org/TR/XMLHttpRequest/#the-response-attribute
 *
 * On browsers with no support for this (Chrome < 10, Firefox < 4, etc), only
 * response types of DEFAULT or TEXT may be used, and the response returned will
 * be the text response.
 *
 * On browsers with Mozilla's draft support for array buffers (Firefox 4, 5),
 * only response types of DEFAULT, TEXT, and ARRAY_BUFFER may be used, and the
 * response returned will be either the text response or the Mozilla
 * implementation of the array buffer response.
 *
 * On browsers will full support, any valid response type supported by the
 * browser may be used, and the response provided by the browser will be
 * returned.
 *
 * @return {*} The response.
 */
goog.net.XhrIo.prototype.getResponse = function() {
  /** @preserveTry */
  try {
    if (!this.xhr_) {
      return null;
    }
    if ('response' in this.xhr_) {
      return this.xhr_.response;
    }
    switch (this.responseType_) {
      case goog.net.XhrIo.ResponseType.DEFAULT:
      case goog.net.XhrIo.ResponseType.TEXT:
        return this.xhr_.responseText;
        // DOCUMENT and BLOB don't need to be handled here because they are
        // introduced in the same spec that adds the .response field, and would
        // have been caught above.
        // ARRAY_BUFFER needs an implementation for Firefox 4, where it was
        // implemented using a draft spec rather than the final spec.
      case goog.net.XhrIo.ResponseType.ARRAY_BUFFER:
        if ('mozResponseArrayBuffer' in this.xhr_) {
          return this.xhr_.mozResponseArrayBuffer;
        }
    }
    // Fell through to a response type that is not supported on this browser.
    goog.log.error(this.logger_,
        'Response type ' + this.responseType_ + ' is not ' +
        'supported on this browser');
    return null;
  } catch (e) {
    goog.log.fine(this.logger_, 'Can not get response: ' + e.message);
    return null;
  }
};


/**
 * Get the value of the response-header with the given name from the Xhr object
 * Will only return correct result when called from the context of a callback
 * and the request has completed
 * @param {string} key The name of the response-header to retrieve.
 * @return {string|undefined} The value of the response-header named key.
 */
goog.net.XhrIo.prototype.getResponseHeader = function(key) {
  return this.xhr_ && this.isComplete() ?
      this.xhr_.getResponseHeader(key) : undefined;
};


/**
 * Gets the text of all the headers in the response.
 * Will only return correct result when called from the context of a callback
 * and the request has completed.
 * @return {string} The value of the response headers or empty string.
 */
goog.net.XhrIo.prototype.getAllResponseHeaders = function() {
  return this.xhr_ && this.isComplete() ?
      this.xhr_.getAllResponseHeaders() : '';
};


/**
 * Returns all response headers as a key-value map.
 * Multiple values for the same header key can be combined into one,
 * separated by a comma and a space.
 * Note that the native getResponseHeader method for retrieving a single header
 * does a case insensitive match on the header name. This method does not
 * include any case normalization logic, it will just return a key-value
 * representation of the headers.
 * See: http://www.w3.org/TR/XMLHttpRequest/#the-getresponseheader()-method
 * @return {!Object<string, string>} An object with the header keys as keys
 *     and header values as values.
 */
goog.net.XhrIo.prototype.getResponseHeaders = function() {
  var headersObject = {};
  var headersArray = this.getAllResponseHeaders().split('\r\n');
  for (var i = 0; i < headersArray.length; i++) {
    if (goog.string.isEmptyOrWhitespace(headersArray[i])) {
      continue;
    }
    var keyValue = goog.string.splitLimit(headersArray[i], ': ', 2);
    if (headersObject[keyValue[0]]) {
      headersObject[keyValue[0]] += ', ' + keyValue[1];
    } else {
      headersObject[keyValue[0]] = keyValue[1];
    }
  }
  return headersObject;
};


/**
 * Get the last error message
 * @return {goog.net.ErrorCode} Last error code.
 */
goog.net.XhrIo.prototype.getLastErrorCode = function() {
  return this.lastErrorCode_;
};


/**
 * Get the last error message
 * @return {string} Last error message.
 */
goog.net.XhrIo.prototype.getLastError = function() {
  return goog.isString(this.lastError_) ? this.lastError_ :
      String(this.lastError_);
};


/**
 * Adds the last method, status and URI to the message.  This is used to add
 * this information to the logging calls.
 * @param {string} msg The message text that we want to add the extra text to.
 * @return {string} The message with the extra text appended.
 * @private
 */
goog.net.XhrIo.prototype.formatMsg_ = function(msg) {
  return msg + ' [' + this.lastMethod_ + ' ' + this.lastUri_ + ' ' +
      this.getStatus() + ']';
};


// Register the xhr handler as an entry point, so that
// it can be monitored for exception handling, etc.
goog.debug.entryPointRegistry.register(
    /**
     * @param {function(!Function): !Function} transformer The transforming
     *     function.
     */
    function(transformer) {
      goog.net.XhrIo.prototype.onReadyStateChangeEntryPoint_ =
          transformer(goog.net.XhrIo.prototype.onReadyStateChangeEntryPoint_);
    });

goog.provide('ol.TileLoadFunctionType');
goog.provide('ol.TileVectorLoadFunctionType');


/**
 * A function that takes an {@link ol.Tile} for the tile and a
 * `{string}` for the url as arguments.
 *
 * @typedef {function(ol.Tile, string)}
 * @api
 */
ol.TileLoadFunctionType;


/**
 * A function that is called with a tile url for the features to load and
 * a callback that takes the loaded features as argument.
 *
 * @typedef {function(string, function(Array.<ol.Feature>))}
 * @api
 */
ol.TileVectorLoadFunctionType;

goog.provide('ol.VectorTile');

goog.require('ol.Tile');
goog.require('ol.TileCoord');
goog.require('ol.TileLoadFunctionType');
goog.require('ol.TileState');
goog.require('ol.dom');
goog.require('ol.proj.Projection');


/**
 * @typedef {{
 *     dirty: boolean,
 *     renderedRenderOrder: (null|function(ol.Feature, ol.Feature):number),
 *     renderedRevision: number,
 *     replayGroup: ol.render.IReplayGroup}}
 */
ol.TileReplayState;


/**
 * @constructor
 * @extends {ol.Tile}
 * @param {ol.TileCoord} tileCoord Tile coordinate.
 * @param {ol.TileState} state State.
 * @param {string} src Data source url.
 * @param {ol.format.Feature} format Feature format.
 * @param {ol.TileLoadFunctionType} tileLoadFunction Tile load function.
 * @param {ol.proj.Projection} projection Feature projection.
 */
ol.VectorTile = function(tileCoord, state, src, format, tileLoadFunction, projection) {

  goog.base(this, tileCoord, state);

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.context_ = ol.dom.createCanvasContext2D();

  /**
   * @private
   * @type {ol.format.Feature}
   */
  this.format_ = format;

  /**
   * @private
   * @type {Array.<ol.Feature>}
   */
  this.features_ = null;

  /**
   * @private
   * @type {ol.FeatureLoader}
   */
  this.loader_;

  /**
   * @private
   * @type {ol.proj.Projection}
   */
  this.projection_ = projection;

  /**
   * @private
   * @type {ol.TileReplayState}
   */
  this.replayState_ = {
    dirty: false,
    renderedRenderOrder: null,
    renderedRevision: -1,
    replayGroup: null
  };

  /**
   * @private
   * @type {ol.TileLoadFunctionType}
   */
  this.tileLoadFunction_ = tileLoadFunction;

  /**
   * @private
   * @type {string}
   */
  this.url_ = src;

};
goog.inherits(ol.VectorTile, ol.Tile);


/**
 * @return {CanvasRenderingContext2D} The rendering context.
 */
ol.VectorTile.prototype.getContext = function() {
  return this.context_;
};


/**
 * @inheritDoc
 */
ol.VectorTile.prototype.disposeInternal = function() {
  goog.base(this, 'disposeInternal');
};


/**
 * Get the feature format assigned for reading this tile's features.
 * @return {ol.format.Feature} Feature format.
 * @api
 */
ol.VectorTile.prototype.getFormat = function() {
  return this.format_;
};


/**
 * @return {Array.<ol.Feature>} Features.
 */
ol.VectorTile.prototype.getFeatures = function() {
  return this.features_;
};


/**
 * @return {ol.TileReplayState} The replay state.
 */
ol.VectorTile.prototype.getReplayState = function() {
  return this.replayState_;
};


/**
 * @inheritDoc
 */
ol.VectorTile.prototype.getKey = function() {
  return this.url_;
};


/**
 * @return {ol.proj.Projection} Feature projection.
 */
ol.VectorTile.prototype.getProjection = function() {
  return this.projection_;
};


/**
 * Load the tile.
 */
ol.VectorTile.prototype.load = function() {
  if (this.state == ol.TileState.IDLE) {
    this.setState(ol.TileState.LOADING);
    this.tileLoadFunction_(this, this.url_);
    this.loader_(null, NaN, this.projection_);
  }
};


/**
 * @param {Array.<ol.Feature>} features Features.
 * @api
 */
ol.VectorTile.prototype.setFeatures = function(features) {
  this.features_ = features;
  this.setState(ol.TileState.LOADED);
};


/**
 * @param {ol.proj.Projection} projection Feature projection.
 */
ol.VectorTile.prototype.setProjection = function(projection) {
  this.projection_ = projection;
};


/**
 * @param {ol.TileState} tileState Tile state.
 */
ol.VectorTile.prototype.setState = function(tileState) {
  this.state = tileState;
  this.changed();
};


/**
 * Set the feature loader for reading this tile's features.
 * @param {ol.FeatureLoader} loader Feature loader.
 * @api
 */
ol.VectorTile.prototype.setLoader = function(loader) {
  this.loader_ = loader;
};

goog.provide('ol.format.FormatType');


/**
 * @enum {string}
 */
ol.format.FormatType = {
  ARRAY_BUFFER: 'arraybuffer',
  JSON: 'json',
  TEXT: 'text',
  XML: 'xml'
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview
 * XML utilities.
 *
 */

goog.provide('goog.dom.xml');

goog.require('goog.dom');
goog.require('goog.dom.NodeType');
goog.require('goog.userAgent');


/**
 * Max XML size for MSXML2.  Used to prevent potential DoS attacks.
 * @type {number}
 */
goog.dom.xml.MAX_XML_SIZE_KB = 2 * 1024;  // In kB


/**
 * Max XML size for MSXML2.  Used to prevent potential DoS attacks.
 * @type {number}
 */
goog.dom.xml.MAX_ELEMENT_DEPTH = 256; // Same default as MSXML6.


/**
 * Check for ActiveXObject support by the browser.
 * @return {boolean} true if browser has ActiveXObject support.
 * @private
 */
goog.dom.xml.hasActiveXObjectSupport_ = function() {
  if (!goog.userAgent.IE) {
    // Avoid raising useless exception in case code is not compiled
    // and browser is not MSIE.
    return false;
  }
  try {
    // Due to lot of changes in IE 9, 10 & 11 behaviour and ActiveX being
    // totally disableable using MSIE's security level, trying to create the
    // ActiveXOjbect is a lot more reliable than testing for the existance of
    // window.ActiveXObject
    new ActiveXObject('MSXML2.DOMDocument');
    return true;
  } catch (e) {
    return false;
  }
};


/**
 * True if browser has ActiveXObject support.
 * Possible override if this test become wrong in coming IE versions.
 * @type {boolean}
 */
goog.dom.xml.ACTIVEX_SUPPORT =
    goog.userAgent.IE && goog.dom.xml.hasActiveXObjectSupport_();


/**
 * Creates an XML document appropriate for the current JS runtime
 * @param {string=} opt_rootTagName The root tag name.
 * @param {string=} opt_namespaceUri Namespace URI of the document element.
 * @param {boolean=} opt_preferActiveX Whether to default to ActiveXObject to
 * create Document in IE. Use this if you need xpath support in IE (e.g.,
 * selectSingleNode or selectNodes), but be aware that the ActiveXObject does
 * not support various DOM-specific Document methods and attributes.
 * @return {Document} The new document.
 * @throws {Error} if browser does not support creating new documents or
 * namespace is provided without a root tag name.
 */
goog.dom.xml.createDocument = function(opt_rootTagName, opt_namespaceUri,
                                       opt_preferActiveX) {
  if (opt_namespaceUri && !opt_rootTagName) {
    throw Error("Can't create document with namespace and no root tag");
  }
  // If document.implementation.createDocument is available and they haven't
  // explicitly opted to use ActiveXObject when possible.
  if (document.implementation && document.implementation.createDocument &&
      !(goog.dom.xml.ACTIVEX_SUPPORT && opt_preferActiveX)) {
    return document.implementation.createDocument(opt_namespaceUri || '',
                                                  opt_rootTagName || '', null);
  } else if (goog.dom.xml.ACTIVEX_SUPPORT) {
    var doc = goog.dom.xml.createMsXmlDocument_();
    if (doc) {
      if (opt_rootTagName) {
        doc.appendChild(doc.createNode(goog.dom.NodeType.ELEMENT,
                                       opt_rootTagName,
                                       opt_namespaceUri || ''));
      }
      return doc;
    }
  }
  throw Error('Your browser does not support creating new documents');
};


/**
 * Creates an XML document from a string
 * @param {string} xml The text.
 * @param {boolean=} opt_preferActiveX Whether to default to ActiveXObject to
 * create Document in IE. Use this if you need xpath support in IE (e.g.,
 * selectSingleNode or selectNodes), but be aware that the ActiveXObject does
 * not support various DOM-specific Document methods and attributes.
 * @return {Document} XML document from the text.
 * @throws {Error} if browser does not support loading XML documents.
 */
goog.dom.xml.loadXml = function(xml, opt_preferActiveX) {
  if (typeof DOMParser != 'undefined' &&
      !(goog.dom.xml.ACTIVEX_SUPPORT && opt_preferActiveX)) {
    return new DOMParser().parseFromString(xml, 'application/xml');
  } else if (goog.dom.xml.ACTIVEX_SUPPORT) {
    var doc = goog.dom.xml.createMsXmlDocument_();
    doc.loadXML(xml);
    return doc;
  }
  throw Error('Your browser does not support loading xml documents');
};


/**
 * Serializes an XML document or subtree to string.
 * @param {Document|Element} xml The document or the root node of the subtree.
 * @return {string} The serialized XML.
 * @throws {Error} if browser does not support XML serialization.
 */
goog.dom.xml.serialize = function(xml) {
  // Compatible with IE/ActiveXObject.
  var text = xml.xml;
  if (text) {
    return text;
  }
  // Compatible with Firefox, Opera and WebKit.
  if (typeof XMLSerializer != 'undefined') {
    return new XMLSerializer().serializeToString(xml);
  }
  throw Error('Your browser does not support serializing XML documents');
};


/**
 * Selects a single node using an Xpath expression and a root node
 * @param {Node} node The root node.
 * @param {string} path Xpath selector.
 * @return {Node} The selected node, or null if no matching node.
 */
goog.dom.xml.selectSingleNode = function(node, path) {
  if (typeof node.selectSingleNode != 'undefined') {
    var doc = goog.dom.getOwnerDocument(node);
    if (typeof doc.setProperty != 'undefined') {
      doc.setProperty('SelectionLanguage', 'XPath');
    }
    return node.selectSingleNode(path);
  } else if (document.implementation.hasFeature('XPath', '3.0')) {
    var doc = goog.dom.getOwnerDocument(node);
    var resolver = doc.createNSResolver(doc.documentElement);
    var result = doc.evaluate(path, node, resolver,
        XPathResult.FIRST_ORDERED_NODE_TYPE, null);
    return result.singleNodeValue;
  }
  // This browser does not support xpath for the given node. If IE, ensure XML
  // Document was created using ActiveXObject
  // TODO(joeltine): This should throw instead of return null.
  return null;
};


/**
 * Selects multiple nodes using an Xpath expression and a root node
 * @param {Node} node The root node.
 * @param {string} path Xpath selector.
 * @return {(NodeList|Array<Node>)} The selected nodes, or empty array if no
 *     matching nodes.
 */
goog.dom.xml.selectNodes = function(node, path) {
  if (typeof node.selectNodes != 'undefined') {
    var doc = goog.dom.getOwnerDocument(node);
    if (typeof doc.setProperty != 'undefined') {
      doc.setProperty('SelectionLanguage', 'XPath');
    }
    return node.selectNodes(path);
  } else if (document.implementation.hasFeature('XPath', '3.0')) {
    var doc = goog.dom.getOwnerDocument(node);
    var resolver = doc.createNSResolver(doc.documentElement);
    var nodes = doc.evaluate(path, node, resolver,
        XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    var results = [];
    var count = nodes.snapshotLength;
    for (var i = 0; i < count; i++) {
      results.push(nodes.snapshotItem(i));
    }
    return results;
  } else {
    // This browser does not support xpath for the given node. If IE, ensure XML
    // Document was created using ActiveXObject.
    // TODO(joeltine): This should throw instead of return empty array.
    return [];
  }
};


/**
 * Sets multiple attributes on an element. Differs from goog.dom.setProperties
 * in that it exclusively uses the element's setAttributes method. Use this
 * when you need to ensure that the exact property is available as an attribute
 * and can be read later by the native getAttribute method.
 * @param {!Element} element XML or DOM element to set attributes on.
 * @param {!Object<string, string>} attributes Map of property:value pairs.
 */
goog.dom.xml.setAttributes = function(element, attributes) {
  for (var key in attributes) {
    if (attributes.hasOwnProperty(key)) {
      element.setAttribute(key, attributes[key]);
    }
  }
};


/**
 * Creates an instance of the MSXML2.DOMDocument.
 * @return {Document} The new document.
 * @private
 */
goog.dom.xml.createMsXmlDocument_ = function() {
  var doc = new ActiveXObject('MSXML2.DOMDocument');
  if (doc) {
    // Prevent potential vulnerabilities exposed by MSXML2, see
    // http://b/1707300 and http://wiki/Main/ISETeamXMLAttacks for details.
    doc.resolveExternals = false;
    doc.validateOnParse = false;
    // Add a try catch block because accessing these properties will throw an
    // error on unsupported MSXML versions. This affects Windows machines
    // running IE6 or IE7 that are on XP SP2 or earlier without MSXML updates.
    // See http://msdn.microsoft.com/en-us/library/ms766391(VS.85).aspx for
    // specific details on which MSXML versions support these properties.
    try {
      doc.setProperty('ProhibitDTD', true);
      doc.setProperty('MaxXMLSize', goog.dom.xml.MAX_XML_SIZE_KB);
      doc.setProperty('MaxElementDepth', goog.dom.xml.MAX_ELEMENT_DEPTH);
    } catch (e) {
      // No-op.
    }
  }
  return doc;
};

goog.provide('ol.xml');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.xml');
goog.require('goog.object');
goog.require('goog.userAgent');


/**
 * When using {@link ol.xml.makeChildAppender} or
 * {@link ol.xml.makeSimpleNodeFactory}, the top `objectStack` item needs to
 * have this structure.
 * @typedef {{node:Node}}
 */
ol.xml.NodeStackItem;


/**
 * @typedef {function(Node, Array.<*>)}
 */
ol.xml.Parser;


/**
 * @typedef {function(Node, *, Array.<*>)}
 */
ol.xml.Serializer;


/**
 * This document should be used when creating nodes for XML serializations. This
 * document is also used by {@link ol.xml.createElementNS} and
 * {@link ol.xml.setAttributeNS}
 * @const
 * @type {Document}
 */
ol.xml.DOCUMENT = goog.dom.xml.createDocument();


/**
 * @param {string} namespaceURI Namespace URI.
 * @param {string} qualifiedName Qualified name.
 * @return {Node} Node.
 * @private
 */
ol.xml.createElementNS_ = function(namespaceURI, qualifiedName) {
  return ol.xml.DOCUMENT.createElementNS(namespaceURI, qualifiedName);
};


/**
 * @param {string} namespaceURI Namespace URI.
 * @param {string} qualifiedName Qualified name.
 * @return {Node} Node.
 * @private
 */
ol.xml.createElementNSActiveX_ = function(namespaceURI, qualifiedName) {
  if (!namespaceURI) {
    namespaceURI = '';
  }
  return ol.xml.DOCUMENT.createNode(1, qualifiedName, namespaceURI);
};


/**
 * @param {string} namespaceURI Namespace URI.
 * @param {string} qualifiedName Qualified name.
 * @return {Node} Node.
 */
ol.xml.createElementNS =
    (document.implementation && document.implementation.createDocument) ?
        ol.xml.createElementNS_ : ol.xml.createElementNSActiveX_;


/**
 * Recursively grab all text content of child nodes into a single string.
 * @param {Node} node Node.
 * @param {boolean} normalizeWhitespace Normalize whitespace: remove all line
 * breaks.
 * @return {string} All text content.
 * @api
 */
ol.xml.getAllTextContent = function(node, normalizeWhitespace) {
  return ol.xml.getAllTextContent_(node, normalizeWhitespace, []).join('');
};


/**
 * Recursively grab all text content of child nodes into a single string.
 * @param {Node} node Node.
 * @param {boolean} normalizeWhitespace Normalize whitespace: remove all line
 * breaks.
 * @param {Array.<String|string>} accumulator Accumulator.
 * @private
 * @return {Array.<String|string>} Accumulator.
 */
ol.xml.getAllTextContent_ = function(node, normalizeWhitespace, accumulator) {
  if (node.nodeType == goog.dom.NodeType.CDATA_SECTION ||
      node.nodeType == goog.dom.NodeType.TEXT) {
    if (normalizeWhitespace) {
      // FIXME understand why goog.dom.getTextContent_ uses String here
      accumulator.push(String(node.nodeValue).replace(/(\r\n|\r|\n)/g, ''));
    } else {
      accumulator.push(node.nodeValue);
    }
  } else {
    var n;
    for (n = node.firstChild; n; n = n.nextSibling) {
      ol.xml.getAllTextContent_(n, normalizeWhitespace, accumulator);
    }
  }
  return accumulator;
};


/**
 * @param {Node} node Node.
 * @private
 * @return {string} Local name.
 */
ol.xml.getLocalName_ = function(node) {
  return node.localName;
};


/**
 * @param {Node} node Node.
 * @private
 * @return {string} Local name.
 */
ol.xml.getLocalNameIE_ = function(node) {
  var localName = node.localName;
  if (localName !== undefined) {
    return localName;
  }
  var baseName = node.baseName;
  goog.asserts.assert(baseName,
      'Failed to get localName/baseName of node %s', node);
  return baseName;
};


/**
 * @param {Node} node Node.
 * @return {string} Local name.
 */
ol.xml.getLocalName = goog.userAgent.IE ?
    ol.xml.getLocalNameIE_ : ol.xml.getLocalName_;


/**
 * @param {?} value Value.
 * @private
 * @return {boolean} Is document.
 */
ol.xml.isDocument_ = function(value) {
  return value instanceof Document;
};


/**
 * @param {?} value Value.
 * @private
 * @return {boolean} Is document.
 */
ol.xml.isDocumentIE_ = function(value) {
  return goog.isObject(value) && value.nodeType == goog.dom.NodeType.DOCUMENT;
};


/**
 * @param {?} value Value.
 * @return {boolean} Is document.
 */
ol.xml.isDocument = goog.userAgent.IE ?
    ol.xml.isDocumentIE_ : ol.xml.isDocument_;


/**
 * @param {?} value Value.
 * @private
 * @return {boolean} Is node.
 */
ol.xml.isNode_ = function(value) {
  return value instanceof Node;
};


/**
 * @param {?} value Value.
 * @private
 * @return {boolean} Is node.
 */
ol.xml.isNodeIE_ = function(value) {
  return goog.isObject(value) && value.nodeType !== undefined;
};


/**
 * @param {?} value Value.
 * @return {boolean} Is node.
 */
ol.xml.isNode = goog.userAgent.IE ? ol.xml.isNodeIE_ : ol.xml.isNode_;


/**
 * @param {Node} node Node.
 * @param {?string} namespaceURI Namespace URI.
 * @param {string} name Attribute name.
 * @return {string} Value
 * @private
 */
ol.xml.getAttributeNS_ = function(node, namespaceURI, name) {
  return node.getAttributeNS(namespaceURI, name) || '';
};


/**
 * @param {Node} node Node.
 * @param {?string} namespaceURI Namespace URI.
 * @param {string} name Attribute name.
 * @return {string} Value
 * @private
 */
ol.xml.getAttributeNSActiveX_ = function(node, namespaceURI, name) {
  var attributeValue = '';
  var attributeNode = ol.xml.getAttributeNodeNS(node, namespaceURI, name);
  if (attributeNode !== undefined) {
    attributeValue = attributeNode.nodeValue;
  }
  return attributeValue;
};


/**
 * @param {Node} node Node.
 * @param {?string} namespaceURI Namespace URI.
 * @param {string} name Attribute name.
 * @return {string} Value
 */
ol.xml.getAttributeNS =
    (document.implementation && document.implementation.createDocument) ?
        ol.xml.getAttributeNS_ : ol.xml.getAttributeNSActiveX_;


/**
 * @param {Node} node Node.
 * @param {?string} namespaceURI Namespace URI.
 * @param {string} name Attribute name.
 * @return {?Node} Attribute node or null if none found.
 * @private
 */
ol.xml.getAttributeNodeNS_ = function(node, namespaceURI, name) {
  return node.getAttributeNodeNS(namespaceURI, name);
};


/**
 * @param {Node} node Node.
 * @param {?string} namespaceURI Namespace URI.
 * @param {string} name Attribute name.
 * @return {?Node} Attribute node or null if none found.
 * @private
 */
ol.xml.getAttributeNodeNSActiveX_ = function(node, namespaceURI, name) {
  var attributeNode = null;
  var attributes = node.attributes;
  var potentialNode, fullName;
  for (var i = 0, len = attributes.length; i < len; ++i) {
    potentialNode = attributes[i];
    if (potentialNode.namespaceURI == namespaceURI) {
      fullName = (potentialNode.prefix) ?
          (potentialNode.prefix + ':' + name) : name;
      if (fullName == potentialNode.nodeName) {
        attributeNode = potentialNode;
        break;
      }
    }
  }
  return attributeNode;
};


/**
 * @param {Node} node Node.
 * @param {?string} namespaceURI Namespace URI.
 * @param {string} name Attribute name.
 * @return {?Node} Attribute node or null if none found.
 */
ol.xml.getAttributeNodeNS =
    (document.implementation && document.implementation.createDocument) ?
        ol.xml.getAttributeNodeNS_ : ol.xml.getAttributeNodeNSActiveX_;


/**
 * @param {Node} node Node.
 * @param {?string} namespaceURI Namespace URI.
 * @param {string} name Attribute name.
 * @param {string|number} value Value.
 * @private
 */
ol.xml.setAttributeNS_ = function(node, namespaceURI, name, value) {
  node.setAttributeNS(namespaceURI, name, value);
};


/**
 * @param {Node} node Node.
 * @param {?string} namespaceURI Namespace URI.
 * @param {string} name Attribute name.
 * @param {string|number} value Value.
 * @private
 */
ol.xml.setAttributeNSActiveX_ = function(node, namespaceURI, name, value) {
  if (namespaceURI) {
    var attribute = node.ownerDocument.createNode(2, name, namespaceURI);
    attribute.nodeValue = value;
    node.setAttributeNode(attribute);
  } else {
    node.setAttribute(name, value);
  }
};


/**
 * @param {Node} node Node.
 * @param {?string} namespaceURI Namespace URI.
 * @param {string} name Attribute name.
 * @param {string|number} value Value.
 */
ol.xml.setAttributeNS =
    (document.implementation && document.implementation.createDocument) ?
        ol.xml.setAttributeNS_ : ol.xml.setAttributeNSActiveX_;


/**
 * Parse an XML string to an XML Document.
 * @param {string} xml XML.
 * @return {Document} Document.
 * @api
 */
ol.xml.parse = function(xml) {
  return new DOMParser().parseFromString(xml, 'application/xml');
};


/**
 * Make an array extender function for extending the array at the top of the
 * object stack.
 * @param {function(this: T, Node, Array.<*>): (Array.<*>|undefined)}
 *     valueReader Value reader.
 * @param {T=} opt_this The object to use as `this` in `valueReader`.
 * @return {ol.xml.Parser} Parser.
 * @template T
 */
ol.xml.makeArrayExtender = function(valueReader, opt_this) {
  return (
      /**
       * @param {Node} node Node.
       * @param {Array.<*>} objectStack Object stack.
       */
      function(node, objectStack) {
        var value = valueReader.call(opt_this, node, objectStack);
        if (value !== undefined) {
          goog.asserts.assert(goog.isArray(value),
              'valueReader function is expected to return an array of values');
          var array = /** @type {Array.<*>} */
              (objectStack[objectStack.length - 1]);
          goog.asserts.assert(goog.isArray(array),
              'objectStack is supposed to be an array of arrays');
          goog.array.extend(array, value);
        }
      });
};


/**
 * Make an array pusher function for pushing to the array at the top of the
 * object stack.
 * @param {function(this: T, Node, Array.<*>): *} valueReader Value reader.
 * @param {T=} opt_this The object to use as `this` in `valueReader`.
 * @return {ol.xml.Parser} Parser.
 * @template T
 */
ol.xml.makeArrayPusher = function(valueReader, opt_this) {
  return (
      /**
       * @param {Node} node Node.
       * @param {Array.<*>} objectStack Object stack.
       */
      function(node, objectStack) {
        var value = valueReader.call(opt_this !== undefined ? opt_this : this,
            node, objectStack);
        if (value !== undefined) {
          var array = objectStack[objectStack.length - 1];
          goog.asserts.assert(goog.isArray(array),
              'objectStack is supposed to be an array of arrays');
          array.push(value);
        }
      });
};


/**
 * Make an object stack replacer function for replacing the object at the
 * top of the stack.
 * @param {function(this: T, Node, Array.<*>): *} valueReader Value reader.
 * @param {T=} opt_this The object to use as `this` in `valueReader`.
 * @return {ol.xml.Parser} Parser.
 * @template T
 */
ol.xml.makeReplacer = function(valueReader, opt_this) {
  return (
      /**
       * @param {Node} node Node.
       * @param {Array.<*>} objectStack Object stack.
       */
      function(node, objectStack) {
        var value = valueReader.call(opt_this !== undefined ? opt_this : this,
            node, objectStack);
        if (value !== undefined) {
          objectStack[objectStack.length - 1] = value;
        }
      });
};


/**
 * Make an object property pusher function for adding a property to the
 * object at the top of the stack.
 * @param {function(this: T, Node, Array.<*>): *} valueReader Value reader.
 * @param {string=} opt_property Property.
 * @param {T=} opt_this The object to use as `this` in `valueReader`.
 * @return {ol.xml.Parser} Parser.
 * @template T
 */
ol.xml.makeObjectPropertyPusher = function(valueReader, opt_property, opt_this) {
  goog.asserts.assert(valueReader !== undefined,
      'undefined valueReader, expected function(this: T, Node, Array.<*>)');
  return (
      /**
       * @param {Node} node Node.
       * @param {Array.<*>} objectStack Object stack.
       */
      function(node, objectStack) {
        var value = valueReader.call(opt_this !== undefined ? opt_this : this,
            node, objectStack);
        if (value !== undefined) {
          var object = /** @type {Object} */
              (objectStack[objectStack.length - 1]);
          var property = opt_property !== undefined ?
              opt_property : node.localName;
          goog.asserts.assert(goog.isObject(object),
              'entity from stack was not an object');
          var array = goog.object.setIfUndefined(object, property, []);
          array.push(value);
        }
      });
};


/**
 * Make an object property setter function.
 * @param {function(this: T, Node, Array.<*>): *} valueReader Value reader.
 * @param {string=} opt_property Property.
 * @param {T=} opt_this The object to use as `this` in `valueReader`.
 * @return {ol.xml.Parser} Parser.
 * @template T
 */
ol.xml.makeObjectPropertySetter = function(valueReader, opt_property, opt_this) {
  goog.asserts.assert(valueReader !== undefined,
      'undefined valueReader, expected function(this: T, Node, Array.<*>)');
  return (
      /**
       * @param {Node} node Node.
       * @param {Array.<*>} objectStack Object stack.
       */
      function(node, objectStack) {
        var value = valueReader.call(opt_this !== undefined ? opt_this : this,
            node, objectStack);
        if (value !== undefined) {
          var object = /** @type {Object} */
              (objectStack[objectStack.length - 1]);
          var property = opt_property !== undefined ?
              opt_property : node.localName;
          goog.asserts.assert(goog.isObject(object),
              'entity from stack was not an object');
          object[property] = value;
        }
      });
};


/**
 * Create a serializer that appends nodes written by its `nodeWriter` to its
 * designated parent. The parent is the `node` of the
 * {@link ol.xml.NodeStackItem} at the top of the `objectStack`.
 * @param {function(this: T, Node, V, Array.<*>)}
 *     nodeWriter Node writer.
 * @param {T=} opt_this The object to use as `this` in `nodeWriter`.
 * @return {ol.xml.Serializer} Serializer.
 * @template T, V
 */
ol.xml.makeChildAppender = function(nodeWriter, opt_this) {
  return function(node, value, objectStack) {
    nodeWriter.call(opt_this !== undefined ? opt_this : this,
        node, value, objectStack);
    var parent = objectStack[objectStack.length - 1];
    goog.asserts.assert(goog.isObject(parent),
        'entity from stack was not an object');
    var parentNode = parent.node;
    goog.asserts.assert(ol.xml.isNode(parentNode) ||
        ol.xml.isDocument(parentNode),
        'expected parentNode %s to be a Node or a Document', parentNode);
    parentNode.appendChild(node);
  };
};


/**
 * Create a serializer that calls the provided `nodeWriter` from
 * {@link ol.xml.serialize}. This can be used by the parent writer to have the
 * 'nodeWriter' called with an array of values when the `nodeWriter` was
 * designed to serialize a single item. An example would be a LineString
 * geometry writer, which could be reused for writing MultiLineString
 * geometries.
 * @param {function(this: T, Node, V, Array.<*>)}
 *     nodeWriter Node writer.
 * @param {T=} opt_this The object to use as `this` in `nodeWriter`.
 * @return {ol.xml.Serializer} Serializer.
 * @template T, V
 */
ol.xml.makeArraySerializer = function(nodeWriter, opt_this) {
  var serializersNS, nodeFactory;
  return function(node, value, objectStack) {
    if (serializersNS === undefined) {
      serializersNS = {};
      var serializers = {};
      serializers[node.localName] = nodeWriter;
      serializersNS[node.namespaceURI] = serializers;
      nodeFactory = ol.xml.makeSimpleNodeFactory(node.localName);
    }
    ol.xml.serialize(serializersNS, nodeFactory, value, objectStack);
  };
};


/**
 * Create a node factory which can use the `opt_keys` passed to
 * {@link ol.xml.serialize} or {@link ol.xml.pushSerializeAndPop} as node names,
 * or a fixed node name. The namespace of the created nodes can either be fixed,
 * or the parent namespace will be used.
 * @param {string=} opt_nodeName Fixed node name which will be used for all
 *     created nodes. If not provided, the 3rd argument to the resulting node
 *     factory needs to be provided and will be the nodeName.
 * @param {string=} opt_namespaceURI Fixed namespace URI which will be used for
 *     all created nodes. If not provided, the namespace of the parent node will
 *     be used.
 * @return {function(*, Array.<*>, string=): (Node|undefined)} Node factory.
 */
ol.xml.makeSimpleNodeFactory = function(opt_nodeName, opt_namespaceURI) {
  var fixedNodeName = opt_nodeName;
  return (
      /**
       * @param {*} value Value.
       * @param {Array.<*>} objectStack Object stack.
       * @param {string=} opt_nodeName Node name.
       * @return {Node} Node.
       */
      function(value, objectStack, opt_nodeName) {
        var context = objectStack[objectStack.length - 1];
        var node = context.node;
        goog.asserts.assert(ol.xml.isNode(node) || ol.xml.isDocument(node),
            'expected node %s to be a Node or a Document', node);
        var nodeName = fixedNodeName;
        if (nodeName === undefined) {
          nodeName = opt_nodeName;
        }
        var namespaceURI = opt_namespaceURI;
        if (opt_namespaceURI === undefined) {
          namespaceURI = node.namespaceURI;
        }
        goog.asserts.assert(nodeName !== undefined, 'nodeName was undefined');
        return ol.xml.createElementNS(namespaceURI, nodeName);
      }
  );
};


/**
 * A node factory that creates a node using the parent's `namespaceURI` and the
 * `nodeName` passed by {@link ol.xml.serialize} or
 * {@link ol.xml.pushSerializeAndPop} to the node factory.
 * @const
 * @type {function(*, Array.<*>, string=): (Node|undefined)}
 */
ol.xml.OBJECT_PROPERTY_NODE_FACTORY = ol.xml.makeSimpleNodeFactory();


/**
 * Create an array of `values` to be used with {@link ol.xml.serialize} or
 * {@link ol.xml.pushSerializeAndPop}, where `orderedKeys` has to be provided as
 * `opt_key` argument.
 * @param {Object.<string, V>} object Key-value pairs for the sequence. Keys can
 *     be a subset of the `orderedKeys`.
 * @param {Array.<string>} orderedKeys Keys in the order of the sequence.
 * @return {Array.<V>} Values in the order of the sequence. The resulting array
 *     has the same length as the `orderedKeys` array. Values that are not
 *     present in `object` will be `undefined` in the resulting array.
 * @template V
 */
ol.xml.makeSequence = function(object, orderedKeys) {
  var length = orderedKeys.length;
  var sequence = new Array(length);
  for (var i = 0; i < length; ++i) {
    sequence[i] = object[orderedKeys[i]];
  }
  return sequence;
};


/**
 * Create a namespaced structure, using the same values for each namespace.
 * This can be used as a starting point for versioned parsers, when only a few
 * values are version specific.
 * @param {Array.<string>} namespaceURIs Namespace URIs.
 * @param {T} structure Structure.
 * @param {Object.<string, T>=} opt_structureNS Namespaced structure to add to.
 * @return {Object.<string, T>} Namespaced structure.
 * @template T
 */
ol.xml.makeStructureNS = function(namespaceURIs, structure, opt_structureNS) {
  /**
   * @type {Object.<string, *>}
   */
  var structureNS = opt_structureNS !== undefined ? opt_structureNS : {};
  var i, ii;
  for (i = 0, ii = namespaceURIs.length; i < ii; ++i) {
    structureNS[namespaceURIs[i]] = structure;
  }
  return structureNS;
};


/**
 * Parse a node using the parsers and object stack.
 * @param {Object.<string, Object.<string, ol.xml.Parser>>} parsersNS
 *     Parsers by namespace.
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @param {*=} opt_this The object to use as `this`.
 */
ol.xml.parseNode = function(parsersNS, node, objectStack, opt_this) {
  var n;
  for (n = node.firstElementChild; n; n = n.nextElementSibling) {
    var parsers = parsersNS[n.namespaceURI];
    if (parsers !== undefined) {
      var parser = parsers[n.localName];
      if (parser !== undefined) {
        parser.call(opt_this, n, objectStack);
      }
    }
  }
};


/**
 * Push an object on top of the stack, parse and return the popped object.
 * @param {T} object Object.
 * @param {Object.<string, Object.<string, ol.xml.Parser>>} parsersNS
 *     Parsers by namespace.
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @param {*=} opt_this The object to use as `this`.
 * @return {T} Object.
 * @template T
 */
ol.xml.pushParseAndPop = function(
    object, parsersNS, node, objectStack, opt_this) {
  objectStack.push(object);
  ol.xml.parseNode(parsersNS, node, objectStack, opt_this);
  return objectStack.pop();
};


/**
 * Walk through an array of `values` and call a serializer for each value.
 * @param {Object.<string, Object.<string, ol.xml.Serializer>>} serializersNS
 *     Namespaced serializers.
 * @param {function(this: T, *, Array.<*>, (string|undefined)): (Node|undefined)} nodeFactory
 *     Node factory. The `nodeFactory` creates the node whose namespace and name
 *     will be used to choose a node writer from `serializersNS`. This
 *     separation allows us to decide what kind of node to create, depending on
 *     the value we want to serialize. An example for this would be different
 *     geometry writers based on the geometry type.
 * @param {Array.<*>} values Values to serialize. An example would be an array
 *     of {@link ol.Feature} instances.
 * @param {Array.<*>} objectStack Node stack.
 * @param {Array.<string>=} opt_keys Keys of the `values`. Will be passed to the
 *     `nodeFactory`. This is used for serializing object literals where the
 *     node name relates to the property key. The array length of `opt_keys` has
 *     to match the length of `values`. For serializing a sequence, `opt_keys`
 *     determines the order of the sequence.
 * @param {T=} opt_this The object to use as `this` for the node factory and
 *     serializers.
 * @template T
 */
ol.xml.serialize = function(
    serializersNS, nodeFactory, values, objectStack, opt_keys, opt_this) {
  var length = (opt_keys !== undefined ? opt_keys : values).length;
  var value, node;
  for (var i = 0; i < length; ++i) {
    value = values[i];
    if (value !== undefined) {
      node = nodeFactory.call(opt_this, value, objectStack,
          opt_keys !== undefined ? opt_keys[i] : undefined);
      if (node !== undefined) {
        serializersNS[node.namespaceURI][node.localName]
            .call(opt_this, node, value, objectStack);
      }
    }
  }
};


/**
 * @param {O} object Object.
 * @param {Object.<string, Object.<string, ol.xml.Serializer>>} serializersNS
 *     Namespaced serializers.
 * @param {function(this: T, *, Array.<*>, (string|undefined)): (Node|undefined)} nodeFactory
 *     Node factory. The `nodeFactory` creates the node whose namespace and name
 *     will be used to choose a node writer from `serializersNS`. This
 *     separation allows us to decide what kind of node to create, depending on
 *     the value we want to serialize. An example for this would be different
 *     geometry writers based on the geometry type.
 * @param {Array.<*>} values Values to serialize. An example would be an array
 *     of {@link ol.Feature} instances.
 * @param {Array.<*>} objectStack Node stack.
 * @param {Array.<string>=} opt_keys Keys of the `values`. Will be passed to the
 *     `nodeFactory`. This is used for serializing object literals where the
 *     node name relates to the property key. The array length of `opt_keys` has
 *     to match the length of `values`. For serializing a sequence, `opt_keys`
 *     determines the order of the sequence.
 * @param {T=} opt_this The object to use as `this` for the node factory and
 *     serializers.
 * @return {O|undefined} Object.
 * @template O, T
 */
ol.xml.pushSerializeAndPop = function(object,
    serializersNS, nodeFactory, values, objectStack, opt_keys, opt_this) {
  objectStack.push(object);
  ol.xml.serialize(
      serializersNS, nodeFactory, values, objectStack, opt_keys, opt_this);
  return objectStack.pop();
};

goog.provide('ol.FeatureLoader');
goog.provide('ol.FeatureUrlFunction');
goog.provide('ol.featureloader');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.net.EventType');
goog.require('goog.net.XhrIo');
goog.require('goog.net.XhrIo.ResponseType');
goog.require('ol.TileState');
goog.require('ol.VectorTile');
goog.require('ol.format.FormatType');
goog.require('ol.proj');
goog.require('ol.proj.Projection');
goog.require('ol.proj.Units');
goog.require('ol.xml');


/**
 * {@link ol.source.Vector} sources use a function of this type to load
 * features.
 *
 * This function takes an {@link ol.Extent} representing the area to be loaded,
 * a `{number}` representing the resolution (map units per pixel) and an
 * {@link ol.proj.Projection} for the projection  as arguments. `this` within
 * the function is bound to the {@link ol.source.Vector} it's called from.
 *
 * The function is responsible for loading the features and adding them to the
 * source.
 * @api
 * @typedef {function(this:ol.source.Vector, ol.Extent, number,
 *                    ol.proj.Projection)}
 */
ol.FeatureLoader;


/**
 * {@link ol.source.Vector} sources use a function of this type to get the url
 * to load features from.
 *
 * This function takes an {@link ol.Extent} representing the area to be loaded,
 * a `{number}` representing the resolution (map units per pixel) and an
 * {@link ol.proj.Projection} for the projection  as arguments and returns a
 * `{string}` representing the URL.
 * @api
 * @typedef {function(ol.Extent, number, ol.proj.Projection) : string}
 */
ol.FeatureUrlFunction;


/**
 * @param {string|ol.FeatureUrlFunction} url Feature URL service.
 * @param {ol.format.Feature} format Feature format.
 * @param {function(this:ol.VectorTile, Array.<ol.Feature>, ol.proj.Projection)|function(this:ol.source.Vector, Array.<ol.Feature>)} success
 *     Function called with the loaded features and optionally with the data
 *     projection. Called with the vector tile or source as `this`.
 * @param {function(this:ol.VectorTile)|function(this:ol.source.Vector)} failure
 *     Function called when loading failed. Called with the vector tile or
 *     source as `this`.
 * @return {ol.FeatureLoader} The feature loader.
 */
ol.featureloader.loadFeaturesXhr = function(url, format, success, failure) {
  return (
      /**
       * @param {ol.Extent} extent Extent.
       * @param {number} resolution Resolution.
       * @param {ol.proj.Projection} projection Projection.
       * @this {ol.source.Vector|ol.VectorTile}
       */
      function(extent, resolution, projection) {
        var xhrIo = new goog.net.XhrIo();
        xhrIo.setResponseType(
            format.getType() == ol.format.FormatType.ARRAY_BUFFER ?
                goog.net.XhrIo.ResponseType.ARRAY_BUFFER :
                goog.net.XhrIo.ResponseType.TEXT);
        goog.events.listen(xhrIo, goog.net.EventType.COMPLETE,
            /**
             * @param {Event} event Event.
             * @private
             * @this {ol.source.Vector}
             */
            function(event) {
              var xhrIo = event.target;
              goog.asserts.assertInstanceof(xhrIo, goog.net.XhrIo,
                  'event.target/xhrIo is an instance of goog.net.XhrIo');
              if (xhrIo.isSuccess()) {
                var type = format.getType();
                /** @type {Document|Node|Object|string|undefined} */
                var source;
                if (type == ol.format.FormatType.JSON) {
                  source = xhrIo.getResponseText();
                } else if (type == ol.format.FormatType.TEXT) {
                  source = xhrIo.getResponseText();
                } else if (type == ol.format.FormatType.XML) {
                  if (!goog.userAgent.IE) {
                    source = xhrIo.getResponseXml();
                  }
                  if (!source) {
                    source = ol.xml.parse(xhrIo.getResponseText());
                  }
                } else if (type == ol.format.FormatType.ARRAY_BUFFER) {
                  source = xhrIo.getResponse();
                } else {
                  goog.asserts.fail('unexpected format type');
                }
                if (source) {
                  success.call(this, format.readFeatures(source,
                      {featureProjection: projection}),
                      format.readProjection(source));
                } else {
                  goog.asserts.fail('undefined or null source');
                }
              } else {
                failure.call(this);
              }
              goog.dispose(xhrIo);
            }, false, this);
        if (goog.isFunction(url)) {
          xhrIo.send(url(extent, resolution, projection));
        } else {
          xhrIo.send(url);
        }

      });
};


/**
 * Create an XHR feature loader for a `url` and `format`. The feature loader
 * loads features (with XHR), parses the features, and adds them to the
 * vector tile.
 * @param {string|ol.FeatureUrlFunction} url Feature URL service.
 * @param {ol.format.Feature} format Feature format.
 * @return {ol.FeatureLoader} The feature loader.
 * @api
 */
ol.featureloader.tile = function(url, format) {
  return ol.featureloader.loadFeaturesXhr(url, format,
      /**
       * @param {Array.<ol.Feature>} features The loaded features.
       * @param {ol.proj.Projection} dataProjection Data projection.
       * @this {ol.VectorTile}
       */
      function(features, dataProjection) {
        var dataUnits = dataProjection.getUnits();
        if (dataUnits === ol.proj.Units.TILE_PIXELS) {
          var projection = new ol.proj.Projection({
            code: this.getProjection().getCode(),
            units: dataUnits
          });
          this.setProjection(projection);
        }
        this.setFeatures(features);
      },
      /**
       * @this {ol.VectorTile}
       */
      function() {
        this.setState(ol.TileState.ERROR);
      });
};


/**
 * Create an XHR feature loader for a `url` and `format`. The feature loader
 * loads features (with XHR), parses the features, and adds them to the
 * vector source.
 * @param {string|ol.FeatureUrlFunction} url Feature URL service.
 * @param {ol.format.Feature} format Feature format.
 * @return {ol.FeatureLoader} The feature loader.
 * @api
 */
ol.featureloader.xhr = function(url, format) {
  return ol.featureloader.loadFeaturesXhr(url, format,
      /**
       * @param {Array.<ol.Feature>} features The loaded features.
       * @param {ol.proj.Projection} dataProjection Data projection.
       * @this {ol.source.Vector}
       */
      function(features, dataProjection) {
        this.addFeatures(features);
      }, /* FIXME handle error */ ol.nullFunction);
};

goog.provide('ol.LoadingStrategy');
goog.provide('ol.loadingstrategy');

goog.require('ol.TileCoord');


/**
 * @typedef {function(ol.Extent, number): Array.<ol.Extent>}
 * @api
 */
ol.LoadingStrategy;


/**
 * Strategy function for loading all features with a single request.
 * @param {ol.Extent} extent Extent.
 * @param {number} resolution Resolution.
 * @return {Array.<ol.Extent>} Extents.
 * @api
 */
ol.loadingstrategy.all = function(extent, resolution) {
  return [[-Infinity, -Infinity, Infinity, Infinity]];
};


/**
 * Strategy function for loading features based on the view's extent and
 * resolution.
 * @param {ol.Extent} extent Extent.
 * @param {number} resolution Resolution.
 * @return {Array.<ol.Extent>} Extents.
 * @api
 */
ol.loadingstrategy.bbox = function(extent, resolution) {
  return [extent];
};


/**
 * Creates a strategy function for loading features based on a tile grid.
 * @param {ol.tilegrid.TileGrid} tileGrid Tile grid.
 * @return {function(ol.Extent, number): Array.<ol.Extent>} Loading strategy.
 * @api
 */
ol.loadingstrategy.tile = function(tileGrid) {
  return (
      /**
       * @param {ol.Extent} extent Extent.
       * @param {number} resolution Resolution.
       * @return {Array.<ol.Extent>} Extents.
       */
      function(extent, resolution) {
        var z = tileGrid.getZForResolution(resolution);
        var tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z);
        /** @type {Array.<ol.Extent>} */
        var extents = [];
        /** @type {ol.TileCoord} */
        var tileCoord = [z, 0, 0];
        for (tileCoord[1] = tileRange.minX; tileCoord[1] <= tileRange.maxX;
             ++tileCoord[1]) {
          for (tileCoord[2] = tileRange.minY; tileCoord[2] <= tileRange.maxY;
               ++tileCoord[2]) {
            extents.push(tileGrid.getTileCoordExtent(tileCoord));
          }
        }
        return extents;
      });
};

goog.provide('ol.ext.rbush');
/** @typedef {function(*)} */
ol.ext.rbush;
(function() {
var exports = {};
var module = {exports: exports};
var define;
/**
 * @fileoverview
 * @suppress {accessControls, ambiguousFunctionDecl, checkDebuggerStatement, checkRegExp, checkTypes, checkVars, const, constantProperty, deprecated, duplicate, es5Strict, fileoverviewTags, missingProperties, nonStandardJsDocs, strictModuleDepCheck, suspiciousCode, undefinedNames, undefinedVars, unknownDefines, uselessCode, visibility}
 */
/*
 (c) 2015, Vladimir Agafonkin
 RBush, a JavaScript library for high-performance 2D spatial indexing of points and rectangles.
 https://github.com/mourner/rbush
*/

(function () {
'use strict';

function rbush(maxEntries, format) {

    // jshint newcap: false, validthis: true
    if (!(this instanceof rbush)) return new rbush(maxEntries, format);

    // max entries in a node is 9 by default; min node fill is 40% for best performance
    this._maxEntries = Math.max(4, maxEntries || 9);
    this._minEntries = Math.max(2, Math.ceil(this._maxEntries * 0.4));

    if (format) {
        this._initFormat(format);
    }

    this.clear();
}

rbush.prototype = {

    all: function () {
        return this._all(this.data, []);
    },

    search: function (bbox) {

        var node = this.data,
            result = [],
            toBBox = this.toBBox;

        if (!intersects(bbox, node.bbox)) return result;

        var nodesToSearch = [],
            i, len, child, childBBox;

        while (node) {
            for (i = 0, len = node.children.length; i < len; i++) {

                child = node.children[i];
                childBBox = node.leaf ? toBBox(child) : child.bbox;

                if (intersects(bbox, childBBox)) {
                    if (node.leaf) result.push(child);
                    else if (contains(bbox, childBBox)) this._all(child, result);
                    else nodesToSearch.push(child);
                }
            }
            node = nodesToSearch.pop();
        }

        return result;
    },

    collides: function (bbox) {

        var node = this.data,
            toBBox = this.toBBox;

        if (!intersects(bbox, node.bbox)) return false;

        var nodesToSearch = [],
            i, len, child, childBBox;

        while (node) {
            for (i = 0, len = node.children.length; i < len; i++) {

                child = node.children[i];
                childBBox = node.leaf ? toBBox(child) : child.bbox;

                if (intersects(bbox, childBBox)) {
                    if (node.leaf || contains(bbox, childBBox)) return true;
                    nodesToSearch.push(child);
                }
            }
            node = nodesToSearch.pop();
        }

        return false;
    },

    load: function (data) {
        if (!(data && data.length)) return this;

        if (data.length < this._minEntries) {
            for (var i = 0, len = data.length; i < len; i++) {
                this.insert(data[i]);
            }
            return this;
        }

        // recursively build the tree with the given data from stratch using OMT algorithm
        var node = this._build(data.slice(), 0, data.length - 1, 0);

        if (!this.data.children.length) {
            // save as is if tree is empty
            this.data = node;

        } else if (this.data.height === node.height) {
            // split root if trees have the same height
            this._splitRoot(this.data, node);

        } else {
            if (this.data.height < node.height) {
                // swap trees if inserted one is bigger
                var tmpNode = this.data;
                this.data = node;
                node = tmpNode;
            }

            // insert the small tree into the large tree at appropriate level
            this._insert(node, this.data.height - node.height - 1, true);
        }

        return this;
    },

    insert: function (item) {
        if (item) this._insert(item, this.data.height - 1);
        return this;
    },

    clear: function () {
        this.data = {
            children: [],
            height: 1,
            bbox: empty(),
            leaf: true
        };
        return this;
    },

    remove: function (item) {
        if (!item) return this;

        var node = this.data,
            bbox = this.toBBox(item),
            path = [],
            indexes = [],
            i, parent, index, goingUp;

        // depth-first iterative tree traversal
        while (node || path.length) {

            if (!node) { // go up
                node = path.pop();
                parent = path[path.length - 1];
                i = indexes.pop();
                goingUp = true;
            }

            if (node.leaf) { // check current node
                index = node.children.indexOf(item);

                if (index !== -1) {
                    // item found, remove the item and condense tree upwards
                    node.children.splice(index, 1);
                    path.push(node);
                    this._condense(path);
                    return this;
                }
            }

            if (!goingUp && !node.leaf && contains(node.bbox, bbox)) { // go down
                path.push(node);
                indexes.push(i);
                i = 0;
                parent = node;
                node = node.children[0];

            } else if (parent) { // go right
                i++;
                node = parent.children[i];
                goingUp = false;

            } else node = null; // nothing found
        }

        return this;
    },

    toBBox: function (item) { return item; },

    compareMinX: function (a, b) { return a[0] - b[0]; },
    compareMinY: function (a, b) { return a[1] - b[1]; },

    toJSON: function () { return this.data; },

    fromJSON: function (data) {
        this.data = data;
        return this;
    },

    _all: function (node, result) {
        var nodesToSearch = [];
        while (node) {
            if (node.leaf) result.push.apply(result, node.children);
            else nodesToSearch.push.apply(nodesToSearch, node.children);

            node = nodesToSearch.pop();
        }
        return result;
    },

    _build: function (items, left, right, height) {

        var N = right - left + 1,
            M = this._maxEntries,
            node;

        if (N <= M) {
            // reached leaf level; return leaf
            node = {
                children: items.slice(left, right + 1),
                height: 1,
                bbox: null,
                leaf: true
            };
            calcBBox(node, this.toBBox);
            return node;
        }

        if (!height) {
            // target height of the bulk-loaded tree
            height = Math.ceil(Math.log(N) / Math.log(M));

            // target number of root entries to maximize storage utilization
            M = Math.ceil(N / Math.pow(M, height - 1));
        }

        node = {
            children: [],
            height: height,
            bbox: null,
            leaf: false
        };

        // split the items into M mostly square tiles

        var N2 = Math.ceil(N / M),
            N1 = N2 * Math.ceil(Math.sqrt(M)),
            i, j, right2, right3;

        multiSelect(items, left, right, N1, this.compareMinX);

        for (i = left; i <= right; i += N1) {

            right2 = Math.min(i + N1 - 1, right);

            multiSelect(items, i, right2, N2, this.compareMinY);

            for (j = i; j <= right2; j += N2) {

                right3 = Math.min(j + N2 - 1, right2);

                // pack each entry recursively
                node.children.push(this._build(items, j, right3, height - 1));
            }
        }

        calcBBox(node, this.toBBox);

        return node;
    },

    _chooseSubtree: function (bbox, node, level, path) {

        var i, len, child, targetNode, area, enlargement, minArea, minEnlargement;

        while (true) {
            path.push(node);

            if (node.leaf || path.length - 1 === level) break;

            minArea = minEnlargement = Infinity;

            for (i = 0, len = node.children.length; i < len; i++) {
                child = node.children[i];
                area = bboxArea(child.bbox);
                enlargement = enlargedArea(bbox, child.bbox) - area;

                // choose entry with the least area enlargement
                if (enlargement < minEnlargement) {
                    minEnlargement = enlargement;
                    minArea = area < minArea ? area : minArea;
                    targetNode = child;

                } else if (enlargement === minEnlargement) {
                    // otherwise choose one with the smallest area
                    if (area < minArea) {
                        minArea = area;
                        targetNode = child;
                    }
                }
            }

            node = targetNode;
        }

        return node;
    },

    _insert: function (item, level, isNode) {

        var toBBox = this.toBBox,
            bbox = isNode ? item.bbox : toBBox(item),
            insertPath = [];

        // find the best node for accommodating the item, saving all nodes along the path too
        var node = this._chooseSubtree(bbox, this.data, level, insertPath);

        // put the item into the node
        node.children.push(item);
        extend(node.bbox, bbox);

        // split on node overflow; propagate upwards if necessary
        while (level >= 0) {
            if (insertPath[level].children.length > this._maxEntries) {
                this._split(insertPath, level);
                level--;
            } else break;
        }

        // adjust bboxes along the insertion path
        this._adjustParentBBoxes(bbox, insertPath, level);
    },

    // split overflowed node into two
    _split: function (insertPath, level) {

        var node = insertPath[level],
            M = node.children.length,
            m = this._minEntries;

        this._chooseSplitAxis(node, m, M);

        var splitIndex = this._chooseSplitIndex(node, m, M);

        var newNode = {
            children: node.children.splice(splitIndex, node.children.length - splitIndex),
            height: node.height,
            bbox: null,
            leaf: false
        };

        if (node.leaf) newNode.leaf = true;

        calcBBox(node, this.toBBox);
        calcBBox(newNode, this.toBBox);

        if (level) insertPath[level - 1].children.push(newNode);
        else this._splitRoot(node, newNode);
    },

    _splitRoot: function (node, newNode) {
        // split root node
        this.data = {
            children: [node, newNode],
            height: node.height + 1,
            bbox: null,
            leaf: false
        };
        calcBBox(this.data, this.toBBox);
    },

    _chooseSplitIndex: function (node, m, M) {

        var i, bbox1, bbox2, overlap, area, minOverlap, minArea, index;

        minOverlap = minArea = Infinity;

        for (i = m; i <= M - m; i++) {
            bbox1 = distBBox(node, 0, i, this.toBBox);
            bbox2 = distBBox(node, i, M, this.toBBox);

            overlap = intersectionArea(bbox1, bbox2);
            area = bboxArea(bbox1) + bboxArea(bbox2);

            // choose distribution with minimum overlap
            if (overlap < minOverlap) {
                minOverlap = overlap;
                index = i;

                minArea = area < minArea ? area : minArea;

            } else if (overlap === minOverlap) {
                // otherwise choose distribution with minimum area
                if (area < minArea) {
                    minArea = area;
                    index = i;
                }
            }
        }

        return index;
    },

    // sorts node children by the best axis for split
    _chooseSplitAxis: function (node, m, M) {

        var compareMinX = node.leaf ? this.compareMinX : compareNodeMinX,
            compareMinY = node.leaf ? this.compareMinY : compareNodeMinY,
            xMargin = this._allDistMargin(node, m, M, compareMinX),
            yMargin = this._allDistMargin(node, m, M, compareMinY);

        // if total distributions margin value is minimal for x, sort by minX,
        // otherwise it's already sorted by minY
        if (xMargin < yMargin) node.children.sort(compareMinX);
    },

    // total margin of all possible split distributions where each node is at least m full
    _allDistMargin: function (node, m, M, compare) {

        node.children.sort(compare);

        var toBBox = this.toBBox,
            leftBBox = distBBox(node, 0, m, toBBox),
            rightBBox = distBBox(node, M - m, M, toBBox),
            margin = bboxMargin(leftBBox) + bboxMargin(rightBBox),
            i, child;

        for (i = m; i < M - m; i++) {
            child = node.children[i];
            extend(leftBBox, node.leaf ? toBBox(child) : child.bbox);
            margin += bboxMargin(leftBBox);
        }

        for (i = M - m - 1; i >= m; i--) {
            child = node.children[i];
            extend(rightBBox, node.leaf ? toBBox(child) : child.bbox);
            margin += bboxMargin(rightBBox);
        }

        return margin;
    },

    _adjustParentBBoxes: function (bbox, path, level) {
        // adjust bboxes along the given tree path
        for (var i = level; i >= 0; i--) {
            extend(path[i].bbox, bbox);
        }
    },

    _condense: function (path) {
        // go through the path, removing empty nodes and updating bboxes
        for (var i = path.length - 1, siblings; i >= 0; i--) {
            if (path[i].children.length === 0) {
                if (i > 0) {
                    siblings = path[i - 1].children;
                    siblings.splice(siblings.indexOf(path[i]), 1);

                } else this.clear();

            } else calcBBox(path[i], this.toBBox);
        }
    },

    _initFormat: function (format) {
        // data format (minX, minY, maxX, maxY accessors)

        // uses eval-type function compilation instead of just accepting a toBBox function
        // because the algorithms are very sensitive to sorting functions performance,
        // so they should be dead simple and without inner calls

        // jshint evil: true

        var compareArr = ['return a', ' - b', ';'];

        this.compareMinX = new Function('a', 'b', compareArr.join(format[0]));
        this.compareMinY = new Function('a', 'b', compareArr.join(format[1]));

        this.toBBox = new Function('a', 'return [a' + format.join(', a') + '];');
    }
};


// calculate node's bbox from bboxes of its children
function calcBBox(node, toBBox) {
    node.bbox = distBBox(node, 0, node.children.length, toBBox);
}

// min bounding rectangle of node children from k to p-1
function distBBox(node, k, p, toBBox) {
    var bbox = empty();

    for (var i = k, child; i < p; i++) {
        child = node.children[i];
        extend(bbox, node.leaf ? toBBox(child) : child.bbox);
    }

    return bbox;
}

function empty() { return [Infinity, Infinity, -Infinity, -Infinity]; }

function extend(a, b) {
    a[0] = Math.min(a[0], b[0]);
    a[1] = Math.min(a[1], b[1]);
    a[2] = Math.max(a[2], b[2]);
    a[3] = Math.max(a[3], b[3]);
    return a;
}

function compareNodeMinX(a, b) { return a.bbox[0] - b.bbox[0]; }
function compareNodeMinY(a, b) { return a.bbox[1] - b.bbox[1]; }

function bboxArea(a)   { return (a[2] - a[0]) * (a[3] - a[1]); }
function bboxMargin(a) { return (a[2] - a[0]) + (a[3] - a[1]); }

function enlargedArea(a, b) {
    return (Math.max(b[2], a[2]) - Math.min(b[0], a[0])) *
           (Math.max(b[3], a[3]) - Math.min(b[1], a[1]));
}

function intersectionArea(a, b) {
    var minX = Math.max(a[0], b[0]),
        minY = Math.max(a[1], b[1]),
        maxX = Math.min(a[2], b[2]),
        maxY = Math.min(a[3], b[3]);

    return Math.max(0, maxX - minX) *
           Math.max(0, maxY - minY);
}

function contains(a, b) {
    return a[0] <= b[0] &&
           a[1] <= b[1] &&
           b[2] <= a[2] &&
           b[3] <= a[3];
}

function intersects(a, b) {
    return b[0] <= a[2] &&
           b[1] <= a[3] &&
           b[2] >= a[0] &&
           b[3] >= a[1];
}

// sort an array so that items come in groups of n unsorted items, with groups sorted between each other;
// combines selection algorithm with binary divide & conquer approach

function multiSelect(arr, left, right, n, compare) {
    var stack = [left, right],
        mid;

    while (stack.length) {
        right = stack.pop();
        left = stack.pop();

        if (right - left <= n) continue;

        mid = left + Math.ceil((right - left) / n / 2) * n;
        select(arr, left, right, mid, compare);

        stack.push(left, mid, mid, right);
    }
}

// Floyd-Rivest selection algorithm:
// sort an array between left and right (inclusive) so that the smallest k elements come first (unordered)
function select(arr, left, right, k, compare) {
    var n, i, z, s, sd, newLeft, newRight, t, j;

    while (right > left) {
        if (right - left > 600) {
            n = right - left + 1;
            i = k - left + 1;
            z = Math.log(n);
            s = 0.5 * Math.exp(2 * z / 3);
            sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (i - n / 2 < 0 ? -1 : 1);
            newLeft = Math.max(left, Math.floor(k - i * s / n + sd));
            newRight = Math.min(right, Math.floor(k + (n - i) * s / n + sd));
            select(arr, newLeft, newRight, k, compare);
        }

        t = arr[k];
        i = left;
        j = right;

        swap(arr, left, k);
        if (compare(arr[right], t) > 0) swap(arr, left, right);

        while (i < j) {
            swap(arr, i, j);
            i++;
            j--;
            while (compare(arr[i], t) < 0) i++;
            while (compare(arr[j], t) > 0) j--;
        }

        if (compare(arr[left], t) === 0) swap(arr, left, j);
        else {
            j++;
            swap(arr, j, right);
        }

        if (j <= k) left = j + 1;
        if (k <= j) right = j - 1;
    }
}

function swap(arr, i, j) {
    var tmp = arr[i];
    arr[i] = arr[j];
    arr[j] = tmp;
}


// export as AMD/CommonJS module or global variable
if (typeof define === 'function' && define.amd) define('rbush', function () { return rbush; });
else if (typeof module !== 'undefined') module.exports = rbush;
else if (typeof self !== 'undefined') self.rbush = rbush;
else window.rbush = rbush;

})();

ol.ext.rbush = module.exports;
})();

goog.provide('ol.structs.RBush');

goog.require('goog.asserts');
goog.require('goog.object');
goog.require('ol.ext.rbush');
goog.require('ol.extent');


/**
 * Wrapper around the RBush by Vladimir Agafonkin.
 *
 * @constructor
 * @param {number=} opt_maxEntries Max entries.
 * @see https://github.com/mourner/rbush
 * @struct
 * @template T
 */
ol.structs.RBush = function(opt_maxEntries) {

  /**
   * @private
   */
  this.rbush_ = ol.ext.rbush(opt_maxEntries);

  /**
   * A mapping between the objects added to this rbush wrapper
   * and the objects that are actually added to the internal rbush.
   * @private
   * @type {Object.<number, Object>}
   */
  this.items_ = {};

  if (goog.DEBUG) {
    /**
     * @private
     * @type {number}
     */
    this.readers_ = 0;
  }
};


/**
 * Insert a value into the RBush.
 * @param {ol.Extent} extent Extent.
 * @param {T} value Value.
 */
ol.structs.RBush.prototype.insert = function(extent, value) {
  if (goog.DEBUG && this.readers_) {
    throw new Error('Can not insert value while reading');
  }
  var item = [
    extent[0],
    extent[1],
    extent[2],
    extent[3],
    value
  ];
  this.rbush_.insert(item);
  // remember the object that was added to the internal rbush
  goog.asserts.assert(!(goog.getUid(value) in this.items_),
      'uid (%s) of value (%s) already exists', goog.getUid(value), value);
  this.items_[goog.getUid(value)] = item;
};


/**
 * Bulk-insert values into the RBush.
 * @param {Array.<ol.Extent>} extents Extents.
 * @param {Array.<T>} values Values.
 */
ol.structs.RBush.prototype.load = function(extents, values) {
  if (goog.DEBUG && this.readers_) {
    throw new Error('Can not insert values while reading');
  }
  goog.asserts.assert(extents.length === values.length,
      'extens and values must have same length (%s === %s)',
      extents.length, values.length);

  var items = new Array(values.length);
  for (var i = 0, l = values.length; i < l; i++) {
    var extent = extents[i];
    var value = values[i];

    var item = [
      extent[0],
      extent[1],
      extent[2],
      extent[3],
      value
    ];
    items[i] = item;
    goog.asserts.assert(!(goog.getUid(value) in this.items_),
        'uid (%s) of value (%s) already exists', goog.getUid(value), value);
    this.items_[goog.getUid(value)] = item;
  }
  this.rbush_.load(items);
};


/**
 * Remove a value from the RBush.
 * @param {T} value Value.
 * @return {boolean} Removed.
 */
ol.structs.RBush.prototype.remove = function(value) {
  if (goog.DEBUG && this.readers_) {
    throw new Error('Can not remove value while reading');
  }
  var uid = goog.getUid(value);
  goog.asserts.assert(uid in this.items_,
      'uid (%s) of value (%s) does not exist', uid, value);

  // get the object in which the value was wrapped when adding to the
  // internal rbush. then use that object to do the removal.
  var item = this.items_[uid];
  delete this.items_[uid];
  return this.rbush_.remove(item) !== null;
};


/**
 * Update the extent of a value in the RBush.
 * @param {ol.Extent} extent Extent.
 * @param {T} value Value.
 */
ol.structs.RBush.prototype.update = function(extent, value) {
  var uid = goog.getUid(value);
  goog.asserts.assert(uid in this.items_,
      'uid (%s) of value (%s) does not exist', uid, value);

  var item = this.items_[uid];
  if (!ol.extent.equals(item.slice(0, 4), extent)) {
    if (goog.DEBUG && this.readers_) {
      throw new Error('Can not update extent while reading');
    }
    this.remove(value);
    this.insert(extent, value);
  }
};


/**
 * Return all values in the RBush.
 * @return {Array.<T>} All.
 */
ol.structs.RBush.prototype.getAll = function() {
  var items = this.rbush_.all();
  return items.map(function(item) {
    return item[4];
  });
};


/**
 * Return all values in the given extent.
 * @param {ol.Extent} extent Extent.
 * @return {Array.<T>} All in extent.
 */
ol.structs.RBush.prototype.getInExtent = function(extent) {
  var items = this.rbush_.search(extent);
  return items.map(function(item) {
    return item[4];
  });
};


/**
 * Calls a callback function with each value in the tree.
 * If the callback returns a truthy value, this value is returned without
 * checking the rest of the tree.
 * @param {function(this: S, T): *} callback Callback.
 * @param {S=} opt_this The object to use as `this` in `callback`.
 * @return {*} Callback return value.
 * @template S
 */
ol.structs.RBush.prototype.forEach = function(callback, opt_this) {
  if (goog.DEBUG) {
    ++this.readers_;
    try {
      return this.forEach_(this.getAll(), callback, opt_this);
    } finally {
      --this.readers_;
    }
  } else {
    return this.forEach_(this.getAll(), callback, opt_this);
  }
};


/**
 * Calls a callback function with each value in the provided extent.
 * @param {ol.Extent} extent Extent.
 * @param {function(this: S, T): *} callback Callback.
 * @param {S=} opt_this The object to use as `this` in `callback`.
 * @return {*} Callback return value.
 * @template S
 */
ol.structs.RBush.prototype.forEachInExtent = function(extent, callback, opt_this) {
  if (goog.DEBUG) {
    ++this.readers_;
    try {
      return this.forEach_(this.getInExtent(extent), callback, opt_this);
    } finally {
      --this.readers_;
    }
  } else {
    return this.forEach_(this.getInExtent(extent), callback, opt_this);
  }
};


/**
 * @param {Array.<T>} values Values.
 * @param {function(this: S, T): *} callback Callback.
 * @param {S=} opt_this The object to use as `this` in `callback`.
 * @private
 * @return {*} Callback return value.
 * @template S
 */
ol.structs.RBush.prototype.forEach_ = function(values, callback, opt_this) {
  var result;
  for (var i = 0, l = values.length; i < l; i++) {
    result = callback.call(opt_this, values[i]);
    if (result) {
      return result;
    }
  }
  return result;
};


/**
 * @return {boolean} Is empty.
 */
ol.structs.RBush.prototype.isEmpty = function() {
  return goog.object.isEmpty(this.items_);
};


/**
 * Remove all values from the RBush.
 */
ol.structs.RBush.prototype.clear = function() {
  this.rbush_.clear();
  this.items_ = {};
};


/**
 * @param {ol.Extent=} opt_extent Extent.
 * @return {!ol.Extent} Extent.
 */
ol.structs.RBush.prototype.getExtent = function(opt_extent) {
  // FIXME add getExtent() to rbush
  return this.rbush_.data.bbox;
};

// FIXME bulk feature upload - suppress events
// FIXME make change-detection more refined (notably, geometry hint)

goog.provide('ol.source.Vector');
goog.provide('ol.source.VectorEvent');
goog.provide('ol.source.VectorEventType');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.EventType');
goog.require('goog.object');
goog.require('ol');
goog.require('ol.Collection');
goog.require('ol.CollectionEventType');
goog.require('ol.Extent');
goog.require('ol.Feature');
goog.require('ol.FeatureLoader');
goog.require('ol.LoadingStrategy');
goog.require('ol.ObjectEventType');
goog.require('ol.extent');
goog.require('ol.featureloader');
goog.require('ol.loadingstrategy');
goog.require('ol.proj');
goog.require('ol.source.Source');
goog.require('ol.source.State');
goog.require('ol.structs.RBush');


/**
 * @enum {string}
 */
ol.source.VectorEventType = {
  /**
   * Triggered when a feature is added to the source.
   * @event ol.source.VectorEvent#addfeature
   * @api stable
   */
  ADDFEATURE: 'addfeature',

  /**
   * Triggered when a feature is updated.
   * @event ol.source.VectorEvent#changefeature
   * @api
   */
  CHANGEFEATURE: 'changefeature',

  /**
   * Triggered when the clear method is called on the source.
   * @event ol.source.VectorEvent#clear
   * @api
   */
  CLEAR: 'clear',

  /**
   * Triggered when a feature is removed from the source.
   * See {@link ol.source.Vector#clear source.clear()} for exceptions.
   * @event ol.source.VectorEvent#removefeature
   * @api stable
   */
  REMOVEFEATURE: 'removefeature'
};


/**
 * @classdesc
 * Provides a source of features for vector layers. Vector features provided
 * by this source are suitable for editing. See {@link ol.source.VectorTile} for
 * vector data that is optimized for rendering.
 *
 * @constructor
 * @extends {ol.source.Source}
 * @fires ol.source.VectorEvent
 * @param {olx.source.VectorOptions=} opt_options Vector source options.
 * @api stable
 */
ol.source.Vector = function(opt_options) {

  var options = opt_options || {};

  goog.base(this, {
    attributions: options.attributions,
    logo: options.logo,
    projection: undefined,
    state: ol.source.State.READY,
    wrapX: options.wrapX !== undefined ? options.wrapX : true
  });

  /**
   * @private
   * @type {ol.FeatureLoader}
   */
  this.loader_ = ol.nullFunction;

  if (options.loader !== undefined) {
    this.loader_ = options.loader;
  } else if (options.url !== undefined) {
    goog.asserts.assert(options.format !== undefined,
        'format must be set when url is set');
    // create a XHR feature loader for "url" and "format"
    this.loader_ = ol.featureloader.xhr(options.url, options.format);
  }

  /**
   * @private
   * @type {ol.LoadingStrategy}
   */
  this.strategy_ = options.strategy !== undefined ? options.strategy :
      ol.loadingstrategy.all;

  var useSpatialIndex =
      options.useSpatialIndex !== undefined ? options.useSpatialIndex : true;

  /**
   * @private
   * @type {ol.structs.RBush.<ol.Feature>}
   */
  this.featuresRtree_ = useSpatialIndex ? new ol.structs.RBush() : null;

  /**
   * @private
   * @type {ol.structs.RBush.<{extent: ol.Extent}>}
   */
  this.loadedExtentsRtree_ = new ol.structs.RBush();

  /**
   * @private
   * @type {Object.<string, ol.Feature>}
   */
  this.nullGeometryFeatures_ = {};

  /**
   * A lookup of features by id (the return from feature.getId()).
   * @private
   * @type {Object.<string, ol.Feature>}
   */
  this.idIndex_ = {};

  /**
   * A lookup of features without id (keyed by goog.getUid(feature)).
   * @private
   * @type {Object.<string, ol.Feature>}
   */
  this.undefIdIndex_ = {};

  /**
   * @private
   * @type {Object.<string, Array.<goog.events.Key>>}
   */
  this.featureChangeKeys_ = {};

  /**
   * @private
   * @type {ol.Collection.<ol.Feature>}
   */
  this.featuresCollection_ = null;

  var collection, features;
  if (options.features instanceof ol.Collection) {
    collection = options.features;
    features = collection.getArray();
  } else if (goog.isArray(options.features)) {
    features = options.features;
  }
  if (!useSpatialIndex && collection === undefined) {
    collection = new ol.Collection(features);
  }
  if (features !== undefined) {
    this.addFeaturesInternal(features);
  }
  if (collection !== undefined) {
    this.bindFeaturesCollection_(collection);
  }

};
goog.inherits(ol.source.Vector, ol.source.Source);


/**
 * Add a single feature to the source.  If you want to add a batch of features
 * at once, call {@link ol.source.Vector#addFeatures source.addFeatures()}
 * instead.
 * @param {ol.Feature} feature Feature to add.
 * @api stable
 */
ol.source.Vector.prototype.addFeature = function(feature) {
  this.addFeatureInternal(feature);
  this.changed();
};


/**
 * Add a feature without firing a `change` event.
 * @param {ol.Feature} feature Feature.
 * @protected
 */
ol.source.Vector.prototype.addFeatureInternal = function(feature) {
  var featureKey = goog.getUid(feature).toString();

  if (!this.addToIndex_(featureKey, feature)) {
    return;
  }

  this.setupChangeEvents_(featureKey, feature);

  var geometry = feature.getGeometry();
  if (geometry) {
    var extent = geometry.getExtent();
    if (this.featuresRtree_) {
      this.featuresRtree_.insert(extent, feature);
    }
  } else {
    this.nullGeometryFeatures_[featureKey] = feature;
  }

  this.dispatchEvent(
      new ol.source.VectorEvent(ol.source.VectorEventType.ADDFEATURE, feature));
};


/**
 * @param {string} featureKey Unique identifier for the feature.
 * @param {ol.Feature} feature The feature.
 * @private
 */
ol.source.Vector.prototype.setupChangeEvents_ = function(featureKey, feature) {
  goog.asserts.assert(!(featureKey in this.featureChangeKeys_),
      'key (%s) not yet registered in featureChangeKey', featureKey);
  this.featureChangeKeys_[featureKey] = [
    goog.events.listen(feature,
        goog.events.EventType.CHANGE,
        this.handleFeatureChange_, false, this),
    goog.events.listen(feature,
        ol.ObjectEventType.PROPERTYCHANGE,
        this.handleFeatureChange_, false, this)
  ];
};


/**
 * @param {string} featureKey Unique identifier for the feature.
 * @param {ol.Feature} feature The feature.
 * @return {boolean} The feature is "valid", in the sense that it is also a
 *     candidate for insertion into the Rtree.
 * @private
 */
ol.source.Vector.prototype.addToIndex_ = function(featureKey, feature) {
  var valid = true;
  var id = feature.getId();
  if (id !== undefined) {
    if (!(id.toString() in this.idIndex_)) {
      this.idIndex_[id.toString()] = feature;
    } else {
      valid = false;
    }
  } else {
    goog.asserts.assert(!(featureKey in this.undefIdIndex_),
        'Feature already added to the source');
    this.undefIdIndex_[featureKey] = feature;
  }
  return valid;
};


/**
 * Add a batch of features to the source.
 * @param {Array.<ol.Feature>} features Features to add.
 * @api stable
 */
ol.source.Vector.prototype.addFeatures = function(features) {
  this.addFeaturesInternal(features);
  this.changed();
};


/**
 * Add features without firing a `change` event.
 * @param {Array.<ol.Feature>} features Features.
 * @protected
 */
ol.source.Vector.prototype.addFeaturesInternal = function(features) {
  var featureKey, i, length, feature;

  var extents = [];
  var newFeatures = [];
  var geometryFeatures = [];

  for (i = 0, length = features.length; i < length; i++) {
    feature = features[i];
    featureKey = goog.getUid(feature).toString();
    if (this.addToIndex_(featureKey, feature)) {
      newFeatures.push(feature);
    }
  }

  for (i = 0, length = newFeatures.length; i < length; i++) {
    feature = newFeatures[i];
    featureKey = goog.getUid(feature).toString();
    this.setupChangeEvents_(featureKey, feature);

    var geometry = feature.getGeometry();
    if (geometry) {
      var extent = geometry.getExtent();
      extents.push(extent);
      geometryFeatures.push(feature);
    } else {
      this.nullGeometryFeatures_[featureKey] = feature;
    }
  }
  if (this.featuresRtree_) {
    this.featuresRtree_.load(extents, geometryFeatures);
  }

  for (i = 0, length = newFeatures.length; i < length; i++) {
    this.dispatchEvent(new ol.source.VectorEvent(
        ol.source.VectorEventType.ADDFEATURE, newFeatures[i]));
  }
};


/**
 * @param {!ol.Collection.<ol.Feature>} collection Collection.
 * @private
 */
ol.source.Vector.prototype.bindFeaturesCollection_ = function(collection) {
  goog.asserts.assert(!this.featuresCollection_,
      'bindFeaturesCollection can only be called once');
  var modifyingCollection = false;
  goog.events.listen(this, ol.source.VectorEventType.ADDFEATURE,
      function(evt) {
        if (!modifyingCollection) {
          modifyingCollection = true;
          collection.push(evt.feature);
          modifyingCollection = false;
        }
      });
  goog.events.listen(this, ol.source.VectorEventType.REMOVEFEATURE,
      function(evt) {
        if (!modifyingCollection) {
          modifyingCollection = true;
          collection.remove(evt.feature);
          modifyingCollection = false;
        }
      });
  goog.events.listen(collection, ol.CollectionEventType.ADD,
      function(evt) {
        if (!modifyingCollection) {
          var feature = evt.element;
          goog.asserts.assertInstanceof(feature, ol.Feature);
          modifyingCollection = true;
          this.addFeature(feature);
          modifyingCollection = false;
        }
      }, false, this);
  goog.events.listen(collection, ol.CollectionEventType.REMOVE,
      function(evt) {
        if (!modifyingCollection) {
          var feature = evt.element;
          goog.asserts.assertInstanceof(feature, ol.Feature);
          modifyingCollection = true;
          this.removeFeature(feature);
          modifyingCollection = false;
        }
      }, false, this);
  this.featuresCollection_ = collection;
};


/**
 * Remove all features from the source.
 * @param {boolean=} opt_fast Skip dispatching of {@link removefeature} events.
 * @api stable
 */
ol.source.Vector.prototype.clear = function(opt_fast) {
  if (opt_fast) {
    for (var featureId in this.featureChangeKeys_) {
      var keys = this.featureChangeKeys_[featureId];
      keys.forEach(goog.events.unlistenByKey);
    }
    if (!this.featuresCollection_) {
      this.featureChangeKeys_ = {};
      this.idIndex_ = {};
      this.undefIdIndex_ = {};
    }
  } else {
    var rmFeatureInternal = this.removeFeatureInternal;
    if (this.featuresRtree_) {
      this.featuresRtree_.forEach(rmFeatureInternal, this);
      goog.object.forEach(this.nullGeometryFeatures_, rmFeatureInternal, this);
    }
  }
  if (this.featuresCollection_) {
    this.featuresCollection_.clear();
  }
  goog.asserts.assert(goog.object.isEmpty(this.featureChangeKeys_),
      'featureChangeKeys is an empty object now');
  goog.asserts.assert(goog.object.isEmpty(this.idIndex_),
      'idIndex is an empty object now');
  goog.asserts.assert(goog.object.isEmpty(this.undefIdIndex_),
      'undefIdIndex is an empty object now');

  if (this.featuresRtree_) {
    this.featuresRtree_.clear();
  }
  this.loadedExtentsRtree_.clear();
  this.nullGeometryFeatures_ = {};

  var clearEvent = new ol.source.VectorEvent(ol.source.VectorEventType.CLEAR);
  this.dispatchEvent(clearEvent);
  this.changed();
};


/**
 * Iterate through all features on the source, calling the provided callback
 * with each one.  If the callback returns any "truthy" value, iteration will
 * stop and the function will return the same value.
 *
 * @param {function(this: T, ol.Feature): S} callback Called with each feature
 *     on the source.  Return a truthy value to stop iteration.
 * @param {T=} opt_this The object to use as `this` in the callback.
 * @return {S|undefined} The return value from the last call to the callback.
 * @template T,S
 * @api stable
 */
ol.source.Vector.prototype.forEachFeature = function(callback, opt_this) {
  if (this.featuresRtree_) {
    return this.featuresRtree_.forEach(callback, opt_this);
  } else if (this.featuresCollection_) {
    return this.featuresCollection_.forEach(callback, opt_this);
  }
};


/**
 * Iterate through all features whose geometries contain the provided
 * coordinate, calling the callback with each feature.  If the callback returns
 * a "truthy" value, iteration will stop and the function will return the same
 * value.
 *
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {function(this: T, ol.Feature): S} callback Called with each feature
 *     whose goemetry contains the provided coordinate.
 * @param {T=} opt_this The object to use as `this` in the callback.
 * @return {S|undefined} The return value from the last call to the callback.
 * @template T,S
 */
ol.source.Vector.prototype.forEachFeatureAtCoordinateDirect = function(coordinate, callback, opt_this) {
  var extent = [coordinate[0], coordinate[1], coordinate[0], coordinate[1]];
  return this.forEachFeatureInExtent(extent, function(feature) {
    var geometry = feature.getGeometry();
    goog.asserts.assert(geometry, 'feature geometry is defined and not null');
    if (geometry.containsCoordinate(coordinate)) {
      return callback.call(opt_this, feature);
    } else {
      return undefined;
    }
  });
};


/**
 * Iterate through all features whose bounding box intersects the provided
 * extent (note that the feature's geometry may not intersect the extent),
 * calling the callback with each feature.  If the callback returns a "truthy"
 * value, iteration will stop and the function will return the same value.
 *
 * If you are interested in features whose geometry intersects an extent, call
 * the {@link ol.source.Vector#forEachFeatureIntersectingExtent
 * source.forEachFeatureIntersectingExtent()} method instead.
 *
 * When `useSpatialIndex` is set to false, this method will loop through all
 * features, equivalent to {@link ol.source.Vector#forEachFeature}.
 *
 * @param {ol.Extent} extent Extent.
 * @param {function(this: T, ol.Feature): S} callback Called with each feature
 *     whose bounding box intersects the provided extent.
 * @param {T=} opt_this The object to use as `this` in the callback.
 * @return {S|undefined} The return value from the last call to the callback.
 * @template T,S
 * @api
 */
ol.source.Vector.prototype.forEachFeatureInExtent = function(extent, callback, opt_this) {
  if (this.featuresRtree_) {
    return this.featuresRtree_.forEachInExtent(extent, callback, opt_this);
  } else if (this.featuresCollection_) {
    return this.featuresCollection_.forEach(callback, opt_this);
  }
};


/**
 * Iterate through all features whose geometry intersects the provided extent,
 * calling the callback with each feature.  If the callback returns a "truthy"
 * value, iteration will stop and the function will return the same value.
 *
 * If you only want to test for bounding box intersection, call the
 * {@link ol.source.Vector#forEachFeatureInExtent
 * source.forEachFeatureInExtent()} method instead.
 *
 * @param {ol.Extent} extent Extent.
 * @param {function(this: T, ol.Feature): S} callback Called with each feature
 *     whose geometry intersects the provided extent.
 * @param {T=} opt_this The object to use as `this` in the callback.
 * @return {S|undefined} The return value from the last call to the callback.
 * @template T,S
 * @api
 */
ol.source.Vector.prototype.forEachFeatureIntersectingExtent = function(extent, callback, opt_this) {
  return this.forEachFeatureInExtent(extent,
      /**
       * @param {ol.Feature} feature Feature.
       * @return {S|undefined}
       * @template S
       */
      function(feature) {
        var geometry = feature.getGeometry();
        goog.asserts.assert(geometry,
            'feature geometry is defined and not null');
        if (geometry.intersectsExtent(extent)) {
          var result = callback.call(opt_this, feature);
          if (result) {
            return result;
          }
        }
      });
};


/**
 * Get the features collection associated with this source. Will be `null`
 * unless the source was configured with `useSpatialIndex` set to `false`, or
 * with an {@link ol.Collection} as `features`.
 * @return {ol.Collection.<ol.Feature>} The collection of features.
 * @api
 */
ol.source.Vector.prototype.getFeaturesCollection = function() {
  return this.featuresCollection_;
};


/**
 * Get all features on the source.
 * @return {Array.<ol.Feature>} Features.
 * @api stable
 */
ol.source.Vector.prototype.getFeatures = function() {
  var features;
  if (this.featuresCollection_) {
    features = this.featuresCollection_.getArray();
  } else if (this.featuresRtree_) {
    features = this.featuresRtree_.getAll();
    if (!goog.object.isEmpty(this.nullGeometryFeatures_)) {
      goog.array.extend(
          features, goog.object.getValues(this.nullGeometryFeatures_));
    }
  }
  goog.asserts.assert(features !== undefined,
      'Neither featuresRtree_ nor featuresCollection_ are available');
  return features;
};


/**
 * Get all features whose geometry intersects the provided coordinate.
 * @param {ol.Coordinate} coordinate Coordinate.
 * @return {Array.<ol.Feature>} Features.
 * @api stable
 */
ol.source.Vector.prototype.getFeaturesAtCoordinate = function(coordinate) {
  var features = [];
  this.forEachFeatureAtCoordinateDirect(coordinate, function(feature) {
    features.push(feature);
  });
  return features;
};


/**
 * Get all features in the provided extent.  Note that this returns all features
 * whose bounding boxes intersect the given extent (so it may include features
 * whose geometries do not intersect the extent).
 *
 * This method is not available when the source is configured with
 * `useSpatialIndex` set to `false`.
 * @param {ol.Extent} extent Extent.
 * @return {Array.<ol.Feature>} Features.
 * @api
 */
ol.source.Vector.prototype.getFeaturesInExtent = function(extent) {
  goog.asserts.assert(this.featuresRtree_,
      'getFeaturesInExtent does not work when useSpatialIndex is set to false');
  return this.featuresRtree_.getInExtent(extent);
};


/**
 * Get the closest feature to the provided coordinate.
 *
 * This method is not available when the source is configured with
 * `useSpatialIndex` set to `false`.
 * @param {ol.Coordinate} coordinate Coordinate.
 * @return {ol.Feature} Closest feature.
 * @api stable
 */
ol.source.Vector.prototype.getClosestFeatureToCoordinate = function(coordinate) {
  // Find the closest feature using branch and bound.  We start searching an
  // infinite extent, and find the distance from the first feature found.  This
  // becomes the closest feature.  We then compute a smaller extent which any
  // closer feature must intersect.  We continue searching with this smaller
  // extent, trying to find a closer feature.  Every time we find a closer
  // feature, we update the extent being searched so that any even closer
  // feature must intersect it.  We continue until we run out of features.
  var x = coordinate[0];
  var y = coordinate[1];
  var closestFeature = null;
  var closestPoint = [NaN, NaN];
  var minSquaredDistance = Infinity;
  var extent = [-Infinity, -Infinity, Infinity, Infinity];
  goog.asserts.assert(this.featuresRtree_,
      'getClosestFeatureToCoordinate does not work with useSpatialIndex set ' +
      'to false');
  this.featuresRtree_.forEachInExtent(extent,
      /**
       * @param {ol.Feature} feature Feature.
       */
      function(feature) {
        var geometry = feature.getGeometry();
        goog.asserts.assert(geometry,
            'feature geometry is defined and not null');
        var previousMinSquaredDistance = minSquaredDistance;
        minSquaredDistance = geometry.closestPointXY(
            x, y, closestPoint, minSquaredDistance);
        if (minSquaredDistance < previousMinSquaredDistance) {
          closestFeature = feature;
          // This is sneaky.  Reduce the extent that it is currently being
          // searched while the R-Tree traversal using this same extent object
          // is still in progress.  This is safe because the new extent is
          // strictly contained by the old extent.
          var minDistance = Math.sqrt(minSquaredDistance);
          extent[0] = x - minDistance;
          extent[1] = y - minDistance;
          extent[2] = x + minDistance;
          extent[3] = y + minDistance;
        }
      });
  return closestFeature;
};


/**
 * Get the extent of the features currently in the source.
 *
 * This method is not available when the source is configured with
 * `useSpatialIndex` set to `false`.
 * @return {!ol.Extent} Extent.
 * @api stable
 */
ol.source.Vector.prototype.getExtent = function() {
  goog.asserts.assert(this.featuresRtree_,
      'getExtent does not work when useSpatialIndex is set to false');
  return this.featuresRtree_.getExtent();
};


/**
 * Get a feature by its identifier (the value returned by feature.getId()).
 * Note that the index treats string and numeric identifiers as the same.  So
 * `source.getFeatureById(2)` will return a feature with id `'2'` or `2`.
 *
 * @param {string|number} id Feature identifier.
 * @return {ol.Feature} The feature (or `null` if not found).
 * @api stable
 */
ol.source.Vector.prototype.getFeatureById = function(id) {
  var feature = this.idIndex_[id.toString()];
  return feature !== undefined ? feature : null;
};


/**
 * @param {goog.events.Event} event Event.
 * @private
 */
ol.source.Vector.prototype.handleFeatureChange_ = function(event) {
  var feature = /** @type {ol.Feature} */ (event.target);
  var featureKey = goog.getUid(feature).toString();
  var geometry = feature.getGeometry();
  if (!geometry) {
    if (!(featureKey in this.nullGeometryFeatures_)) {
      if (this.featuresRtree_) {
        this.featuresRtree_.remove(feature);
      }
      this.nullGeometryFeatures_[featureKey] = feature;
    }
  } else {
    var extent = geometry.getExtent();
    if (featureKey in this.nullGeometryFeatures_) {
      delete this.nullGeometryFeatures_[featureKey];
      if (this.featuresRtree_) {
        this.featuresRtree_.insert(extent, feature);
      }
    } else {
      if (this.featuresRtree_) {
        this.featuresRtree_.update(extent, feature);
      }
    }
  }
  var id = feature.getId();
  var removed;
  if (id !== undefined) {
    var sid = id.toString();
    if (featureKey in this.undefIdIndex_) {
      delete this.undefIdIndex_[featureKey];
      this.idIndex_[sid] = feature;
    } else {
      if (this.idIndex_[sid] !== feature) {
        removed = this.removeFromIdIndex_(feature);
        goog.asserts.assert(removed,
            'Expected feature to be removed from index');
        this.idIndex_[sid] = feature;
      }
    }
  } else {
    if (!(featureKey in this.undefIdIndex_)) {
      removed = this.removeFromIdIndex_(feature);
      goog.asserts.assert(removed,
          'Expected feature to be removed from index');
      this.undefIdIndex_[featureKey] = feature;
    } else {
      goog.asserts.assert(this.undefIdIndex_[featureKey] === feature,
          'feature keyed under %s in undefIdKeys', featureKey);
    }
  }
  this.changed();
  this.dispatchEvent(new ol.source.VectorEvent(
      ol.source.VectorEventType.CHANGEFEATURE, feature));
};


/**
 * @param {ol.Feature} feature Feature.
 * @return {boolean} Feature is in source.
 */
ol.source.Vector.prototype.hasFeature = function(feature) {
  var id = feature.getId();
  return id ? id in this.idIndex_ : goog.getUid(feature) in this.undefIdIndex_;
};


/**
 * @return {boolean} Is empty.
 */
ol.source.Vector.prototype.isEmpty = function() {
  return this.featuresRtree_.isEmpty() &&
      goog.object.isEmpty(this.nullGeometryFeatures_);
};


/**
 * @param {ol.Extent} extent Extent.
 * @param {number} resolution Resolution.
 * @param {ol.proj.Projection} projection Projection.
 */
ol.source.Vector.prototype.loadFeatures = function(
    extent, resolution, projection) {
  var loadedExtentsRtree = this.loadedExtentsRtree_;
  var extentsToLoad = this.strategy_(extent, resolution);
  var i, ii;
  for (i = 0, ii = extentsToLoad.length; i < ii; ++i) {
    var extentToLoad = extentsToLoad[i];
    var alreadyLoaded = loadedExtentsRtree.forEachInExtent(extentToLoad,
        /**
         * @param {{extent: ol.Extent}} object Object.
         * @return {boolean} Contains.
         */
        function(object) {
          return ol.extent.containsExtent(object.extent, extentToLoad);
        });
    if (!alreadyLoaded) {
      this.loader_.call(this, extentToLoad, resolution, projection);
      loadedExtentsRtree.insert(extentToLoad, {extent: extentToLoad.slice()});
    }
  }
};


/**
 * Remove a single feature from the source.  If you want to remove all features
 * at once, use the {@link ol.source.Vector#clear source.clear()} method
 * instead.
 * @param {ol.Feature} feature Feature to remove.
 * @api stable
 */
ol.source.Vector.prototype.removeFeature = function(feature) {
  var featureKey = goog.getUid(feature).toString();
  if (featureKey in this.nullGeometryFeatures_) {
    delete this.nullGeometryFeatures_[featureKey];
  } else {
    if (this.featuresRtree_) {
      this.featuresRtree_.remove(feature);
    }
  }
  this.removeFeatureInternal(feature);
  this.changed();
};


/**
 * Remove feature without firing a `change` event.
 * @param {ol.Feature} feature Feature.
 * @protected
 */
ol.source.Vector.prototype.removeFeatureInternal = function(feature) {
  var featureKey = goog.getUid(feature).toString();
  goog.asserts.assert(featureKey in this.featureChangeKeys_,
      'featureKey exists in featureChangeKeys');
  this.featureChangeKeys_[featureKey].forEach(goog.events.unlistenByKey);
  delete this.featureChangeKeys_[featureKey];
  var id = feature.getId();
  if (id !== undefined) {
    delete this.idIndex_[id.toString()];
  } else {
    delete this.undefIdIndex_[featureKey];
  }
  this.dispatchEvent(new ol.source.VectorEvent(
      ol.source.VectorEventType.REMOVEFEATURE, feature));
};


/**
 * Remove a feature from the id index.  Called internally when the feature id
 * may have changed.
 * @param {ol.Feature} feature The feature.
 * @return {boolean} Removed the feature from the index.
 * @private
 */
ol.source.Vector.prototype.removeFromIdIndex_ = function(feature) {
  var removed = false;
  for (var id in this.idIndex_) {
    if (this.idIndex_[id] === feature) {
      delete this.idIndex_[id];
      removed = true;
      break;
    }
  }
  return removed;
};


/**
 * @classdesc
 * Events emitted by {@link ol.source.Vector} instances are instances of this
 * type.
 *
 * @constructor
 * @extends {goog.events.Event}
 * @implements {oli.source.VectorEvent}
 * @param {string} type Type.
 * @param {ol.Feature=} opt_feature Feature.
 */
ol.source.VectorEvent = function(type, opt_feature) {

  goog.base(this, type);

  /**
   * The feature being added or removed.
   * @type {ol.Feature|undefined}
   * @api stable
   */
  this.feature = opt_feature;

};
goog.inherits(ol.source.VectorEvent, goog.events.Event);

goog.provide('ol.source.ImageVector');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.vec.Mat4');
goog.require('ol.dom');
goog.require('ol.extent');
goog.require('ol.render.canvas.ReplayGroup');
goog.require('ol.renderer.vector');
goog.require('ol.source.ImageCanvas');
goog.require('ol.source.Vector');
goog.require('ol.style.Style');
goog.require('ol.vec.Mat4');


/**
 * @classdesc
 * An image source whose images are canvas elements into which vector features
 * read from a vector source (`ol.source.Vector`) are drawn. An
 * `ol.source.ImageVector` object is to be used as the `source` of an image
 * layer (`ol.layer.Image`). Image layers are rotated, scaled, and translated,
 * as opposed to being re-rendered, during animations and interactions. So, like
 * any other image layer, an image layer configured with an
 * `ol.source.ImageVector` will exhibit this behaviour. This is in contrast to a
 * vector layer, where vector features are re-drawn during animations and
 * interactions.
 *
 * @constructor
 * @extends {ol.source.ImageCanvas}
 * @param {olx.source.ImageVectorOptions} options Options.
 * @api
 */
ol.source.ImageVector = function(options) {

  /**
   * @private
   * @type {ol.source.Vector}
   */
  this.source_ = options.source;

  /**
   * @private
   * @type {!goog.vec.Mat4.Number}
   */
  this.transform_ = goog.vec.Mat4.createNumber();

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.canvasContext_ = ol.dom.createCanvasContext2D();

  /**
   * @private
   * @type {ol.Size}
   */
  this.canvasSize_ = [0, 0];

  /**
   * @private
   * @type {ol.render.canvas.ReplayGroup}
   */
  this.replayGroup_ = null;

  goog.base(this, {
    attributions: options.attributions,
    canvasFunction: this.canvasFunctionInternal_.bind(this),
    logo: options.logo,
    projection: options.projection,
    ratio: options.ratio,
    resolutions: options.resolutions,
    state: this.source_.getState()
  });

  /**
   * User provided style.
   * @type {ol.style.Style|Array.<ol.style.Style>|ol.style.StyleFunction}
   * @private
   */
  this.style_ = null;

  /**
   * Style function for use within the library.
   * @type {ol.style.StyleFunction|undefined}
   * @private
   */
  this.styleFunction_ = undefined;

  this.setStyle(options.style);

  goog.events.listen(this.source_, goog.events.EventType.CHANGE,
      this.handleSourceChange_, undefined, this);

};
goog.inherits(ol.source.ImageVector, ol.source.ImageCanvas);


/**
 * @param {ol.Extent} extent Extent.
 * @param {number} resolution Resolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {ol.Size} size Size.
 * @param {ol.proj.Projection} projection Projection;
 * @return {HTMLCanvasElement} Canvas element.
 * @private
 */
ol.source.ImageVector.prototype.canvasFunctionInternal_ = function(extent, resolution, pixelRatio, size, projection) {

  var replayGroup = new ol.render.canvas.ReplayGroup(
      ol.renderer.vector.getTolerance(resolution, pixelRatio), extent,
      resolution);

  this.source_.loadFeatures(extent, resolution, projection);

  var loading = false;
  this.source_.forEachFeatureInExtent(extent,
      /**
       * @param {ol.Feature} feature Feature.
       */
      function(feature) {
        loading = loading ||
            this.renderFeature_(feature, resolution, pixelRatio, replayGroup);
      }, this);
  replayGroup.finish();

  if (loading) {
    return null;
  }

  if (this.canvasSize_[0] != size[0] || this.canvasSize_[1] != size[1]) {
    this.canvasContext_.canvas.width = size[0];
    this.canvasContext_.canvas.height = size[1];
    this.canvasSize_[0] = size[0];
    this.canvasSize_[1] = size[1];
  } else {
    this.canvasContext_.clearRect(0, 0, size[0], size[1]);
  }

  var transform = this.getTransform_(ol.extent.getCenter(extent),
      resolution, pixelRatio, size);
  replayGroup.replay(this.canvasContext_, pixelRatio, transform, 0, {});

  this.replayGroup_ = replayGroup;

  return this.canvasContext_.canvas;
};


/**
 * @inheritDoc
 */
ol.source.ImageVector.prototype.forEachFeatureAtCoordinate = function(
    coordinate, resolution, rotation, skippedFeatureUids, callback) {
  if (!this.replayGroup_) {
    return undefined;
  } else {
    /** @type {Object.<string, boolean>} */
    var features = {};
    return this.replayGroup_.forEachFeatureAtCoordinate(
        coordinate, resolution, 0, skippedFeatureUids,
        /**
         * @param {ol.Feature|ol.render.Feature} feature Feature.
         * @return {?} Callback result.
         */
        function(feature) {
          goog.asserts.assert(feature !== undefined, 'passed a feature');
          var key = goog.getUid(feature).toString();
          if (!(key in features)) {
            features[key] = true;
            return callback(feature);
          }
        });
  }
};


/**
 * Get a reference to the wrapped source.
 * @return {ol.source.Vector} Source.
 * @api
 */
ol.source.ImageVector.prototype.getSource = function() {
  return this.source_;
};


/**
 * Get the style for features.  This returns whatever was passed to the `style`
 * option at construction or to the `setStyle` method.
 * @return {ol.style.Style|Array.<ol.style.Style>|ol.style.StyleFunction}
 *     Layer style.
 * @api stable
 */
ol.source.ImageVector.prototype.getStyle = function() {
  return this.style_;
};


/**
 * Get the style function.
 * @return {ol.style.StyleFunction|undefined} Layer style function.
 * @api stable
 */
ol.source.ImageVector.prototype.getStyleFunction = function() {
  return this.styleFunction_;
};


/**
 * @param {ol.Coordinate} center Center.
 * @param {number} resolution Resolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {ol.Size} size Size.
 * @return {!goog.vec.Mat4.Number} Transform.
 * @private
 */
ol.source.ImageVector.prototype.getTransform_ = function(center, resolution, pixelRatio, size) {
  return ol.vec.Mat4.makeTransform2D(this.transform_,
      size[0] / 2, size[1] / 2,
      pixelRatio / resolution, -pixelRatio / resolution,
      0,
      -center[0], -center[1]);
};


/**
 * Handle changes in image style state.
 * @param {goog.events.Event} event Image style change event.
 * @private
 */
ol.source.ImageVector.prototype.handleImageChange_ = function(event) {
  this.changed();
};


/**
 * @private
 */
ol.source.ImageVector.prototype.handleSourceChange_ = function() {
  // setState will trigger a CHANGE event, so we always rely
  // change events by calling setState.
  this.setState(this.source_.getState());
};


/**
 * @param {ol.Feature} feature Feature.
 * @param {number} resolution Resolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {ol.render.canvas.ReplayGroup} replayGroup Replay group.
 * @return {boolean} `true` if an image is loading.
 * @private
 */
ol.source.ImageVector.prototype.renderFeature_ = function(feature, resolution, pixelRatio, replayGroup) {
  var styles;
  var styleFunction = feature.getStyleFunction();
  if (styleFunction) {
    styles = styleFunction.call(feature, resolution);
  } else if (this.styleFunction_) {
    styles = this.styleFunction_(feature, resolution);
  }
  if (!styles) {
    return false;
  }
  var i, ii, loading = false;
  for (i = 0, ii = styles.length; i < ii; ++i) {
    loading = ol.renderer.vector.renderFeature(
        replayGroup, feature, styles[i],
        ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio),
        this.handleImageChange_, this) || loading;
  }
  return loading;
};


/**
 * Set the style for features.  This can be a single style object, an array
 * of styles, or a function that takes a feature and resolution and returns
 * an array of styles. If it is `undefined` the default style is used. If
 * it is `null` the layer has no style (a `null` style), so only features
 * that have their own styles will be rendered in the layer. See
 * {@link ol.style} for information on the default style.
 * @param {ol.style.Style|Array.<ol.style.Style>|ol.style.StyleFunction|undefined}
 *     style Layer style.
 * @api stable
 */
ol.source.ImageVector.prototype.setStyle = function(style) {
  this.style_ = style !== undefined ? style : ol.style.defaultStyleFunction;
  this.styleFunction_ = !style ?
      undefined : ol.style.createStyleFunction(this.style_);
  this.changed();
};

goog.provide('ol.renderer.canvas.ImageLayer');

goog.require('goog.asserts');
goog.require('goog.functions');
goog.require('goog.vec.Mat4');
goog.require('ol.ImageBase');
goog.require('ol.ViewHint');
goog.require('ol.dom');
goog.require('ol.extent');
goog.require('ol.layer.Image');
goog.require('ol.proj');
goog.require('ol.renderer.canvas.Layer');
goog.require('ol.source.ImageVector');
goog.require('ol.vec.Mat4');


/**
 * @constructor
 * @extends {ol.renderer.canvas.Layer}
 * @param {ol.layer.Image} imageLayer Single image layer.
 */
ol.renderer.canvas.ImageLayer = function(imageLayer) {

  goog.base(this, imageLayer);

  /**
   * @private
   * @type {?ol.ImageBase}
   */
  this.image_ = null;

  /**
   * @private
   * @type {!goog.vec.Mat4.Number}
   */
  this.imageTransform_ = goog.vec.Mat4.createNumber();

  /**
   * @private
   * @type {?goog.vec.Mat4.Number}
   */
  this.imageTransformInv_ = null;

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.hitCanvasContext_ = null;

};
goog.inherits(ol.renderer.canvas.ImageLayer, ol.renderer.canvas.Layer);


/**
 * @inheritDoc
 */
ol.renderer.canvas.ImageLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) {
  var layer = this.getLayer();
  var source = layer.getSource();
  var resolution = frameState.viewState.resolution;
  var rotation = frameState.viewState.rotation;
  var skippedFeatureUids = frameState.skippedFeatureUids;
  return source.forEachFeatureAtCoordinate(
      coordinate, resolution, rotation, skippedFeatureUids,
      /**
       * @param {ol.Feature|ol.render.Feature} feature Feature.
       * @return {?} Callback result.
       */
      function(feature) {
        return callback.call(thisArg, feature, layer);
      });
};


/**
 * @inheritDoc
 */
ol.renderer.canvas.ImageLayer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) {
  if (!this.getImage()) {
    return undefined;
  }

  if (this.getLayer().getSource() instanceof ol.source.ImageVector) {
    // for ImageVector sources use the original hit-detection logic,
    // so that for example also transparent polygons are detected
    var coordinate = pixel.slice();
    ol.vec.Mat4.multVec2(
        frameState.pixelToCoordinateMatrix, coordinate, coordinate);
    var hasFeature = this.forEachFeatureAtCoordinate(
        coordinate, frameState, goog.functions.TRUE, this);

    if (hasFeature) {
      return callback.call(thisArg, this.getLayer());
    } else {
      return undefined;
    }
  } else {
    // for all other image sources directly check the image
    if (!this.imageTransformInv_) {
      this.imageTransformInv_ = goog.vec.Mat4.createNumber();
      goog.vec.Mat4.invert(this.imageTransform_, this.imageTransformInv_);
    }

    var pixelOnCanvas =
        this.getPixelOnCanvas(pixel, this.imageTransformInv_);

    if (!this.hitCanvasContext_) {
      this.hitCanvasContext_ = ol.dom.createCanvasContext2D(1, 1);
    }

    this.hitCanvasContext_.clearRect(0, 0, 1, 1);
    this.hitCanvasContext_.drawImage(
        this.getImage(), pixelOnCanvas[0], pixelOnCanvas[1], 1, 1, 0, 0, 1, 1);

    var imageData = this.hitCanvasContext_.getImageData(0, 0, 1, 1).data;
    if (imageData[3] > 0) {
      return callback.call(thisArg, this.getLayer());
    } else {
      return undefined;
    }
  }
};


/**
 * @inheritDoc
 */
ol.renderer.canvas.ImageLayer.prototype.getImage = function() {
  return !this.image_ ? null : this.image_.getImage();
};


/**
 * @inheritDoc
 */
ol.renderer.canvas.ImageLayer.prototype.getImageTransform = function() {
  return this.imageTransform_;
};


/**
 * @inheritDoc
 */
ol.renderer.canvas.ImageLayer.prototype.prepareFrame = function(frameState, layerState) {

  var pixelRatio = frameState.pixelRatio;
  var viewState = frameState.viewState;
  var viewCenter = viewState.center;
  var viewResolution = viewState.resolution;
  var viewRotation = viewState.rotation;

  var image;
  var imageLayer = this.getLayer();
  goog.asserts.assertInstanceof(imageLayer, ol.layer.Image,
      'layer is an instance of ol.layer.Image');
  var imageSource = imageLayer.getSource();

  var hints = frameState.viewHints;

  var renderedExtent = frameState.extent;
  if (layerState.extent !== undefined) {
    renderedExtent = ol.extent.getIntersection(
        renderedExtent, layerState.extent);
  }

  if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.INTERACTING] &&
      !ol.extent.isEmpty(renderedExtent)) {
    var projection = viewState.projection;
    if (!ol.ENABLE_RASTER_REPROJECTION) {
      var sourceProjection = imageSource.getProjection();
      if (sourceProjection) {
        goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection),
            'projection and sourceProjection are equivalent');
        projection = sourceProjection;
      }
    }
    image = imageSource.getImage(
        renderedExtent, viewResolution, pixelRatio, projection);
    if (image) {
      var loaded = this.loadImage(image);
      if (loaded) {
        this.image_ = image;
      }
    }
  }

  if (this.image_) {
    image = this.image_;
    var imageExtent = image.getExtent();
    var imageResolution = image.getResolution();
    var imagePixelRatio = image.getPixelRatio();
    var scale = pixelRatio * imageResolution /
        (viewResolution * imagePixelRatio);
    ol.vec.Mat4.makeTransform2D(this.imageTransform_,
        pixelRatio * frameState.size[0] / 2,
        pixelRatio * frameState.size[1] / 2,
        scale, scale,
        viewRotation,
        imagePixelRatio * (imageExtent[0] - viewCenter[0]) / imageResolution,
        imagePixelRatio * (viewCenter[1] - imageExtent[3]) / imageResolution);
    this.imageTransformInv_ = null;
    this.updateAttributions(frameState.attributions, image.getAttributions());
    this.updateLogos(frameState, imageSource);
  }

  return true;
};

// FIXME find correct globalCompositeOperation
// FIXME optimize :-)

goog.provide('ol.renderer.canvas.TileLayer');

goog.require('goog.asserts');
goog.require('goog.vec.Mat4');
goog.require('ol.Size');
goog.require('ol.TileRange');
goog.require('ol.TileState');
goog.require('ol.array');
goog.require('ol.dom');
goog.require('ol.extent');
goog.require('ol.layer.Tile');
goog.require('ol.renderer.canvas.Layer');
goog.require('ol.size');
goog.require('ol.vec.Mat4');


/**
 * @constructor
 * @extends {ol.renderer.canvas.Layer}
 * @param {ol.layer.Tile} tileLayer Tile layer.
 */
ol.renderer.canvas.TileLayer = function(tileLayer) {

  goog.base(this, tileLayer);

  /**
   * @private
   * @type {HTMLCanvasElement}
   */
  this.canvas_ = null;

  /**
   * @private
   * @type {ol.Size}
   */
  this.canvasSize_ = null;

  /**
   * @private
   * @type {boolean}
   */
  this.canvasTooBig_ = false;

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.context_ = null;

  /**
   * @private
   * @type {!goog.vec.Mat4.Number}
   */
  this.imageTransform_ = goog.vec.Mat4.createNumber();

  /**
   * @private
   * @type {?goog.vec.Mat4.Number}
   */
  this.imageTransformInv_ = null;

  /**
   * @private
   * @type {number}
   */
  this.renderedCanvasZ_ = NaN;

  /**
   * @private
   * @type {number}
   */
  this.renderedTileWidth_ = NaN;

  /**
   * @private
   * @type {number}
   */
  this.renderedTileHeight_ = NaN;

  /**
   * @private
   * @type {ol.TileRange}
   */
  this.renderedCanvasTileRange_ = null;

  /**
   * @private
   * @type {Array.<ol.Tile|undefined>}
   */
  this.renderedTiles_ = null;

  /**
   * @private
   * @type {ol.Size}
   */
  this.tmpSize_ = [0, 0];

};
goog.inherits(ol.renderer.canvas.TileLayer, ol.renderer.canvas.Layer);


/**
 * @inheritDoc
 */
ol.renderer.canvas.TileLayer.prototype.getImage = function() {
  return this.canvas_;
};


/**
 * @inheritDoc
 */
ol.renderer.canvas.TileLayer.prototype.getImageTransform = function() {
  return this.imageTransform_;
};


/**
 * @inheritDoc
 */
ol.renderer.canvas.TileLayer.prototype.prepareFrame = function(frameState, layerState) {

  //
  // Warning! You're entering a dangerous zone!
  //
  // The canvas tile layer renderering is highly optimized, hence
  // the complexity of this function. For best performance we try
  // to minimize the number of pixels to update on the canvas. This
  // includes:
  //
  // - Only drawing pixels that will be visible.
  // - Not re-drawing pixels/tiles that are already correct.
  // - Minimizing calls to clearRect.
  // - Never shrink the canvas. Just make it bigger when necessary.
  //   Re-sizing the canvas also clears it, which further means
  //   re-creating it (expensive).
  //
  // The various steps performed by this functions:
  //
  // - Create a canvas element if none has been created yet.
  //
  // - Make the canvas bigger if it's too small. Note that we never shrink
  //   the canvas, we just make it bigger when necessary, when rotating for
  //   example. Note also that the canvas always contains a whole number
  //   of tiles.
  //
  // - Invalidate the canvas tile range (renderedCanvasTileRange_ = null)
  //   if (1) the canvas has been enlarged, or (2) the zoom level changes,
  //   or (3) the canvas tile range doesn't contain the required tile
  //   range. This canvas tile range invalidation thing is related to
  //   an optimization where we attempt to redraw as few pixels as
  //   possible on each prepareFrame call.
  //
  // - If the canvas tile range has been invalidated we reset
  //   renderedCanvasTileRange_ and reset the renderedTiles_ array.
  //   The renderedTiles_ array is the structure used to determine
  //   the canvas pixels that need not be redrawn from one prepareFrame
  //   call to another. It records while tile has been rendered at
  //   which position in the canvas.
  //
  // - We then determine the tiles to draw on the canvas. Tiles for
  //   the target resolution may not be loaded yet. In that case we
  //   use low-resolution/interim tiles if loaded already. And, if
  //   for a non-yet-loaded tile we haven't found a corresponding
  //   low-resolution tile we indicate that the pixels for that
  //   tile must be cleared on the canvas. Note: determining the
  //   interim tiles is based on tile extents instead of tile
  //   coords, this is to be able to handler irregular tile grids.
  //
  // - We're now ready to render. We start by calling clearRect
  //   for the tiles that aren't loaded yet and are not fully covered
  //   by a low-resolution tile (if they're loaded, we'll draw them;
  //   if they're fully covered by a low-resolution tile then there's
  //   no need to clear). We then render the tiles "back to front",
  //   i.e. starting with the low-resolution tiles.
  //
  // - After rendering some bookkeeping is performed (updateUsedTiles,
  //   etc.). manageTilePyramid is what enqueue tiles in the tile
  //   queue for loading.
  //
  // - The last step involves updating the image transform matrix,
  //   which will be used by the map renderer for the final
  //   composition and positioning.
  //

  var pixelRatio = frameState.pixelRatio;
  var viewState = frameState.viewState;
  var projection = viewState.projection;

  var tileLayer = this.getLayer();
  goog.asserts.assertInstanceof(tileLayer, ol.layer.Tile,
      'layer is an instance of ol.layer.Tile');
  var tileSource = tileLayer.getSource();
  var tileGrid = tileSource.getTileGridForProjection(projection);
  var tileGutter = tileSource.getGutter();
  var z = tileGrid.getZForResolution(viewState.resolution);
  var tilePixelSize =
      tileSource.getTilePixelSize(z, frameState.pixelRatio, projection);
  var tilePixelRatio = tilePixelSize[0] /
      ol.size.toSize(tileGrid.getTileSize(z), this.tmpSize_)[0];
  var tileResolution = tileGrid.getResolution(z);
  var tilePixelResolution = tileResolution / tilePixelRatio;
  var center = viewState.center;
  var extent;
  if (tileResolution == viewState.resolution) {
    center = this.snapCenterToPixel(center, tileResolution, frameState.size);
    extent = ol.extent.getForViewAndSize(
        center, tileResolution, viewState.rotation, frameState.size);
  } else {
    extent = frameState.extent;
  }

  if (layerState.extent !== undefined) {
    extent = ol.extent.getIntersection(extent, layerState.extent);
  }
  if (ol.extent.isEmpty(extent)) {
    // Return false to prevent the rendering of the layer.
    return false;
  }

  var tileRange = tileGrid.getTileRangeForExtentAndResolution(
      extent, tileResolution);

  var canvasWidth = tilePixelSize[0] * tileRange.getWidth();
  var canvasHeight = tilePixelSize[1] * tileRange.getHeight();

  var canvas, context;
  if (!this.canvas_) {
    goog.asserts.assert(!this.canvasSize_,
        'canvasSize is null (because canvas is null)');
    goog.asserts.assert(!this.context_,
        'context is null (because canvas is null)');
    goog.asserts.assert(!this.renderedCanvasTileRange_,
        'renderedCanvasTileRange is null (because canvas is null)');
    context = ol.dom.createCanvasContext2D(canvasWidth, canvasHeight);
    this.canvas_ = context.canvas;
    this.canvasSize_ = [canvasWidth, canvasHeight];
    this.context_ = context;
    this.canvasTooBig_ =
        !ol.renderer.canvas.Layer.testCanvasSize(this.canvasSize_);
  } else {
    goog.asserts.assert(this.canvasSize_,
        'non-null canvasSize (because canvas is not null)');
    goog.asserts.assert(this.context_,
        'non-null context (because canvas is not null)');
    canvas = this.canvas_;
    context = this.context_;
    if (this.canvasSize_[0] < canvasWidth ||
        this.canvasSize_[1] < canvasHeight ||
        this.renderedTileWidth_ !== tilePixelSize[0] ||
        this.renderedTileHeight_ !== tilePixelSize[1] ||
        (this.canvasTooBig_ && (this.canvasSize_[0] > canvasWidth ||
        this.canvasSize_[1] > canvasHeight))) {
      // Canvas is too small or tileSize has changed, resize it.
      // We never shrink the canvas, unless
      // we know that the current canvas size exceeds the maximum size
      canvas.width = canvasWidth;
      canvas.height = canvasHeight;
      this.canvasSize_ = [canvasWidth, canvasHeight];
      this.canvasTooBig_ =
          !ol.renderer.canvas.Layer.testCanvasSize(this.canvasSize_);
      this.renderedCanvasTileRange_ = null;
    } else {
      canvasWidth = this.canvasSize_[0];
      canvasHeight = this.canvasSize_[1];
      if (z != this.renderedCanvasZ_ ||
          !this.renderedCanvasTileRange_.containsTileRange(tileRange)) {
        this.renderedCanvasTileRange_ = null;
      }
    }
  }

  var canvasTileRange, canvasTileRangeWidth, minX, minY;
  if (!this.renderedCanvasTileRange_) {
    canvasTileRangeWidth = canvasWidth / tilePixelSize[0];
    var canvasTileRangeHeight = canvasHeight / tilePixelSize[1];
    minX = tileRange.minX -
        Math.floor((canvasTileRangeWidth - tileRange.getWidth()) / 2);
    minY = tileRange.minY -
        Math.floor((canvasTileRangeHeight - tileRange.getHeight()) / 2);
    this.renderedCanvasZ_ = z;
    this.renderedTileWidth_ = tilePixelSize[0];
    this.renderedTileHeight_ = tilePixelSize[1];
    this.renderedCanvasTileRange_ = new ol.TileRange(
        minX, minX + canvasTileRangeWidth - 1,
        minY, minY + canvasTileRangeHeight - 1);
    this.renderedTiles_ =
        new Array(canvasTileRangeWidth * canvasTileRangeHeight);
    canvasTileRange = this.renderedCanvasTileRange_;
  } else {
    canvasTileRange = this.renderedCanvasTileRange_;
    canvasTileRangeWidth = canvasTileRange.getWidth();
  }

  goog.asserts.assert(canvasTileRange.containsTileRange(tileRange),
      'tileRange is contained in canvasTileRange');

  /**
   * @type {Object.<number, Object.<string, ol.Tile>>}
   */
  var tilesToDrawByZ = {};
  tilesToDrawByZ[z] = {};
  /** @type {Array.<ol.Tile>} */
  var tilesToClear = [];

  var findLoadedTiles = this.createLoadedTileFinder(
      tileSource, projection, tilesToDrawByZ);

  var useInterimTilesOnError = tileLayer.getUseInterimTilesOnError();

  var tmpExtent = ol.extent.createEmpty();
  var tmpTileRange = new ol.TileRange(0, 0, 0, 0);
  var childTileRange, fullyLoaded, tile, x, y;
  var drawableTile = (
      /**
       * @param {!ol.Tile} tile Tile.
       * @return {boolean} Tile is selected.
       */
      function(tile) {
        var tileState = tile.getState();
        return tileState == ol.TileState.LOADED ||
            tileState == ol.TileState.EMPTY ||
            tileState == ol.TileState.ERROR && !useInterimTilesOnError;
      });
  for (x = tileRange.minX; x <= tileRange.maxX; ++x) {
    for (y = tileRange.minY; y <= tileRange.maxY; ++y) {
      tile = tileSource.getTile(z, x, y, pixelRatio, projection);
      if (!drawableTile(tile) && tile.interimTile) {
        tile = tile.interimTile;
      }
      goog.asserts.assert(tile);
      if (drawableTile(tile)) {
        tilesToDrawByZ[z][tile.tileCoord.toString()] = tile;
        continue;
      }
      fullyLoaded = tileGrid.forEachTileCoordParentTileRange(
          tile.tileCoord, findLoadedTiles, null, tmpTileRange, tmpExtent);
      if (!fullyLoaded) {
        // FIXME we do not need to clear the tile if it is fully covered by its
        //       children
        tilesToClear.push(tile);
        childTileRange = tileGrid.getTileCoordChildTileRange(
            tile.tileCoord, tmpTileRange, tmpExtent);
        if (childTileRange) {
          findLoadedTiles(z + 1, childTileRange);
        }
      }

    }
  }

  var i, ii;
  for (i = 0, ii = tilesToClear.length; i < ii; ++i) {
    tile = tilesToClear[i];
    x = tilePixelSize[0] * (tile.tileCoord[1] - canvasTileRange.minX);
    y = tilePixelSize[1] * (canvasTileRange.maxY - tile.tileCoord[2]);
    context.clearRect(x, y, tilePixelSize[0], tilePixelSize[1]);
  }

  /** @type {Array.<number>} */
  var zs = Object.keys(tilesToDrawByZ).map(Number);
  zs.sort(ol.array.numberSafeCompareFunction);
  var opaque = tileSource.getOpaque(projection);
  var origin = ol.extent.getTopLeft(tileGrid.getTileCoordExtent(
      [z, canvasTileRange.minX, canvasTileRange.maxY],
      tmpExtent));
  var currentZ, index, scale, tileCoordKey, tileExtent, tileState, tilesToDraw;
  var ix, iy, interimTileRange, maxX, maxY;
  var height, width;
  for (i = 0, ii = zs.length; i < ii; ++i) {
    currentZ = zs[i];
    tilePixelSize =
        tileSource.getTilePixelSize(currentZ, pixelRatio, projection);
    tilesToDraw = tilesToDrawByZ[currentZ];
    if (currentZ == z) {
      for (tileCoordKey in tilesToDraw) {
        tile = tilesToDraw[tileCoordKey];
        index =
            (tile.tileCoord[2] - canvasTileRange.minY) * canvasTileRangeWidth +
            (tile.tileCoord[1] - canvasTileRange.minX);
        if (this.renderedTiles_[index] != tile) {
          x = tilePixelSize[0] * (tile.tileCoord[1] - canvasTileRange.minX);
          y = tilePixelSize[1] * (canvasTileRange.maxY - tile.tileCoord[2]);
          tileState = tile.getState();
          if (tileState == ol.TileState.EMPTY ||
              (tileState == ol.TileState.ERROR && !useInterimTilesOnError) ||
              !opaque) {
            context.clearRect(x, y, tilePixelSize[0], tilePixelSize[1]);
          }
          if (tileState == ol.TileState.LOADED) {
            context.drawImage(tile.getImage(),
                tileGutter, tileGutter, tilePixelSize[0], tilePixelSize[1],
                x, y, tilePixelSize[0], tilePixelSize[1]);
          }
          this.renderedTiles_[index] = tile;
        }
      }
    } else {
      scale = tileGrid.getResolution(currentZ) / tileResolution;
      for (tileCoordKey in tilesToDraw) {
        tile = tilesToDraw[tileCoordKey];
        tileExtent = tileGrid.getTileCoordExtent(tile.tileCoord, tmpExtent);
        x = (tileExtent[0] - origin[0]) / tilePixelResolution;
        y = (origin[1] - tileExtent[3]) / tilePixelResolution;
        width = scale * tilePixelSize[0];
        height = scale * tilePixelSize[1];
        tileState = tile.getState();
        if (tileState == ol.TileState.EMPTY || !opaque) {
          context.clearRect(x, y, width, height);
        }
        if (tileState == ol.TileState.LOADED) {
          context.drawImage(tile.getImage(),
              tileGutter, tileGutter, tilePixelSize[0], tilePixelSize[1],
              x, y, width, height);
        }
        interimTileRange =
            tileGrid.getTileRangeForExtentAndZ(tileExtent, z, tmpTileRange);
        minX = Math.max(interimTileRange.minX, canvasTileRange.minX);
        maxX = Math.min(interimTileRange.maxX, canvasTileRange.maxX);
        minY = Math.max(interimTileRange.minY, canvasTileRange.minY);
        maxY = Math.min(interimTileRange.maxY, canvasTileRange.maxY);
        for (ix = minX; ix <= maxX; ++ix) {
          for (iy = minY; iy <= maxY; ++iy) {
            index = (iy - canvasTileRange.minY) * canvasTileRangeWidth +
                    (ix - canvasTileRange.minX);
            this.renderedTiles_[index] = undefined;
          }
        }
      }
    }
  }

  this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange);
  this.manageTilePyramid(frameState, tileSource, tileGrid, pixelRatio,
      projection, extent, z, tileLayer.getPreload());
  this.scheduleExpireCache(frameState, tileSource);
  this.updateLogos(frameState, tileSource);

  ol.vec.Mat4.makeTransform2D(this.imageTransform_,
      pixelRatio * frameState.size[0] / 2,
      pixelRatio * frameState.size[1] / 2,
      pixelRatio * tilePixelResolution / viewState.resolution,
      pixelRatio * tilePixelResolution / viewState.resolution,
      viewState.rotation,
      (origin[0] - center[0]) / tilePixelResolution,
      (center[1] - origin[1]) / tilePixelResolution);
  this.imageTransformInv_ = null;

  return true;
};


/**
 * @inheritDoc
 */
ol.renderer.canvas.TileLayer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) {
  if (!this.context_) {
    return undefined;
  }

  if (!this.imageTransformInv_) {
    this.imageTransformInv_ = goog.vec.Mat4.createNumber();
    goog.vec.Mat4.invert(this.imageTransform_, this.imageTransformInv_);
  }

  var pixelOnCanvas =
      this.getPixelOnCanvas(pixel, this.imageTransformInv_);

  var imageData = this.context_.getImageData(
      pixelOnCanvas[0], pixelOnCanvas[1], 1, 1).data;

  if (imageData[3] > 0) {
    return callback.call(thisArg, this.getLayer());
  } else {
    return undefined;
  }
};

goog.provide('ol.renderer.canvas.VectorLayer');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('ol.ViewHint');
goog.require('ol.dom');
goog.require('ol.extent');
goog.require('ol.layer.Vector');
goog.require('ol.render.EventType');
goog.require('ol.render.canvas.ReplayGroup');
goog.require('ol.renderer.canvas.Layer');
goog.require('ol.renderer.vector');
goog.require('ol.source.Vector');


/**
 * @constructor
 * @extends {ol.renderer.canvas.Layer}
 * @param {ol.layer.Vector} vectorLayer Vector layer.
 */
ol.renderer.canvas.VectorLayer = function(vectorLayer) {

  goog.base(this, vectorLayer);

  /**
   * @private
   * @type {boolean}
   */
  this.dirty_ = false;

  /**
   * @private
   * @type {number}
   */
  this.renderedRevision_ = -1;

  /**
   * @private
   * @type {number}
   */
  this.renderedResolution_ = NaN;

  /**
   * @private
   * @type {ol.Extent}
   */
  this.renderedExtent_ = ol.extent.createEmpty();

  /**
   * @private
   * @type {function(ol.Feature, ol.Feature): number|null}
   */
  this.renderedRenderOrder_ = null;

  /**
   * @private
   * @type {ol.render.canvas.ReplayGroup}
   */
  this.replayGroup_ = null;

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.context_ = ol.dom.createCanvasContext2D();

};
goog.inherits(ol.renderer.canvas.VectorLayer, ol.renderer.canvas.Layer);


/**
 * @inheritDoc
 */
ol.renderer.canvas.VectorLayer.prototype.composeFrame = function(frameState, layerState, context) {

  var extent = frameState.extent;
  var pixelRatio = frameState.pixelRatio;
  var skippedFeatureUids = layerState.managed ?
      frameState.skippedFeatureUids : {};
  var viewState = frameState.viewState;
  var projection = viewState.projection;
  var rotation = viewState.rotation;
  var projectionExtent = projection.getExtent();
  var vectorSource = this.getLayer().getSource();
  goog.asserts.assertInstanceof(vectorSource, ol.source.Vector);

  var transform = this.getTransform(frameState, 0);

  this.dispatchPreComposeEvent(context, frameState, transform);

  var replayGroup = this.replayGroup_;
  if (replayGroup && !replayGroup.isEmpty()) {
    var layer = this.getLayer();
    var replayContext;
    if (layer.hasListener(ol.render.EventType.RENDER)) {
      // resize and clear
      this.context_.canvas.width = context.canvas.width;
      this.context_.canvas.height = context.canvas.height;
      replayContext = this.context_;
    } else {
      replayContext = context;
    }
    // for performance reasons, context.save / context.restore is not used
    // to save and restore the transformation matrix and the opacity.
    // see http://jsperf.com/context-save-restore-versus-variable
    var alpha = replayContext.globalAlpha;
    replayContext.globalAlpha = layerState.opacity;

    replayGroup.replay(replayContext, pixelRatio, transform, rotation,
        skippedFeatureUids);
    if (vectorSource.getWrapX() && projection.canWrapX() &&
        !ol.extent.containsExtent(projectionExtent, extent)) {
      var startX = extent[0];
      var worldWidth = ol.extent.getWidth(projectionExtent);
      var world = 0;
      var offsetX;
      while (startX < projectionExtent[0]) {
        --world;
        offsetX = worldWidth * world;
        transform = this.getTransform(frameState, offsetX);
        replayGroup.replay(replayContext, pixelRatio, transform, rotation,
            skippedFeatureUids);
        startX += worldWidth;
      }
      world = 0;
      startX = extent[2];
      while (startX > projectionExtent[2]) {
        ++world;
        offsetX = worldWidth * world;
        transform = this.getTransform(frameState, offsetX);
        replayGroup.replay(replayContext, pixelRatio, transform, rotation,
            skippedFeatureUids);
        startX -= worldWidth;
      }
      // restore original transform for render and compose events
      transform = this.getTransform(frameState, 0);
    }

    if (replayContext != context) {
      this.dispatchRenderEvent(replayContext, frameState, transform);
      context.drawImage(replayContext.canvas, 0, 0);
    }
    replayContext.globalAlpha = alpha;
  }

  this.dispatchPostComposeEvent(context, frameState, transform);

};


/**
 * @inheritDoc
 */
ol.renderer.canvas.VectorLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) {
  if (!this.replayGroup_) {
    return undefined;
  } else {
    var resolution = frameState.viewState.resolution;
    var rotation = frameState.viewState.rotation;
    var layer = this.getLayer();
    var layerState = frameState.layerStates[goog.getUid(layer)];
    /** @type {Object.<string, boolean>} */
    var features = {};
    return this.replayGroup_.forEachFeatureAtCoordinate(coordinate, resolution,
        rotation, layerState.managed ? frameState.skippedFeatureUids : {},
        /**
         * @param {ol.Feature|ol.render.Feature} feature Feature.
         * @return {?} Callback result.
         */
        function(feature) {
          goog.asserts.assert(feature !== undefined, 'received a feature');
          var key = goog.getUid(feature).toString();
          if (!(key in features)) {
            features[key] = true;
            return callback.call(thisArg, feature, layer);
          }
        });
  }
};


/**
 * Handle changes in image style state.
 * @param {goog.events.Event} event Image style change event.
 * @private
 */
ol.renderer.canvas.VectorLayer.prototype.handleStyleImageChange_ = function(event) {
  this.renderIfReadyAndVisible();
};


/**
 * @inheritDoc
 */
ol.renderer.canvas.VectorLayer.prototype.prepareFrame = function(frameState, layerState) {

  var vectorLayer = /** @type {ol.layer.Vector} */ (this.getLayer());
  goog.asserts.assertInstanceof(vectorLayer, ol.layer.Vector,
      'layer is an instance of ol.layer.Vector');
  var vectorSource = vectorLayer.getSource();

  this.updateAttributions(
      frameState.attributions, vectorSource.getAttributions());
  this.updateLogos(frameState, vectorSource);

  var animating = frameState.viewHints[ol.ViewHint.ANIMATING];
  var interacting = frameState.viewHints[ol.ViewHint.INTERACTING];
  var updateWhileAnimating = vectorLayer.getUpdateWhileAnimating();
  var updateWhileInteracting = vectorLayer.getUpdateWhileInteracting();

  if (!this.dirty_ && (!updateWhileAnimating && animating) ||
      (!updateWhileInteracting && interacting)) {
    return true;
  }

  var frameStateExtent = frameState.extent;
  var viewState = frameState.viewState;
  var projection = viewState.projection;
  var resolution = viewState.resolution;
  var pixelRatio = frameState.pixelRatio;
  var vectorLayerRevision = vectorLayer.getRevision();
  var vectorLayerRenderBuffer = vectorLayer.getRenderBuffer();
  var vectorLayerRenderOrder = vectorLayer.getRenderOrder();

  if (vectorLayerRenderOrder === undefined) {
    vectorLayerRenderOrder = ol.renderer.vector.defaultOrder;
  }

  var extent = ol.extent.buffer(frameStateExtent,
      vectorLayerRenderBuffer * resolution);
  var projectionExtent = viewState.projection.getExtent();

  if (vectorSource.getWrapX() && viewState.projection.canWrapX() &&
      !ol.extent.containsExtent(projectionExtent, frameState.extent)) {
    // For the replay group, we need an extent that intersects the real world
    // (-180° to +180°). To support geometries in a coordinate range from -540°
    // to +540°, we add at least 1 world width on each side of the projection
    // extent. If the viewport is wider than the world, we need to add half of
    // the viewport width to make sure we cover the whole viewport.
    var worldWidth = ol.extent.getWidth(projectionExtent);
    var buffer = Math.max(ol.extent.getWidth(extent) / 2, worldWidth);
    extent[0] = projectionExtent[0] - buffer;
    extent[2] = projectionExtent[2] + buffer;
  }

  if (!this.dirty_ &&
      this.renderedResolution_ == resolution &&
      this.renderedRevision_ == vectorLayerRevision &&
      this.renderedRenderOrder_ == vectorLayerRenderOrder &&
      ol.extent.containsExtent(this.renderedExtent_, extent)) {
    return true;
  }

  // FIXME dispose of old replayGroup in post render
  goog.dispose(this.replayGroup_);
  this.replayGroup_ = null;

  this.dirty_ = false;

  var replayGroup =
      new ol.render.canvas.ReplayGroup(
          ol.renderer.vector.getTolerance(resolution, pixelRatio), extent,
          resolution, vectorLayer.getRenderBuffer());
  vectorSource.loadFeatures(extent, resolution, projection);
  /**
   * @param {ol.Feature} feature Feature.
   * @this {ol.renderer.canvas.VectorLayer}
   */
  var renderFeature = function(feature) {
    var styles;
    var styleFunction = feature.getStyleFunction();
    if (styleFunction) {
      styles = styleFunction.call(feature, resolution);
    } else {
      styleFunction = vectorLayer.getStyleFunction();
      if (styleFunction) {
        styles = styleFunction(feature, resolution);
      }
    }
    if (styles) {
      var dirty = this.renderFeature(
          feature, resolution, pixelRatio, styles, replayGroup);
      this.dirty_ = this.dirty_ || dirty;
    }
  };
  if (vectorLayerRenderOrder) {
    /** @type {Array.<ol.Feature>} */
    var features = [];
    vectorSource.forEachFeatureInExtent(extent,
        /**
         * @param {ol.Feature} feature Feature.
         */
        function(feature) {
          features.push(feature);
        }, this);
    features.sort(vectorLayerRenderOrder);
    features.forEach(renderFeature, this);
  } else {
    vectorSource.forEachFeatureInExtent(extent, renderFeature, this);
  }
  replayGroup.finish();

  this.renderedResolution_ = resolution;
  this.renderedRevision_ = vectorLayerRevision;
  this.renderedRenderOrder_ = vectorLayerRenderOrder;
  this.renderedExtent_ = extent;
  this.replayGroup_ = replayGroup;

  return true;
};


/**
 * @param {ol.Feature} feature Feature.
 * @param {number} resolution Resolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {(ol.style.Style|Array.<ol.style.Style>)} styles The style or array of
 *     styles.
 * @param {ol.render.canvas.ReplayGroup} replayGroup Replay group.
 * @return {boolean} `true` if an image is loading.
 */
ol.renderer.canvas.VectorLayer.prototype.renderFeature = function(feature, resolution, pixelRatio, styles, replayGroup) {
  if (!styles) {
    return false;
  }
  var loading = false;
  if (goog.isArray(styles)) {
    for (var i = 0, ii = styles.length; i < ii; ++i) {
      loading = ol.renderer.vector.renderFeature(
          replayGroup, feature, styles[i],
          ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio),
          this.handleStyleImageChange_, this) || loading;
    }
  } else {
    loading = ol.renderer.vector.renderFeature(
        replayGroup, feature, styles,
        ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio),
        this.handleStyleImageChange_, this) || loading;
  }
  return loading;
};

goog.provide('ol.TileUrlFunction');
goog.provide('ol.TileUrlFunctionType');

goog.require('goog.asserts');
goog.require('goog.math');
goog.require('ol.TileCoord');
goog.require('ol.tilecoord');


/**
 * {@link ol.source.Tile} sources use a function of this type to get the url
 * that provides a tile for a given tile coordinate.
 *
 * This function takes an {@link ol.TileCoord} for the tile coordinate, a
 * `{number}` representing the pixel ratio and an {@link ol.proj.Projection} for
 * the projection  as arguments and returns a `{string}` representing the tile
 * URL, or undefined if no tile should be requested for the passed tile
 * coordinate.
 *
 * @typedef {function(ol.TileCoord, number,
 *           ol.proj.Projection): (string|undefined)}
 * @api
 */
ol.TileUrlFunctionType;


/**
 * @typedef {function(ol.TileCoord, ol.proj.Projection, ol.TileCoord=):
 *     ol.TileCoord}
 */
ol.TileCoordTransformType;


/**
 * @param {string} template Template.
 * @param {ol.tilegrid.TileGrid} tileGrid Tile grid.
 * @return {ol.TileUrlFunctionType} Tile URL function.
 */
ol.TileUrlFunction.createFromTemplate = function(template, tileGrid) {
  var zRegEx = /\{z\}/g;
  var xRegEx = /\{x\}/g;
  var yRegEx = /\{y\}/g;
  var dashYRegEx = /\{-y\}/g;
  return (
      /**
       * @param {ol.TileCoord} tileCoord Tile Coordinate.
       * @param {number} pixelRatio Pixel ratio.
       * @param {ol.proj.Projection} projection Projection.
       * @return {string|undefined} Tile URL.
       */
      function(tileCoord, pixelRatio, projection) {
        if (!tileCoord) {
          return undefined;
        } else {
          return template.replace(zRegEx, tileCoord[0].toString())
              .replace(xRegEx, tileCoord[1].toString())
              .replace(yRegEx, function() {
                var y = -tileCoord[2] - 1;
                return y.toString();
              })
              .replace(dashYRegEx, function() {
                var z = tileCoord[0];
                var range = tileGrid.getFullTileRange(z);
                goog.asserts.assert(range,
                    'The {-y} template requires a tile grid with extent');
                var y = range.getHeight() + tileCoord[2];
                return y.toString();
              });
        }
      });
};


/**
 * @param {Array.<string>} templates Templates.
 * @param {ol.tilegrid.TileGrid} tileGrid Tile grid.
 * @return {ol.TileUrlFunctionType} Tile URL function.
 */
ol.TileUrlFunction.createFromTemplates = function(templates, tileGrid) {
  var len = templates.length;
  var tileUrlFunctions = new Array(len);
  for (var i = 0; i < len; ++i) {
    tileUrlFunctions[i] = ol.TileUrlFunction.createFromTemplate(
        templates[i], tileGrid);
  }
  return ol.TileUrlFunction.createFromTileUrlFunctions(tileUrlFunctions);
};


/**
 * @param {Array.<ol.TileUrlFunctionType>} tileUrlFunctions Tile URL Functions.
 * @return {ol.TileUrlFunctionType} Tile URL function.
 */
ol.TileUrlFunction.createFromTileUrlFunctions = function(tileUrlFunctions) {
  goog.asserts.assert(tileUrlFunctions.length > 0,
      'Length of tile url functions should be greater than 0');
  if (tileUrlFunctions.length === 1) {
    return tileUrlFunctions[0];
  }
  return (
      /**
       * @param {ol.TileCoord} tileCoord Tile Coordinate.
       * @param {number} pixelRatio Pixel ratio.
       * @param {ol.proj.Projection} projection Projection.
       * @return {string|undefined} Tile URL.
       */
      function(tileCoord, pixelRatio, projection) {
        if (!tileCoord) {
          return undefined;
        } else {
          var h = ol.tilecoord.hash(tileCoord);
          var index = goog.math.modulo(h, tileUrlFunctions.length);
          return tileUrlFunctions[index](tileCoord, pixelRatio, projection);
        }
      });
};


/**
 * @param {ol.TileCoord} tileCoord Tile coordinate.
 * @param {number} pixelRatio Pixel ratio.
 * @param {ol.proj.Projection} projection Projection.
 * @return {string|undefined} Tile URL.
 */
ol.TileUrlFunction.nullTileUrlFunction = function(tileCoord, pixelRatio, projection) {
  return undefined;
};


/**
 * @param {string} url URL.
 * @return {Array.<string>} Array of urls.
 */
ol.TileUrlFunction.expandUrl = function(url) {
  var urls = [];
  var match = /\{(\d)-(\d)\}/.exec(url) || /\{([a-z])-([a-z])\}/.exec(url);
  if (match) {
    var startCharCode = match[1].charCodeAt(0);
    var stopCharCode = match[2].charCodeAt(0);
    var charCode;
    for (charCode = startCharCode; charCode <= stopCharCode; ++charCode) {
      urls.push(url.replace(match[0], String.fromCharCode(charCode)));
    }
  } else {
    urls.push(url);
  }
  return urls;
};

goog.provide('ol.source.UrlTile');

goog.require('goog.events');
goog.require('ol.TileLoadFunctionType');
goog.require('ol.TileState');
goog.require('ol.TileUrlFunction');
goog.require('ol.TileUrlFunctionType');
goog.require('ol.proj');
goog.require('ol.source.Tile');
goog.require('ol.source.TileEvent');


/**
 * @typedef {{attributions: (Array.<ol.Attribution>|undefined),
 *            extent: (ol.Extent|undefined),
 *            logo: (string|olx.LogoOptions|undefined),
 *            opaque: (boolean|undefined),
 *            projection: ol.proj.ProjectionLike,
 *            state: (ol.source.State|string|undefined),
 *            tileGrid: (ol.tilegrid.TileGrid|undefined),
 *            tileLoadFunction: ol.TileLoadFunctionType,
 *            tilePixelRatio: (number|undefined),
 *            tileUrlFunction: (ol.TileUrlFunctionType|undefined),
 *            url: (string|undefined),
 *            urls: (Array.<string>|undefined),
 *            wrapX: (boolean|undefined)}}
 */
ol.source.UrlTileOptions;


/**
 * @classdesc
 * Base class for sources providing tiles divided into a tile grid over http.
 *
 * @constructor
 * @fires ol.source.TileEvent
 * @extends {ol.source.Tile}
 * @param {ol.source.UrlTileOptions} options Image tile options.
 */
ol.source.UrlTile = function(options) {

  goog.base(this, {
    attributions: options.attributions,
    cacheSize: options.cacheSize,
    extent: options.extent,
    logo: options.logo,
    opaque: options.opaque,
    projection: options.projection,
    state: options.state ?
        /** @type {ol.source.State} */ (options.state) : undefined,
    tileGrid: options.tileGrid,
    tilePixelRatio: options.tilePixelRatio,
    wrapX: options.wrapX
  });

  /**
   * @protected
   * @type {ol.TileLoadFunctionType}
   */
  this.tileLoadFunction = options.tileLoadFunction;

  /**
   * @protected
   * @type {ol.TileUrlFunctionType}
   */
  this.tileUrlFunction =
      this.fixedTileUrlFunction || ol.TileUrlFunction.nullTileUrlFunction;

  /**
   * @protected
   * @type {!Array.<string>|null}
   */
  this.urls = null;

  if (options.urls) {
    this.setUrls(options.urls);
  } else if (options.url) {
    this.setUrl(options.url);
  }
  if (options.tileUrlFunction) {
    this.setTileUrlFunction(options.tileUrlFunction);
  }

};
goog.inherits(ol.source.UrlTile, ol.source.Tile);


/**
 * @type {ol.TileUrlFunctionType|undefined}
 * @protected
 */
ol.source.UrlTile.prototype.fixedTileUrlFunction;

/**
 * Return the tile load function of the source.
 * @return {ol.TileLoadFunctionType} TileLoadFunction
 * @api
 */
ol.source.UrlTile.prototype.getTileLoadFunction = function() {
  return this.tileLoadFunction;
};


/**
 * Return the tile URL function of the source.
 * @return {ol.TileUrlFunctionType} TileUrlFunction
 * @api
 */
ol.source.UrlTile.prototype.getTileUrlFunction = function() {
  return this.tileUrlFunction;
};


/**
 * Return the URLs used for this source.
 * When a tileUrlFunction is used instead of url or urls,
 * null will be returned.
 * @return {!Array.<string>|null} URLs.
 * @api
 */
ol.source.UrlTile.prototype.getUrls = function() {
  return this.urls;
};


/**
 * Handle tile change events.
 * @param {goog.events.Event} event Event.
 * @protected
 */
ol.source.UrlTile.prototype.handleTileChange = function(event) {
  var tile = /** @type {ol.Tile} */ (event.target);
  switch (tile.getState()) {
    case ol.TileState.LOADING:
      this.dispatchEvent(
          new ol.source.TileEvent(ol.source.TileEventType.TILELOADSTART, tile));
      break;
    case ol.TileState.LOADED:
      this.dispatchEvent(
          new ol.source.TileEvent(ol.source.TileEventType.TILELOADEND, tile));
      break;
    case ol.TileState.ERROR:
      this.dispatchEvent(
          new ol.source.TileEvent(ol.source.TileEventType.TILELOADERROR, tile));
      break;
    default:
      // pass
  }
};


/**
 * Set the tile load function of the source.
 * @param {ol.TileLoadFunctionType} tileLoadFunction Tile load function.
 * @api
 */
ol.source.UrlTile.prototype.setTileLoadFunction = function(tileLoadFunction) {
  this.tileCache.clear();
  this.tileLoadFunction = tileLoadFunction;
  this.changed();
};


/**
 * Set the tile URL function of the source.
 * @param {ol.TileUrlFunctionType} tileUrlFunction Tile URL function.
 * @api
 */
ol.source.UrlTile.prototype.setTileUrlFunction = function(tileUrlFunction) {
  // FIXME It should be possible to be more intelligent and avoid clearing the
  // FIXME cache.  The tile URL function would need to be incorporated into the
  // FIXME cache key somehow.
  this.tileCache.clear();
  this.tileUrlFunction = tileUrlFunction;
  this.changed();
};


/**
 * Set the URL to use for requests.
 * @param {string} url URL.
 * @api stable
 */
ol.source.UrlTile.prototype.setUrl = function(url) {
  var urls = this.urls = ol.TileUrlFunction.expandUrl(url);
  this.setTileUrlFunction(this.fixedTileUrlFunction ?
      this.fixedTileUrlFunction.bind(this) :
      ol.TileUrlFunction.createFromTemplates(urls, this.tileGrid));
};


/**
 * Set the URLs to use for requests.
 * @param {Array.<string>} urls URLs.
 * @api stable
 */
ol.source.UrlTile.prototype.setUrls = function(urls) {
  this.urls = urls;
  this.setTileUrlFunction(this.fixedTileUrlFunction ||
      ol.TileUrlFunction.createFromTemplates(urls, this.tileGrid));
};


/**
 * @inheritDoc
 */
ol.source.UrlTile.prototype.useTile = function(z, x, y) {
  var tileCoordKey = this.getKeyZXY(z, x, y);
  if (this.tileCache.containsKey(tileCoordKey)) {
    this.tileCache.get(tileCoordKey);
  }
};

goog.provide('ol.source.VectorTile');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('ol.TileState');
goog.require('ol.VectorTile');
goog.require('ol.featureloader');
goog.require('ol.source.UrlTile');


/**
 * @classdesc
 * Class for layer sources providing vector data divided into a tile grid, to be
 * used with {@link ol.layer.VectorTile}. Although this source receives tiles
 * with vector features from the server, it is not meant for feature editing.
 * Features are optimized for rendering, their geometries are clipped at or near
 * tile boundaries and simplified for a view resolution. See
 * {@link ol.source.Vector} for vector sources that are suitable for feature
 * editing.
 *
 * @constructor
 * @fires ol.source.TileEvent
 * @extends {ol.source.UrlTile}
 * @param {olx.source.VectorTileOptions} options Vector tile options.
 * @api
 */
ol.source.VectorTile = function(options) {

  goog.base(this, {
    attributions: options.attributions,
    cacheSize: ol.DEFAULT_TILE_CACHE_HIGH_WATER_MARK / 16,
    extent: options.extent,
    logo: options.logo,
    opaque: options.opaque,
    projection: options.projection,
    state: options.state ?
        /** @type {ol.source.State} */ (options.state) : undefined,
    tileGrid: options.tileGrid,
    tileLoadFunction: options.tileLoadFunction ?
        options.tileLoadFunction : ol.source.VectorTile.defaultTileLoadFunction,
    tileUrlFunction: options.tileUrlFunction,
    tilePixelRatio: options.tilePixelRatio,
    url: options.url,
    urls: options.urls,
    wrapX: options.wrapX === undefined ? true : options.wrapX
  });

  /**
   * @private
   * @type {ol.format.Feature}
   */
  this.format_ = options.format ? options.format : null;

  /**
   * @protected
   * @type {function(new: ol.VectorTile, ol.TileCoord, ol.TileState, string,
   *        ol.format.Feature, ol.TileLoadFunctionType, ol.proj.Projection)}
   */
  this.tileClass = options.tileClass ? options.tileClass : ol.VectorTile;

};
goog.inherits(ol.source.VectorTile, ol.source.UrlTile);


/**
 * @inheritDoc
 */
ol.source.VectorTile.prototype.getTile = function(z, x, y, pixelRatio, projection) {
  var tileCoordKey = this.getKeyZXY(z, x, y);
  if (this.tileCache.containsKey(tileCoordKey)) {
    return /** @type {!ol.Tile} */ (this.tileCache.get(tileCoordKey));
  } else {
    goog.asserts.assert(projection, 'argument projection is truthy');
    var tileCoord = [z, x, y];
    var urlTileCoord = this.getTileCoordForTileUrlFunction(
        tileCoord, projection);
    var tileUrl = urlTileCoord ?
        this.tileUrlFunction(urlTileCoord, pixelRatio, projection) : undefined;
    var tile = new this.tileClass(
        tileCoord,
        tileUrl !== undefined ? ol.TileState.IDLE : ol.TileState.EMPTY,
        tileUrl !== undefined ? tileUrl : '',
        this.format_, this.tileLoadFunction, projection);
    goog.events.listen(tile, goog.events.EventType.CHANGE,
        this.handleTileChange, false, this);

    this.tileCache.set(tileCoordKey, tile);
    return tile;
  }
};


/**
 * @param {ol.VectorTile} vectorTile Vector tile.
 * @param {string} url URL.
 */
ol.source.VectorTile.defaultTileLoadFunction = function(vectorTile, url) {
  vectorTile.setLoader(ol.featureloader.tile(url, vectorTile.getFormat()));
};

goog.provide('ol.renderer.canvas.VectorTileLayer');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.vec.Mat4');
goog.require('ol.Feature');
goog.require('ol.TileRange');
goog.require('ol.TileState');
goog.require('ol.VectorTile');
goog.require('ol.ViewHint');
goog.require('ol.array');
goog.require('ol.dom');
goog.require('ol.extent');
goog.require('ol.geom.flat.transform');
goog.require('ol.layer.VectorTile');
goog.require('ol.proj.Units');
goog.require('ol.render.EventType');
goog.require('ol.render.canvas.ReplayGroup');
goog.require('ol.renderer.canvas.Layer');
goog.require('ol.renderer.vector');
goog.require('ol.size');
goog.require('ol.source.VectorTile');
goog.require('ol.vec.Mat4');


/**
 * @constructor
 * @extends {ol.renderer.canvas.Layer}
 * @param {ol.layer.VectorTile} layer VectorTile layer.
 */
ol.renderer.canvas.VectorTileLayer = function(layer) {

  goog.base(this, layer);

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.context_ = ol.dom.createCanvasContext2D();

  /**
   * @private
   * @type {boolean}
   */
  this.dirty_ = false;

  /**
   * @private
   * @type {Array.<ol.VectorTile>}
   */
  this.renderedTiles_ = [];

  /**
   * @private
   * @type {ol.Extent}
   */
  this.tmpExtent_ = ol.extent.createEmpty();

  /**
   * @private
   * @type {ol.Size}
   */
  this.tmpSize_ = [NaN, NaN];

  /**
   * @private
   * @type {!goog.vec.Mat4.Number}
   */
  this.tmpTransform_ = goog.vec.Mat4.createNumber();

};
goog.inherits(ol.renderer.canvas.VectorTileLayer, ol.renderer.canvas.Layer);


/**
 * @inheritDoc
 */
ol.renderer.canvas.VectorTileLayer.prototype.composeFrame = function(frameState, layerState, context) {

  var pixelRatio = frameState.pixelRatio;
  var skippedFeatureUids = layerState.managed ?
      frameState.skippedFeatureUids : {};
  var viewState = frameState.viewState;
  var center = viewState.center;
  var projection = viewState.projection;
  var resolution = viewState.resolution;
  var rotation = viewState.rotation;
  var size = frameState.size;
  var pixelScale = pixelRatio / resolution;
  var layer = this.getLayer();
  var source = layer.getSource();
  goog.asserts.assertInstanceof(source, ol.source.VectorTile,
      'Source is an ol.source.VectorTile');
  var tilePixelRatio = source.getTilePixelRatio(pixelRatio);
  var maxScale = tilePixelRatio / pixelRatio;

  var transform = this.getTransform(frameState, 0);

  this.dispatchPreComposeEvent(context, frameState, transform);

  var replayContext;
  if (layer.hasListener(ol.render.EventType.RENDER)) {
    // resize and clear
    this.context_.canvas.width = context.canvas.width;
    this.context_.canvas.height = context.canvas.height;
    replayContext = this.context_;
  } else {
    replayContext = context;
  }
  // for performance reasons, context.save / context.restore is not used
  // to save and restore the transformation matrix and the opacity.
  // see http://jsperf.com/context-save-restore-versus-variable
  var alpha = replayContext.globalAlpha;
  replayContext.globalAlpha = layerState.opacity;

  var tilesToDraw = this.renderedTiles_;
  var tileGrid = source.getTileGrid();

  var currentZ, height, i, ii, insertPoint, insertTransform, offsetX, offsetY;
  var origin, pixelSpace, replayState, scale, tile, tileCenter, tileContext;
  var tileExtent, tilePixelResolution, tilePixelSize, tileResolution, tileSize;
  var tileTransform, width;
  for (i = 0, ii = tilesToDraw.length; i < ii; ++i) {
    tile = tilesToDraw[i];
    replayState = tile.getReplayState();
    tileExtent = tileGrid.getTileCoordExtent(
        tile.getTileCoord(), this.tmpExtent_);
    currentZ = tile.getTileCoord()[0];
    tileSize = ol.size.toSize(tileGrid.getTileSize(currentZ), this.tmpSize_);
    pixelSpace = tile.getProjection().getUnits() == ol.proj.Units.TILE_PIXELS;
    tileResolution = tileGrid.getResolution(currentZ);
    tilePixelResolution = tileResolution / tilePixelRatio;
    scale = tileResolution / resolution;
    offsetX = Math.round(pixelRatio * size[0] / 2);
    offsetY = Math.round(pixelRatio * size[1] / 2);
    width = tileSize[0] * pixelRatio * scale;
    height = tileSize[1] * pixelRatio * scale;
    if (width < 1 || scale > maxScale) {
      if (pixelSpace) {
        origin = ol.extent.getTopLeft(tileExtent);
        tileTransform = ol.vec.Mat4.makeTransform2D(this.tmpTransform_,
            offsetX, offsetY,
            pixelScale * tilePixelResolution,
            pixelScale * tilePixelResolution,
            rotation,
            (origin[0] - center[0]) / tilePixelResolution,
            (center[1] - origin[1]) / tilePixelResolution);
      } else {
        tileTransform = transform;
      }
      replayState.replayGroup.replay(replayContext, pixelRatio,
          tileTransform, rotation, skippedFeatureUids);
    } else {
      tilePixelSize = source.getTilePixelSize(currentZ, pixelRatio, projection);
      if (pixelSpace) {
        tileTransform = ol.vec.Mat4.makeTransform2D(this.tmpTransform_,
            0, 0,
            pixelScale * tilePixelResolution, pixelScale * tilePixelResolution,
            rotation,
            -tilePixelSize[0] / 2, -tilePixelSize[1] / 2);
      } else {
        tileCenter = ol.extent.getCenter(tileExtent);
        tileTransform = ol.vec.Mat4.makeTransform2D(this.tmpTransform_,
            0, 0,
            pixelScale, -pixelScale,
            -rotation,
            -tileCenter[0], -tileCenter[1]);
      }
      tileContext = tile.getContext();
      if (replayState.resolution !== resolution ||
          replayState.rotation !== rotation) {
        replayState.resolution = resolution;
        replayState.rotation = rotation;
        tileContext.canvas.width = width + 0.5;
        tileContext.canvas.height = height + 0.5;
        tileContext.translate(width / 2, height / 2);
        tileContext.rotate(-rotation);
        replayState.replayGroup.replay(tileContext, pixelRatio,
            tileTransform, rotation, skippedFeatureUids, false);
      }
      insertTransform = ol.vec.Mat4.makeTransform2D(this.tmpTransform_,
          0, 0, pixelScale, -pixelScale, 0, -center[0], -center[1]);
      insertPoint = ol.geom.flat.transform.transform2D(
          ol.extent.getTopLeft(tileExtent), 0, 1, 2, insertTransform);
      replayContext.translate(offsetX, offsetY);
      replayContext.rotate(rotation);
      replayContext.drawImage(tileContext.canvas,
          Math.round(insertPoint[0]), Math.round(insertPoint[1]));
      replayContext.rotate(-rotation);
      replayContext.translate(-offsetX, -offsetY);
    }
  }

  if (replayContext != context) {
    this.dispatchRenderEvent(replayContext, frameState, transform);
    context.drawImage(replayContext.canvas, 0, 0);
  }
  replayContext.globalAlpha = alpha;

  this.dispatchPostComposeEvent(context, frameState, transform);
};


/**
 * @param {ol.VectorTile} tile Tile.
 * @param {ol.layer.VectorTile} layer Vector tile layer.
 * @param {number} pixelRatio Pixel ratio.
 */
ol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup = function(tile,
    layer, pixelRatio) {
  var revision = layer.getRevision();
  var renderOrder = layer.getRenderOrder() || null;

  var replayState = tile.getReplayState();
  if (!replayState.dirty && replayState.renderedRevision == revision &&
      replayState.renderedRenderOrder == renderOrder) {
    return;
  }

  // FIXME dispose of old replayGroup in post render
  goog.dispose(replayState.replayGroup);
  replayState.replayGroup = null;
  replayState.dirty = false;

  var source = layer.getSource();
  goog.asserts.assertInstanceof(source, ol.source.VectorTile,
      'Source is an ol.source.VectorTile');
  var tileGrid = source.getTileGrid();
  var tileCoord = tile.getTileCoord();
  var pixelSpace = tile.getProjection().getUnits() == ol.proj.Units.TILE_PIXELS;
  var extent;
  if (pixelSpace) {
    var tilePixelSize = source.getTilePixelSize(tileCoord[0], pixelRatio,
        tile.getProjection());
    extent = [0, 0, tilePixelSize[0], tilePixelSize[1]];
  } else {
    extent = tileGrid.getTileCoordExtent(tileCoord);
  }
  var resolution = tileGrid.getResolution(tileCoord[0]);
  var tileResolution =
      pixelSpace ? source.getTilePixelRatio(pixelRatio) : resolution;
  replayState.dirty = false;
  var replayGroup = new ol.render.canvas.ReplayGroup(0, extent,
      tileResolution, layer.getRenderBuffer());
  var squaredTolerance = ol.renderer.vector.getSquaredTolerance(
      tileResolution, pixelRatio);

  /**
   * @param {ol.Feature|ol.render.Feature} feature Feature.
   * @this {ol.renderer.canvas.VectorTileLayer}
   */
  function renderFeature(feature) {
    var styles;
    var styleFunction = feature.getStyleFunction();
    if (styleFunction) {
      goog.asserts.assertInstanceof(feature, ol.Feature, 'Got an ol.Feature');
      styles = styleFunction.call(feature, resolution);
    } else {
      styleFunction = layer.getStyleFunction();
      if (styleFunction) {
        styles = styleFunction(feature, resolution);
      }
    }
    if (styles) {
      if (!goog.isArray(styles)) {
        styles = [styles];
      }
      var dirty = this.renderFeature(feature, squaredTolerance, styles,
          replayGroup);
      this.dirty_ = this.dirty_ || dirty;
      replayState.dirty = replayState.dirty || dirty;
    }
  }

  var features = tile.getFeatures();
  if (renderOrder && renderOrder !== replayState.renderedRenderOrder) {
    features.sort(renderOrder);
  }
  features.forEach(renderFeature, this);

  replayGroup.finish();

  replayState.renderedRevision = revision;
  replayState.renderedRenderOrder = renderOrder;
  replayState.replayGroup = replayGroup;
  replayState.resolution = NaN;
};


/**
 * @inheritDoc
 */
ol.renderer.canvas.VectorTileLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) {
  var pixelRatio = frameState.pixelRatio;
  var resolution = frameState.viewState.resolution;
  var rotation = frameState.viewState.rotation;
  var layer = this.getLayer();
  var layerState = frameState.layerStates[goog.getUid(layer)];
  /** @type {Object.<string, boolean>} */
  var features = {};

  var replayables = this.renderedTiles_;
  var source = layer.getSource();
  goog.asserts.assertInstanceof(source, ol.source.VectorTile,
      'Source is an ol.source.VectorTile');
  var tileGrid = source.getTileGrid();
  var found, tileSpaceCoordinate;
  var i, ii, origin, replayGroup;
  var tile, tileCoord, tileExtent, tilePixelRatio, tileResolution;
  for (i = 0, ii = replayables.length; i < ii; ++i) {
    tile = replayables[i];
    tileCoord = tile.getTileCoord();
    tileExtent = source.getTileGrid().getTileCoordExtent(tileCoord,
        this.tmpExtent_);
    if (!ol.extent.containsCoordinate(tileExtent, coordinate)) {
      continue;
    }
    if (tile.getProjection().getUnits() === ol.proj.Units.TILE_PIXELS) {
      origin = ol.extent.getTopLeft(tileExtent);
      tilePixelRatio = source.getTilePixelRatio(pixelRatio);
      tileResolution = tileGrid.getResolution(tileCoord[0]) / tilePixelRatio;
      tileSpaceCoordinate = [
        (coordinate[0] - origin[0]) / tileResolution,
        (origin[1] - coordinate[1]) / tileResolution
      ];
      resolution = tilePixelRatio;
    } else {
      tileSpaceCoordinate = coordinate;
    }
    replayGroup = tile.getReplayState().replayGroup;
    found = found || replayGroup.forEachFeatureAtCoordinate(
        tileSpaceCoordinate, resolution, rotation,
        layerState.managed ? frameState.skippedFeatureUids : {},
        /**
         * @param {ol.Feature|ol.render.Feature} feature Feature.
         * @return {?} Callback result.
         */
        function(feature) {
          goog.asserts.assert(feature, 'received a feature');
          var key = goog.getUid(feature).toString();
          if (!(key in features)) {
            features[key] = true;
            return callback.call(thisArg, feature, layer);
          }
        });
  }
  return found;
};


/**
 * Handle changes in image style state.
 * @param {goog.events.Event} event Image style change event.
 * @private
 */
ol.renderer.canvas.VectorTileLayer.prototype.handleStyleImageChange_ = function(event) {
  this.renderIfReadyAndVisible();
};


/**
 * @inheritDoc
 */
ol.renderer.canvas.VectorTileLayer.prototype.prepareFrame = function(frameState, layerState) {
  var layer = /** @type {ol.layer.Vector} */ (this.getLayer());
  goog.asserts.assertInstanceof(layer, ol.layer.VectorTile,
      'layer is an instance of ol.layer.VectorTile');
  var source = layer.getSource();
  goog.asserts.assertInstanceof(source, ol.source.VectorTile,
      'Source is an ol.source.VectorTile');

  this.updateAttributions(
      frameState.attributions, source.getAttributions());
  this.updateLogos(frameState, source);

  var animating = frameState.viewHints[ol.ViewHint.ANIMATING];
  var interacting = frameState.viewHints[ol.ViewHint.INTERACTING];
  var updateWhileAnimating = layer.getUpdateWhileAnimating();
  var updateWhileInteracting = layer.getUpdateWhileInteracting();

  if (!this.dirty_ && (!updateWhileAnimating && animating) ||
      (!updateWhileInteracting && interacting)) {
    return true;
  }

  var extent = frameState.extent;
  if (layerState.extent) {
    extent = ol.extent.getIntersection(extent, layerState.extent);
  }
  if (ol.extent.isEmpty(extent)) {
    // Return false to prevent the rendering of the layer.
    return false;
  }

  var viewState = frameState.viewState;
  var projection = viewState.projection;
  var resolution = viewState.resolution;
  var pixelRatio = frameState.pixelRatio;

  var tileGrid = source.getTileGrid();
  var resolutions = tileGrid.getResolutions();
  var z = resolutions.length - 1;
  while (z > 0 && resolutions[z] < resolution) {
    --z;
  }
  var tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z);
  this.updateUsedTiles(frameState.usedTiles, source, z, tileRange);
  this.manageTilePyramid(frameState, source, tileGrid, pixelRatio,
      projection, extent, z, layer.getPreload());
  this.scheduleExpireCache(frameState, source);

  /**
   * @type {Object.<number, Object.<string, ol.VectorTile>>}
   */
  var tilesToDrawByZ = {};
  tilesToDrawByZ[z] = {};

  var findLoadedTiles = this.createLoadedTileFinder(source, projection,
      tilesToDrawByZ);

  var useInterimTilesOnError = layer.getUseInterimTilesOnError();

  var tmpExtent = this.tmpExtent_;
  var tmpTileRange = new ol.TileRange(0, 0, 0, 0);
  var childTileRange, fullyLoaded, tile, tileState, x, y;
  for (x = tileRange.minX; x <= tileRange.maxX; ++x) {
    for (y = tileRange.minY; y <= tileRange.maxY; ++y) {

      tile = source.getTile(z, x, y, pixelRatio, projection);
      goog.asserts.assertInstanceof(tile, ol.VectorTile,
          'Tile is an ol.VectorTile');
      tileState = tile.getState();
      if (tileState == ol.TileState.LOADED ||
          tileState == ol.TileState.EMPTY ||
          (tileState == ol.TileState.ERROR && !useInterimTilesOnError)) {
        tilesToDrawByZ[z][tile.tileCoord.toString()] = tile;
        continue;
      }

      fullyLoaded = tileGrid.forEachTileCoordParentTileRange(
          tile.tileCoord, findLoadedTiles, null, tmpTileRange, tmpExtent);
      if (!fullyLoaded) {
        childTileRange = tileGrid.getTileCoordChildTileRange(
            tile.tileCoord, tmpTileRange, tmpExtent);
        if (childTileRange) {
          findLoadedTiles(z + 1, childTileRange);
        }
      }

    }
  }

  this.dirty_ = false;

  /** @type {Array.<number>} */
  var zs = Object.keys(tilesToDrawByZ).map(Number);
  zs.sort(ol.array.numberSafeCompareFunction);
  var replayables = [];
  var i, ii, currentZ, tileCoordKey, tilesToDraw;
  for (i = 0, ii = zs.length; i < ii; ++i) {
    currentZ = zs[i];
    tilesToDraw = tilesToDrawByZ[currentZ];
    for (tileCoordKey in tilesToDraw) {
      tile = tilesToDraw[tileCoordKey];
      if (tile.getState() == ol.TileState.LOADED) {
        replayables.push(tile);
        this.createReplayGroup(tile, layer, pixelRatio);
      }
    }
  }

  this.renderedTiles_ = replayables;

  return true;
};


/**
 * @param {ol.Feature|ol.render.Feature} feature Feature.
 * @param {number} squaredTolerance Squared tolerance.
 * @param {(ol.style.Style|Array.<ol.style.Style>)} styles The style or array of
 *     styles.
 * @param {ol.render.canvas.ReplayGroup} replayGroup Replay group.
 * @return {boolean} `true` if an image is loading.
 */
ol.renderer.canvas.VectorTileLayer.prototype.renderFeature = function(feature, squaredTolerance, styles, replayGroup) {
  if (!styles) {
    return false;
  }
  var loading = false;
  if (goog.isArray(styles)) {
    for (var i = 0, ii = styles.length; i < ii; ++i) {
      loading = ol.renderer.vector.renderFeature(
          replayGroup, feature, styles[i], squaredTolerance,
          this.handleStyleImageChange_, this) || loading;
    }
  } else {
    loading = ol.renderer.vector.renderFeature(
        replayGroup, feature, styles, squaredTolerance,
        this.handleStyleImageChange_, this) || loading;
  }
  return loading;
};

// FIXME offset panning

goog.provide('ol.renderer.canvas.Map');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.style');
goog.require('goog.vec.Mat4');
goog.require('ol');
goog.require('ol.RendererType');
goog.require('ol.css');
goog.require('ol.dom');
goog.require('ol.layer.Image');
goog.require('ol.layer.Layer');
goog.require('ol.layer.Tile');
goog.require('ol.layer.Vector');
goog.require('ol.layer.VectorTile');
goog.require('ol.render.Event');
goog.require('ol.render.EventType');
goog.require('ol.render.canvas.Immediate');
goog.require('ol.renderer.Map');
goog.require('ol.renderer.canvas.ImageLayer');
goog.require('ol.renderer.canvas.Layer');
goog.require('ol.renderer.canvas.TileLayer');
goog.require('ol.renderer.canvas.VectorLayer');
goog.require('ol.renderer.canvas.VectorTileLayer');
goog.require('ol.source.State');
goog.require('ol.vec.Mat4');


/**
 * @constructor
 * @extends {ol.renderer.Map}
 * @param {Element} container Container.
 * @param {ol.Map} map Map.
 */
ol.renderer.canvas.Map = function(container, map) {

  goog.base(this, container, map);

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.context_ = ol.dom.createCanvasContext2D();

  /**
   * @private
   * @type {HTMLCanvasElement}
   */
  this.canvas_ = this.context_.canvas;

  this.canvas_.style.width = '100%';
  this.canvas_.style.height = '100%';
  this.canvas_.className = ol.css.CLASS_UNSELECTABLE;
  goog.dom.insertChildAt(container, this.canvas_, 0);

  /**
   * @private
   * @type {boolean}
   */
  this.renderedVisible_ = true;

  /**
   * @private
   * @type {!goog.vec.Mat4.Number}
   */
  this.transform_ = goog.vec.Mat4.createNumber();

};
goog.inherits(ol.renderer.canvas.Map, ol.renderer.Map);


/**
 * @inheritDoc
 */
ol.renderer.canvas.Map.prototype.createLayerRenderer = function(layer) {
  if (ol.ENABLE_IMAGE && layer instanceof ol.layer.Image) {
    return new ol.renderer.canvas.ImageLayer(layer);
  } else if (ol.ENABLE_TILE && layer instanceof ol.layer.Tile) {
    return new ol.renderer.canvas.TileLayer(layer);
  } else if (ol.ENABLE_VECTOR_TILE && layer instanceof ol.layer.VectorTile) {
    return new ol.renderer.canvas.VectorTileLayer(layer);
  } else if (ol.ENABLE_VECTOR && layer instanceof ol.layer.Vector) {
    return new ol.renderer.canvas.VectorLayer(layer);
  } else {
    goog.asserts.fail('unexpected layer configuration');
    return null;
  }
};


/**
 * @param {ol.render.EventType} type Event type.
 * @param {olx.FrameState} frameState Frame state.
 * @private
 */
ol.renderer.canvas.Map.prototype.dispatchComposeEvent_ = function(type, frameState) {
  var map = this.getMap();
  var context = this.context_;
  if (map.hasListener(type)) {
    var extent = frameState.extent;
    var pixelRatio = frameState.pixelRatio;
    var viewState = frameState.viewState;
    var rotation = viewState.rotation;

    var transform = this.getTransform(frameState);

    var vectorContext = new ol.render.canvas.Immediate(context, pixelRatio,
        extent, transform, rotation);
    var composeEvent = new ol.render.Event(type, map, vectorContext,
        frameState, context, null);
    map.dispatchEvent(composeEvent);

    vectorContext.flush();
  }
};


/**
 * @param {olx.FrameState} frameState Frame state.
 * @protected
 * @return {!goog.vec.Mat4.Number} Transform.
 */
ol.renderer.canvas.Map.prototype.getTransform = function(frameState) {
  var pixelRatio = frameState.pixelRatio;
  var viewState = frameState.viewState;
  var resolution = viewState.resolution;
  return ol.vec.Mat4.makeTransform2D(this.transform_,
      this.canvas_.width / 2, this.canvas_.height / 2,
      pixelRatio / resolution, -pixelRatio / resolution,
      -viewState.rotation,
      -viewState.center[0], -viewState.center[1]);
};


/**
 * @inheritDoc
 */
ol.renderer.canvas.Map.prototype.getType = function() {
  return ol.RendererType.CANVAS;
};


/**
 * @inheritDoc
 */
ol.renderer.canvas.Map.prototype.renderFrame = function(frameState) {

  if (!frameState) {
    if (this.renderedVisible_) {
      goog.style.setElementShown(this.canvas_, false);
      this.renderedVisible_ = false;
    }
    return;
  }

  var context = this.context_;
  var width = frameState.size[0] * frameState.pixelRatio;
  var height = frameState.size[1] * frameState.pixelRatio;
  if (this.canvas_.width != width || this.canvas_.height != height) {
    this.canvas_.width = width;
    this.canvas_.height = height;
  } else {
    context.clearRect(0, 0, this.canvas_.width, this.canvas_.height);
  }

  this.calculateMatrices2D(frameState);

  this.dispatchComposeEvent_(ol.render.EventType.PRECOMPOSE, frameState);

  var layerStatesArray = frameState.layerStatesArray;
  goog.array.stableSort(layerStatesArray, ol.renderer.Map.sortByZIndex);

  var viewResolution = frameState.viewState.resolution;
  var i, ii, layer, layerRenderer, layerState;
  for (i = 0, ii = layerStatesArray.length; i < ii; ++i) {
    layerState = layerStatesArray[i];
    layer = layerState.layer;
    layerRenderer = this.getLayerRenderer(layer);
    goog.asserts.assertInstanceof(layerRenderer, ol.renderer.canvas.Layer,
        'layerRenderer is an instance of ol.renderer.canvas.Layer');
    if (!ol.layer.Layer.visibleAtResolution(layerState, viewResolution) ||
        layerState.sourceState != ol.source.State.READY) {
      continue;
    }
    if (layerRenderer.prepareFrame(frameState, layerState)) {
      layerRenderer.composeFrame(frameState, layerState, context);
    }
  }

  this.dispatchComposeEvent_(
      ol.render.EventType.POSTCOMPOSE, frameState);

  if (!this.renderedVisible_) {
    goog.style.setElementShown(this.canvas_, true);
    this.renderedVisible_ = true;
  }

  this.scheduleRemoveUnusedLayerRenderers(frameState);
  this.scheduleExpireIconCache(frameState);
};

goog.provide('ol.renderer.dom.Layer');

goog.require('ol');
goog.require('ol.layer.Layer');
goog.require('ol.renderer.Layer');


/**
 * @constructor
 * @extends {ol.renderer.Layer}
 * @param {ol.layer.Layer} layer Layer.
 * @param {!Element} target Target.
 */
ol.renderer.dom.Layer = function(layer, target) {

  goog.base(this, layer);

  /**
   * @type {!Element}
   * @protected
   */
  this.target = target;

};
goog.inherits(ol.renderer.dom.Layer, ol.renderer.Layer);


/**
 * Clear rendered elements.
 */
ol.renderer.dom.Layer.prototype.clearFrame = ol.nullFunction;


/**
 * @param {olx.FrameState} frameState Frame state.
 * @param {ol.layer.LayerState} layerState Layer state.
 */
ol.renderer.dom.Layer.prototype.composeFrame = ol.nullFunction;


/**
 * @return {!Element} Target.
 */
ol.renderer.dom.Layer.prototype.getTarget = function() {
  return this.target;
};


/**
 * @param {olx.FrameState} frameState Frame state.
 * @param {ol.layer.LayerState} layerState Layer state.
 * @return {boolean} whether composeFrame should be called.
 */
ol.renderer.dom.Layer.prototype.prepareFrame = goog.abstractMethod;

goog.provide('ol.renderer.dom.ImageLayer');

goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.vec.Mat4');
goog.require('ol.ImageBase');
goog.require('ol.ViewHint');
goog.require('ol.dom');
goog.require('ol.extent');
goog.require('ol.layer.Image');
goog.require('ol.proj');
goog.require('ol.renderer.dom.Layer');
goog.require('ol.vec.Mat4');


/**
 * @constructor
 * @extends {ol.renderer.dom.Layer}
 * @param {ol.layer.Image} imageLayer Image layer.
 */
ol.renderer.dom.ImageLayer = function(imageLayer) {
  var target = document.createElement('DIV');
  target.style.position = 'absolute';

  goog.base(this, imageLayer, target);

  /**
   * The last rendered image.
   * @private
   * @type {?ol.ImageBase}
   */
  this.image_ = null;

  /**
   * @private
   * @type {goog.vec.Mat4.Number}
   */
  this.transform_ = goog.vec.Mat4.createNumberIdentity();

};
goog.inherits(ol.renderer.dom.ImageLayer, ol.renderer.dom.Layer);


/**
 * @inheritDoc
 */
ol.renderer.dom.ImageLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) {
  var layer = this.getLayer();
  var source = layer.getSource();
  var resolution = frameState.viewState.resolution;
  var rotation = frameState.viewState.rotation;
  var skippedFeatureUids = frameState.skippedFeatureUids;
  return source.forEachFeatureAtCoordinate(
      coordinate, resolution, rotation, skippedFeatureUids,
      /**
       * @param {ol.Feature|ol.render.Feature} feature Feature.
       * @return {?} Callback result.
       */
      function(feature) {
        return callback.call(thisArg, feature, layer);
      });
};


/**
 * @inheritDoc
 */
ol.renderer.dom.ImageLayer.prototype.clearFrame = function() {
  goog.dom.removeChildren(this.target);
  this.image_ = null;
};


/**
 * @inheritDoc
 */
ol.renderer.dom.ImageLayer.prototype.prepareFrame = function(frameState, layerState) {

  var viewState = frameState.viewState;
  var viewCenter = viewState.center;
  var viewResolution = viewState.resolution;
  var viewRotation = viewState.rotation;

  var image = this.image_;
  var imageLayer = this.getLayer();
  goog.asserts.assertInstanceof(imageLayer, ol.layer.Image,
      'layer is an instance of ol.layer.Image');
  var imageSource = imageLayer.getSource();

  var hints = frameState.viewHints;

  var renderedExtent = frameState.extent;
  if (layerState.extent !== undefined) {
    renderedExtent = ol.extent.getIntersection(
        renderedExtent, layerState.extent);
  }

  if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.INTERACTING] &&
      !ol.extent.isEmpty(renderedExtent)) {
    var projection = viewState.projection;
    if (!ol.ENABLE_RASTER_REPROJECTION) {
      var sourceProjection = imageSource.getProjection();
      if (sourceProjection) {
        goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection),
            'projection and sourceProjection are equivalent');
        projection = sourceProjection;
      }
    }
    var image_ = imageSource.getImage(renderedExtent, viewResolution,
        frameState.pixelRatio, projection);
    if (image_) {
      var loaded = this.loadImage(image_);
      if (loaded) {
        image = image_;
      }
    }
  }

  if (image) {
    var imageExtent = image.getExtent();
    var imageResolution = image.getResolution();
    var transform = goog.vec.Mat4.createNumber();
    ol.vec.Mat4.makeTransform2D(transform,
        frameState.size[0] / 2, frameState.size[1] / 2,
        imageResolution / viewResolution, imageResolution / viewResolution,
        viewRotation,
        (imageExtent[0] - viewCenter[0]) / imageResolution,
        (viewCenter[1] - imageExtent[3]) / imageResolution);
    if (image != this.image_) {
      var imageElement = image.getImage(this);
      // Bootstrap sets the style max-width: 100% for all images, which breaks
      // prevents the image from being displayed in FireFox.  Workaround by
      // overriding the max-width style.
      imageElement.style.maxWidth = 'none';
      imageElement.style.position = 'absolute';
      goog.dom.removeChildren(this.target);
      this.target.appendChild(imageElement);
      this.image_ = image;
    }
    this.setTransform_(transform);
    this.updateAttributions(frameState.attributions, image.getAttributions());
    this.updateLogos(frameState, imageSource);
  }

  return true;
};


/**
 * @param {goog.vec.Mat4.Number} transform Transform.
 * @private
 */
ol.renderer.dom.ImageLayer.prototype.setTransform_ = function(transform) {
  if (!ol.vec.Mat4.equals2D(transform, this.transform_)) {
    ol.dom.transformElement2D(this.target, transform, 6);
    goog.vec.Mat4.setFromArray(this.transform_, transform);
  }
};

// FIXME probably need to reset TileLayerZ if offsets get too large
// FIXME when zooming out, preserve higher Z divs to avoid white flash

goog.provide('ol.renderer.dom.TileLayer');

goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.style');
goog.require('goog.vec.Mat4');
goog.require('ol');
goog.require('ol.Coordinate');
goog.require('ol.TileCoord');
goog.require('ol.TileRange');
goog.require('ol.TileState');
goog.require('ol.ViewHint');
goog.require('ol.array');
goog.require('ol.dom');
goog.require('ol.extent');
goog.require('ol.layer.Tile');
goog.require('ol.renderer.dom.Layer');
goog.require('ol.size');
goog.require('ol.tilegrid.TileGrid');
goog.require('ol.vec.Mat4');


/**
 * @constructor
 * @extends {ol.renderer.dom.Layer}
 * @param {ol.layer.Tile} tileLayer Tile layer.
 */
ol.renderer.dom.TileLayer = function(tileLayer) {

  var target = document.createElement('DIV');
  target.style.position = 'absolute';

  goog.base(this, tileLayer, target);

  /**
   * @private
   * @type {boolean}
   */
  this.renderedVisible_ = true;

  /**
   * @private
   * @type {number}
   */
  this.renderedOpacity_ = 1;

  /**
   * @private
   * @type {number}
   */
  this.renderedRevision_ = 0;

  /**
   * @private
   * @type {!Object.<number, ol.renderer.dom.TileLayerZ_>}
   */
  this.tileLayerZs_ = {};

};
goog.inherits(ol.renderer.dom.TileLayer, ol.renderer.dom.Layer);


/**
 * @inheritDoc
 */
ol.renderer.dom.TileLayer.prototype.clearFrame = function() {
  goog.dom.removeChildren(this.target);
  this.renderedRevision_ = 0;
};


/**
 * @inheritDoc
 */
ol.renderer.dom.TileLayer.prototype.prepareFrame = function(frameState, layerState) {

  if (!layerState.visible) {
    if (this.renderedVisible_) {
      goog.style.setElementShown(this.target, false);
      this.renderedVisible_ = false;
    }
    return true;
  }

  var pixelRatio = frameState.pixelRatio;
  var viewState = frameState.viewState;
  var projection = viewState.projection;

  var tileLayer = this.getLayer();
  goog.asserts.assertInstanceof(tileLayer, ol.layer.Tile,
      'layer is an instance of ol.layer.Tile');
  var tileSource = tileLayer.getSource();
  var tileGrid = tileSource.getTileGridForProjection(projection);
  var tileGutter = tileSource.getGutter();
  var z = tileGrid.getZForResolution(viewState.resolution);
  var tileResolution = tileGrid.getResolution(z);
  var center = viewState.center;
  var extent;
  if (tileResolution == viewState.resolution) {
    center = this.snapCenterToPixel(center, tileResolution, frameState.size);
    extent = ol.extent.getForViewAndSize(
        center, tileResolution, viewState.rotation, frameState.size);
  } else {
    extent = frameState.extent;
  }

  if (layerState.extent !== undefined) {
    extent = ol.extent.getIntersection(extent, layerState.extent);
  }

  var tileRange = tileGrid.getTileRangeForExtentAndResolution(
      extent, tileResolution);

  /** @type {Object.<number, Object.<string, ol.Tile>>} */
  var tilesToDrawByZ = {};
  tilesToDrawByZ[z] = {};

  var findLoadedTiles = this.createLoadedTileFinder(
      tileSource, projection, tilesToDrawByZ);

  var useInterimTilesOnError = tileLayer.getUseInterimTilesOnError();

  var tmpExtent = ol.extent.createEmpty();
  var tmpTileRange = new ol.TileRange(0, 0, 0, 0);
  var childTileRange, drawable, fullyLoaded, tile, tileState, x, y;
  for (x = tileRange.minX; x <= tileRange.maxX; ++x) {
    for (y = tileRange.minY; y <= tileRange.maxY; ++y) {
      tile = tileSource.getTile(z, x, y, pixelRatio, projection);
      tileState = tile.getState();
      drawable = tileState == ol.TileState.LOADED ||
          tileState == ol.TileState.EMPTY ||
          tileState == ol.TileState.ERROR && !useInterimTilesOnError;
      if (!drawable && tile.interimTile) {
        tile = tile.interimTile;
      }
      goog.asserts.assert(tile);
      tileState = tile.getState();
      if (tileState == ol.TileState.LOADED) {
        tilesToDrawByZ[z][tile.tileCoord.toString()] = tile;
        continue;
      } else if (tileState == ol.TileState.EMPTY ||
                 (tileState == ol.TileState.ERROR &&
                  !useInterimTilesOnError)) {
        continue;
      }
      fullyLoaded = tileGrid.forEachTileCoordParentTileRange(
          tile.tileCoord, findLoadedTiles, null, tmpTileRange, tmpExtent);
      if (!fullyLoaded) {
        childTileRange = tileGrid.getTileCoordChildTileRange(
            tile.tileCoord, tmpTileRange, tmpExtent);
        if (childTileRange) {
          findLoadedTiles(z + 1, childTileRange);
        }
      }

    }

  }

  // If the tile source revision changes, we destroy the existing DOM structure
  // so that a new one will be created.  It would be more efficient to modify
  // the existing structure.
  var tileLayerZ, tileLayerZKey;
  if (this.renderedRevision_ != tileSource.getRevision()) {
    for (tileLayerZKey in this.tileLayerZs_) {
      tileLayerZ = this.tileLayerZs_[+tileLayerZKey];
      goog.dom.removeNode(tileLayerZ.target);
    }
    this.tileLayerZs_ = {};
    this.renderedRevision_ = tileSource.getRevision();
  }

  /** @type {Array.<number>} */
  var zs = Object.keys(tilesToDrawByZ).map(Number);
  zs.sort(ol.array.numberSafeCompareFunction);

  /** @type {Object.<number, boolean>} */
  var newTileLayerZKeys = {};

  var iz, iziz, tileCoordKey, tileCoordOrigin, tilesToDraw;
  for (iz = 0, iziz = zs.length; iz < iziz; ++iz) {
    tileLayerZKey = zs[iz];
    if (tileLayerZKey in this.tileLayerZs_) {
      tileLayerZ = this.tileLayerZs_[tileLayerZKey];
    } else {
      tileCoordOrigin =
          tileGrid.getTileCoordForCoordAndZ(center, tileLayerZKey);
      tileLayerZ = new ol.renderer.dom.TileLayerZ_(tileGrid, tileCoordOrigin);
      newTileLayerZKeys[tileLayerZKey] = true;
      this.tileLayerZs_[tileLayerZKey] = tileLayerZ;
    }
    tilesToDraw = tilesToDrawByZ[tileLayerZKey];
    for (tileCoordKey in tilesToDraw) {
      tileLayerZ.addTile(tilesToDraw[tileCoordKey], tileGutter);
    }
    tileLayerZ.finalizeAddTiles();
  }

  /** @type {Array.<number>} */
  var tileLayerZKeys = Object.keys(this.tileLayerZs_).map(Number);
  tileLayerZKeys.sort(ol.array.numberSafeCompareFunction);

  var i, ii, j, origin, resolution;
  var transform = goog.vec.Mat4.createNumber();
  for (i = 0, ii = tileLayerZKeys.length; i < ii; ++i) {
    tileLayerZKey = tileLayerZKeys[i];
    tileLayerZ = this.tileLayerZs_[tileLayerZKey];
    if (!(tileLayerZKey in tilesToDrawByZ)) {
      goog.dom.removeNode(tileLayerZ.target);
      delete this.tileLayerZs_[tileLayerZKey];
      continue;
    }
    resolution = tileLayerZ.getResolution();
    origin = tileLayerZ.getOrigin();
    ol.vec.Mat4.makeTransform2D(transform,
        frameState.size[0] / 2, frameState.size[1] / 2,
        resolution / viewState.resolution,
        resolution / viewState.resolution,
        viewState.rotation,
        (origin[0] - center[0]) / resolution,
        (center[1] - origin[1]) / resolution);
    tileLayerZ.setTransform(transform);
    if (tileLayerZKey in newTileLayerZKeys) {
      for (j = tileLayerZKey - 1; j >= 0; --j) {
        if (j in this.tileLayerZs_) {
          goog.dom.insertSiblingAfter(
              tileLayerZ.target, this.tileLayerZs_[j].target);
          break;
        }
      }
      if (j < 0) {
        goog.dom.insertChildAt(this.target, tileLayerZ.target, 0);
      }
    } else {
      if (!frameState.viewHints[ol.ViewHint.ANIMATING] &&
          !frameState.viewHints[ol.ViewHint.INTERACTING]) {
        tileLayerZ.removeTilesOutsideExtent(extent, tmpTileRange);
      }
    }
  }

  if (layerState.opacity != this.renderedOpacity_) {
    this.target.style.opacity = layerState.opacity;
    this.renderedOpacity_ = layerState.opacity;
  }

  if (layerState.visible && !this.renderedVisible_) {
    goog.style.setElementShown(this.target, true);
    this.renderedVisible_ = true;
  }

  this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange);
  this.manageTilePyramid(frameState, tileSource, tileGrid, pixelRatio,
      projection, extent, z, tileLayer.getPreload());
  this.scheduleExpireCache(frameState, tileSource);
  this.updateLogos(frameState, tileSource);

  return true;
};


/**
 * @constructor
 * @private
 * @param {ol.tilegrid.TileGrid} tileGrid Tile grid.
 * @param {ol.TileCoord} tileCoordOrigin Tile coord origin.
 */
ol.renderer.dom.TileLayerZ_ = function(tileGrid, tileCoordOrigin) {

  /**
   * @type {!Element}
   */
  this.target = document.createElement('DIV');
  this.target.style.position = 'absolute';
  this.target.style.width = '100%';
  this.target.style.height = '100%';

  /**
   * @private
   * @type {ol.tilegrid.TileGrid}
   */
  this.tileGrid_ = tileGrid;

  /**
   * @private
   * @type {ol.TileCoord}
   */
  this.tileCoordOrigin_ = tileCoordOrigin;

  /**
   * @private
   * @type {ol.Coordinate}
   */
  this.origin_ =
      ol.extent.getTopLeft(tileGrid.getTileCoordExtent(tileCoordOrigin));

  /**
   * @private
   * @type {number}
   */
  this.resolution_ = tileGrid.getResolution(tileCoordOrigin[0]);

  /**
   * @private
   * @type {Object.<string, ol.Tile>}
   */
  this.tiles_ = {};

  /**
   * @private
   * @type {DocumentFragment}
   */
  this.documentFragment_ = null;

  /**
   * @private
   * @type {goog.vec.Mat4.Number}
   */
  this.transform_ = goog.vec.Mat4.createNumberIdentity();

  /**
   * @private
   * @type {ol.Size}
   */
  this.tmpSize_ = [0, 0];

};


/**
 * @param {ol.Tile} tile Tile.
 * @param {number} tileGutter Tile gutter.
 */
ol.renderer.dom.TileLayerZ_.prototype.addTile = function(tile, tileGutter) {
  var tileCoord = tile.tileCoord;
  var tileCoordZ = tileCoord[0];
  var tileCoordX = tileCoord[1];
  var tileCoordY = tileCoord[2];
  goog.asserts.assert(tileCoordZ == this.tileCoordOrigin_[0],
      'tileCoordZ matches z of tileCoordOrigin');
  var tileCoordKey = tileCoord.toString();
  if (tileCoordKey in this.tiles_) {
    return;
  }
  var tileSize = ol.size.toSize(
      this.tileGrid_.getTileSize(tileCoordZ), this.tmpSize_);
  var image = tile.getImage(this);
  var imageStyle = image.style;
  // Bootstrap sets the style max-width: 100% for all images, which
  // prevents the tile from being displayed in FireFox.  Workaround
  // by overriding the max-width style.
  imageStyle.maxWidth = 'none';
  var tileElement;
  var tileElementStyle;
  if (tileGutter > 0) {
    tileElement = document.createElement('DIV');
    tileElementStyle = tileElement.style;
    tileElementStyle.overflow = 'hidden';
    tileElementStyle.width = tileSize[0] + 'px';
    tileElementStyle.height = tileSize[1] + 'px';
    imageStyle.position = 'absolute';
    imageStyle.left = -tileGutter + 'px';
    imageStyle.top = -tileGutter + 'px';
    imageStyle.width = (tileSize[0] + 2 * tileGutter) + 'px';
    imageStyle.height = (tileSize[1] + 2 * tileGutter) + 'px';
    tileElement.appendChild(image);
  } else {
    imageStyle.width = tileSize[0] + 'px';
    imageStyle.height = tileSize[1] + 'px';
    tileElement = image;
    tileElementStyle = imageStyle;
  }
  tileElementStyle.position = 'absolute';
  tileElementStyle.left =
      ((tileCoordX - this.tileCoordOrigin_[1]) * tileSize[0]) + 'px';
  tileElementStyle.top =
      ((this.tileCoordOrigin_[2] - tileCoordY) * tileSize[1]) + 'px';
  if (!this.documentFragment_) {
    this.documentFragment_ = document.createDocumentFragment();
  }
  this.documentFragment_.appendChild(tileElement);
  this.tiles_[tileCoordKey] = tile;
};


/**
 * FIXME empty description for jsdoc
 */
ol.renderer.dom.TileLayerZ_.prototype.finalizeAddTiles = function() {
  if (this.documentFragment_) {
    this.target.appendChild(this.documentFragment_);
    this.documentFragment_ = null;
  }
};


/**
 * @return {ol.Coordinate} Origin.
 */
ol.renderer.dom.TileLayerZ_.prototype.getOrigin = function() {
  return this.origin_;
};


/**
 * @return {number} Resolution.
 */
ol.renderer.dom.TileLayerZ_.prototype.getResolution = function() {
  return this.resolution_;
};


/**
 * @param {ol.Extent} extent Extent.
 * @param {ol.TileRange=} opt_tileRange Temporary ol.TileRange object.
 */
ol.renderer.dom.TileLayerZ_.prototype.removeTilesOutsideExtent = function(extent, opt_tileRange) {
  var tileRange = this.tileGrid_.getTileRangeForExtentAndZ(
      extent, this.tileCoordOrigin_[0], opt_tileRange);
  /** @type {Array.<ol.Tile>} */
  var tilesToRemove = [];
  var tile, tileCoordKey;
  for (tileCoordKey in this.tiles_) {
    tile = this.tiles_[tileCoordKey];
    if (!tileRange.contains(tile.tileCoord)) {
      tilesToRemove.push(tile);
    }
  }
  var i, ii;
  for (i = 0, ii = tilesToRemove.length; i < ii; ++i) {
    tile = tilesToRemove[i];
    tileCoordKey = tile.tileCoord.toString();
    goog.dom.removeNode(tile.getImage(this));
    delete this.tiles_[tileCoordKey];
  }
};


/**
 * @param {goog.vec.Mat4.Number} transform Transform.
 */
ol.renderer.dom.TileLayerZ_.prototype.setTransform = function(transform) {
  if (!ol.vec.Mat4.equals2D(transform, this.transform_)) {
    ol.dom.transformElement2D(this.target, transform, 6);
    goog.vec.Mat4.setFromArray(this.transform_, transform);
  }
};

goog.provide('ol.renderer.dom.VectorLayer');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.vec.Mat4');
goog.require('ol.ViewHint');
goog.require('ol.dom');
goog.require('ol.extent');
goog.require('ol.layer.Vector');
goog.require('ol.render.Event');
goog.require('ol.render.EventType');
goog.require('ol.render.canvas.Immediate');
goog.require('ol.render.canvas.ReplayGroup');
goog.require('ol.renderer.dom.Layer');
goog.require('ol.renderer.vector');
goog.require('ol.vec.Mat4');


/**
 * @constructor
 * @extends {ol.renderer.dom.Layer}
 * @param {ol.layer.Vector} vectorLayer Vector layer.
 */
ol.renderer.dom.VectorLayer = function(vectorLayer) {

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.context_ = ol.dom.createCanvasContext2D();

  var target = this.context_.canvas;
  // Bootstrap sets the style max-width: 100% for all images, which breaks
  // prevents the image from being displayed in FireFox.  Workaround by
  // overriding the max-width style.
  target.style.maxWidth = 'none';
  target.style.position = 'absolute';

  goog.base(this, vectorLayer, target);

  /**
   * @private
   * @type {boolean}
   */
  this.dirty_ = false;

  /**
   * @private
   * @type {number}
   */
  this.renderedRevision_ = -1;

  /**
   * @private
   * @type {number}
   */
  this.renderedResolution_ = NaN;

  /**
   * @private
   * @type {ol.Extent}
   */
  this.renderedExtent_ = ol.extent.createEmpty();

  /**
   * @private
   * @type {function(ol.Feature, ol.Feature): number|null}
   */
  this.renderedRenderOrder_ = null;

  /**
   * @private
   * @type {ol.render.canvas.ReplayGroup}
   */
  this.replayGroup_ = null;

  /**
   * @private
   * @type {goog.vec.Mat4.Number}
   */
  this.transform_ = goog.vec.Mat4.createNumber();

  /**
   * @private
   * @type {goog.vec.Mat4.Number}
   */
  this.elementTransform_ = goog.vec.Mat4.createNumber();

};
goog.inherits(ol.renderer.dom.VectorLayer, ol.renderer.dom.Layer);


/**
 * @inheritDoc
 */
ol.renderer.dom.VectorLayer.prototype.composeFrame = function(frameState, layerState) {

  var vectorLayer = /** @type {ol.layer.Vector} */ (this.getLayer());
  goog.asserts.assertInstanceof(vectorLayer, ol.layer.Vector,
      'layer is an instance of ol.layer.Vector');

  var viewState = frameState.viewState;
  var viewCenter = viewState.center;
  var viewRotation = viewState.rotation;
  var viewResolution = viewState.resolution;
  var pixelRatio = frameState.pixelRatio;
  var viewWidth = frameState.size[0];
  var viewHeight = frameState.size[1];
  var imageWidth = viewWidth * pixelRatio;
  var imageHeight = viewHeight * pixelRatio;

  var transform = ol.vec.Mat4.makeTransform2D(this.transform_,
      pixelRatio * viewWidth / 2,
      pixelRatio * viewHeight / 2,
      pixelRatio / viewResolution,
      -pixelRatio / viewResolution,
      -viewRotation,
      -viewCenter[0], -viewCenter[1]);

  var context = this.context_;

  // Clear the canvas and set the correct size
  context.canvas.width = imageWidth;
  context.canvas.height = imageHeight;

  var elementTransform = ol.vec.Mat4.makeTransform2D(this.elementTransform_,
      0, 0,
      1 / pixelRatio, 1 / pixelRatio,
      0,
      -(imageWidth - viewWidth) / 2 * pixelRatio,
      -(imageHeight - viewHeight) / 2 * pixelRatio);
  ol.dom.transformElement2D(context.canvas, elementTransform, 6);

  this.dispatchEvent_(ol.render.EventType.PRECOMPOSE, frameState, transform);

  var replayGroup = this.replayGroup_;

  if (replayGroup && !replayGroup.isEmpty()) {

    context.globalAlpha = layerState.opacity;
    replayGroup.replay(context, pixelRatio, transform, viewRotation,
        layerState.managed ? frameState.skippedFeatureUids : {});

    this.dispatchEvent_(ol.render.EventType.RENDER, frameState, transform);
  }

  this.dispatchEvent_(ol.render.EventType.POSTCOMPOSE, frameState, transform);
};


/**
 * @param {ol.render.EventType} type Event type.
 * @param {olx.FrameState} frameState Frame state.
 * @param {goog.vec.Mat4.Number} transform Transform.
 * @private
 */
ol.renderer.dom.VectorLayer.prototype.dispatchEvent_ = function(type, frameState, transform) {
  var context = this.context_;
  var layer = this.getLayer();
  if (layer.hasListener(type)) {
    var render = new ol.render.canvas.Immediate(
        context, frameState.pixelRatio, frameState.extent, transform,
        frameState.viewState.rotation);
    var event = new ol.render.Event(type, layer, render, frameState,
        context, null);
    layer.dispatchEvent(event);
    render.flush();
  }
};


/**
 * @inheritDoc
 */
ol.renderer.dom.VectorLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) {
  if (!this.replayGroup_) {
    return undefined;
  } else {
    var resolution = frameState.viewState.resolution;
    var rotation = frameState.viewState.rotation;
    var layer = this.getLayer();
    var layerState = frameState.layerStates[goog.getUid(layer)];
    /** @type {Object.<string, boolean>} */
    var features = {};
    return this.replayGroup_.forEachFeatureAtCoordinate(coordinate, resolution,
        rotation, layerState.managed ? frameState.skippedFeatureUids : {},
        /**
         * @param {ol.Feature|ol.render.Feature} feature Feature.
         * @return {?} Callback result.
         */
        function(feature) {
          goog.asserts.assert(feature !== undefined, 'received a feature');
          var key = goog.getUid(feature).toString();
          if (!(key in features)) {
            features[key] = true;
            return callback.call(thisArg, feature, layer);
          }
        });
  }
};


/**
 * Handle changes in image style state.
 * @param {goog.events.Event} event Image style change event.
 * @private
 */
ol.renderer.dom.VectorLayer.prototype.handleStyleImageChange_ = function(event) {
  this.renderIfReadyAndVisible();
};


/**
 * @inheritDoc
 */
ol.renderer.dom.VectorLayer.prototype.prepareFrame = function(frameState, layerState) {

  var vectorLayer = /** @type {ol.layer.Vector} */ (this.getLayer());
  goog.asserts.assertInstanceof(vectorLayer, ol.layer.Vector,
      'layer is an instance of ol.layer.Vector');
  var vectorSource = vectorLayer.getSource();

  this.updateAttributions(
      frameState.attributions, vectorSource.getAttributions());
  this.updateLogos(frameState, vectorSource);

  var animating = frameState.viewHints[ol.ViewHint.ANIMATING];
  var interacting = frameState.viewHints[ol.ViewHint.INTERACTING];
  var updateWhileAnimating = vectorLayer.getUpdateWhileAnimating();
  var updateWhileInteracting = vectorLayer.getUpdateWhileInteracting();

  if (!this.dirty_ && (!updateWhileAnimating && animating) ||
      (!updateWhileInteracting && interacting)) {
    return true;
  }

  var frameStateExtent = frameState.extent;
  var viewState = frameState.viewState;
  var projection = viewState.projection;
  var resolution = viewState.resolution;
  var pixelRatio = frameState.pixelRatio;
  var vectorLayerRevision = vectorLayer.getRevision();
  var vectorLayerRenderBuffer = vectorLayer.getRenderBuffer();
  var vectorLayerRenderOrder = vectorLayer.getRenderOrder();

  if (vectorLayerRenderOrder === undefined) {
    vectorLayerRenderOrder = ol.renderer.vector.defaultOrder;
  }

  var extent = ol.extent.buffer(frameStateExtent,
      vectorLayerRenderBuffer * resolution);

  if (!this.dirty_ &&
      this.renderedResolution_ == resolution &&
      this.renderedRevision_ == vectorLayerRevision &&
      this.renderedRenderOrder_ == vectorLayerRenderOrder &&
      ol.extent.containsExtent(this.renderedExtent_, extent)) {
    return true;
  }

  // FIXME dispose of old replayGroup in post render
  goog.dispose(this.replayGroup_);
  this.replayGroup_ = null;

  this.dirty_ = false;

  var replayGroup =
      new ol.render.canvas.ReplayGroup(
          ol.renderer.vector.getTolerance(resolution, pixelRatio), extent,
          resolution, vectorLayer.getRenderBuffer());
  vectorSource.loadFeatures(extent, resolution, projection);
  /**
   * @param {ol.Feature} feature Feature.
   * @this {ol.renderer.dom.VectorLayer}
   */
  var renderFeature = function(feature) {
    var styles;
    var styleFunction = feature.getStyleFunction();
    if (styleFunction) {
      styles = styleFunction.call(feature, resolution);
    } else {
      styleFunction = vectorLayer.getStyleFunction();
      if (styleFunction) {
        styles = styleFunction(feature, resolution);
      }
    }
    if (styles) {
      var dirty = this.renderFeature(
          feature, resolution, pixelRatio, styles, replayGroup);
      this.dirty_ = this.dirty_ || dirty;
    }
  };
  if (vectorLayerRenderOrder) {
    /** @type {Array.<ol.Feature>} */
    var features = [];
    vectorSource.forEachFeatureInExtent(extent,
        /**
         * @param {ol.Feature} feature Feature.
         */
        function(feature) {
          features.push(feature);
        }, this);
    features.sort(vectorLayerRenderOrder);
    features.forEach(renderFeature, this);
  } else {
    vectorSource.forEachFeatureInExtent(extent, renderFeature, this);
  }
  replayGroup.finish();

  this.renderedResolution_ = resolution;
  this.renderedRevision_ = vectorLayerRevision;
  this.renderedRenderOrder_ = vectorLayerRenderOrder;
  this.renderedExtent_ = extent;
  this.replayGroup_ = replayGroup;

  return true;
};


/**
 * @param {ol.Feature} feature Feature.
 * @param {number} resolution Resolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {(ol.style.Style|Array.<ol.style.Style>)} styles The style or array of
 *     styles.
 * @param {ol.render.canvas.ReplayGroup} replayGroup Replay group.
 * @return {boolean} `true` if an image is loading.
 */
ol.renderer.dom.VectorLayer.prototype.renderFeature = function(feature, resolution, pixelRatio, styles, replayGroup) {
  if (!styles) {
    return false;
  }
  var loading = false;
  if (goog.isArray(styles)) {
    for (var i = 0, ii = styles.length; i < ii; ++i) {
      loading = ol.renderer.vector.renderFeature(
          replayGroup, feature, styles[i],
          ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio),
          this.handleStyleImageChange_, this) || loading;
    }
  } else {
    loading = ol.renderer.vector.renderFeature(
        replayGroup, feature, styles,
        ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio),
        this.handleStyleImageChange_, this) || loading;
  }
  return loading;
};

goog.provide('ol.renderer.dom.Map');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.EventType');
goog.require('goog.style');
goog.require('goog.vec.Mat4');
goog.require('ol');
goog.require('ol.RendererType');
goog.require('ol.css');
goog.require('ol.dom');
goog.require('ol.layer.Image');
goog.require('ol.layer.Layer');
goog.require('ol.layer.Tile');
goog.require('ol.layer.Vector');
goog.require('ol.render.Event');
goog.require('ol.render.EventType');
goog.require('ol.render.canvas.Immediate');
goog.require('ol.renderer.Map');
goog.require('ol.renderer.dom.ImageLayer');
goog.require('ol.renderer.dom.Layer');
goog.require('ol.renderer.dom.TileLayer');
goog.require('ol.renderer.dom.VectorLayer');
goog.require('ol.source.State');
goog.require('ol.vec.Mat4');


/**
 * @constructor
 * @extends {ol.renderer.Map}
 * @param {Element} container Container.
 * @param {ol.Map} map Map.
 */
ol.renderer.dom.Map = function(container, map) {

  goog.base(this, container, map);

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.context_ = ol.dom.createCanvasContext2D();
  var canvas = this.context_.canvas;
  canvas.style.position = 'absolute';
  canvas.style.width = '100%';
  canvas.style.height = '100%';
  canvas.className = ol.css.CLASS_UNSELECTABLE;
  goog.dom.insertChildAt(container, canvas, 0);

  /**
   * @private
   * @type {!goog.vec.Mat4.Number}
   */
  this.transform_ = goog.vec.Mat4.createNumber();

  /**
   * @type {!Element}
   * @private
   */
  this.layersPane_ = document.createElement('DIV');
  this.layersPane_.className = ol.css.CLASS_UNSELECTABLE;
  var style = this.layersPane_.style;
  style.position = 'absolute';
  style.width = '100%';
  style.height = '100%';

  // prevent the img context menu on mobile devices
  goog.events.listen(this.layersPane_, goog.events.EventType.TOUCHSTART,
      goog.events.Event.preventDefault);

  goog.dom.insertChildAt(container, this.layersPane_, 0);

  /**
   * @private
   * @type {boolean}
   */
  this.renderedVisible_ = true;

};
goog.inherits(ol.renderer.dom.Map, ol.renderer.Map);


/**
 * @inheritDoc
 */
ol.renderer.dom.Map.prototype.disposeInternal = function() {
  goog.dom.removeNode(this.layersPane_);
  goog.base(this, 'disposeInternal');
};


/**
 * @inheritDoc
 */
ol.renderer.dom.Map.prototype.createLayerRenderer = function(layer) {
  var layerRenderer;
  if (ol.ENABLE_IMAGE && layer instanceof ol.layer.Image) {
    layerRenderer = new ol.renderer.dom.ImageLayer(layer);
  } else if (ol.ENABLE_TILE && layer instanceof ol.layer.Tile) {
    layerRenderer = new ol.renderer.dom.TileLayer(layer);
  } else if (ol.ENABLE_VECTOR && layer instanceof ol.layer.Vector) {
    layerRenderer = new ol.renderer.dom.VectorLayer(layer);
  } else {
    goog.asserts.fail('unexpected layer configuration');
    return null;
  }
  return layerRenderer;
};


/**
 * @param {ol.render.EventType} type Event type.
 * @param {olx.FrameState} frameState Frame state.
 * @private
 */
ol.renderer.dom.Map.prototype.dispatchComposeEvent_ = function(type, frameState) {
  var map = this.getMap();
  if (map.hasListener(type)) {
    var extent = frameState.extent;
    var pixelRatio = frameState.pixelRatio;
    var viewState = frameState.viewState;
    var rotation = viewState.rotation;
    var context = this.context_;
    var canvas = context.canvas;

    ol.vec.Mat4.makeTransform2D(this.transform_,
        canvas.width / 2,
        canvas.height / 2,
        pixelRatio / viewState.resolution,
        -pixelRatio / viewState.resolution,
        -viewState.rotation,
        -viewState.center[0], -viewState.center[1]);
    var vectorContext = new ol.render.canvas.Immediate(context, pixelRatio,
        extent, this.transform_, rotation);
    var composeEvent = new ol.render.Event(type, map, vectorContext,
        frameState, context, null);
    map.dispatchEvent(composeEvent);
    vectorContext.flush();
  }
};


/**
 * @inheritDoc
 */
ol.renderer.dom.Map.prototype.getType = function() {
  return ol.RendererType.DOM;
};


/**
 * @inheritDoc
 */
ol.renderer.dom.Map.prototype.renderFrame = function(frameState) {

  if (!frameState) {
    if (this.renderedVisible_) {
      goog.style.setElementShown(this.layersPane_, false);
      this.renderedVisible_ = false;
    }
    return;
  }

  var map = this.getMap();
  if (map.hasListener(ol.render.EventType.PRECOMPOSE) ||
      map.hasListener(ol.render.EventType.POSTCOMPOSE)) {
    var canvas = this.context_.canvas;
    var pixelRatio = frameState.pixelRatio;
    canvas.width = frameState.size[0] * pixelRatio;
    canvas.height = frameState.size[1] * pixelRatio;
  }

  this.dispatchComposeEvent_(ol.render.EventType.PRECOMPOSE, frameState);

  var layerStatesArray = frameState.layerStatesArray;
  goog.array.stableSort(layerStatesArray, ol.renderer.Map.sortByZIndex);

  var viewResolution = frameState.viewState.resolution;
  var i, ii, layer, layerRenderer, layerState;
  for (i = 0, ii = layerStatesArray.length; i < ii; ++i) {
    layerState = layerStatesArray[i];
    layer = layerState.layer;
    layerRenderer = /** @type {ol.renderer.dom.Layer} */ (
        this.getLayerRenderer(layer));
    goog.asserts.assertInstanceof(layerRenderer, ol.renderer.dom.Layer,
        'renderer is an instance of ol.renderer.dom.Layer');
    goog.dom.insertChildAt(this.layersPane_, layerRenderer.getTarget(), i);
    if (ol.layer.Layer.visibleAtResolution(layerState, viewResolution) &&
        layerState.sourceState == ol.source.State.READY) {
      if (layerRenderer.prepareFrame(frameState, layerState)) {
        layerRenderer.composeFrame(frameState, layerState);
      }
    } else {
      layerRenderer.clearFrame();
    }
  }

  var layerStates = frameState.layerStates;
  var layerKey;
  for (layerKey in this.getLayerRenderers()) {
    if (!(layerKey in layerStates)) {
      layerRenderer = this.getLayerRendererByKey(layerKey);
      goog.asserts.assertInstanceof(layerRenderer, ol.renderer.dom.Layer,
          'renderer is an instance of ol.renderer.dom.Layer');
      goog.dom.removeNode(layerRenderer.getTarget());
    }
  }

  if (!this.renderedVisible_) {
    goog.style.setElementShown(this.layersPane_, true);
    this.renderedVisible_ = true;
  }

  this.calculateMatrices2D(frameState);
  this.scheduleRemoveUnusedLayerRenderers(frameState);
  this.scheduleExpireIconCache(frameState);

  this.dispatchComposeEvent_(ol.render.EventType.POSTCOMPOSE, frameState);
};

// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// 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.


/**
 * @fileoverview Constants used by the WebGL rendering, including all of the
 * constants used from the WebGL context.  For example, instead of using
 * context.ARRAY_BUFFER, your code can use
 * goog.webgl.ARRAY_BUFFER. The benefits for doing this include allowing
 * the compiler to optimize your code so that the compiled code does not have to
 * contain large strings to reference these properties, and reducing runtime
 * property access.
 *
 * Values are taken from the WebGL Spec:
 * https://www.khronos.org/registry/webgl/specs/1.0/#WEBGLRENDERINGCONTEXT
 */

goog.provide('goog.webgl');


/**
 * @const
 * @type {number}
 */
goog.webgl.DEPTH_BUFFER_BIT = 0x00000100;


/**
 * @const
 * @type {number}
 */
goog.webgl.STENCIL_BUFFER_BIT = 0x00000400;


/**
 * @const
 * @type {number}
 */
goog.webgl.COLOR_BUFFER_BIT = 0x00004000;


/**
 * @const
 * @type {number}
 */
goog.webgl.POINTS = 0x0000;


/**
 * @const
 * @type {number}
 */
goog.webgl.LINES = 0x0001;


/**
 * @const
 * @type {number}
 */
goog.webgl.LINE_LOOP = 0x0002;


/**
 * @const
 * @type {number}
 */
goog.webgl.LINE_STRIP = 0x0003;


/**
 * @const
 * @type {number}
 */
goog.webgl.TRIANGLES = 0x0004;


/**
 * @const
 * @type {number}
 */
goog.webgl.TRIANGLE_STRIP = 0x0005;


/**
 * @const
 * @type {number}
 */
goog.webgl.TRIANGLE_FAN = 0x0006;


/**
 * @const
 * @type {number}
 */
goog.webgl.ZERO = 0;


/**
 * @const
 * @type {number}
 */
goog.webgl.ONE = 1;


/**
 * @const
 * @type {number}
 */
goog.webgl.SRC_COLOR = 0x0300;


/**
 * @const
 * @type {number}
 */
goog.webgl.ONE_MINUS_SRC_COLOR = 0x0301;


/**
 * @const
 * @type {number}
 */
goog.webgl.SRC_ALPHA = 0x0302;


/**
 * @const
 * @type {number}
 */
goog.webgl.ONE_MINUS_SRC_ALPHA = 0x0303;


/**
 * @const
 * @type {number}
 */
goog.webgl.DST_ALPHA = 0x0304;


/**
 * @const
 * @type {number}
 */
goog.webgl.ONE_MINUS_DST_ALPHA = 0x0305;


/**
 * @const
 * @type {number}
 */
goog.webgl.DST_COLOR = 0x0306;


/**
 * @const
 * @type {number}
 */
goog.webgl.ONE_MINUS_DST_COLOR = 0x0307;


/**
 * @const
 * @type {number}
 */
goog.webgl.SRC_ALPHA_SATURATE = 0x0308;


/**
 * @const
 * @type {number}
 */
goog.webgl.FUNC_ADD = 0x8006;


/**
 * @const
 * @type {number}
 */
goog.webgl.BLEND_EQUATION = 0x8009;


/**
 * Same as BLEND_EQUATION
 * @const
 * @type {number}
 */
goog.webgl.BLEND_EQUATION_RGB = 0x8009;


/**
 * @const
 * @type {number}
 */
goog.webgl.BLEND_EQUATION_ALPHA = 0x883D;


/**
 * @const
 * @type {number}
 */
goog.webgl.FUNC_SUBTRACT = 0x800A;


/**
 * @const
 * @type {number}
 */
goog.webgl.FUNC_REVERSE_SUBTRACT = 0x800B;


/**
 * @const
 * @type {number}
 */
goog.webgl.BLEND_DST_RGB = 0x80C8;


/**
 * @const
 * @type {number}
 */
goog.webgl.BLEND_SRC_RGB = 0x80C9;


/**
 * @const
 * @type {number}
 */
goog.webgl.BLEND_DST_ALPHA = 0x80CA;


/**
 * @const
 * @type {number}
 */
goog.webgl.BLEND_SRC_ALPHA = 0x80CB;


/**
 * @const
 * @type {number}
 */
goog.webgl.CONSTANT_COLOR = 0x8001;


/**
 * @const
 * @type {number}
 */
goog.webgl.ONE_MINUS_CONSTANT_COLOR = 0x8002;


/**
 * @const
 * @type {number}
 */
goog.webgl.CONSTANT_ALPHA = 0x8003;


/**
 * @const
 * @type {number}
 */
goog.webgl.ONE_MINUS_CONSTANT_ALPHA = 0x8004;


/**
 * @const
 * @type {number}
 */
goog.webgl.BLEND_COLOR = 0x8005;


/**
 * @const
 * @type {number}
 */
goog.webgl.ARRAY_BUFFER = 0x8892;


/**
 * @const
 * @type {number}
 */
goog.webgl.ELEMENT_ARRAY_BUFFER = 0x8893;


/**
 * @const
 * @type {number}
 */
goog.webgl.ARRAY_BUFFER_BINDING = 0x8894;


/**
 * @const
 * @type {number}
 */
goog.webgl.ELEMENT_ARRAY_BUFFER_BINDING = 0x8895;


/**
 * @const
 * @type {number}
 */
goog.webgl.STREAM_DRAW = 0x88E0;


/**
 * @const
 * @type {number}
 */
goog.webgl.STATIC_DRAW = 0x88E4;


/**
 * @const
 * @type {number}
 */
goog.webgl.DYNAMIC_DRAW = 0x88E8;


/**
 * @const
 * @type {number}
 */
goog.webgl.BUFFER_SIZE = 0x8764;


/**
 * @const
 * @type {number}
 */
goog.webgl.BUFFER_USAGE = 0x8765;


/**
 * @const
 * @type {number}
 */
goog.webgl.CURRENT_VERTEX_ATTRIB = 0x8626;


/**
 * @const
 * @type {number}
 */
goog.webgl.FRONT = 0x0404;


/**
 * @const
 * @type {number}
 */
goog.webgl.BACK = 0x0405;


/**
 * @const
 * @type {number}
 */
goog.webgl.FRONT_AND_BACK = 0x0408;


/**
 * @const
 * @type {number}
 */
goog.webgl.CULL_FACE = 0x0B44;


/**
 * @const
 * @type {number}
 */
goog.webgl.BLEND = 0x0BE2;


/**
 * @const
 * @type {number}
 */
goog.webgl.DITHER = 0x0BD0;


/**
 * @const
 * @type {number}
 */
goog.webgl.STENCIL_TEST = 0x0B90;


/**
 * @const
 * @type {number}
 */
goog.webgl.DEPTH_TEST = 0x0B71;


/**
 * @const
 * @type {number}
 */
goog.webgl.SCISSOR_TEST = 0x0C11;


/**
 * @const
 * @type {number}
 */
goog.webgl.POLYGON_OFFSET_FILL = 0x8037;


/**
 * @const
 * @type {number}
 */
goog.webgl.SAMPLE_ALPHA_TO_COVERAGE = 0x809E;


/**
 * @const
 * @type {number}
 */
goog.webgl.SAMPLE_COVERAGE = 0x80A0;


/**
 * @const
 * @type {number}
 */
goog.webgl.NO_ERROR = 0;


/**
 * @const
 * @type {number}
 */
goog.webgl.INVALID_ENUM = 0x0500;


/**
 * @const
 * @type {number}
 */
goog.webgl.INVALID_VALUE = 0x0501;


/**
 * @const
 * @type {number}
 */
goog.webgl.INVALID_OPERATION = 0x0502;


/**
 * @const
 * @type {number}
 */
goog.webgl.OUT_OF_MEMORY = 0x0505;


/**
 * @const
 * @type {number}
 */
goog.webgl.CW = 0x0900;


/**
 * @const
 * @type {number}
 */
goog.webgl.CCW = 0x0901;


/**
 * @const
 * @type {number}
 */
goog.webgl.LINE_WIDTH = 0x0B21;


/**
 * @const
 * @type {number}
 */
goog.webgl.ALIASED_POINT_SIZE_RANGE = 0x846D;


/**
 * @const
 * @type {number}
 */
goog.webgl.ALIASED_LINE_WIDTH_RANGE = 0x846E;


/**
 * @const
 * @type {number}
 */
goog.webgl.CULL_FACE_MODE = 0x0B45;


/**
 * @const
 * @type {number}
 */
goog.webgl.FRONT_FACE = 0x0B46;


/**
 * @const
 * @type {number}
 */
goog.webgl.DEPTH_RANGE = 0x0B70;


/**
 * @const
 * @type {number}
 */
goog.webgl.DEPTH_WRITEMASK = 0x0B72;


/**
 * @const
 * @type {number}
 */
goog.webgl.DEPTH_CLEAR_VALUE = 0x0B73;


/**
 * @const
 * @type {number}
 */
goog.webgl.DEPTH_FUNC = 0x0B74;


/**
 * @const
 * @type {number}
 */
goog.webgl.STENCIL_CLEAR_VALUE = 0x0B91;


/**
 * @const
 * @type {number}
 */
goog.webgl.STENCIL_FUNC = 0x0B92;


/**
 * @const
 * @type {number}
 */
goog.webgl.STENCIL_FAIL = 0x0B94;


/**
 * @const
 * @type {number}
 */
goog.webgl.STENCIL_PASS_DEPTH_FAIL = 0x0B95;


/**
 * @const
 * @type {number}
 */
goog.webgl.STENCIL_PASS_DEPTH_PASS = 0x0B96;


/**
 * @const
 * @type {number}
 */
goog.webgl.STENCIL_REF = 0x0B97;


/**
 * @const
 * @type {number}
 */
goog.webgl.STENCIL_VALUE_MASK = 0x0B93;


/**
 * @const
 * @type {number}
 */
goog.webgl.STENCIL_WRITEMASK = 0x0B98;


/**
 * @const
 * @type {number}
 */
goog.webgl.STENCIL_BACK_FUNC = 0x8800;


/**
 * @const
 * @type {number}
 */
goog.webgl.STENCIL_BACK_FAIL = 0x8801;


/**
 * @const
 * @type {number}
 */
goog.webgl.STENCIL_BACK_PASS_DEPTH_FAIL = 0x8802;


/**
 * @const
 * @type {number}
 */
goog.webgl.STENCIL_BACK_PASS_DEPTH_PASS = 0x8803;


/**
 * @const
 * @type {number}
 */
goog.webgl.STENCIL_BACK_REF = 0x8CA3;


/**
 * @const
 * @type {number}
 */
goog.webgl.STENCIL_BACK_VALUE_MASK = 0x8CA4;


/**
 * @const
 * @type {number}
 */
goog.webgl.STENCIL_BACK_WRITEMASK = 0x8CA5;


/**
 * @const
 * @type {number}
 */
goog.webgl.VIEWPORT = 0x0BA2;


/**
 * @const
 * @type {number}
 */
goog.webgl.SCISSOR_BOX = 0x0C10;


/**
 * @const
 * @type {number}
 */
goog.webgl.COLOR_CLEAR_VALUE = 0x0C22;


/**
 * @const
 * @type {number}
 */
goog.webgl.COLOR_WRITEMASK = 0x0C23;


/**
 * @const
 * @type {number}
 */
goog.webgl.UNPACK_ALIGNMENT = 0x0CF5;


/**
 * @const
 * @type {number}
 */
goog.webgl.PACK_ALIGNMENT = 0x0D05;


/**
 * @const
 * @type {number}
 */
goog.webgl.MAX_TEXTURE_SIZE = 0x0D33;


/**
 * @const
 * @type {number}
 */
goog.webgl.MAX_VIEWPORT_DIMS = 0x0D3A;


/**
 * @const
 * @type {number}
 */
goog.webgl.SUBPIXEL_BITS = 0x0D50;


/**
 * @const
 * @type {number}
 */
goog.webgl.RED_BITS = 0x0D52;


/**
 * @const
 * @type {number}
 */
goog.webgl.GREEN_BITS = 0x0D53;


/**
 * @const
 * @type {number}
 */
goog.webgl.BLUE_BITS = 0x0D54;


/**
 * @const
 * @type {number}
 */
goog.webgl.ALPHA_BITS = 0x0D55;


/**
 * @const
 * @type {number}
 */
goog.webgl.DEPTH_BITS = 0x0D56;


/**
 * @const
 * @type {number}
 */
goog.webgl.STENCIL_BITS = 0x0D57;


/**
 * @const
 * @type {number}
 */
goog.webgl.POLYGON_OFFSET_UNITS = 0x2A00;


/**
 * @const
 * @type {number}
 */
goog.webgl.POLYGON_OFFSET_FACTOR = 0x8038;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE_BINDING_2D = 0x8069;


/**
 * @const
 * @type {number}
 */
goog.webgl.SAMPLE_BUFFERS = 0x80A8;


/**
 * @const
 * @type {number}
 */
goog.webgl.SAMPLES = 0x80A9;


/**
 * @const
 * @type {number}
 */
goog.webgl.SAMPLE_COVERAGE_VALUE = 0x80AA;


/**
 * @const
 * @type {number}
 */
goog.webgl.SAMPLE_COVERAGE_INVERT = 0x80AB;


/**
 * @const
 * @type {number}
 */
goog.webgl.COMPRESSED_TEXTURE_FORMATS = 0x86A3;


/**
 * @const
 * @type {number}
 */
goog.webgl.DONT_CARE = 0x1100;


/**
 * @const
 * @type {number}
 */
goog.webgl.FASTEST = 0x1101;


/**
 * @const
 * @type {number}
 */
goog.webgl.NICEST = 0x1102;


/**
 * @const
 * @type {number}
 */
goog.webgl.GENERATE_MIPMAP_HINT = 0x8192;


/**
 * @const
 * @type {number}
 */
goog.webgl.BYTE = 0x1400;


/**
 * @const
 * @type {number}
 */
goog.webgl.UNSIGNED_BYTE = 0x1401;


/**
 * @const
 * @type {number}
 */
goog.webgl.SHORT = 0x1402;


/**
 * @const
 * @type {number}
 */
goog.webgl.UNSIGNED_SHORT = 0x1403;


/**
 * @const
 * @type {number}
 */
goog.webgl.INT = 0x1404;


/**
 * @const
 * @type {number}
 */
goog.webgl.UNSIGNED_INT = 0x1405;


/**
 * @const
 * @type {number}
 */
goog.webgl.FLOAT = 0x1406;


/**
 * @const
 * @type {number}
 */
goog.webgl.DEPTH_COMPONENT = 0x1902;


/**
 * @const
 * @type {number}
 */
goog.webgl.ALPHA = 0x1906;


/**
 * @const
 * @type {number}
 */
goog.webgl.RGB = 0x1907;


/**
 * @const
 * @type {number}
 */
goog.webgl.RGBA = 0x1908;


/**
 * @const
 * @type {number}
 */
goog.webgl.LUMINANCE = 0x1909;


/**
 * @const
 * @type {number}
 */
goog.webgl.LUMINANCE_ALPHA = 0x190A;


/**
 * @const
 * @type {number}
 */
goog.webgl.UNSIGNED_SHORT_4_4_4_4 = 0x8033;


/**
 * @const
 * @type {number}
 */
goog.webgl.UNSIGNED_SHORT_5_5_5_1 = 0x8034;


/**
 * @const
 * @type {number}
 */
goog.webgl.UNSIGNED_SHORT_5_6_5 = 0x8363;


/**
 * @const
 * @type {number}
 */
goog.webgl.FRAGMENT_SHADER = 0x8B30;


/**
 * @const
 * @type {number}
 */
goog.webgl.VERTEX_SHADER = 0x8B31;


/**
 * @const
 * @type {number}
 */
goog.webgl.MAX_VERTEX_ATTRIBS = 0x8869;


/**
 * @const
 * @type {number}
 */
goog.webgl.MAX_VERTEX_UNIFORM_VECTORS = 0x8DFB;


/**
 * @const
 * @type {number}
 */
goog.webgl.MAX_VARYING_VECTORS = 0x8DFC;


/**
 * @const
 * @type {number}
 */
goog.webgl.MAX_COMBINED_TEXTURE_IMAGE_UNITS = 0x8B4D;


/**
 * @const
 * @type {number}
 */
goog.webgl.MAX_VERTEX_TEXTURE_IMAGE_UNITS = 0x8B4C;


/**
 * @const
 * @type {number}
 */
goog.webgl.MAX_TEXTURE_IMAGE_UNITS = 0x8872;


/**
 * @const
 * @type {number}
 */
goog.webgl.MAX_FRAGMENT_UNIFORM_VECTORS = 0x8DFD;


/**
 * @const
 * @type {number}
 */
goog.webgl.SHADER_TYPE = 0x8B4F;


/**
 * @const
 * @type {number}
 */
goog.webgl.DELETE_STATUS = 0x8B80;


/**
 * @const
 * @type {number}
 */
goog.webgl.LINK_STATUS = 0x8B82;


/**
 * @const
 * @type {number}
 */
goog.webgl.VALIDATE_STATUS = 0x8B83;


/**
 * @const
 * @type {number}
 */
goog.webgl.ATTACHED_SHADERS = 0x8B85;


/**
 * @const
 * @type {number}
 */
goog.webgl.ACTIVE_UNIFORMS = 0x8B86;


/**
 * @const
 * @type {number}
 */
goog.webgl.ACTIVE_ATTRIBUTES = 0x8B89;


/**
 * @const
 * @type {number}
 */
goog.webgl.SHADING_LANGUAGE_VERSION = 0x8B8C;


/**
 * @const
 * @type {number}
 */
goog.webgl.CURRENT_PROGRAM = 0x8B8D;


/**
 * @const
 * @type {number}
 */
goog.webgl.NEVER = 0x0200;


/**
 * @const
 * @type {number}
 */
goog.webgl.LESS = 0x0201;


/**
 * @const
 * @type {number}
 */
goog.webgl.EQUAL = 0x0202;


/**
 * @const
 * @type {number}
 */
goog.webgl.LEQUAL = 0x0203;


/**
 * @const
 * @type {number}
 */
goog.webgl.GREATER = 0x0204;


/**
 * @const
 * @type {number}
 */
goog.webgl.NOTEQUAL = 0x0205;


/**
 * @const
 * @type {number}
 */
goog.webgl.GEQUAL = 0x0206;


/**
 * @const
 * @type {number}
 */
goog.webgl.ALWAYS = 0x0207;


/**
 * @const
 * @type {number}
 */
goog.webgl.KEEP = 0x1E00;


/**
 * @const
 * @type {number}
 */
goog.webgl.REPLACE = 0x1E01;


/**
 * @const
 * @type {number}
 */
goog.webgl.INCR = 0x1E02;


/**
 * @const
 * @type {number}
 */
goog.webgl.DECR = 0x1E03;


/**
 * @const
 * @type {number}
 */
goog.webgl.INVERT = 0x150A;


/**
 * @const
 * @type {number}
 */
goog.webgl.INCR_WRAP = 0x8507;


/**
 * @const
 * @type {number}
 */
goog.webgl.DECR_WRAP = 0x8508;


/**
 * @const
 * @type {number}
 */
goog.webgl.VENDOR = 0x1F00;


/**
 * @const
 * @type {number}
 */
goog.webgl.RENDERER = 0x1F01;


/**
 * @const
 * @type {number}
 */
goog.webgl.VERSION = 0x1F02;


/**
 * @const
 * @type {number}
 */
goog.webgl.NEAREST = 0x2600;


/**
 * @const
 * @type {number}
 */
goog.webgl.LINEAR = 0x2601;


/**
 * @const
 * @type {number}
 */
goog.webgl.NEAREST_MIPMAP_NEAREST = 0x2700;


/**
 * @const
 * @type {number}
 */
goog.webgl.LINEAR_MIPMAP_NEAREST = 0x2701;


/**
 * @const
 * @type {number}
 */
goog.webgl.NEAREST_MIPMAP_LINEAR = 0x2702;


/**
 * @const
 * @type {number}
 */
goog.webgl.LINEAR_MIPMAP_LINEAR = 0x2703;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE_MAG_FILTER = 0x2800;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE_MIN_FILTER = 0x2801;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE_WRAP_S = 0x2802;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE_WRAP_T = 0x2803;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE_2D = 0x0DE1;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE = 0x1702;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE_CUBE_MAP = 0x8513;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE_BINDING_CUBE_MAP = 0x8514;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE_CUBE_MAP_POSITIVE_X = 0x8515;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE_CUBE_MAP_NEGATIVE_X = 0x8516;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE_CUBE_MAP_POSITIVE_Y = 0x8517;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE_CUBE_MAP_NEGATIVE_Y = 0x8518;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE_CUBE_MAP_POSITIVE_Z = 0x8519;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE_CUBE_MAP_NEGATIVE_Z = 0x851A;


/**
 * @const
 * @type {number}
 */
goog.webgl.MAX_CUBE_MAP_TEXTURE_SIZE = 0x851C;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE0 = 0x84C0;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE1 = 0x84C1;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE2 = 0x84C2;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE3 = 0x84C3;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE4 = 0x84C4;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE5 = 0x84C5;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE6 = 0x84C6;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE7 = 0x84C7;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE8 = 0x84C8;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE9 = 0x84C9;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE10 = 0x84CA;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE11 = 0x84CB;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE12 = 0x84CC;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE13 = 0x84CD;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE14 = 0x84CE;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE15 = 0x84CF;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE16 = 0x84D0;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE17 = 0x84D1;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE18 = 0x84D2;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE19 = 0x84D3;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE20 = 0x84D4;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE21 = 0x84D5;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE22 = 0x84D6;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE23 = 0x84D7;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE24 = 0x84D8;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE25 = 0x84D9;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE26 = 0x84DA;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE27 = 0x84DB;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE28 = 0x84DC;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE29 = 0x84DD;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE30 = 0x84DE;


/**
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE31 = 0x84DF;


/**
 * @const
 * @type {number}
 */
goog.webgl.ACTIVE_TEXTURE = 0x84E0;


/**
 * @const
 * @type {number}
 */
goog.webgl.REPEAT = 0x2901;


/**
 * @const
 * @type {number}
 */
goog.webgl.CLAMP_TO_EDGE = 0x812F;


/**
 * @const
 * @type {number}
 */
goog.webgl.MIRRORED_REPEAT = 0x8370;


/**
 * @const
 * @type {number}
 */
goog.webgl.FLOAT_VEC2 = 0x8B50;


/**
 * @const
 * @type {number}
 */
goog.webgl.FLOAT_VEC3 = 0x8B51;


/**
 * @const
 * @type {number}
 */
goog.webgl.FLOAT_VEC4 = 0x8B52;


/**
 * @const
 * @type {number}
 */
goog.webgl.INT_VEC2 = 0x8B53;


/**
 * @const
 * @type {number}
 */
goog.webgl.INT_VEC3 = 0x8B54;


/**
 * @const
 * @type {number}
 */
goog.webgl.INT_VEC4 = 0x8B55;


/**
 * @const
 * @type {number}
 */
goog.webgl.BOOL = 0x8B56;


/**
 * @const
 * @type {number}
 */
goog.webgl.BOOL_VEC2 = 0x8B57;


/**
 * @const
 * @type {number}
 */
goog.webgl.BOOL_VEC3 = 0x8B58;


/**
 * @const
 * @type {number}
 */
goog.webgl.BOOL_VEC4 = 0x8B59;


/**
 * @const
 * @type {number}
 */
goog.webgl.FLOAT_MAT2 = 0x8B5A;


/**
 * @const
 * @type {number}
 */
goog.webgl.FLOAT_MAT3 = 0x8B5B;


/**
 * @const
 * @type {number}
 */
goog.webgl.FLOAT_MAT4 = 0x8B5C;


/**
 * @const
 * @type {number}
 */
goog.webgl.SAMPLER_2D = 0x8B5E;


/**
 * @const
 * @type {number}
 */
goog.webgl.SAMPLER_CUBE = 0x8B60;


/**
 * @const
 * @type {number}
 */
goog.webgl.VERTEX_ATTRIB_ARRAY_ENABLED = 0x8622;


/**
 * @const
 * @type {number}
 */
goog.webgl.VERTEX_ATTRIB_ARRAY_SIZE = 0x8623;


/**
 * @const
 * @type {number}
 */
goog.webgl.VERTEX_ATTRIB_ARRAY_STRIDE = 0x8624;


/**
 * @const
 * @type {number}
 */
goog.webgl.VERTEX_ATTRIB_ARRAY_TYPE = 0x8625;


/**
 * @const
 * @type {number}
 */
goog.webgl.VERTEX_ATTRIB_ARRAY_NORMALIZED = 0x886A;


/**
 * @const
 * @type {number}
 */
goog.webgl.VERTEX_ATTRIB_ARRAY_POINTER = 0x8645;


/**
 * @const
 * @type {number}
 */
goog.webgl.VERTEX_ATTRIB_ARRAY_BUFFER_BINDING = 0x889F;


/**
 * @const
 * @type {number}
 */
goog.webgl.COMPILE_STATUS = 0x8B81;


/**
 * @const
 * @type {number}
 */
goog.webgl.LOW_FLOAT = 0x8DF0;


/**
 * @const
 * @type {number}
 */
goog.webgl.MEDIUM_FLOAT = 0x8DF1;


/**
 * @const
 * @type {number}
 */
goog.webgl.HIGH_FLOAT = 0x8DF2;


/**
 * @const
 * @type {number}
 */
goog.webgl.LOW_INT = 0x8DF3;


/**
 * @const
 * @type {number}
 */
goog.webgl.MEDIUM_INT = 0x8DF4;


/**
 * @const
 * @type {number}
 */
goog.webgl.HIGH_INT = 0x8DF5;


/**
 * @const
 * @type {number}
 */
goog.webgl.FRAMEBUFFER = 0x8D40;


/**
 * @const
 * @type {number}
 */
goog.webgl.RENDERBUFFER = 0x8D41;


/**
 * @const
 * @type {number}
 */
goog.webgl.RGBA4 = 0x8056;


/**
 * @const
 * @type {number}
 */
goog.webgl.RGB5_A1 = 0x8057;


/**
 * @const
 * @type {number}
 */
goog.webgl.RGB565 = 0x8D62;


/**
 * @const
 * @type {number}
 */
goog.webgl.DEPTH_COMPONENT16 = 0x81A5;


/**
 * @const
 * @type {number}
 */
goog.webgl.STENCIL_INDEX = 0x1901;


/**
 * @const
 * @type {number}
 */
goog.webgl.STENCIL_INDEX8 = 0x8D48;


/**
 * @const
 * @type {number}
 */
goog.webgl.DEPTH_STENCIL = 0x84F9;


/**
 * @const
 * @type {number}
 */
goog.webgl.RENDERBUFFER_WIDTH = 0x8D42;


/**
 * @const
 * @type {number}
 */
goog.webgl.RENDERBUFFER_HEIGHT = 0x8D43;


/**
 * @const
 * @type {number}
 */
goog.webgl.RENDERBUFFER_INTERNAL_FORMAT = 0x8D44;


/**
 * @const
 * @type {number}
 */
goog.webgl.RENDERBUFFER_RED_SIZE = 0x8D50;


/**
 * @const
 * @type {number}
 */
goog.webgl.RENDERBUFFER_GREEN_SIZE = 0x8D51;


/**
 * @const
 * @type {number}
 */
goog.webgl.RENDERBUFFER_BLUE_SIZE = 0x8D52;


/**
 * @const
 * @type {number}
 */
goog.webgl.RENDERBUFFER_ALPHA_SIZE = 0x8D53;


/**
 * @const
 * @type {number}
 */
goog.webgl.RENDERBUFFER_DEPTH_SIZE = 0x8D54;


/**
 * @const
 * @type {number}
 */
goog.webgl.RENDERBUFFER_STENCIL_SIZE = 0x8D55;


/**
 * @const
 * @type {number}
 */
goog.webgl.FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE = 0x8CD0;


/**
 * @const
 * @type {number}
 */
goog.webgl.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME = 0x8CD1;


/**
 * @const
 * @type {number}
 */
goog.webgl.FRAMEBUFFER_ATTACHMENT_TEXTURE_LEVEL = 0x8CD2;


/**
 * @const
 * @type {number}
 */
goog.webgl.FRAMEBUFFER_ATTACHMENT_TEXTURE_CUBE_MAP_FACE = 0x8CD3;


/**
 * @const
 * @type {number}
 */
goog.webgl.COLOR_ATTACHMENT0 = 0x8CE0;


/**
 * @const
 * @type {number}
 */
goog.webgl.DEPTH_ATTACHMENT = 0x8D00;


/**
 * @const
 * @type {number}
 */
goog.webgl.STENCIL_ATTACHMENT = 0x8D20;


/**
 * @const
 * @type {number}
 */
goog.webgl.DEPTH_STENCIL_ATTACHMENT = 0x821A;


/**
 * @const
 * @type {number}
 */
goog.webgl.NONE = 0;


/**
 * @const
 * @type {number}
 */
goog.webgl.FRAMEBUFFER_COMPLETE = 0x8CD5;


/**
 * @const
 * @type {number}
 */
goog.webgl.FRAMEBUFFER_INCOMPLETE_ATTACHMENT = 0x8CD6;


/**
 * @const
 * @type {number}
 */
goog.webgl.FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT = 0x8CD7;


/**
 * @const
 * @type {number}
 */
goog.webgl.FRAMEBUFFER_INCOMPLETE_DIMENSIONS = 0x8CD9;


/**
 * @const
 * @type {number}
 */
goog.webgl.FRAMEBUFFER_UNSUPPORTED = 0x8CDD;


/**
 * @const
 * @type {number}
 */
goog.webgl.FRAMEBUFFER_BINDING = 0x8CA6;


/**
 * @const
 * @type {number}
 */
goog.webgl.RENDERBUFFER_BINDING = 0x8CA7;


/**
 * @const
 * @type {number}
 */
goog.webgl.MAX_RENDERBUFFER_SIZE = 0x84E8;


/**
 * @const
 * @type {number}
 */
goog.webgl.INVALID_FRAMEBUFFER_OPERATION = 0x0506;


/**
 * @const
 * @type {number}
 */
goog.webgl.UNPACK_FLIP_Y_WEBGL = 0x9240;


/**
 * @const
 * @type {number}
 */
goog.webgl.UNPACK_PREMULTIPLY_ALPHA_WEBGL = 0x9241;


/**
 * @const
 * @type {number}
 */
goog.webgl.CONTEXT_LOST_WEBGL = 0x9242;


/**
 * @const
 * @type {number}
 */
goog.webgl.UNPACK_COLORSPACE_CONVERSION_WEBGL = 0x9243;


/**
 * @const
 * @type {number}
 */
goog.webgl.BROWSER_DEFAULT_WEBGL = 0x9244;


/**
 * From the OES_texture_half_float extension.
 * http://www.khronos.org/registry/webgl/extensions/OES_texture_half_float/
 * @const
 * @type {number}
 */
goog.webgl.HALF_FLOAT_OES = 0x8D61;


/**
 * From the OES_standard_derivatives extension.
 * http://www.khronos.org/registry/webgl/extensions/OES_standard_derivatives/
 * @const
 * @type {number}
 */
goog.webgl.FRAGMENT_SHADER_DERIVATIVE_HINT_OES = 0x8B8B;


/**
 * From the OES_vertex_array_object extension.
 * http://www.khronos.org/registry/webgl/extensions/OES_vertex_array_object/
 * @const
 * @type {number}
 */
goog.webgl.VERTEX_ARRAY_BINDING_OES = 0x85B5;


/**
 * From the WEBGL_debug_renderer_info extension.
 * http://www.khronos.org/registry/webgl/extensions/WEBGL_debug_renderer_info/
 * @const
 * @type {number}
 */
goog.webgl.UNMASKED_VENDOR_WEBGL = 0x9245;


/**
 * From the WEBGL_debug_renderer_info extension.
 * http://www.khronos.org/registry/webgl/extensions/WEBGL_debug_renderer_info/
 * @const
 * @type {number}
 */
goog.webgl.UNMASKED_RENDERER_WEBGL = 0x9246;


/**
 * From the WEBGL_compressed_texture_s3tc extension.
 * http://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_s3tc/
 * @const
 * @type {number}
 */
goog.webgl.COMPRESSED_RGB_S3TC_DXT1_EXT = 0x83F0;


/**
 * From the WEBGL_compressed_texture_s3tc extension.
 * http://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_s3tc/
 * @const
 * @type {number}
 */
goog.webgl.COMPRESSED_RGBA_S3TC_DXT1_EXT = 0x83F1;


/**
 * From the WEBGL_compressed_texture_s3tc extension.
 * http://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_s3tc/
 * @const
 * @type {number}
 */
goog.webgl.COMPRESSED_RGBA_S3TC_DXT3_EXT = 0x83F2;


/**
 * From the WEBGL_compressed_texture_s3tc extension.
 * http://www.khronos.org/registry/webgl/extensions/WEBGL_compressed_texture_s3tc/
 * @const
 * @type {number}
 */
goog.webgl.COMPRESSED_RGBA_S3TC_DXT5_EXT = 0x83F3;


/**
 * From the EXT_texture_filter_anisotropic extension.
 * http://www.khronos.org/registry/webgl/extensions/EXT_texture_filter_anisotropic/
 * @const
 * @type {number}
 */
goog.webgl.TEXTURE_MAX_ANISOTROPY_EXT = 0x84FE;


/**
 * From the EXT_texture_filter_anisotropic extension.
 * http://www.khronos.org/registry/webgl/extensions/EXT_texture_filter_anisotropic/
 * @const
 * @type {number}
 */
goog.webgl.MAX_TEXTURE_MAX_ANISOTROPY_EXT = 0x84FF;

goog.provide('ol.webgl.Fragment');
goog.provide('ol.webgl.Shader');
goog.provide('ol.webgl.Vertex');
goog.provide('ol.webgl.shader');

goog.require('goog.functions');
goog.require('goog.webgl');
goog.require('ol.webgl');


/**
 * @constructor
 * @param {string} source Source.
 * @struct
 */
ol.webgl.Shader = function(source) {

  /**
   * @private
   * @type {string}
   */
  this.source_ = source;

};


/**
 * @return {number} Type.
 */
ol.webgl.Shader.prototype.getType = goog.abstractMethod;


/**
 * @return {string} Source.
 */
ol.webgl.Shader.prototype.getSource = function() {
  return this.source_;
};


/**
 * @return {boolean} Is animated?
 */
ol.webgl.Shader.prototype.isAnimated = goog.functions.FALSE;


/**
 * @constructor
 * @extends {ol.webgl.Shader}
 * @param {string} source Source.
 * @struct
 */
ol.webgl.shader.Fragment = function(source) {
  goog.base(this, source);
};
goog.inherits(ol.webgl.shader.Fragment, ol.webgl.Shader);


/**
 * @inheritDoc
 */
ol.webgl.shader.Fragment.prototype.getType = function() {
  return goog.webgl.FRAGMENT_SHADER;
};


/**
 * @constructor
 * @extends {ol.webgl.Shader}
 * @param {string} source Source.
 * @struct
 */
ol.webgl.shader.Vertex = function(source) {
  goog.base(this, source);
};
goog.inherits(ol.webgl.shader.Vertex, ol.webgl.Shader);


/**
 * @inheritDoc
 */
ol.webgl.shader.Vertex.prototype.getType = function() {
  return goog.webgl.VERTEX_SHADER;
};

// This file is automatically generated, do not edit
goog.provide('ol.render.webgl.imagereplay.shader.Default');
goog.provide('ol.render.webgl.imagereplay.shader.Default.Locations');
goog.provide('ol.render.webgl.imagereplay.shader.DefaultFragment');
goog.provide('ol.render.webgl.imagereplay.shader.DefaultVertex');

goog.require('ol.webgl.shader');


/**
 * @constructor
 * @extends {ol.webgl.shader.Fragment}
 * @struct
 */
ol.render.webgl.imagereplay.shader.DefaultFragment = function() {
  goog.base(this, ol.render.webgl.imagereplay.shader.DefaultFragment.SOURCE);
};
goog.inherits(ol.render.webgl.imagereplay.shader.DefaultFragment, ol.webgl.shader.Fragment);
goog.addSingletonGetter(ol.render.webgl.imagereplay.shader.DefaultFragment);


/**
 * @const
 * @type {string}
 */
ol.render.webgl.imagereplay.shader.DefaultFragment.DEBUG_SOURCE = 'precision mediump float;\nvarying vec2 v_texCoord;\nvarying float v_opacity;\n\nuniform float u_opacity;\nuniform sampler2D u_image;\n\nvoid main(void) {\n  vec4 texColor = texture2D(u_image, v_texCoord);\n  gl_FragColor.rgb = texColor.rgb;\n  float alpha = texColor.a * v_opacity * u_opacity;\n  if (alpha == 0.0) {\n    discard;\n  }\n  gl_FragColor.a = alpha;\n}\n';


/**
 * @const
 * @type {string}
 */
ol.render.webgl.imagereplay.shader.DefaultFragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;varying float b;uniform float k;uniform sampler2D l;void main(void){vec4 texColor=texture2D(l,a);gl_FragColor.rgb=texColor.rgb;float alpha=texColor.a*b*k;if(alpha==0.0){discard;}gl_FragColor.a=alpha;}';


/**
 * @const
 * @type {string}
 */
ol.render.webgl.imagereplay.shader.DefaultFragment.SOURCE = goog.DEBUG ?
    ol.render.webgl.imagereplay.shader.DefaultFragment.DEBUG_SOURCE :
    ol.render.webgl.imagereplay.shader.DefaultFragment.OPTIMIZED_SOURCE;


/**
 * @constructor
 * @extends {ol.webgl.shader.Vertex}
 * @struct
 */
ol.render.webgl.imagereplay.shader.DefaultVertex = function() {
  goog.base(this, ol.render.webgl.imagereplay.shader.DefaultVertex.SOURCE);
};
goog.inherits(ol.render.webgl.imagereplay.shader.DefaultVertex, ol.webgl.shader.Vertex);
goog.addSingletonGetter(ol.render.webgl.imagereplay.shader.DefaultVertex);


/**
 * @const
 * @type {string}
 */
ol.render.webgl.imagereplay.shader.DefaultVertex.DEBUG_SOURCE = 'varying vec2 v_texCoord;\nvarying float v_opacity;\n\nattribute vec2 a_position;\nattribute vec2 a_texCoord;\nattribute vec2 a_offsets;\nattribute float a_opacity;\nattribute float a_rotateWithView;\n\nuniform mat4 u_projectionMatrix;\nuniform mat4 u_offsetScaleMatrix;\nuniform mat4 u_offsetRotateMatrix;\n\nvoid main(void) {\n  mat4 offsetMatrix = u_offsetScaleMatrix;\n  if (a_rotateWithView == 1.0) {\n    offsetMatrix = u_offsetScaleMatrix * u_offsetRotateMatrix;\n  }\n  vec4 offsets = offsetMatrix * vec4(a_offsets, 0., 0.);\n  gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.) + offsets;\n  v_texCoord = a_texCoord;\n  v_opacity = a_opacity;\n}\n\n\n';


/**
 * @const
 * @type {string}
 */
ol.render.webgl.imagereplay.shader.DefaultVertex.OPTIMIZED_SOURCE = 'varying vec2 a;varying float b;attribute vec2 c;attribute vec2 d;attribute vec2 e;attribute float f;attribute float g;uniform mat4 h;uniform mat4 i;uniform mat4 j;void main(void){mat4 offsetMatrix=i;if(g==1.0){offsetMatrix=i*j;}vec4 offsets=offsetMatrix*vec4(e,0.,0.);gl_Position=h*vec4(c,0.,1.)+offsets;a=d;b=f;}';


/**
 * @const
 * @type {string}
 */
ol.render.webgl.imagereplay.shader.DefaultVertex.SOURCE = goog.DEBUG ?
    ol.render.webgl.imagereplay.shader.DefaultVertex.DEBUG_SOURCE :
    ol.render.webgl.imagereplay.shader.DefaultVertex.OPTIMIZED_SOURCE;


/**
 * @constructor
 * @param {WebGLRenderingContext} gl GL.
 * @param {WebGLProgram} program Program.
 * @struct
 */
ol.render.webgl.imagereplay.shader.Default.Locations = function(gl, program) {

  /**
   * @type {WebGLUniformLocation}
   */
  this.u_image = gl.getUniformLocation(
      program, goog.DEBUG ? 'u_image' : 'l');

  /**
   * @type {WebGLUniformLocation}
   */
  this.u_offsetRotateMatrix = gl.getUniformLocation(
      program, goog.DEBUG ? 'u_offsetRotateMatrix' : 'j');

  /**
   * @type {WebGLUniformLocation}
   */
  this.u_offsetScaleMatrix = gl.getUniformLocation(
      program, goog.DEBUG ? 'u_offsetScaleMatrix' : 'i');

  /**
   * @type {WebGLUniformLocation}
   */
  this.u_opacity = gl.getUniformLocation(
      program, goog.DEBUG ? 'u_opacity' : 'k');

  /**
   * @type {WebGLUniformLocation}
   */
  this.u_projectionMatrix = gl.getUniformLocation(
      program, goog.DEBUG ? 'u_projectionMatrix' : 'h');

  /**
   * @type {number}
   */
  this.a_offsets = gl.getAttribLocation(
      program, goog.DEBUG ? 'a_offsets' : 'e');

  /**
   * @type {number}
   */
  this.a_opacity = gl.getAttribLocation(
      program, goog.DEBUG ? 'a_opacity' : 'f');

  /**
   * @type {number}
   */
  this.a_position = gl.getAttribLocation(
      program, goog.DEBUG ? 'a_position' : 'c');

  /**
   * @type {number}
   */
  this.a_rotateWithView = gl.getAttribLocation(
      program, goog.DEBUG ? 'a_rotateWithView' : 'g');

  /**
   * @type {number}
   */
  this.a_texCoord = gl.getAttribLocation(
      program, goog.DEBUG ? 'a_texCoord' : 'd');
};

goog.provide('ol.webgl.Buffer');

goog.require('goog.webgl');
goog.require('ol');


/**
 * @enum {number}
 */
ol.webgl.BufferUsage = {
  STATIC_DRAW: goog.webgl.STATIC_DRAW,
  STREAM_DRAW: goog.webgl.STREAM_DRAW,
  DYNAMIC_DRAW: goog.webgl.DYNAMIC_DRAW
};


/**
 * @constructor
 * @param {Array.<number>=} opt_arr Array.
 * @param {number=} opt_usage Usage.
 * @struct
 */
ol.webgl.Buffer = function(opt_arr, opt_usage) {

  /**
   * @private
   * @type {Array.<number>}
   */
  this.arr_ = opt_arr !== undefined ? opt_arr : [];

  /**
   * @private
   * @type {number}
   */
  this.usage_ = opt_usage !== undefined ?
      opt_usage : ol.webgl.BufferUsage.STATIC_DRAW;

};


/**
 * @return {Array.<number>} Array.
 */
ol.webgl.Buffer.prototype.getArray = function() {
  return this.arr_;
};


/**
 * @return {number} Usage.
 */
ol.webgl.Buffer.prototype.getUsage = function() {
  return this.usage_;
};

goog.provide('ol.webgl.Context');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.log');
goog.require('goog.object');
goog.require('ol');
goog.require('ol.array');
goog.require('ol.webgl.Buffer');
goog.require('ol.webgl.WebGLContextEventType');


/**
 * @typedef {{buf: ol.webgl.Buffer,
 *            buffer: WebGLBuffer}}
 */
ol.webgl.BufferCacheEntry;


/**
 * @classdesc
 * A WebGL context for accessing low-level WebGL capabilities.
 *
 * @constructor
 * @extends {goog.events.EventTarget}
 * @param {HTMLCanvasElement} canvas Canvas.
 * @param {WebGLRenderingContext} gl GL.
 */
ol.webgl.Context = function(canvas, gl) {

  /**
   * @private
   * @type {HTMLCanvasElement}
   */
  this.canvas_ = canvas;

  /**
   * @private
   * @type {WebGLRenderingContext}
   */
  this.gl_ = gl;

  /**
   * @private
   * @type {Object.<number, ol.webgl.BufferCacheEntry>}
   */
  this.bufferCache_ = {};

  /**
   * @private
   * @type {Object.<number, WebGLShader>}
   */
  this.shaderCache_ = {};

  /**
   * @private
   * @type {Object.<string, WebGLProgram>}
   */
  this.programCache_ = {};

  /**
   * @private
   * @type {WebGLProgram}
   */
  this.currentProgram_ = null;

  /**
   * @private
   * @type {WebGLFramebuffer}
   */
  this.hitDetectionFramebuffer_ = null;

  /**
   * @private
   * @type {WebGLTexture}
   */
  this.hitDetectionTexture_ = null;

  /**
   * @private
   * @type {WebGLRenderbuffer}
   */
  this.hitDetectionRenderbuffer_ = null;

  /**
   * @type {boolean}
   */
  this.hasOESElementIndexUint = ol.array.includes(
      ol.WEBGL_EXTENSIONS, 'OES_element_index_uint');

  // use the OES_element_index_uint extension if available
  if (this.hasOESElementIndexUint) {
    var ext = gl.getExtension('OES_element_index_uint');
    goog.asserts.assert(ext,
        'Failed to get extension "OES_element_index_uint"');
  }

  goog.events.listen(this.canvas_, ol.webgl.WebGLContextEventType.LOST,
      this.handleWebGLContextLost, false, this);
  goog.events.listen(this.canvas_, ol.webgl.WebGLContextEventType.RESTORED,
      this.handleWebGLContextRestored, false, this);

};


/**
 * Just bind the buffer if it's in the cache. Otherwise create
 * the WebGL buffer, bind it, populate it, and add an entry to
 * the cache.
 * @param {number} target Target.
 * @param {ol.webgl.Buffer} buf Buffer.
 */
ol.webgl.Context.prototype.bindBuffer = function(target, buf) {
  var gl = this.getGL();
  var arr = buf.getArray();
  var bufferKey = goog.getUid(buf);
  if (bufferKey in this.bufferCache_) {
    var bufferCacheEntry = this.bufferCache_[bufferKey];
    gl.bindBuffer(target, bufferCacheEntry.buffer);
  } else {
    var buffer = gl.createBuffer();
    gl.bindBuffer(target, buffer);
    goog.asserts.assert(target == goog.webgl.ARRAY_BUFFER ||
        target == goog.webgl.ELEMENT_ARRAY_BUFFER,
        'target is supposed to be an ARRAY_BUFFER or ELEMENT_ARRAY_BUFFER');
    var /** @type {ArrayBufferView} */ arrayBuffer;
    if (target == goog.webgl.ARRAY_BUFFER) {
      arrayBuffer = new Float32Array(arr);
    } else if (target == goog.webgl.ELEMENT_ARRAY_BUFFER) {
      arrayBuffer = this.hasOESElementIndexUint ?
          new Uint32Array(arr) : new Uint16Array(arr);
    } else {
      goog.asserts.fail();
    }
    gl.bufferData(target, arrayBuffer, buf.getUsage());
    this.bufferCache_[bufferKey] = {
      buf: buf,
      buffer: buffer
    };
  }
};


/**
 * @param {ol.webgl.Buffer} buf Buffer.
 */
ol.webgl.Context.prototype.deleteBuffer = function(buf) {
  var gl = this.getGL();
  var bufferKey = goog.getUid(buf);
  goog.asserts.assert(bufferKey in this.bufferCache_,
      'attempted to delete uncached buffer');
  var bufferCacheEntry = this.bufferCache_[bufferKey];
  if (!gl.isContextLost()) {
    gl.deleteBuffer(bufferCacheEntry.buffer);
  }
  delete this.bufferCache_[bufferKey];
};


/**
 * @inheritDoc
 */
ol.webgl.Context.prototype.disposeInternal = function() {
  var gl = this.getGL();
  if (!gl.isContextLost()) {
    goog.object.forEach(this.bufferCache_, function(bufferCacheEntry) {
      gl.deleteBuffer(bufferCacheEntry.buffer);
    });
    goog.object.forEach(this.programCache_, function(program) {
      gl.deleteProgram(program);
    });
    goog.object.forEach(this.shaderCache_, function(shader) {
      gl.deleteShader(shader);
    });
    // delete objects for hit-detection
    gl.deleteFramebuffer(this.hitDetectionFramebuffer_);
    gl.deleteRenderbuffer(this.hitDetectionRenderbuffer_);
    gl.deleteTexture(this.hitDetectionTexture_);
  }
};


/**
 * @return {HTMLCanvasElement} Canvas.
 */
ol.webgl.Context.prototype.getCanvas = function() {
  return this.canvas_;
};


/**
 * Get the WebGL rendering context
 * @return {WebGLRenderingContext} The rendering context.
 * @api
 */
ol.webgl.Context.prototype.getGL = function() {
  return this.gl_;
};


/**
 * Get the frame buffer for hit detection.
 * @return {WebGLFramebuffer} The hit detection frame buffer.
 */
ol.webgl.Context.prototype.getHitDetectionFramebuffer = function() {
  if (!this.hitDetectionFramebuffer_) {
    this.initHitDetectionFramebuffer_();
  }
  return this.hitDetectionFramebuffer_;
};


/**
 * Get shader from the cache if it's in the cache. Otherwise, create
 * the WebGL shader, compile it, and add entry to cache.
 * @param {ol.webgl.Shader} shaderObject Shader object.
 * @return {WebGLShader} Shader.
 */
ol.webgl.Context.prototype.getShader = function(shaderObject) {
  var shaderKey = goog.getUid(shaderObject);
  if (shaderKey in this.shaderCache_) {
    return this.shaderCache_[shaderKey];
  } else {
    var gl = this.getGL();
    var shader = gl.createShader(shaderObject.getType());
    gl.shaderSource(shader, shaderObject.getSource());
    gl.compileShader(shader);
    if (goog.DEBUG) {
      if (!gl.getShaderParameter(shader, goog.webgl.COMPILE_STATUS) &&
          !gl.isContextLost()) {
        goog.log.error(this.logger_, gl.getShaderInfoLog(shader));
      }
    }
    goog.asserts.assert(
        gl.getShaderParameter(shader, goog.webgl.COMPILE_STATUS) ||
        gl.isContextLost(),
        'illegal state, shader not compiled or context lost');
    this.shaderCache_[shaderKey] = shader;
    return shader;
  }
};


/**
 * Get the program from the cache if it's in the cache. Otherwise create
 * the WebGL program, attach the shaders to it, and add an entry to the
 * cache.
 * @param {ol.webgl.shader.Fragment} fragmentShaderObject Fragment shader.
 * @param {ol.webgl.shader.Vertex} vertexShaderObject Vertex shader.
 * @return {WebGLProgram} Program.
 */
ol.webgl.Context.prototype.getProgram = function(
    fragmentShaderObject, vertexShaderObject) {
  var programKey =
      goog.getUid(fragmentShaderObject) + '/' + goog.getUid(vertexShaderObject);
  if (programKey in this.programCache_) {
    return this.programCache_[programKey];
  } else {
    var gl = this.getGL();
    var program = gl.createProgram();
    gl.attachShader(program, this.getShader(fragmentShaderObject));
    gl.attachShader(program, this.getShader(vertexShaderObject));
    gl.linkProgram(program);
    if (goog.DEBUG) {
      if (!gl.getProgramParameter(program, goog.webgl.LINK_STATUS) &&
          !gl.isContextLost()) {
        goog.log.error(this.logger_, gl.getProgramInfoLog(program));
      }
    }
    goog.asserts.assert(
        gl.getProgramParameter(program, goog.webgl.LINK_STATUS) ||
        gl.isContextLost(),
        'illegal state, shader not linked or context lost');
    this.programCache_[programKey] = program;
    return program;
  }
};


/**
 * FIXME empy description for jsdoc
 */
ol.webgl.Context.prototype.handleWebGLContextLost = function() {
  goog.object.clear(this.bufferCache_);
  goog.object.clear(this.shaderCache_);
  goog.object.clear(this.programCache_);
  this.currentProgram_ = null;
  this.hitDetectionFramebuffer_ = null;
  this.hitDetectionTexture_ = null;
  this.hitDetectionRenderbuffer_ = null;
};


/**
 * FIXME empy description for jsdoc
 */
ol.webgl.Context.prototype.handleWebGLContextRestored = function() {
};


/**
 * Creates a 1x1 pixel framebuffer for the hit-detection.
 * @private
 */
ol.webgl.Context.prototype.initHitDetectionFramebuffer_ = function() {
  var gl = this.gl_;
  var framebuffer = gl.createFramebuffer();
  gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);

  var texture = ol.webgl.Context.createEmptyTexture(gl, 1, 1);
  var renderbuffer = gl.createRenderbuffer();
  gl.bindRenderbuffer(gl.RENDERBUFFER, renderbuffer);
  gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, 1, 1);
  gl.framebufferTexture2D(
      gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
  gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT,
      gl.RENDERBUFFER, renderbuffer);

  gl.bindTexture(gl.TEXTURE_2D, null);
  gl.bindRenderbuffer(gl.RENDERBUFFER, null);
  gl.bindFramebuffer(gl.FRAMEBUFFER, null);

  this.hitDetectionFramebuffer_ = framebuffer;
  this.hitDetectionTexture_ = texture;
  this.hitDetectionRenderbuffer_ = renderbuffer;
};


/**
 * Use a program.  If the program is already in use, this will return `false`.
 * @param {WebGLProgram} program Program.
 * @return {boolean} Changed.
 * @api
 */
ol.webgl.Context.prototype.useProgram = function(program) {
  if (program == this.currentProgram_) {
    return false;
  } else {
    var gl = this.getGL();
    gl.useProgram(program);
    this.currentProgram_ = program;
    return true;
  }
};


/**
 * @private
 * @type {goog.log.Logger}
 */
ol.webgl.Context.prototype.logger_ = goog.log.getLogger('ol.webgl.Context');


/**
 * @param {WebGLRenderingContext} gl WebGL rendering context.
 * @param {number=} opt_wrapS wrapS.
 * @param {number=} opt_wrapT wrapT.
 * @return {WebGLTexture} The texture.
 * @private
 */
ol.webgl.Context.createTexture_ = function(gl, opt_wrapS, opt_wrapT) {
  var texture = gl.createTexture();
  gl.bindTexture(gl.TEXTURE_2D, texture);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
  gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);

  if (opt_wrapS !== undefined) {
    gl.texParameteri(
        goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_WRAP_S, opt_wrapS);
  }
  if (opt_wrapT !== undefined) {
    gl.texParameteri(
        goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_WRAP_T, opt_wrapT);
  }

  return texture;
};


/**
 * @param {WebGLRenderingContext} gl WebGL rendering context.
 * @param {number} width Width.
 * @param {number} height Height.
 * @param {number=} opt_wrapS wrapS.
 * @param {number=} opt_wrapT wrapT.
 * @return {WebGLTexture} The texture.
 */
ol.webgl.Context.createEmptyTexture = function(
    gl, width, height, opt_wrapS, opt_wrapT) {
  var texture = ol.webgl.Context.createTexture_(gl, opt_wrapS, opt_wrapT);
  gl.texImage2D(
      gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE,
      null);

  return texture;
};


/**
 * @param {WebGLRenderingContext} gl WebGL rendering context.
 * @param {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} image Image.
 * @param {number=} opt_wrapS wrapS.
 * @param {number=} opt_wrapT wrapT.
 * @return {WebGLTexture} The texture.
 */
ol.webgl.Context.createTexture = function(gl, image, opt_wrapS, opt_wrapT) {
  var texture = ol.webgl.Context.createTexture_(gl, opt_wrapS, opt_wrapT);
  gl.texImage2D(
      gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);

  return texture;
};

goog.provide('ol.render.webgl.ImageReplay');
goog.provide('ol.render.webgl.ReplayGroup');

goog.require('goog.asserts');
goog.require('goog.functions');
goog.require('goog.object');
goog.require('goog.vec.Mat4');
goog.require('ol.extent');
goog.require('ol.render.IReplayGroup');
goog.require('ol.render.VectorContext');
goog.require('ol.render.webgl.imagereplay.shader.Default');
goog.require('ol.render.webgl.imagereplay.shader.Default.Locations');
goog.require('ol.render.webgl.imagereplay.shader.DefaultFragment');
goog.require('ol.render.webgl.imagereplay.shader.DefaultVertex');
goog.require('ol.vec.Mat4');
goog.require('ol.webgl.Buffer');
goog.require('ol.webgl.Context');


/**
 * @constructor
 * @extends {ol.render.VectorContext}
 * @param {number} tolerance Tolerance.
 * @param {ol.Extent} maxExtent Max extent.
 * @protected
 * @struct
 */
ol.render.webgl.ImageReplay = function(tolerance, maxExtent) {
  goog.base(this);

  /**
   * @type {number|undefined}
   * @private
   */
  this.anchorX_ = undefined;

  /**
   * @type {number|undefined}
   * @private
   */
  this.anchorY_ = undefined;

  /**
   * The origin of the coordinate system for the point coordinates sent to
   * the GPU. To eliminate jitter caused by precision problems in the GPU
   * we use the "Rendering Relative to Eye" technique described in the "3D
   * Engine Design for Virtual Globes" book.
   * @private
   * @type {ol.Coordinate}
   */
  this.origin_ = ol.extent.getCenter(maxExtent);

  /**
   * @type {Array.<number>}
   * @private
   */
  this.groupIndices_ = [];

  /**
   * @type {Array.<number>}
   * @private
   */
  this.hitDetectionGroupIndices_ = [];

  /**
   * @type {number|undefined}
   * @private
   */
  this.height_ = undefined;

  /**
   * @type {Array.<HTMLCanvasElement|HTMLImageElement|HTMLVideoElement>}
   * @private
   */
  this.images_ = [];

  /**
   * @type {Array.<HTMLCanvasElement|HTMLImageElement|HTMLVideoElement>}
   * @private
   */
  this.hitDetectionImages_ = [];

  /**
   * @type {number|undefined}
   * @private
   */
  this.imageHeight_ = undefined;

  /**
   * @type {number|undefined}
   * @private
   */
  this.imageWidth_ = undefined;

  /**
   * @type {Array.<number>}
   * @private
   */
  this.indices_ = [];

  /**
   * @type {ol.webgl.Buffer}
   * @private
   */
  this.indicesBuffer_ = null;

  /**
   * @private
   * @type {ol.render.webgl.imagereplay.shader.Default.Locations}
   */
  this.defaultLocations_ = null;

  /**
   * @private
   * @type {number|undefined}
   */
  this.opacity_ = undefined;

  /**
   * @type {!goog.vec.Mat4.Number}
   * @private
   */
  this.offsetRotateMatrix_ = goog.vec.Mat4.createNumberIdentity();

  /**
   * @type {!goog.vec.Mat4.Number}
   * @private
   */
  this.offsetScaleMatrix_ = goog.vec.Mat4.createNumberIdentity();

  /**
   * @type {number|undefined}
   * @private
   */
  this.originX_ = undefined;

  /**
   * @type {number|undefined}
   * @private
   */
  this.originY_ = undefined;

  /**
   * @type {!goog.vec.Mat4.Number}
   * @private
   */
  this.projectionMatrix_ = goog.vec.Mat4.createNumberIdentity();

  /**
   * @private
   * @type {boolean|undefined}
   */
  this.rotateWithView_ = undefined;

  /**
   * @private
   * @type {number|undefined}
   */
  this.rotation_ = undefined;

  /**
   * @private
   * @type {number|undefined}
   */
  this.scale_ = undefined;

  /**
   * @type {Array.<WebGLTexture>}
   * @private
   */
  this.textures_ = [];

  /**
   * @type {Array.<WebGLTexture>}
   * @private
   */
  this.hitDetectionTextures_ = [];

  /**
   * @type {Array.<number>}
   * @private
   */
  this.vertices_ = [];

  /**
   * @type {ol.webgl.Buffer}
   * @private
   */
  this.verticesBuffer_ = null;

  /**
   * Start index per feature (the index).
   * @type {Array.<number>}
   * @private
   */
  this.startIndices_ = [];

  /**
   * Start index per feature (the feature).
   * @type {Array.<ol.Feature>}
   * @private
   */
  this.startIndicesFeature_ = [];

  /**
   * @type {number|undefined}
   * @private
   */
  this.width_ = undefined;
};
goog.inherits(ol.render.webgl.ImageReplay, ol.render.VectorContext);


/**
 * @param {ol.webgl.Context} context WebGL context.
 * @return {function()} Delete resources function.
 */
ol.render.webgl.ImageReplay.prototype.getDeleteResourcesFunction = function(context) {
  // We only delete our stuff here. The shaders and the program may
  // be used by other ImageReplay instances (for other layers). And
  // they will be deleted when disposing of the ol.webgl.Context
  // object.
  goog.asserts.assert(this.verticesBuffer_,
      'verticesBuffer must not be null');
  goog.asserts.assert(this.indicesBuffer_,
      'indicesBuffer must not be null');
  var verticesBuffer = this.verticesBuffer_;
  var indicesBuffer = this.indicesBuffer_;
  var textures = this.textures_;
  var hitDetectionTextures = this.hitDetectionTextures_;
  var gl = context.getGL();
  return function() {
    if (!gl.isContextLost()) {
      var i, ii;
      for (i = 0, ii = textures.length; i < ii; ++i) {
        gl.deleteTexture(textures[i]);
      }
      for (i = 0, ii = hitDetectionTextures.length; i < ii; ++i) {
        gl.deleteTexture(hitDetectionTextures[i]);
      }
    }
    context.deleteBuffer(verticesBuffer);
    context.deleteBuffer(indicesBuffer);
  };
};


/**
 * @inheritDoc
 */
ol.render.webgl.ImageReplay.prototype.drawAsync = goog.abstractMethod;


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @return {number} My end.
 * @private
 */
ol.render.webgl.ImageReplay.prototype.drawCoordinates_ = function(flatCoordinates, offset, end, stride) {
  goog.asserts.assert(this.anchorX_ !== undefined, 'anchorX is defined');
  goog.asserts.assert(this.anchorY_ !== undefined, 'anchorY is defined');
  goog.asserts.assert(this.height_ !== undefined, 'height is defined');
  goog.asserts.assert(this.imageHeight_ !== undefined,
      'imageHeight is defined');
  goog.asserts.assert(this.imageWidth_ !== undefined, 'imageWidth is defined');
  goog.asserts.assert(this.opacity_ !== undefined, 'opacity is defined');
  goog.asserts.assert(this.originX_ !== undefined, 'originX is defined');
  goog.asserts.assert(this.originY_ !== undefined, 'originY is defined');
  goog.asserts.assert(this.rotateWithView_ !== undefined,
      'rotateWithView is defined');
  goog.asserts.assert(this.rotation_ !== undefined, 'rotation is defined');
  goog.asserts.assert(this.scale_ !== undefined, 'scale is defined');
  goog.asserts.assert(this.width_ !== undefined, 'width is defined');
  var anchorX = this.anchorX_;
  var anchorY = this.anchorY_;
  var height = this.height_;
  var imageHeight = this.imageHeight_;
  var imageWidth = this.imageWidth_;
  var opacity = this.opacity_;
  var originX = this.originX_;
  var originY = this.originY_;
  var rotateWithView = this.rotateWithView_ ? 1.0 : 0.0;
  var rotation = this.rotation_;
  var scale = this.scale_;
  var width = this.width_;
  var cos = Math.cos(rotation);
  var sin = Math.sin(rotation);
  var numIndices = this.indices_.length;
  var numVertices = this.vertices_.length;
  var i, n, offsetX, offsetY, x, y;
  for (i = offset; i < end; i += stride) {
    x = flatCoordinates[i] - this.origin_[0];
    y = flatCoordinates[i + 1] - this.origin_[1];

    // There are 4 vertices per [x, y] point, one for each corner of the
    // rectangle we're going to draw. We'd use 1 vertex per [x, y] point if
    // WebGL supported Geometry Shaders (which can emit new vertices), but that
    // is not currently the case.
    //
    // And each vertex includes 8 values: the x and y coordinates, the x and
    // y offsets used to calculate the position of the corner, the u and
    // v texture coordinates for the corner, the opacity, and whether the
    // the image should be rotated with the view (rotateWithView).

    n = numVertices / 8;

    // bottom-left corner
    offsetX = -scale * anchorX;
    offsetY = -scale * (height - anchorY);
    this.vertices_[numVertices++] = x;
    this.vertices_[numVertices++] = y;
    this.vertices_[numVertices++] = offsetX * cos - offsetY * sin;
    this.vertices_[numVertices++] = offsetX * sin + offsetY * cos;
    this.vertices_[numVertices++] = originX / imageWidth;
    this.vertices_[numVertices++] = (originY + height) / imageHeight;
    this.vertices_[numVertices++] = opacity;
    this.vertices_[numVertices++] = rotateWithView;

    // bottom-right corner
    offsetX = scale * (width - anchorX);
    offsetY = -scale * (height - anchorY);
    this.vertices_[numVertices++] = x;
    this.vertices_[numVertices++] = y;
    this.vertices_[numVertices++] = offsetX * cos - offsetY * sin;
    this.vertices_[numVertices++] = offsetX * sin + offsetY * cos;
    this.vertices_[numVertices++] = (originX + width) / imageWidth;
    this.vertices_[numVertices++] = (originY + height) / imageHeight;
    this.vertices_[numVertices++] = opacity;
    this.vertices_[numVertices++] = rotateWithView;

    // top-right corner
    offsetX = scale * (width - anchorX);
    offsetY = scale * anchorY;
    this.vertices_[numVertices++] = x;
    this.vertices_[numVertices++] = y;
    this.vertices_[numVertices++] = offsetX * cos - offsetY * sin;
    this.vertices_[numVertices++] = offsetX * sin + offsetY * cos;
    this.vertices_[numVertices++] = (originX + width) / imageWidth;
    this.vertices_[numVertices++] = originY / imageHeight;
    this.vertices_[numVertices++] = opacity;
    this.vertices_[numVertices++] = rotateWithView;

    // top-left corner
    offsetX = -scale * anchorX;
    offsetY = scale * anchorY;
    this.vertices_[numVertices++] = x;
    this.vertices_[numVertices++] = y;
    this.vertices_[numVertices++] = offsetX * cos - offsetY * sin;
    this.vertices_[numVertices++] = offsetX * sin + offsetY * cos;
    this.vertices_[numVertices++] = originX / imageWidth;
    this.vertices_[numVertices++] = originY / imageHeight;
    this.vertices_[numVertices++] = opacity;
    this.vertices_[numVertices++] = rotateWithView;

    this.indices_[numIndices++] = n;
    this.indices_[numIndices++] = n + 1;
    this.indices_[numIndices++] = n + 2;
    this.indices_[numIndices++] = n;
    this.indices_[numIndices++] = n + 2;
    this.indices_[numIndices++] = n + 3;
  }

  return numVertices;
};


/**
 * @inheritDoc
 */
ol.render.webgl.ImageReplay.prototype.drawMultiPointGeometry = function(multiPointGeometry, feature) {
  this.startIndices_.push(this.indices_.length);
  this.startIndicesFeature_.push(feature);
  var flatCoordinates = multiPointGeometry.getFlatCoordinates();
  var stride = multiPointGeometry.getStride();
  this.drawCoordinates_(
      flatCoordinates, 0, flatCoordinates.length, stride);
};


/**
 * @inheritDoc
 */
ol.render.webgl.ImageReplay.prototype.drawPointGeometry = function(pointGeometry, feature) {
  this.startIndices_.push(this.indices_.length);
  this.startIndicesFeature_.push(feature);
  var flatCoordinates = pointGeometry.getFlatCoordinates();
  var stride = pointGeometry.getStride();
  this.drawCoordinates_(
      flatCoordinates, 0, flatCoordinates.length, stride);
};


/**
 * @param {ol.webgl.Context} context Context.
 */
ol.render.webgl.ImageReplay.prototype.finish = function(context) {
  var gl = context.getGL();

  this.groupIndices_.push(this.indices_.length);
  goog.asserts.assert(this.images_.length === this.groupIndices_.length,
      'number of images and groupIndices match');
  this.hitDetectionGroupIndices_.push(this.indices_.length);
  goog.asserts.assert(this.hitDetectionImages_.length ===
      this.hitDetectionGroupIndices_.length,
      'number of hitDetectionImages and hitDetectionGroupIndices match');

  // create, bind, and populate the vertices buffer
  this.verticesBuffer_ = new ol.webgl.Buffer(this.vertices_);
  context.bindBuffer(goog.webgl.ARRAY_BUFFER, this.verticesBuffer_);

  var indices = this.indices_;
  var bits = context.hasOESElementIndexUint ? 32 : 16;
  goog.asserts.assert(indices[indices.length - 1] < Math.pow(2, bits),
      'Too large element index detected [%s] (OES_element_index_uint "%s")',
      indices[indices.length - 1], context.hasOESElementIndexUint);

  // create, bind, and populate the indices buffer
  this.indicesBuffer_ = new ol.webgl.Buffer(indices);
  context.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer_);

  // create textures
  /** @type {Object.<string, WebGLTexture>} */
  var texturePerImage = {};

  this.createTextures_(this.textures_, this.images_, texturePerImage, gl);
  goog.asserts.assert(this.textures_.length === this.groupIndices_.length,
      'number of textures and groupIndices match');

  this.createTextures_(this.hitDetectionTextures_, this.hitDetectionImages_,
      texturePerImage, gl);
  goog.asserts.assert(this.hitDetectionTextures_.length ===
      this.hitDetectionGroupIndices_.length,
      'number of hitDetectionTextures and hitDetectionGroupIndices match');

  this.anchorX_ = undefined;
  this.anchorY_ = undefined;
  this.height_ = undefined;
  this.images_ = null;
  this.hitDetectionImages_ = null;
  this.imageHeight_ = undefined;
  this.imageWidth_ = undefined;
  this.indices_ = null;
  this.opacity_ = undefined;
  this.originX_ = undefined;
  this.originY_ = undefined;
  this.rotateWithView_ = undefined;
  this.rotation_ = undefined;
  this.scale_ = undefined;
  this.vertices_ = null;
  this.width_ = undefined;
};


/**
 * @private
 * @param {Array.<WebGLTexture>} textures Textures.
 * @param {Array.<HTMLCanvasElement|HTMLImageElement|HTMLVideoElement>} images
 *    Images.
 * @param {Object.<string, WebGLTexture>} texturePerImage Texture cache.
 * @param {WebGLRenderingContext} gl Gl.
 */
ol.render.webgl.ImageReplay.prototype.createTextures_ = function(textures, images, texturePerImage, gl) {
  goog.asserts.assert(textures.length === 0,
      'upon creation, textures is empty');

  var texture, image, uid, i;
  var ii = images.length;
  for (i = 0; i < ii; ++i) {
    image = images[i];

    uid = goog.getUid(image).toString();
    if (uid in texturePerImage) {
      texture = texturePerImage[uid];
    } else {
      texture = ol.webgl.Context.createTexture(
          gl, image, goog.webgl.CLAMP_TO_EDGE, goog.webgl.CLAMP_TO_EDGE);
      texturePerImage[uid] = texture;
    }
    textures[i] = texture;
  }
};


/**
 * @param {ol.webgl.Context} context Context.
 * @param {ol.Coordinate} center Center.
 * @param {number} resolution Resolution.
 * @param {number} rotation Rotation.
 * @param {ol.Size} size Size.
 * @param {number} pixelRatio Pixel ratio.
 * @param {number} opacity Global opacity.
 * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
 *  to skip.
 * @param {function(ol.Feature): T|undefined} featureCallback Feature callback.
 * @param {boolean} oneByOne Draw features one-by-one for the hit-detecion.
 * @param {ol.Extent=} opt_hitExtent Hit extent: Only features intersecting
 *  this extent are checked.
 * @return {T|undefined} Callback result.
 * @template T
 */
ol.render.webgl.ImageReplay.prototype.replay = function(context,
    center, resolution, rotation, size, pixelRatio,
    opacity, skippedFeaturesHash,
    featureCallback, oneByOne, opt_hitExtent) {
  var gl = context.getGL();

  // bind the vertices buffer
  goog.asserts.assert(this.verticesBuffer_,
      'verticesBuffer must not be null');
  context.bindBuffer(goog.webgl.ARRAY_BUFFER, this.verticesBuffer_);

  // bind the indices buffer
  goog.asserts.assert(this.indicesBuffer_,
      'indecesBuffer must not be null');
  context.bindBuffer(goog.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer_);

  // get the program
  var fragmentShader =
      ol.render.webgl.imagereplay.shader.DefaultFragment.getInstance();
  var vertexShader =
      ol.render.webgl.imagereplay.shader.DefaultVertex.getInstance();
  var program = context.getProgram(fragmentShader, vertexShader);

  // get the locations
  var locations;
  if (!this.defaultLocations_) {
    locations =
        new ol.render.webgl.imagereplay.shader.Default.Locations(gl, program);
    this.defaultLocations_ = locations;
  } else {
    locations = this.defaultLocations_;
  }

  // use the program (FIXME: use the return value)
  context.useProgram(program);

  // enable the vertex attrib arrays
  gl.enableVertexAttribArray(locations.a_position);
  gl.vertexAttribPointer(locations.a_position, 2, goog.webgl.FLOAT,
      false, 32, 0);

  gl.enableVertexAttribArray(locations.a_offsets);
  gl.vertexAttribPointer(locations.a_offsets, 2, goog.webgl.FLOAT,
      false, 32, 8);

  gl.enableVertexAttribArray(locations.a_texCoord);
  gl.vertexAttribPointer(locations.a_texCoord, 2, goog.webgl.FLOAT,
      false, 32, 16);

  gl.enableVertexAttribArray(locations.a_opacity);
  gl.vertexAttribPointer(locations.a_opacity, 1, goog.webgl.FLOAT,
      false, 32, 24);

  gl.enableVertexAttribArray(locations.a_rotateWithView);
  gl.vertexAttribPointer(locations.a_rotateWithView, 1, goog.webgl.FLOAT,
      false, 32, 28);

  // set the "uniform" values
  var projectionMatrix = this.projectionMatrix_;
  ol.vec.Mat4.makeTransform2D(projectionMatrix,
      0.0, 0.0,
      2 / (resolution * size[0]),
      2 / (resolution * size[1]),
      -rotation,
      -(center[0] - this.origin_[0]), -(center[1] - this.origin_[1]));

  var offsetScaleMatrix = this.offsetScaleMatrix_;
  goog.vec.Mat4.makeScale(offsetScaleMatrix, 2 / size[0], 2 / size[1], 1);

  var offsetRotateMatrix = this.offsetRotateMatrix_;
  goog.vec.Mat4.makeIdentity(offsetRotateMatrix);
  if (rotation !== 0) {
    goog.vec.Mat4.rotateZ(offsetRotateMatrix, -rotation);
  }

  gl.uniformMatrix4fv(locations.u_projectionMatrix, false, projectionMatrix);
  gl.uniformMatrix4fv(locations.u_offsetScaleMatrix, false, offsetScaleMatrix);
  gl.uniformMatrix4fv(locations.u_offsetRotateMatrix, false,
      offsetRotateMatrix);
  gl.uniform1f(locations.u_opacity, opacity);

  // draw!
  var result;
  if (featureCallback === undefined) {
    this.drawReplay_(gl, context, skippedFeaturesHash,
        this.textures_, this.groupIndices_);
  } else {
    // draw feature by feature for the hit-detection
    result = this.drawHitDetectionReplay_(gl, context, skippedFeaturesHash,
        featureCallback, oneByOne, opt_hitExtent);
  }

  // disable the vertex attrib arrays
  gl.disableVertexAttribArray(locations.a_position);
  gl.disableVertexAttribArray(locations.a_offsets);
  gl.disableVertexAttribArray(locations.a_texCoord);
  gl.disableVertexAttribArray(locations.a_opacity);
  gl.disableVertexAttribArray(locations.a_rotateWithView);

  return result;
};


/**
 * @private
 * @param {WebGLRenderingContext} gl gl.
 * @param {ol.webgl.Context} context Context.
 * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
 *  to skip.
 * @param {Array.<WebGLTexture>} textures Textures.
 * @param {Array.<number>} groupIndices Texture group indices.
 */
ol.render.webgl.ImageReplay.prototype.drawReplay_ = function(gl, context, skippedFeaturesHash, textures, groupIndices) {
  goog.asserts.assert(textures.length === groupIndices.length,
      'number of textures and groupIndeces match');
  var elementType = context.hasOESElementIndexUint ?
      goog.webgl.UNSIGNED_INT : goog.webgl.UNSIGNED_SHORT;
  var elementSize = context.hasOESElementIndexUint ? 4 : 2;

  if (!goog.object.isEmpty(skippedFeaturesHash)) {
    this.drawReplaySkipping_(
        gl, skippedFeaturesHash, textures, groupIndices,
        elementType, elementSize);
  } else {
    var i, ii, start;
    for (i = 0, ii = textures.length, start = 0; i < ii; ++i) {
      gl.bindTexture(goog.webgl.TEXTURE_2D, textures[i]);
      var end = groupIndices[i];
      this.drawElements_(gl, start, end, elementType, elementSize);
      start = end;
    }
  }
};


/**
 * Draw the replay while paying attention to skipped features.
 *
 * This functions creates groups of features that can be drawn to together,
 * so that the number of `drawElements` calls is minimized.
 *
 * For example given the following texture groups:
 *
 *    Group 1: A B C
 *    Group 2: D [E] F G
 *
 * If feature E should be skipped, the following `drawElements` calls will be
 * made:
 *
 *    drawElements with feature A, B and C
 *    drawElements with feature D
 *    drawElements with feature F and G
 *
 * @private
 * @param {WebGLRenderingContext} gl gl.
 * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
 *  to skip.
 * @param {Array.<WebGLTexture>} textures Textures.
 * @param {Array.<number>} groupIndices Texture group indices.
 * @param {number} elementType Element type.
 * @param {number} elementSize Element Size.
 */
ol.render.webgl.ImageReplay.prototype.drawReplaySkipping_ = function(gl, skippedFeaturesHash, textures, groupIndices,
    elementType, elementSize) {
  var featureIndex = 0;

  var i, ii;
  for (i = 0, ii = textures.length; i < ii; ++i) {
    gl.bindTexture(goog.webgl.TEXTURE_2D, textures[i]);
    var groupStart = (i > 0) ? groupIndices[i - 1] : 0;
    var groupEnd = groupIndices[i];

    var start = groupStart;
    var end = groupStart;
    while (featureIndex < this.startIndices_.length &&
        this.startIndices_[featureIndex] <= groupEnd) {
      var feature = this.startIndicesFeature_[featureIndex];

      var featureUid = goog.getUid(feature).toString();
      if (skippedFeaturesHash[featureUid] !== undefined) {
        // feature should be skipped
        if (start !== end) {
          // draw the features so far
          this.drawElements_(gl, start, end, elementType, elementSize);
        }
        // continue with the next feature
        start = (featureIndex === this.startIndices_.length - 1) ?
            groupEnd : this.startIndices_[featureIndex + 1];
        end = start;
      } else {
        // the feature is not skipped, augment the end index
        end = (featureIndex === this.startIndices_.length - 1) ?
            groupEnd : this.startIndices_[featureIndex + 1];
      }
      featureIndex++;
    }

    if (start !== end) {
      // draw the remaining features (in case there was no skipped feature
      // in this texture group, all features of a group are drawn together)
      this.drawElements_(gl, start, end, elementType, elementSize);
    }
  }
};


/**
 * @private
 * @param {WebGLRenderingContext} gl gl.
 * @param {number} start Start index.
 * @param {number} end End index.
 * @param {number} elementType Element type.
 * @param {number} elementSize Element Size.
 */
ol.render.webgl.ImageReplay.prototype.drawElements_ = function(
    gl, start, end, elementType, elementSize) {
  var numItems = end - start;
  var offsetInBytes = start * elementSize;
  gl.drawElements(goog.webgl.TRIANGLES, numItems, elementType, offsetInBytes);
};


/**
 * @private
 * @param {WebGLRenderingContext} gl gl.
 * @param {ol.webgl.Context} context Context.
 * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
 *  to skip.
 * @param {function(ol.Feature): T|undefined} featureCallback Feature callback.
 * @param {boolean} oneByOne Draw features one-by-one for the hit-detecion.
 * @param {ol.Extent=} opt_hitExtent Hit extent: Only features intersecting
 *  this extent are checked.
 * @return {T|undefined} Callback result.
 * @template T
 */
ol.render.webgl.ImageReplay.prototype.drawHitDetectionReplay_ = function(gl, context, skippedFeaturesHash, featureCallback, oneByOne,
    opt_hitExtent) {
  if (!oneByOne) {
    // draw all hit-detection features in "once" (by texture group)
    return this.drawHitDetectionReplayAll_(gl, context,
        skippedFeaturesHash, featureCallback);
  } else {
    // draw hit-detection features one by one
    return this.drawHitDetectionReplayOneByOne_(gl, context,
        skippedFeaturesHash, featureCallback, opt_hitExtent);
  }
};


/**
 * @private
 * @param {WebGLRenderingContext} gl gl.
 * @param {ol.webgl.Context} context Context.
 * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
 *  to skip.
 * @param {function(ol.Feature): T|undefined} featureCallback Feature callback.
 * @return {T|undefined} Callback result.
 * @template T
 */
ol.render.webgl.ImageReplay.prototype.drawHitDetectionReplayAll_ = function(gl, context, skippedFeaturesHash, featureCallback) {
  gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
  this.drawReplay_(gl, context, skippedFeaturesHash,
      this.hitDetectionTextures_, this.hitDetectionGroupIndices_);

  var result = featureCallback(null);
  if (result) {
    return result;
  } else {
    return undefined;
  }
};


/**
 * @private
 * @param {WebGLRenderingContext} gl gl.
 * @param {ol.webgl.Context} context Context.
 * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
 *  to skip.
 * @param {function(ol.Feature): T|undefined} featureCallback Feature callback.
 * @param {ol.Extent=} opt_hitExtent Hit extent: Only features intersecting
 *  this extent are checked.
 * @return {T|undefined} Callback result.
 * @template T
 */
ol.render.webgl.ImageReplay.prototype.drawHitDetectionReplayOneByOne_ = function(gl, context, skippedFeaturesHash, featureCallback,
    opt_hitExtent) {
  goog.asserts.assert(this.hitDetectionTextures_.length ===
      this.hitDetectionGroupIndices_.length,
      'number of hitDetectionTextures and hitDetectionGroupIndices match');
  var elementType = context.hasOESElementIndexUint ?
      goog.webgl.UNSIGNED_INT : goog.webgl.UNSIGNED_SHORT;
  var elementSize = context.hasOESElementIndexUint ? 4 : 2;

  var i, groupStart, start, end, feature, featureUid;
  var featureIndex = this.startIndices_.length - 1;
  for (i = this.hitDetectionTextures_.length - 1; i >= 0; --i) {
    gl.bindTexture(goog.webgl.TEXTURE_2D, this.hitDetectionTextures_[i]);
    groupStart = (i > 0) ? this.hitDetectionGroupIndices_[i - 1] : 0;
    end = this.hitDetectionGroupIndices_[i];

    // draw all features for this texture group
    while (featureIndex >= 0 &&
        this.startIndices_[featureIndex] >= groupStart) {
      start = this.startIndices_[featureIndex];
      feature = this.startIndicesFeature_[featureIndex];
      featureUid = goog.getUid(feature).toString();

      if (skippedFeaturesHash[featureUid] === undefined &&
          feature.getGeometry() &&
          (opt_hitExtent === undefined || ol.extent.intersects(
              /** @type {Array<number>} */ (opt_hitExtent),
              feature.getGeometry().getExtent()))) {
        gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
        this.drawElements_(gl, start, end, elementType, elementSize);

        var result = featureCallback(feature);
        if (result) {
          return result;
        }
      }

      end = start;
      featureIndex--;
    }
  }
  return undefined;
};


/**
 * @inheritDoc
 */
ol.render.webgl.ImageReplay.prototype.setFillStrokeStyle = goog.abstractMethod;


/**
 * @inheritDoc
 */
ol.render.webgl.ImageReplay.prototype.setImageStyle = function(imageStyle) {
  var anchor = imageStyle.getAnchor();
  var image = imageStyle.getImage(1);
  var imageSize = imageStyle.getImageSize();
  var hitDetectionImage = imageStyle.getHitDetectionImage(1);
  var hitDetectionImageSize = imageStyle.getHitDetectionImageSize();
  var opacity = imageStyle.getOpacity();
  var origin = imageStyle.getOrigin();
  var rotateWithView = imageStyle.getRotateWithView();
  var rotation = imageStyle.getRotation();
  var size = imageStyle.getSize();
  var scale = imageStyle.getScale();
  goog.asserts.assert(anchor, 'imageStyle anchor is not null');
  goog.asserts.assert(image, 'imageStyle image is not null');
  goog.asserts.assert(imageSize,
      'imageStyle imageSize is not null');
  goog.asserts.assert(hitDetectionImage,
      'imageStyle hitDetectionImage is not null');
  goog.asserts.assert(hitDetectionImageSize,
      'imageStyle hitDetectionImageSize is not null');
  goog.asserts.assert(opacity !== undefined, 'imageStyle opacity is defined');
  goog.asserts.assert(origin, 'imageStyle origin is not null');
  goog.asserts.assert(rotateWithView !== undefined,
      'imageStyle rotateWithView is defined');
  goog.asserts.assert(rotation !== undefined, 'imageStyle rotation is defined');
  goog.asserts.assert(size, 'imageStyle size is not null');
  goog.asserts.assert(scale !== undefined, 'imageStyle scale is defined');

  var currentImage;
  if (this.images_.length === 0) {
    this.images_.push(image);
  } else {
    currentImage = this.images_[this.images_.length - 1];
    if (goog.getUid(currentImage) != goog.getUid(image)) {
      this.groupIndices_.push(this.indices_.length);
      goog.asserts.assert(this.groupIndices_.length === this.images_.length,
          'number of groupIndices and images match');
      this.images_.push(image);
    }
  }

  if (this.hitDetectionImages_.length === 0) {
    this.hitDetectionImages_.push(hitDetectionImage);
  } else {
    currentImage =
        this.hitDetectionImages_[this.hitDetectionImages_.length - 1];
    if (goog.getUid(currentImage) != goog.getUid(hitDetectionImage)) {
      this.hitDetectionGroupIndices_.push(this.indices_.length);
      goog.asserts.assert(this.hitDetectionGroupIndices_.length ===
          this.hitDetectionImages_.length,
          'number of hitDetectionGroupIndices and hitDetectionImages match');
      this.hitDetectionImages_.push(hitDetectionImage);
    }
  }

  this.anchorX_ = anchor[0];
  this.anchorY_ = anchor[1];
  this.height_ = size[1];
  this.imageHeight_ = imageSize[1];
  this.imageWidth_ = imageSize[0];
  this.opacity_ = opacity;
  this.originX_ = origin[0];
  this.originY_ = origin[1];
  this.rotation_ = rotation;
  this.rotateWithView_ = rotateWithView;
  this.scale_ = scale;
  this.width_ = size[0];
};


/**
 * @constructor
 * @implements {ol.render.IReplayGroup}
 * @param {number} tolerance Tolerance.
 * @param {ol.Extent} maxExtent Max extent.
 * @param {number=} opt_renderBuffer Render buffer.
 * @struct
 */
ol.render.webgl.ReplayGroup = function(
    tolerance, maxExtent, opt_renderBuffer) {

  /**
   * @type {ol.Extent}
   * @private
   */
  this.maxExtent_ = maxExtent;

  /**
   * @type {number}
   * @private
   */
  this.tolerance_ = tolerance;

  /**
   * @type {number|undefined}
   * @private
   */
  this.renderBuffer_ = opt_renderBuffer;

  /**
   * ImageReplay only is supported at this point.
   * @type {Object.<ol.render.ReplayType, ol.render.webgl.ImageReplay>}
   * @private
   */
  this.replays_ = {};

};


/**
 * @param {ol.webgl.Context} context WebGL context.
 * @return {function()} Delete resources function.
 */
ol.render.webgl.ReplayGroup.prototype.getDeleteResourcesFunction = function(context) {
  var functions = [];
  var replayKey;
  for (replayKey in this.replays_) {
    functions.push(
        this.replays_[replayKey].getDeleteResourcesFunction(context));
  }
  return goog.functions.sequence.apply(null, functions);
};


/**
 * @param {ol.webgl.Context} context Context.
 */
ol.render.webgl.ReplayGroup.prototype.finish = function(context) {
  var replayKey;
  for (replayKey in this.replays_) {
    this.replays_[replayKey].finish(context);
  }
};


/**
 * @inheritDoc
 */
ol.render.webgl.ReplayGroup.prototype.getReplay = function(zIndex, replayType) {
  var replay = this.replays_[replayType];
  if (replay === undefined) {
    var constructor = ol.render.webgl.BATCH_CONSTRUCTORS_[replayType];
    goog.asserts.assert(constructor !== undefined,
        replayType +
        ' constructor missing from ol.render.webgl.BATCH_CONSTRUCTORS_');
    replay = new constructor(this.tolerance_, this.maxExtent_);
    this.replays_[replayType] = replay;
  }
  return replay;
};


/**
 * @inheritDoc
 */
ol.render.webgl.ReplayGroup.prototype.isEmpty = function() {
  return goog.object.isEmpty(this.replays_);
};


/**
 * @param {ol.webgl.Context} context Context.
 * @param {ol.Coordinate} center Center.
 * @param {number} resolution Resolution.
 * @param {number} rotation Rotation.
 * @param {ol.Size} size Size.
 * @param {number} pixelRatio Pixel ratio.
 * @param {number} opacity Global opacity.
 * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
 *  to skip.
 */
ol.render.webgl.ReplayGroup.prototype.replay = function(context,
    center, resolution, rotation, size, pixelRatio,
    opacity, skippedFeaturesHash) {
  var i, ii, replay;
  for (i = 0, ii = ol.render.REPLAY_ORDER.length; i < ii; ++i) {
    replay = this.replays_[ol.render.REPLAY_ORDER[i]];
    if (replay !== undefined) {
      replay.replay(context,
          center, resolution, rotation, size, pixelRatio,
          opacity, skippedFeaturesHash,
          undefined, false);
    }
  }
};


/**
 * @private
 * @param {ol.webgl.Context} context Context.
 * @param {ol.Coordinate} center Center.
 * @param {number} resolution Resolution.
 * @param {number} rotation Rotation.
 * @param {ol.Size} size Size.
 * @param {number} pixelRatio Pixel ratio.
 * @param {number} opacity Global opacity.
 * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
 *  to skip.
 * @param {function(ol.Feature): T|undefined} featureCallback Feature callback.
 * @param {boolean} oneByOne Draw features one-by-one for the hit-detecion.
 * @param {ol.Extent=} opt_hitExtent Hit extent: Only features intersecting
 *  this extent are checked.
 * @return {T|undefined} Callback result.
 * @template T
 */
ol.render.webgl.ReplayGroup.prototype.replayHitDetection_ = function(context,
    center, resolution, rotation, size, pixelRatio, opacity,
    skippedFeaturesHash, featureCallback, oneByOne, opt_hitExtent) {
  var i, replay, result;
  for (i = ol.render.REPLAY_ORDER.length - 1; i >= 0; --i) {
    replay = this.replays_[ol.render.REPLAY_ORDER[i]];
    if (replay !== undefined) {
      result = replay.replay(context,
          center, resolution, rotation, size, pixelRatio, opacity,
          skippedFeaturesHash, featureCallback, oneByOne, opt_hitExtent);
      if (result) {
        return result;
      }
    }
  }
  return undefined;
};


/**
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {ol.webgl.Context} context Context.
 * @param {ol.Coordinate} center Center.
 * @param {number} resolution Resolution.
 * @param {number} rotation Rotation.
 * @param {ol.Size} size Size.
 * @param {number} pixelRatio Pixel ratio.
 * @param {number} opacity Global opacity.
 * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
 *  to skip.
 * @param {function(ol.Feature): T|undefined} callback Feature callback.
 * @return {T|undefined} Callback result.
 * @template T
 */
ol.render.webgl.ReplayGroup.prototype.forEachFeatureAtCoordinate = function(
    coordinate, context, center, resolution, rotation, size, pixelRatio,
    opacity, skippedFeaturesHash,
    callback) {
  var gl = context.getGL();
  gl.bindFramebuffer(
      gl.FRAMEBUFFER, context.getHitDetectionFramebuffer());


  /**
   * @type {ol.Extent}
   */
  var hitExtent;
  if (this.renderBuffer_ !== undefined) {
    // build an extent around the coordinate, so that only features that
    // intersect this extent are checked
    hitExtent = ol.extent.buffer(
        ol.extent.createOrUpdateFromCoordinate(coordinate),
        resolution * this.renderBuffer_);
  }

  return this.replayHitDetection_(context,
      coordinate, resolution, rotation, ol.render.webgl.HIT_DETECTION_SIZE_,
      pixelRatio, opacity, skippedFeaturesHash,
      /**
       * @param {ol.Feature} feature Feature.
       * @return {?} Callback result.
       */
      function(feature) {
        var imageData = new Uint8Array(4);
        gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, imageData);

        if (imageData[3] > 0) {
          var result = callback(feature);
          if (result) {
            return result;
          }
        }
      }, true, hitExtent);
};


/**
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {ol.webgl.Context} context Context.
 * @param {ol.Coordinate} center Center.
 * @param {number} resolution Resolution.
 * @param {number} rotation Rotation.
 * @param {ol.Size} size Size.
 * @param {number} pixelRatio Pixel ratio.
 * @param {number} opacity Global opacity.
 * @param {Object.<string, boolean>} skippedFeaturesHash Ids of features
 *  to skip.
 * @return {boolean} Is there a feature at the given coordinate?
 */
ol.render.webgl.ReplayGroup.prototype.hasFeatureAtCoordinate = function(
    coordinate, context, center, resolution, rotation, size, pixelRatio,
    opacity, skippedFeaturesHash) {
  var gl = context.getGL();
  gl.bindFramebuffer(
      gl.FRAMEBUFFER, context.getHitDetectionFramebuffer());

  var hasFeature = this.replayHitDetection_(context,
      coordinate, resolution, rotation, ol.render.webgl.HIT_DETECTION_SIZE_,
      pixelRatio, opacity, skippedFeaturesHash,
      /**
       * @param {ol.Feature} feature Feature.
       * @return {boolean} Is there a feature?
       */
      function(feature) {
        var imageData = new Uint8Array(4);
        gl.readPixels(0, 0, 1, 1, gl.RGBA, gl.UNSIGNED_BYTE, imageData);
        return imageData[3] > 0;
      }, false);

  return hasFeature !== undefined;
};


/**
 * @const
 * @private
 * @type {Object.<ol.render.ReplayType,
 *                function(new: ol.render.webgl.ImageReplay, number,
 *                ol.Extent)>}
 */
ol.render.webgl.BATCH_CONSTRUCTORS_ = {
  'Image': ol.render.webgl.ImageReplay
};


/**
 * @const
 * @private
 * @type {Array.<number>}
 */
ol.render.webgl.HIT_DETECTION_SIZE_ = [1, 1];

goog.provide('ol.render.webgl.Immediate');
goog.require('ol.array');
goog.require('ol.extent');
goog.require('ol.render.VectorContext');
goog.require('ol.render.webgl.ImageReplay');
goog.require('ol.render.webgl.ReplayGroup');


/**
 * @constructor
 * @extends {ol.render.VectorContext}
 * @param {ol.webgl.Context} context Context.
 * @param {ol.Coordinate} center Center.
 * @param {number} resolution Resolution.
 * @param {number} rotation Rotation.
 * @param {ol.Size} size Size.
 * @param {ol.Extent} extent Extent.
 * @param {number} pixelRatio Pixel ratio.
 * @struct
 */
ol.render.webgl.Immediate = function(context,
    center, resolution, rotation, size, extent, pixelRatio) {
  goog.base(this);

  /**
   * @private
   */
  this.context_ = context;

  /**
   * @private
   */
  this.center_ = center;

  /**
   * @private
   */
  this.extent_ = extent;

  /**
   * @private
   */
  this.pixelRatio_ = pixelRatio;

  /**
   * @private
   */
  this.size_ = size;

  /**
   * @private
   */
  this.rotation_ = rotation;

  /**
   * @private
   */
  this.resolution_ = resolution;

  /**
   * @private
   * @type {ol.style.Image}
   */
  this.imageStyle_ = null;

  /**
   * @private
   * @type {!Object.<string,
   *        Array.<function(ol.render.webgl.Immediate)>>}
   */
  this.callbacksByZIndex_ = {};
};
goog.inherits(ol.render.webgl.Immediate, ol.render.VectorContext);


/**
 * FIXME: empty description for jsdoc
 */
ol.render.webgl.Immediate.prototype.flush = function() {
  /** @type {Array.<number>} */
  var zs = Object.keys(this.callbacksByZIndex_).map(Number);
  zs.sort(ol.array.numberSafeCompareFunction);
  var i, ii, callbacks, j, jj;
  for (i = 0, ii = zs.length; i < ii; ++i) {
    callbacks = this.callbacksByZIndex_[zs[i].toString()];
    for (j = 0, jj = callbacks.length; j < jj; ++j) {
      callbacks[j](this);
    }
  }
};


/**
 * Register a function to be called for rendering at a given zIndex.  The
 * function will be called asynchronously.  The callback will receive a
 * reference to {@link ol.render.canvas.Immediate} context for drawing.
 * @param {number} zIndex Z index.
 * @param {function(ol.render.webgl.Immediate)} callback Callback.
 * @api
 */
ol.render.webgl.Immediate.prototype.drawAsync = function(zIndex, callback) {
  var zIndexKey = zIndex.toString();
  var callbacks = this.callbacksByZIndex_[zIndexKey];
  if (callbacks !== undefined) {
    callbacks.push(callback);
  } else {
    this.callbacksByZIndex_[zIndexKey] = [callback];
  }
};


/**
 * @inheritDoc
 * @api
 */
ol.render.webgl.Immediate.prototype.drawCircleGeometry = function(circleGeometry, data) {
};


/**
 * @inheritDoc
 * @api
 */
ol.render.webgl.Immediate.prototype.drawFeature = function(feature, style) {
  var geometry = style.getGeometryFunction()(feature);
  if (!geometry ||
      !ol.extent.intersects(this.extent_, geometry.getExtent())) {
    return;
  }
  var zIndex = style.getZIndex();
  if (zIndex === undefined) {
    zIndex = 0;
  }
  this.drawAsync(zIndex, function(render) {
    render.setFillStrokeStyle(style.getFill(), style.getStroke());
    render.setImageStyle(style.getImage());
    render.setTextStyle(style.getText());
    var type = geometry.getType();
    var renderGeometry = ol.render.webgl.Immediate.GEOMETRY_RENDERERS_[type];
    // Do not assert since all kinds of geometries are not handled yet.
    // In spite, render what we support.
    if (renderGeometry) {
      renderGeometry.call(render, geometry, null);
    }
  });
};


/**
 * @inheritDoc
 * @api
 */
ol.render.webgl.Immediate.prototype.drawGeometryCollectionGeometry = function(geometryCollectionGeometry, data) {
  var geometries = geometryCollectionGeometry.getGeometriesArray();
  var renderers = ol.render.webgl.Immediate.GEOMETRY_RENDERERS_;
  var i, ii;
  for (i = 0, ii = geometries.length; i < ii; ++i) {
    var geometry = geometries[i];
    var geometryRenderer = renderers[geometry.getType()];
    // Do not assert since all kinds of geometries are not handled yet.
    // In order to support hierarchies, delegate instead what we can to
    // valid renderers.
    if (geometryRenderer) {
      geometryRenderer.call(this, geometry, data);
    }
  }
};


/**
 * @inheritDoc
 * @api
 */
ol.render.webgl.Immediate.prototype.drawPointGeometry = function(pointGeometry, data) {
  var context = this.context_;
  var replayGroup = new ol.render.webgl.ReplayGroup(1, this.extent_);
  var replay = /** @type {ol.render.webgl.ImageReplay} */ (
      replayGroup.getReplay(0, ol.render.ReplayType.IMAGE));
  replay.setImageStyle(this.imageStyle_);
  replay.drawPointGeometry(pointGeometry, data);
  replay.finish(context);
  // default colors
  var opacity = 1;
  var skippedFeatures = {};
  var featureCallback;
  var oneByOne = false;
  replay.replay(this.context_, this.center_, this.resolution_, this.rotation_,
      this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback,
      oneByOne);
  replay.getDeleteResourcesFunction(context)();
};


/**
 * @inheritDoc
 * @api
 */
ol.render.webgl.Immediate.prototype.drawLineStringGeometry = function(lineStringGeometry, data) {
};


/**
 * @inheritDoc
 * @api
 */
ol.render.webgl.Immediate.prototype.drawMultiLineStringGeometry = function(multiLineStringGeometry, data) {
};


/**
 * @inheritDoc
 * @api
 */
ol.render.webgl.Immediate.prototype.drawMultiPointGeometry = function(multiPointGeometry, data) {
  var context = this.context_;
  var replayGroup = new ol.render.webgl.ReplayGroup(1, this.extent_);
  var replay = /** @type {ol.render.webgl.ImageReplay} */ (
      replayGroup.getReplay(0, ol.render.ReplayType.IMAGE));
  replay.setImageStyle(this.imageStyle_);
  replay.drawMultiPointGeometry(multiPointGeometry, data);
  replay.finish(context);
  var opacity = 1;
  var skippedFeatures = {};
  var featureCallback;
  var oneByOne = false;
  replay.replay(this.context_, this.center_, this.resolution_, this.rotation_,
      this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback,
      oneByOne);
  replay.getDeleteResourcesFunction(context)();
};


/**
 * @inheritDoc
 * @api
 */
ol.render.webgl.Immediate.prototype.drawMultiPolygonGeometry = function(multiPolygonGeometry, data) {
};


/**
 * @inheritDoc
 * @api
 */
ol.render.webgl.Immediate.prototype.drawPolygonGeometry = function(polygonGeometry, data) {
};


/**
 * @inheritDoc
 * @api
 */
ol.render.webgl.Immediate.prototype.drawText = function(flatCoordinates, offset, end, stride, geometry, data) {
};


/**
 * @inheritDoc
 * @api
 */
ol.render.webgl.Immediate.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) {
};


/**
 * @inheritDoc
 * @api
 */
ol.render.webgl.Immediate.prototype.setImageStyle = function(imageStyle) {
  this.imageStyle_ = imageStyle;
};


/**
 * @inheritDoc
 * @api
 */
ol.render.webgl.Immediate.prototype.setTextStyle = function(textStyle) {
};


/**
 * @const
 * @private
 * @type {Object.<ol.geom.GeometryType,
 *      function(this: ol.render.webgl.Immediate,
 *          (ol.geom.Geometry|ol.render.Feature), Object)>}
 */
ol.render.webgl.Immediate.GEOMETRY_RENDERERS_ = {
  'Point': ol.render.webgl.Immediate.prototype.drawPointGeometry,
  'MultiPoint': ol.render.webgl.Immediate.prototype.drawMultiPointGeometry,
  'GeometryCollection':
      ol.render.webgl.Immediate.prototype.drawGeometryCollectionGeometry
};

// This file is automatically generated, do not edit
goog.provide('ol.renderer.webgl.map.shader.Default');
goog.provide('ol.renderer.webgl.map.shader.Default.Locations');
goog.provide('ol.renderer.webgl.map.shader.DefaultFragment');
goog.provide('ol.renderer.webgl.map.shader.DefaultVertex');

goog.require('ol.webgl.shader');


/**
 * @constructor
 * @extends {ol.webgl.shader.Fragment}
 * @struct
 */
ol.renderer.webgl.map.shader.DefaultFragment = function() {
  goog.base(this, ol.renderer.webgl.map.shader.DefaultFragment.SOURCE);
};
goog.inherits(ol.renderer.webgl.map.shader.DefaultFragment, ol.webgl.shader.Fragment);
goog.addSingletonGetter(ol.renderer.webgl.map.shader.DefaultFragment);


/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.map.shader.DefaultFragment.DEBUG_SOURCE = 'precision mediump float;\nvarying vec2 v_texCoord;\n\n\nuniform float u_opacity;\nuniform sampler2D u_texture;\n\nvoid main(void) {\n  vec4 texColor = texture2D(u_texture, v_texCoord);\n  gl_FragColor.rgb = texColor.rgb;\n  gl_FragColor.a = texColor.a * u_opacity;\n}\n';


/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.map.shader.DefaultFragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;uniform float f;uniform sampler2D g;void main(void){vec4 texColor=texture2D(g,a);gl_FragColor.rgb=texColor.rgb;gl_FragColor.a=texColor.a*f;}';


/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.map.shader.DefaultFragment.SOURCE = goog.DEBUG ?
    ol.renderer.webgl.map.shader.DefaultFragment.DEBUG_SOURCE :
    ol.renderer.webgl.map.shader.DefaultFragment.OPTIMIZED_SOURCE;


/**
 * @constructor
 * @extends {ol.webgl.shader.Vertex}
 * @struct
 */
ol.renderer.webgl.map.shader.DefaultVertex = function() {
  goog.base(this, ol.renderer.webgl.map.shader.DefaultVertex.SOURCE);
};
goog.inherits(ol.renderer.webgl.map.shader.DefaultVertex, ol.webgl.shader.Vertex);
goog.addSingletonGetter(ol.renderer.webgl.map.shader.DefaultVertex);


/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.map.shader.DefaultVertex.DEBUG_SOURCE = 'varying vec2 v_texCoord;\n\n\nattribute vec2 a_position;\nattribute vec2 a_texCoord;\n\nuniform mat4 u_texCoordMatrix;\nuniform mat4 u_projectionMatrix;\n\nvoid main(void) {\n  gl_Position = u_projectionMatrix * vec4(a_position, 0., 1.);\n  v_texCoord = (u_texCoordMatrix * vec4(a_texCoord, 0., 1.)).st;\n}\n\n\n';


/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.map.shader.DefaultVertex.OPTIMIZED_SOURCE = 'varying vec2 a;attribute vec2 b;attribute vec2 c;uniform mat4 d;uniform mat4 e;void main(void){gl_Position=e*vec4(b,0.,1.);a=(d*vec4(c,0.,1.)).st;}';


/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.map.shader.DefaultVertex.SOURCE = goog.DEBUG ?
    ol.renderer.webgl.map.shader.DefaultVertex.DEBUG_SOURCE :
    ol.renderer.webgl.map.shader.DefaultVertex.OPTIMIZED_SOURCE;


/**
 * @constructor
 * @param {WebGLRenderingContext} gl GL.
 * @param {WebGLProgram} program Program.
 * @struct
 */
ol.renderer.webgl.map.shader.Default.Locations = function(gl, program) {

  /**
   * @type {WebGLUniformLocation}
   */
  this.u_opacity = gl.getUniformLocation(
      program, goog.DEBUG ? 'u_opacity' : 'f');

  /**
   * @type {WebGLUniformLocation}
   */
  this.u_projectionMatrix = gl.getUniformLocation(
      program, goog.DEBUG ? 'u_projectionMatrix' : 'e');

  /**
   * @type {WebGLUniformLocation}
   */
  this.u_texCoordMatrix = gl.getUniformLocation(
      program, goog.DEBUG ? 'u_texCoordMatrix' : 'd');

  /**
   * @type {WebGLUniformLocation}
   */
  this.u_texture = gl.getUniformLocation(
      program, goog.DEBUG ? 'u_texture' : 'g');

  /**
   * @type {number}
   */
  this.a_position = gl.getAttribLocation(
      program, goog.DEBUG ? 'a_position' : 'b');

  /**
   * @type {number}
   */
  this.a_texCoord = gl.getAttribLocation(
      program, goog.DEBUG ? 'a_texCoord' : 'c');
};

goog.provide('ol.renderer.webgl.Layer');

goog.require('goog.vec.Mat4');
goog.require('goog.webgl');
goog.require('ol.layer.Layer');
goog.require('ol.render.Event');
goog.require('ol.render.EventType');
goog.require('ol.render.webgl.Immediate');
goog.require('ol.renderer.Layer');
goog.require('ol.renderer.webgl.map.shader.Default');
goog.require('ol.renderer.webgl.map.shader.Default.Locations');
goog.require('ol.renderer.webgl.map.shader.DefaultFragment');
goog.require('ol.renderer.webgl.map.shader.DefaultVertex');
goog.require('ol.webgl.Buffer');
goog.require('ol.webgl.Context');


/**
 * @constructor
 * @extends {ol.renderer.Layer}
 * @param {ol.renderer.webgl.Map} mapRenderer Map renderer.
 * @param {ol.layer.Layer} layer Layer.
 */
ol.renderer.webgl.Layer = function(mapRenderer, layer) {

  goog.base(this, layer);

  /**
   * @protected
   * @type {ol.renderer.webgl.Map}
   */
  this.mapRenderer = mapRenderer;

  /**
   * @private
   * @type {ol.webgl.Buffer}
   */
  this.arrayBuffer_ = new ol.webgl.Buffer([
    -1, -1, 0, 0,
    1, -1, 1, 0,
    -1, 1, 0, 1,
    1, 1, 1, 1
  ]);

  /**
   * @protected
   * @type {WebGLTexture}
   */
  this.texture = null;

  /**
   * @protected
   * @type {WebGLFramebuffer}
   */
  this.framebuffer = null;

  /**
   * @protected
   * @type {number|undefined}
   */
  this.framebufferDimension = undefined;

  /**
   * @protected
   * @type {!goog.vec.Mat4.Number}
   */
  this.texCoordMatrix = goog.vec.Mat4.createNumber();

  /**
   * @protected
   * @type {!goog.vec.Mat4.Number}
   */
  this.projectionMatrix = goog.vec.Mat4.createNumberIdentity();

  /**
   * @private
   * @type {ol.renderer.webgl.map.shader.Default.Locations}
   */
  this.defaultLocations_ = null;

};
goog.inherits(ol.renderer.webgl.Layer, ol.renderer.Layer);


/**
 * @param {olx.FrameState} frameState Frame state.
 * @param {number} framebufferDimension Framebuffer dimension.
 * @protected
 */
ol.renderer.webgl.Layer.prototype.bindFramebuffer = function(frameState, framebufferDimension) {

  var gl = this.mapRenderer.getGL();

  if (this.framebufferDimension === undefined ||
      this.framebufferDimension != framebufferDimension) {

    frameState.postRenderFunctions.push(
        goog.partial(
            /**
             * @param {WebGLRenderingContext} gl GL.
             * @param {WebGLFramebuffer} framebuffer Framebuffer.
             * @param {WebGLTexture} texture Texture.
             */
            function(gl, framebuffer, texture) {
              if (!gl.isContextLost()) {
                gl.deleteFramebuffer(framebuffer);
                gl.deleteTexture(texture);
              }
            }, gl, this.framebuffer, this.texture));

    var texture = ol.webgl.Context.createEmptyTexture(
        gl, framebufferDimension, framebufferDimension);

    var framebuffer = gl.createFramebuffer();
    gl.bindFramebuffer(goog.webgl.FRAMEBUFFER, framebuffer);
    gl.framebufferTexture2D(goog.webgl.FRAMEBUFFER,
        goog.webgl.COLOR_ATTACHMENT0, goog.webgl.TEXTURE_2D, texture, 0);

    this.texture = texture;
    this.framebuffer = framebuffer;
    this.framebufferDimension = framebufferDimension;

  } else {
    gl.bindFramebuffer(goog.webgl.FRAMEBUFFER, this.framebuffer);
  }

};


/**
 * @param {olx.FrameState} frameState Frame state.
 * @param {ol.layer.LayerState} layerState Layer state.
 * @param {ol.webgl.Context} context Context.
 */
ol.renderer.webgl.Layer.prototype.composeFrame = function(frameState, layerState, context) {

  this.dispatchComposeEvent_(
      ol.render.EventType.PRECOMPOSE, context, frameState);

  context.bindBuffer(goog.webgl.ARRAY_BUFFER, this.arrayBuffer_);

  var gl = context.getGL();

  var fragmentShader =
      ol.renderer.webgl.map.shader.DefaultFragment.getInstance();
  var vertexShader = ol.renderer.webgl.map.shader.DefaultVertex.getInstance();

  var program = context.getProgram(fragmentShader, vertexShader);

  var locations;
  if (!this.defaultLocations_) {
    locations =
        new ol.renderer.webgl.map.shader.Default.Locations(gl, program);
    this.defaultLocations_ = locations;
  } else {
    locations = this.defaultLocations_;
  }

  if (context.useProgram(program)) {
    gl.enableVertexAttribArray(locations.a_position);
    gl.vertexAttribPointer(
        locations.a_position, 2, goog.webgl.FLOAT, false, 16, 0);
    gl.enableVertexAttribArray(locations.a_texCoord);
    gl.vertexAttribPointer(
        locations.a_texCoord, 2, goog.webgl.FLOAT, false, 16, 8);
    gl.uniform1i(locations.u_texture, 0);
  }

  gl.uniformMatrix4fv(
      locations.u_texCoordMatrix, false, this.getTexCoordMatrix());
  gl.uniformMatrix4fv(locations.u_projectionMatrix, false,
      this.getProjectionMatrix());
  gl.uniform1f(locations.u_opacity, layerState.opacity);
  gl.bindTexture(goog.webgl.TEXTURE_2D, this.getTexture());
  gl.drawArrays(goog.webgl.TRIANGLE_STRIP, 0, 4);

  this.dispatchComposeEvent_(
      ol.render.EventType.POSTCOMPOSE, context, frameState);

};


/**
 * @param {ol.render.EventType} type Event type.
 * @param {ol.webgl.Context} context WebGL context.
 * @param {olx.FrameState} frameState Frame state.
 * @private
 */
ol.renderer.webgl.Layer.prototype.dispatchComposeEvent_ = function(type, context, frameState) {
  var layer = this.getLayer();
  if (layer.hasListener(type)) {
    var viewState = frameState.viewState;
    var resolution = viewState.resolution;
    var pixelRatio = frameState.pixelRatio;
    var extent = frameState.extent;
    var center = viewState.center;
    var rotation = viewState.rotation;
    var size = frameState.size;

    var render = new ol.render.webgl.Immediate(
        context, center, resolution, rotation, size, extent, pixelRatio);
    var composeEvent = new ol.render.Event(
        type, layer, render, frameState, null, context);
    layer.dispatchEvent(composeEvent);
  }
};


/**
 * @return {!goog.vec.Mat4.Number} Matrix.
 */
ol.renderer.webgl.Layer.prototype.getTexCoordMatrix = function() {
  return this.texCoordMatrix;
};


/**
 * @return {WebGLTexture} Texture.
 */
ol.renderer.webgl.Layer.prototype.getTexture = function() {
  return this.texture;
};


/**
 * @return {!goog.vec.Mat4.Number} Matrix.
 */
ol.renderer.webgl.Layer.prototype.getProjectionMatrix = function() {
  return this.projectionMatrix;
};


/**
 * Handle webglcontextlost.
 */
ol.renderer.webgl.Layer.prototype.handleWebGLContextLost = function() {
  this.texture = null;
  this.framebuffer = null;
  this.framebufferDimension = undefined;
};


/**
 * @param {olx.FrameState} frameState Frame state.
 * @param {ol.layer.LayerState} layerState Layer state.
 * @param {ol.webgl.Context} context Context.
 * @return {boolean} whether composeFrame should be called.
 */
ol.renderer.webgl.Layer.prototype.prepareFrame = goog.abstractMethod;

goog.provide('ol.renderer.webgl.ImageLayer');

goog.require('goog.asserts');
goog.require('goog.functions');
goog.require('goog.vec.Mat4');
goog.require('goog.webgl');
goog.require('ol.Coordinate');
goog.require('ol.Extent');
goog.require('ol.ImageBase');
goog.require('ol.ViewHint');
goog.require('ol.dom');
goog.require('ol.extent');
goog.require('ol.layer.Image');
goog.require('ol.proj');
goog.require('ol.renderer.webgl.Layer');
goog.require('ol.source.ImageVector');
goog.require('ol.vec.Mat4');
goog.require('ol.webgl.Context');


/**
 * @constructor
 * @extends {ol.renderer.webgl.Layer}
 * @param {ol.renderer.webgl.Map} mapRenderer Map renderer.
 * @param {ol.layer.Image} imageLayer Tile layer.
 */
ol.renderer.webgl.ImageLayer = function(mapRenderer, imageLayer) {

  goog.base(this, mapRenderer, imageLayer);

  /**
   * The last rendered image.
   * @private
   * @type {?ol.ImageBase}
   */
  this.image_ = null;

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.hitCanvasContext_ = null;

  /**
   * @private
   * @type {?goog.vec.Mat4.Number}
   */
  this.hitTransformationMatrix_ = null;

};
goog.inherits(ol.renderer.webgl.ImageLayer, ol.renderer.webgl.Layer);


/**
 * @param {ol.ImageBase} image Image.
 * @private
 * @return {WebGLTexture} Texture.
 */
ol.renderer.webgl.ImageLayer.prototype.createTexture_ = function(image) {

  // We meet the conditions to work with non-power of two textures.
  // http://www.khronos.org/webgl/wiki/WebGL_and_OpenGL_Differences#Non-Power_of_Two_Texture_Support
  // http://learningwebgl.com/blog/?p=2101

  var imageElement = image.getImage();
  var gl = this.mapRenderer.getGL();

  return ol.webgl.Context.createTexture(
      gl, imageElement, goog.webgl.CLAMP_TO_EDGE, goog.webgl.CLAMP_TO_EDGE);
};


/**
 * @inheritDoc
 */
ol.renderer.webgl.ImageLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) {
  var layer = this.getLayer();
  var source = layer.getSource();
  var resolution = frameState.viewState.resolution;
  var rotation = frameState.viewState.rotation;
  var skippedFeatureUids = frameState.skippedFeatureUids;
  return source.forEachFeatureAtCoordinate(
      coordinate, resolution, rotation, skippedFeatureUids,

      /**
       * @param {ol.Feature|ol.render.Feature} feature Feature.
       * @return {?} Callback result.
       */
      function(feature) {
        return callback.call(thisArg, feature, layer);
      });
};


/**
 * @inheritDoc
 */
ol.renderer.webgl.ImageLayer.prototype.prepareFrame = function(frameState, layerState, context) {

  var gl = this.mapRenderer.getGL();

  var pixelRatio = frameState.pixelRatio;
  var viewState = frameState.viewState;
  var viewCenter = viewState.center;
  var viewResolution = viewState.resolution;
  var viewRotation = viewState.rotation;

  var image = this.image_;
  var texture = this.texture;
  var imageLayer = this.getLayer();
  goog.asserts.assertInstanceof(imageLayer, ol.layer.Image,
      'layer is an instance of ol.layer.Image');
  var imageSource = imageLayer.getSource();

  var hints = frameState.viewHints;

  var renderedExtent = frameState.extent;
  if (layerState.extent !== undefined) {
    renderedExtent = ol.extent.getIntersection(
        renderedExtent, layerState.extent);
  }
  if (!hints[ol.ViewHint.ANIMATING] && !hints[ol.ViewHint.INTERACTING] &&
      !ol.extent.isEmpty(renderedExtent)) {
    var projection = viewState.projection;
    if (!ol.ENABLE_RASTER_REPROJECTION) {
      var sourceProjection = imageSource.getProjection();
      if (sourceProjection) {
        goog.asserts.assert(ol.proj.equivalent(projection, sourceProjection),
            'projection and sourceProjection are equivalent');
        projection = sourceProjection;
      }
    }
    var image_ = imageSource.getImage(renderedExtent, viewResolution,
        pixelRatio, projection);
    if (image_) {
      var loaded = this.loadImage(image_);
      if (loaded) {
        image = image_;
        texture = this.createTexture_(image_);
        if (this.texture) {
          frameState.postRenderFunctions.push(
              goog.partial(
                  /**
                   * @param {WebGLRenderingContext} gl GL.
                   * @param {WebGLTexture} texture Texture.
                   */
                  function(gl, texture) {
                    if (!gl.isContextLost()) {
                      gl.deleteTexture(texture);
                    }
                  }, gl, this.texture));
        }
      }
    }
  }

  if (image) {
    goog.asserts.assert(texture, 'texture is truthy');

    var canvas = this.mapRenderer.getContext().getCanvas();

    this.updateProjectionMatrix_(canvas.width, canvas.height,
        pixelRatio, viewCenter, viewResolution, viewRotation,
        image.getExtent());
    this.hitTransformationMatrix_ = null;

    // Translate and scale to flip the Y coord.
    var texCoordMatrix = this.texCoordMatrix;
    goog.vec.Mat4.makeIdentity(texCoordMatrix);
    goog.vec.Mat4.scale(texCoordMatrix, 1, -1, 1);
    goog.vec.Mat4.translate(texCoordMatrix, 0, -1, 0);

    this.image_ = image;
    this.texture = texture;

    this.updateAttributions(frameState.attributions, image.getAttributions());
    this.updateLogos(frameState, imageSource);
  }

  return true;
};


/**
 * @param {number} canvasWidth Canvas width.
 * @param {number} canvasHeight Canvas height.
 * @param {number} pixelRatio Pixel ratio.
 * @param {ol.Coordinate} viewCenter View center.
 * @param {number} viewResolution View resolution.
 * @param {number} viewRotation View rotation.
 * @param {ol.Extent} imageExtent Image extent.
 * @private
 */
ol.renderer.webgl.ImageLayer.prototype.updateProjectionMatrix_ = function(canvasWidth, canvasHeight, pixelRatio,
        viewCenter, viewResolution, viewRotation, imageExtent) {

  var canvasExtentWidth = canvasWidth * viewResolution;
  var canvasExtentHeight = canvasHeight * viewResolution;

  var projectionMatrix = this.projectionMatrix;
  goog.vec.Mat4.makeIdentity(projectionMatrix);
  goog.vec.Mat4.scale(projectionMatrix,
      pixelRatio * 2 / canvasExtentWidth,
      pixelRatio * 2 / canvasExtentHeight, 1);
  goog.vec.Mat4.rotateZ(projectionMatrix, -viewRotation);
  goog.vec.Mat4.translate(projectionMatrix,
      imageExtent[0] - viewCenter[0],
      imageExtent[1] - viewCenter[1],
      0);
  goog.vec.Mat4.scale(projectionMatrix,
      (imageExtent[2] - imageExtent[0]) / 2,
      (imageExtent[3] - imageExtent[1]) / 2,
      1);
  goog.vec.Mat4.translate(projectionMatrix, 1, 1, 0);

};


/**
 * @inheritDoc
 */
ol.renderer.webgl.ImageLayer.prototype.hasFeatureAtCoordinate = function(coordinate, frameState) {
  var hasFeature = this.forEachFeatureAtCoordinate(
      coordinate, frameState, goog.functions.TRUE, this);
  return hasFeature !== undefined;
};


/**
 * @inheritDoc
 */
ol.renderer.webgl.ImageLayer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) {
  if (!this.image_ || !this.image_.getImage()) {
    return undefined;
  }

  if (this.getLayer().getSource() instanceof ol.source.ImageVector) {
    // for ImageVector sources use the original hit-detection logic,
    // so that for example also transparent polygons are detected
    var coordinate = pixel.slice();
    ol.vec.Mat4.multVec2(
        frameState.pixelToCoordinateMatrix, coordinate, coordinate);
    var hasFeature = this.forEachFeatureAtCoordinate(
        coordinate, frameState, goog.functions.TRUE, this);

    if (hasFeature) {
      return callback.call(thisArg, this.getLayer());
    } else {
      return undefined;
    }
  } else {
    var imageSize =
        [this.image_.getImage().width, this.image_.getImage().height];

    if (!this.hitTransformationMatrix_) {
      this.hitTransformationMatrix_ = this.getHitTransformationMatrix_(
          frameState.size, imageSize);
    }

    var pixelOnFrameBuffer = [0, 0];
    ol.vec.Mat4.multVec2(
        this.hitTransformationMatrix_, pixel, pixelOnFrameBuffer);

    if (pixelOnFrameBuffer[0] < 0 || pixelOnFrameBuffer[0] > imageSize[0] ||
        pixelOnFrameBuffer[1] < 0 || pixelOnFrameBuffer[1] > imageSize[1]) {
      // outside the image, no need to check
      return undefined;
    }

    if (!this.hitCanvasContext_) {
      this.hitCanvasContext_ = ol.dom.createCanvasContext2D(1, 1);
    }

    this.hitCanvasContext_.clearRect(0, 0, 1, 1);
    this.hitCanvasContext_.drawImage(this.image_.getImage(),
        pixelOnFrameBuffer[0], pixelOnFrameBuffer[1], 1, 1, 0, 0, 1, 1);

    var imageData = this.hitCanvasContext_.getImageData(0, 0, 1, 1).data;
    if (imageData[3] > 0) {
      return callback.call(thisArg, this.getLayer());
    } else {
      return undefined;
    }
  }
};


/**
 * The transformation matrix to get the pixel on the image for a
 * pixel on the map.
 * @param {ol.Size} mapSize The map size.
 * @param {ol.Size} imageSize The image size.
 * @return {goog.vec.Mat4.Number} The transformation matrix.
 * @private
 */
ol.renderer.webgl.ImageLayer.prototype.getHitTransformationMatrix_ = function(mapSize, imageSize) {
  // the first matrix takes a map pixel, flips the y-axis and scales to
  // a range between -1 ... 1
  var mapCoordMatrix = goog.vec.Mat4.createNumber();
  goog.vec.Mat4.makeIdentity(mapCoordMatrix);
  goog.vec.Mat4.translate(mapCoordMatrix, -1, -1, 0);
  goog.vec.Mat4.scale(mapCoordMatrix, 2 / mapSize[0], 2 / mapSize[1], 1);
  goog.vec.Mat4.translate(mapCoordMatrix, 0, mapSize[1], 0);
  goog.vec.Mat4.scale(mapCoordMatrix, 1, -1, 1);

  // the second matrix is the inverse of the projection matrix used in the
  // shader for drawing
  var projectionMatrixInv = goog.vec.Mat4.createNumber();
  goog.vec.Mat4.invert(this.projectionMatrix, projectionMatrixInv);

  // the third matrix scales to the image dimensions and flips the y-axis again
  var imageCoordMatrix = goog.vec.Mat4.createNumber();
  goog.vec.Mat4.makeIdentity(imageCoordMatrix);
  goog.vec.Mat4.translate(imageCoordMatrix, 0, imageSize[1], 0);
  goog.vec.Mat4.scale(imageCoordMatrix, 1, -1, 1);
  goog.vec.Mat4.scale(imageCoordMatrix, imageSize[0] / 2, imageSize[1] / 2, 1);
  goog.vec.Mat4.translate(imageCoordMatrix, 1, 1, 0);

  var transformMatrix = goog.vec.Mat4.createNumber();
  goog.vec.Mat4.multMat(
      imageCoordMatrix, projectionMatrixInv, transformMatrix);
  goog.vec.Mat4.multMat(
      transformMatrix, mapCoordMatrix, transformMatrix);

  return transformMatrix;
};

// This file is automatically generated, do not edit
goog.provide('ol.renderer.webgl.tilelayer.shader');
goog.provide('ol.renderer.webgl.tilelayer.shader.Locations');
goog.provide('ol.renderer.webgl.tilelayer.shader.Fragment');
goog.provide('ol.renderer.webgl.tilelayer.shader.Vertex');

goog.require('ol.webgl.shader');


/**
 * @constructor
 * @extends {ol.webgl.shader.Fragment}
 * @struct
 */
ol.renderer.webgl.tilelayer.shader.Fragment = function() {
  goog.base(this, ol.renderer.webgl.tilelayer.shader.Fragment.SOURCE);
};
goog.inherits(ol.renderer.webgl.tilelayer.shader.Fragment, ol.webgl.shader.Fragment);
goog.addSingletonGetter(ol.renderer.webgl.tilelayer.shader.Fragment);


/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.tilelayer.shader.Fragment.DEBUG_SOURCE = 'precision mediump float;\nvarying vec2 v_texCoord;\n\n\nuniform sampler2D u_texture;\n\nvoid main(void) {\n  gl_FragColor = texture2D(u_texture, v_texCoord);\n}\n';


/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.tilelayer.shader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;uniform sampler2D e;void main(void){gl_FragColor=texture2D(e,a);}';


/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.tilelayer.shader.Fragment.SOURCE = goog.DEBUG ?
    ol.renderer.webgl.tilelayer.shader.Fragment.DEBUG_SOURCE :
    ol.renderer.webgl.tilelayer.shader.Fragment.OPTIMIZED_SOURCE;


/**
 * @constructor
 * @extends {ol.webgl.shader.Vertex}
 * @struct
 */
ol.renderer.webgl.tilelayer.shader.Vertex = function() {
  goog.base(this, ol.renderer.webgl.tilelayer.shader.Vertex.SOURCE);
};
goog.inherits(ol.renderer.webgl.tilelayer.shader.Vertex, ol.webgl.shader.Vertex);
goog.addSingletonGetter(ol.renderer.webgl.tilelayer.shader.Vertex);


/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.tilelayer.shader.Vertex.DEBUG_SOURCE = 'varying vec2 v_texCoord;\n\n\nattribute vec2 a_position;\nattribute vec2 a_texCoord;\nuniform vec4 u_tileOffset;\n\nvoid main(void) {\n  gl_Position = vec4(a_position * u_tileOffset.xy + u_tileOffset.zw, 0., 1.);\n  v_texCoord = a_texCoord;\n}\n\n\n';


/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.tilelayer.shader.Vertex.OPTIMIZED_SOURCE = 'varying vec2 a;attribute vec2 b;attribute vec2 c;uniform vec4 d;void main(void){gl_Position=vec4(b*d.xy+d.zw,0.,1.);a=c;}';


/**
 * @const
 * @type {string}
 */
ol.renderer.webgl.tilelayer.shader.Vertex.SOURCE = goog.DEBUG ?
    ol.renderer.webgl.tilelayer.shader.Vertex.DEBUG_SOURCE :
    ol.renderer.webgl.tilelayer.shader.Vertex.OPTIMIZED_SOURCE;


/**
 * @constructor
 * @param {WebGLRenderingContext} gl GL.
 * @param {WebGLProgram} program Program.
 * @struct
 */
ol.renderer.webgl.tilelayer.shader.Locations = function(gl, program) {

  /**
   * @type {WebGLUniformLocation}
   */
  this.u_texture = gl.getUniformLocation(
      program, goog.DEBUG ? 'u_texture' : 'e');

  /**
   * @type {WebGLUniformLocation}
   */
  this.u_tileOffset = gl.getUniformLocation(
      program, goog.DEBUG ? 'u_tileOffset' : 'd');

  /**
   * @type {number}
   */
  this.a_position = gl.getAttribLocation(
      program, goog.DEBUG ? 'a_position' : 'b');

  /**
   * @type {number}
   */
  this.a_texCoord = gl.getAttribLocation(
      program, goog.DEBUG ? 'a_texCoord' : 'c');
};

// FIXME large resolutions lead to too large framebuffers :-(
// FIXME animated shaders! check in redraw

goog.provide('ol.renderer.webgl.TileLayer');

goog.require('goog.asserts');
goog.require('goog.vec.Mat4');
goog.require('goog.vec.Vec4');
goog.require('goog.webgl');
goog.require('ol.TileRange');
goog.require('ol.TileState');
goog.require('ol.array');
goog.require('ol.extent');
goog.require('ol.layer.Tile');
goog.require('ol.math');
goog.require('ol.renderer.webgl.Layer');
goog.require('ol.renderer.webgl.tilelayer.shader.Fragment');
goog.require('ol.renderer.webgl.tilelayer.shader.Locations');
goog.require('ol.renderer.webgl.tilelayer.shader.Vertex');
goog.require('ol.size');
goog.require('ol.vec.Mat4');
goog.require('ol.webgl.Buffer');


/**
 * @constructor
 * @extends {ol.renderer.webgl.Layer}
 * @param {ol.renderer.webgl.Map} mapRenderer Map renderer.
 * @param {ol.layer.Tile} tileLayer Tile layer.
 */
ol.renderer.webgl.TileLayer = function(mapRenderer, tileLayer) {

  goog.base(this, mapRenderer, tileLayer);

  /**
   * @private
   * @type {ol.webgl.shader.Fragment}
   */
  this.fragmentShader_ =
      ol.renderer.webgl.tilelayer.shader.Fragment.getInstance();

  /**
   * @private
   * @type {ol.webgl.shader.Vertex}
   */
  this.vertexShader_ = ol.renderer.webgl.tilelayer.shader.Vertex.getInstance();

  /**
   * @private
   * @type {ol.renderer.webgl.tilelayer.shader.Locations}
   */
  this.locations_ = null;

  /**
   * @private
   * @type {ol.webgl.Buffer}
   */
  this.renderArrayBuffer_ = new ol.webgl.Buffer([
    0, 0, 0, 1,
    1, 0, 1, 1,
    0, 1, 0, 0,
    1, 1, 1, 0
  ]);

  /**
   * @private
   * @type {ol.TileRange}
   */
  this.renderedTileRange_ = null;

  /**
   * @private
   * @type {ol.Extent}
   */
  this.renderedFramebufferExtent_ = null;

  /**
   * @private
   * @type {number}
   */
  this.renderedRevision_ = -1;

  /**
   * @private
   * @type {ol.Size}
   */
  this.tmpSize_ = [0, 0];

};
goog.inherits(ol.renderer.webgl.TileLayer, ol.renderer.webgl.Layer);


/**
 * @inheritDoc
 */
ol.renderer.webgl.TileLayer.prototype.disposeInternal = function() {
  var context = this.mapRenderer.getContext();
  context.deleteBuffer(this.renderArrayBuffer_);
  goog.base(this, 'disposeInternal');
};


/**
 * Create a function that adds loaded tiles to the tile lookup.
 * @param {ol.source.Tile} source Tile source.
 * @param {ol.proj.Projection} projection Projection of the tiles.
 * @param {Object.<number, Object.<string, ol.Tile>>} tiles Lookup of loaded
 *     tiles by zoom level.
 * @return {function(number, ol.TileRange):boolean} A function that can be
 *     called with a zoom level and a tile range to add loaded tiles to the
 *     lookup.
 * @protected
 */
ol.renderer.webgl.TileLayer.prototype.createLoadedTileFinder = function(source, projection, tiles) {
  var mapRenderer = this.mapRenderer;

  return (
      /**
       * @param {number} zoom Zoom level.
       * @param {ol.TileRange} tileRange Tile range.
       * @return {boolean} The tile range is fully loaded.
       */
      function(zoom, tileRange) {
        function callback(tile) {
          var loaded = mapRenderer.isTileTextureLoaded(tile);
          if (loaded) {
            if (!tiles[zoom]) {
              tiles[zoom] = {};
            }
            tiles[zoom][tile.tileCoord.toString()] = tile;
          }
          return loaded;
        }
        return source.forEachLoadedTile(projection, zoom, tileRange, callback);
      });
};


/**
 * @inheritDoc
 */
ol.renderer.webgl.TileLayer.prototype.handleWebGLContextLost = function() {
  goog.base(this, 'handleWebGLContextLost');
  this.locations_ = null;
};


/**
 * @inheritDoc
 */
ol.renderer.webgl.TileLayer.prototype.prepareFrame = function(frameState, layerState, context) {

  var mapRenderer = this.mapRenderer;
  var gl = context.getGL();

  var viewState = frameState.viewState;
  var projection = viewState.projection;

  var tileLayer = this.getLayer();
  goog.asserts.assertInstanceof(tileLayer, ol.layer.Tile,
      'layer is an instance of ol.layer.Tile');
  var tileSource = tileLayer.getSource();
  var tileGrid = tileSource.getTileGridForProjection(projection);
  var z = tileGrid.getZForResolution(viewState.resolution);
  var tileResolution = tileGrid.getResolution(z);

  var tilePixelSize =
      tileSource.getTilePixelSize(z, frameState.pixelRatio, projection);
  var pixelRatio = tilePixelSize[0] /
      ol.size.toSize(tileGrid.getTileSize(z), this.tmpSize_)[0];
  var tilePixelResolution = tileResolution / pixelRatio;
  var tileGutter = tileSource.getGutter();

  var center = viewState.center;
  var extent;
  if (tileResolution == viewState.resolution) {
    center = this.snapCenterToPixel(center, tileResolution, frameState.size);
    extent = ol.extent.getForViewAndSize(
        center, tileResolution, viewState.rotation, frameState.size);
  } else {
    extent = frameState.extent;
  }
  var tileRange = tileGrid.getTileRangeForExtentAndResolution(
      extent, tileResolution);

  var framebufferExtent;
  if (this.renderedTileRange_ &&
      this.renderedTileRange_.equals(tileRange) &&
      this.renderedRevision_ == tileSource.getRevision()) {
    framebufferExtent = this.renderedFramebufferExtent_;
  } else {

    var tileRangeSize = tileRange.getSize();

    var maxDimension = Math.max(
        tileRangeSize[0] * tilePixelSize[0],
        tileRangeSize[1] * tilePixelSize[1]);
    var framebufferDimension = ol.math.roundUpToPowerOfTwo(maxDimension);
    var framebufferExtentDimension = tilePixelResolution * framebufferDimension;
    var origin = tileGrid.getOrigin(z);
    var minX = origin[0] +
        tileRange.minX * tilePixelSize[0] * tilePixelResolution;
    var minY = origin[1] +
        tileRange.minY * tilePixelSize[1] * tilePixelResolution;
    framebufferExtent = [
      minX, minY,
      minX + framebufferExtentDimension, minY + framebufferExtentDimension
    ];

    this.bindFramebuffer(frameState, framebufferDimension);
    gl.viewport(0, 0, framebufferDimension, framebufferDimension);

    gl.clearColor(0, 0, 0, 0);
    gl.clear(goog.webgl.COLOR_BUFFER_BIT);
    gl.disable(goog.webgl.BLEND);

    var program = context.getProgram(this.fragmentShader_, this.vertexShader_);
    context.useProgram(program);
    if (!this.locations_) {
      this.locations_ =
          new ol.renderer.webgl.tilelayer.shader.Locations(gl, program);
    }

    context.bindBuffer(goog.webgl.ARRAY_BUFFER, this.renderArrayBuffer_);
    gl.enableVertexAttribArray(this.locations_.a_position);
    gl.vertexAttribPointer(
        this.locations_.a_position, 2, goog.webgl.FLOAT, false, 16, 0);
    gl.enableVertexAttribArray(this.locations_.a_texCoord);
    gl.vertexAttribPointer(
        this.locations_.a_texCoord, 2, goog.webgl.FLOAT, false, 16, 8);
    gl.uniform1i(this.locations_.u_texture, 0);

    /**
     * @type {Object.<number, Object.<string, ol.Tile>>}
     */
    var tilesToDrawByZ = {};
    tilesToDrawByZ[z] = {};

    var findLoadedTiles = this.createLoadedTileFinder(
        tileSource, projection, tilesToDrawByZ);

    var useInterimTilesOnError = tileLayer.getUseInterimTilesOnError();
    var allTilesLoaded = true;
    var tmpExtent = ol.extent.createEmpty();
    var tmpTileRange = new ol.TileRange(0, 0, 0, 0);
    var childTileRange, drawable, fullyLoaded, tile, tileState;
    var x, y, tileExtent;
    for (x = tileRange.minX; x <= tileRange.maxX; ++x) {
      for (y = tileRange.minY; y <= tileRange.maxY; ++y) {

        tile = tileSource.getTile(z, x, y, pixelRatio, projection);
        if (layerState.extent !== undefined) {
          // ignore tiles outside layer extent
          tileExtent = tileGrid.getTileCoordExtent(tile.tileCoord, tmpExtent);
          if (!ol.extent.intersects(tileExtent, layerState.extent)) {
            continue;
          }
        }
        tileState = tile.getState();
        drawable = tileState == ol.TileState.LOADED ||
            tileState == ol.TileState.EMPTY ||
            tileState == ol.TileState.ERROR && !useInterimTilesOnError;
        if (!drawable && tile.interimTile) {
          tile = tile.interimTile;
        }
        goog.asserts.assert(tile);
        tileState = tile.getState();
        if (tileState == ol.TileState.LOADED) {
          if (mapRenderer.isTileTextureLoaded(tile)) {
            tilesToDrawByZ[z][tile.tileCoord.toString()] = tile;
            continue;
          }
        } else if (tileState == ol.TileState.EMPTY ||
                   (tileState == ol.TileState.ERROR &&
                    !useInterimTilesOnError)) {
          continue;
        }

        allTilesLoaded = false;
        fullyLoaded = tileGrid.forEachTileCoordParentTileRange(
            tile.tileCoord, findLoadedTiles, null, tmpTileRange, tmpExtent);
        if (!fullyLoaded) {
          childTileRange = tileGrid.getTileCoordChildTileRange(
              tile.tileCoord, tmpTileRange, tmpExtent);
          if (childTileRange) {
            findLoadedTiles(z + 1, childTileRange);
          }
        }

      }

    }

    /** @type {Array.<number>} */
    var zs = Object.keys(tilesToDrawByZ).map(Number);
    zs.sort(ol.array.numberSafeCompareFunction);
    var u_tileOffset = goog.vec.Vec4.createFloat32();
    var i, ii, sx, sy, tileKey, tilesToDraw, tx, ty;
    for (i = 0, ii = zs.length; i < ii; ++i) {
      tilesToDraw = tilesToDrawByZ[zs[i]];
      for (tileKey in tilesToDraw) {
        tile = tilesToDraw[tileKey];
        tileExtent = tileGrid.getTileCoordExtent(tile.tileCoord, tmpExtent);
        sx = 2 * (tileExtent[2] - tileExtent[0]) /
            framebufferExtentDimension;
        sy = 2 * (tileExtent[3] - tileExtent[1]) /
            framebufferExtentDimension;
        tx = 2 * (tileExtent[0] - framebufferExtent[0]) /
            framebufferExtentDimension - 1;
        ty = 2 * (tileExtent[1] - framebufferExtent[1]) /
            framebufferExtentDimension - 1;
        goog.vec.Vec4.setFromValues(u_tileOffset, sx, sy, tx, ty);
        gl.uniform4fv(this.locations_.u_tileOffset, u_tileOffset);
        mapRenderer.bindTileTexture(tile, tilePixelSize,
            tileGutter * pixelRatio, goog.webgl.LINEAR, goog.webgl.LINEAR);
        gl.drawArrays(goog.webgl.TRIANGLE_STRIP, 0, 4);
      }
    }

    if (allTilesLoaded) {
      this.renderedTileRange_ = tileRange;
      this.renderedFramebufferExtent_ = framebufferExtent;
      this.renderedRevision_ = tileSource.getRevision();
    } else {
      this.renderedTileRange_ = null;
      this.renderedFramebufferExtent_ = null;
      this.renderedRevision_ = -1;
      frameState.animate = true;
    }

  }

  this.updateUsedTiles(frameState.usedTiles, tileSource, z, tileRange);
  var tileTextureQueue = mapRenderer.getTileTextureQueue();
  this.manageTilePyramid(
      frameState, tileSource, tileGrid, pixelRatio, projection, extent, z,
      tileLayer.getPreload(),
      /**
       * @param {ol.Tile} tile Tile.
       */
      function(tile) {
        if (tile.getState() == ol.TileState.LOADED &&
            !mapRenderer.isTileTextureLoaded(tile) &&
            !tileTextureQueue.isKeyQueued(tile.getKey())) {
          tileTextureQueue.enqueue([
            tile,
            tileGrid.getTileCoordCenter(tile.tileCoord),
            tileGrid.getResolution(tile.tileCoord[0]),
            tilePixelSize, tileGutter * pixelRatio
          ]);
        }
      }, this);
  this.scheduleExpireCache(frameState, tileSource);
  this.updateLogos(frameState, tileSource);

  var texCoordMatrix = this.texCoordMatrix;
  goog.vec.Mat4.makeIdentity(texCoordMatrix);
  goog.vec.Mat4.translate(texCoordMatrix,
      (center[0] - framebufferExtent[0]) /
          (framebufferExtent[2] - framebufferExtent[0]),
      (center[1] - framebufferExtent[1]) /
          (framebufferExtent[3] - framebufferExtent[1]),
      0);
  if (viewState.rotation !== 0) {
    goog.vec.Mat4.rotateZ(texCoordMatrix, viewState.rotation);
  }
  goog.vec.Mat4.scale(texCoordMatrix,
      frameState.size[0] * viewState.resolution /
          (framebufferExtent[2] - framebufferExtent[0]),
      frameState.size[1] * viewState.resolution /
          (framebufferExtent[3] - framebufferExtent[1]),
      1);
  goog.vec.Mat4.translate(texCoordMatrix,
      -0.5,
      -0.5,
      0);

  return true;
};


/**
 * @inheritDoc
 */
ol.renderer.webgl.TileLayer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) {
  if (!this.framebuffer) {
    return undefined;
  }

  var pixelOnMapScaled = [
    pixel[0] / frameState.size[0],
    (frameState.size[1] - pixel[1]) / frameState.size[1]];

  var pixelOnFrameBufferScaled = [0, 0];
  ol.vec.Mat4.multVec2(
      this.texCoordMatrix, pixelOnMapScaled, pixelOnFrameBufferScaled);
  var pixelOnFrameBuffer = [
    pixelOnFrameBufferScaled[0] * this.framebufferDimension,
    pixelOnFrameBufferScaled[1] * this.framebufferDimension];

  var gl = this.mapRenderer.getContext().getGL();
  gl.bindFramebuffer(gl.FRAMEBUFFER, this.framebuffer);
  var imageData = new Uint8Array(4);
  gl.readPixels(pixelOnFrameBuffer[0], pixelOnFrameBuffer[1], 1, 1,
      gl.RGBA, gl.UNSIGNED_BYTE, imageData);

  if (imageData[3] > 0) {
    return callback.call(thisArg, this.getLayer());
  } else {
    return undefined;
  }
};

goog.provide('ol.renderer.webgl.VectorLayer');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('ol.ViewHint');
goog.require('ol.extent');
goog.require('ol.layer.Vector');
goog.require('ol.render.webgl.ReplayGroup');
goog.require('ol.renderer.vector');
goog.require('ol.renderer.webgl.Layer');
goog.require('ol.vec.Mat4');


/**
 * @constructor
 * @extends {ol.renderer.webgl.Layer}
 * @param {ol.renderer.webgl.Map} mapRenderer Map renderer.
 * @param {ol.layer.Vector} vectorLayer Vector layer.
 */
ol.renderer.webgl.VectorLayer = function(mapRenderer, vectorLayer) {

  goog.base(this, mapRenderer, vectorLayer);

  /**
   * @private
   * @type {boolean}
   */
  this.dirty_ = false;

  /**
   * @private
   * @type {number}
   */
  this.renderedRevision_ = -1;

  /**
   * @private
   * @type {number}
   */
  this.renderedResolution_ = NaN;

  /**
   * @private
   * @type {ol.Extent}
   */
  this.renderedExtent_ = ol.extent.createEmpty();

  /**
   * @private
   * @type {function(ol.Feature, ol.Feature): number|null}
   */
  this.renderedRenderOrder_ = null;

  /**
   * @private
   * @type {ol.render.webgl.ReplayGroup}
   */
  this.replayGroup_ = null;

  /**
   * The last layer state.
   * @private
   * @type {?ol.layer.LayerState}
   */
  this.layerState_ = null;

};
goog.inherits(ol.renderer.webgl.VectorLayer, ol.renderer.webgl.Layer);


/**
 * @inheritDoc
 */
ol.renderer.webgl.VectorLayer.prototype.composeFrame = function(frameState, layerState, context) {
  this.layerState_ = layerState;
  var viewState = frameState.viewState;
  var replayGroup = this.replayGroup_;
  if (replayGroup && !replayGroup.isEmpty()) {
    replayGroup.replay(context,
        viewState.center, viewState.resolution, viewState.rotation,
        frameState.size, frameState.pixelRatio, layerState.opacity,
        layerState.managed ? frameState.skippedFeatureUids : {});
  }

};


/**
 * @inheritDoc
 */
ol.renderer.webgl.VectorLayer.prototype.disposeInternal = function() {
  var replayGroup = this.replayGroup_;
  if (replayGroup) {
    var context = this.mapRenderer.getContext();
    replayGroup.getDeleteResourcesFunction(context)();
    this.replayGroup_ = null;
  }
  goog.base(this, 'disposeInternal');
};


/**
 * @inheritDoc
 */
ol.renderer.webgl.VectorLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg) {
  if (!this.replayGroup_ || !this.layerState_) {
    return undefined;
  } else {
    var context = this.mapRenderer.getContext();
    var viewState = frameState.viewState;
    var layer = this.getLayer();
    var layerState = this.layerState_;
    /** @type {Object.<string, boolean>} */
    var features = {};
    return this.replayGroup_.forEachFeatureAtCoordinate(coordinate,
        context, viewState.center, viewState.resolution, viewState.rotation,
        frameState.size, frameState.pixelRatio, layerState.opacity,
        layerState.managed ? frameState.skippedFeatureUids : {},
        /**
         * @param {ol.Feature} feature Feature.
         * @return {?} Callback result.
         */
        function(feature) {
          goog.asserts.assert(feature !== undefined, 'received a feature');
          var key = goog.getUid(feature).toString();
          if (!(key in features)) {
            features[key] = true;
            return callback.call(thisArg, feature, layer);
          }
        });
  }
};


/**
 * @inheritDoc
 */
ol.renderer.webgl.VectorLayer.prototype.hasFeatureAtCoordinate = function(coordinate, frameState) {
  if (!this.replayGroup_ || !this.layerState_) {
    return false;
  } else {
    var context = this.mapRenderer.getContext();
    var viewState = frameState.viewState;
    var layerState = this.layerState_;
    return this.replayGroup_.hasFeatureAtCoordinate(coordinate,
        context, viewState.center, viewState.resolution, viewState.rotation,
        frameState.size, frameState.pixelRatio, layerState.opacity,
        frameState.skippedFeatureUids);
  }
};


/**
 * @inheritDoc
 */
ol.renderer.webgl.VectorLayer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) {
  var coordinate = pixel.slice();
  ol.vec.Mat4.multVec2(
      frameState.pixelToCoordinateMatrix, coordinate, coordinate);
  var hasFeature = this.hasFeatureAtCoordinate(coordinate, frameState);

  if (hasFeature) {
    return callback.call(thisArg, this.getLayer());
  } else {
    return undefined;
  }
};


/**
 * Handle changes in image style state.
 * @param {goog.events.Event} event Image style change event.
 * @private
 */
ol.renderer.webgl.VectorLayer.prototype.handleStyleImageChange_ = function(event) {
  this.renderIfReadyAndVisible();
};


/**
 * @inheritDoc
 */
ol.renderer.webgl.VectorLayer.prototype.prepareFrame = function(frameState, layerState, context) {

  var vectorLayer = /** @type {ol.layer.Vector} */ (this.getLayer());
  goog.asserts.assertInstanceof(vectorLayer, ol.layer.Vector,
      'layer is an instance of ol.layer.Vector');
  var vectorSource = vectorLayer.getSource();

  this.updateAttributions(
      frameState.attributions, vectorSource.getAttributions());
  this.updateLogos(frameState, vectorSource);

  var animating = frameState.viewHints[ol.ViewHint.ANIMATING];
  var interacting = frameState.viewHints[ol.ViewHint.INTERACTING];
  var updateWhileAnimating = vectorLayer.getUpdateWhileAnimating();
  var updateWhileInteracting = vectorLayer.getUpdateWhileInteracting();

  if (!this.dirty_ && (!updateWhileAnimating && animating) ||
      (!updateWhileInteracting && interacting)) {
    return true;
  }

  var frameStateExtent = frameState.extent;
  var viewState = frameState.viewState;
  var projection = viewState.projection;
  var resolution = viewState.resolution;
  var pixelRatio = frameState.pixelRatio;
  var vectorLayerRevision = vectorLayer.getRevision();
  var vectorLayerRenderBuffer = vectorLayer.getRenderBuffer();
  var vectorLayerRenderOrder = vectorLayer.getRenderOrder();

  if (vectorLayerRenderOrder === undefined) {
    vectorLayerRenderOrder = ol.renderer.vector.defaultOrder;
  }

  var extent = ol.extent.buffer(frameStateExtent,
      vectorLayerRenderBuffer * resolution);

  if (!this.dirty_ &&
      this.renderedResolution_ == resolution &&
      this.renderedRevision_ == vectorLayerRevision &&
      this.renderedRenderOrder_ == vectorLayerRenderOrder &&
      ol.extent.containsExtent(this.renderedExtent_, extent)) {
    return true;
  }

  if (this.replayGroup_) {
    frameState.postRenderFunctions.push(
        this.replayGroup_.getDeleteResourcesFunction(context));
  }

  this.dirty_ = false;

  var replayGroup = new ol.render.webgl.ReplayGroup(
      ol.renderer.vector.getTolerance(resolution, pixelRatio),
      extent, vectorLayer.getRenderBuffer());
  vectorSource.loadFeatures(extent, resolution, projection);
  /**
   * @param {ol.Feature} feature Feature.
   * @this {ol.renderer.webgl.VectorLayer}
   */
  var renderFeature = function(feature) {
    var styles;
    var styleFunction = feature.getStyleFunction();
    if (styleFunction) {
      styles = styleFunction.call(feature, resolution);
    } else {
      styleFunction = vectorLayer.getStyleFunction();
      if (styleFunction) {
        styles = styleFunction(feature, resolution);
      }
    }
    if (styles) {
      var dirty = this.renderFeature(
          feature, resolution, pixelRatio, styles, replayGroup);
      this.dirty_ = this.dirty_ || dirty;
    }
  };
  if (vectorLayerRenderOrder) {
    /** @type {Array.<ol.Feature>} */
    var features = [];
    vectorSource.forEachFeatureInExtent(extent,
        /**
         * @param {ol.Feature} feature Feature.
         */
        function(feature) {
          features.push(feature);
        }, this);
    features.sort(vectorLayerRenderOrder);
    features.forEach(renderFeature, this);
  } else {
    vectorSource.forEachFeatureInExtent(extent, renderFeature, this);
  }
  replayGroup.finish(context);

  this.renderedResolution_ = resolution;
  this.renderedRevision_ = vectorLayerRevision;
  this.renderedRenderOrder_ = vectorLayerRenderOrder;
  this.renderedExtent_ = extent;
  this.replayGroup_ = replayGroup;

  return true;
};


/**
 * @param {ol.Feature} feature Feature.
 * @param {number} resolution Resolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {(ol.style.Style|Array.<ol.style.Style>)} styles The style or array of
 *     styles.
 * @param {ol.render.webgl.ReplayGroup} replayGroup Replay group.
 * @return {boolean} `true` if an image is loading.
 */
ol.renderer.webgl.VectorLayer.prototype.renderFeature = function(feature, resolution, pixelRatio, styles, replayGroup) {
  if (!styles) {
    return false;
  }
  var loading = false;
  if (goog.isArray(styles)) {
    for (var i = 0, ii = styles.length; i < ii; ++i) {
      loading = ol.renderer.vector.renderFeature(
          replayGroup, feature, styles[i],
          ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio),
          this.handleStyleImageChange_, this) || loading;
    }
  } else {
    loading = ol.renderer.vector.renderFeature(
        replayGroup, feature, styles,
        ol.renderer.vector.getSquaredTolerance(resolution, pixelRatio),
        this.handleStyleImageChange_, this) || loading;
  }
  return loading;
};

// FIXME check against gl.getParameter(webgl.MAX_TEXTURE_SIZE)

goog.provide('ol.renderer.webgl.Map');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.log');
goog.require('goog.log.Logger');
goog.require('goog.object');
goog.require('goog.style');
goog.require('goog.webgl');
goog.require('ol');
goog.require('ol.RendererType');
goog.require('ol.css');
goog.require('ol.dom');
goog.require('ol.layer.Image');
goog.require('ol.layer.Layer');
goog.require('ol.layer.Tile');
goog.require('ol.layer.Vector');
goog.require('ol.render.Event');
goog.require('ol.render.EventType');
goog.require('ol.render.webgl.Immediate');
goog.require('ol.renderer.Map');
goog.require('ol.renderer.webgl.ImageLayer');
goog.require('ol.renderer.webgl.Layer');
goog.require('ol.renderer.webgl.TileLayer');
goog.require('ol.renderer.webgl.VectorLayer');
goog.require('ol.source.State');
goog.require('ol.structs.LRUCache');
goog.require('ol.structs.PriorityQueue');
goog.require('ol.webgl');
goog.require('ol.webgl.Context');
goog.require('ol.webgl.WebGLContextEventType');


/**
 * @typedef {{magFilter: number, minFilter: number, texture: WebGLTexture}}
 */
ol.renderer.webgl.TextureCacheEntry;


/**
 * @constructor
 * @extends {ol.renderer.Map}
 * @param {Element} container Container.
 * @param {ol.Map} map Map.
 */
ol.renderer.webgl.Map = function(container, map) {

  goog.base(this, container, map);

  /**
   * @private
   * @type {HTMLCanvasElement}
   */
  this.canvas_ = /** @type {HTMLCanvasElement} */
      (document.createElement('CANVAS'));
  this.canvas_.style.width = '100%';
  this.canvas_.style.height = '100%';
  this.canvas_.className = ol.css.CLASS_UNSELECTABLE;
  goog.dom.insertChildAt(container, this.canvas_, 0);

  /**
   * @private
   * @type {number}
   */
  this.clipTileCanvasWidth_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.clipTileCanvasHeight_ = 0;

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.clipTileContext_ = ol.dom.createCanvasContext2D();

  /**
   * @private
   * @type {boolean}
   */
  this.renderedVisible_ = true;

  /**
   * @private
   * @type {WebGLRenderingContext}
   */
  this.gl_ = ol.webgl.getContext(this.canvas_, {
    antialias: true,
    depth: false,
    failIfMajorPerformanceCaveat: true,
    preserveDrawingBuffer: false,
    stencil: true
  });
  goog.asserts.assert(this.gl_, 'got a WebGLRenderingContext');

  /**
   * @private
   * @type {ol.webgl.Context}
   */
  this.context_ = new ol.webgl.Context(this.canvas_, this.gl_);

  goog.events.listen(this.canvas_, ol.webgl.WebGLContextEventType.LOST,
      this.handleWebGLContextLost, false, this);
  goog.events.listen(this.canvas_, ol.webgl.WebGLContextEventType.RESTORED,
      this.handleWebGLContextRestored, false, this);

  /**
   * @private
   * @type {ol.structs.LRUCache.<ol.renderer.webgl.TextureCacheEntry|null>}
   */
  this.textureCache_ = new ol.structs.LRUCache();

  /**
   * @private
   * @type {ol.Coordinate}
   */
  this.focus_ = null;

  /**
   * @private
   * @type {ol.structs.PriorityQueue.<Array>}
   */
  this.tileTextureQueue_ = new ol.structs.PriorityQueue(
      /**
       * @param {Array.<*>} element Element.
       * @return {number} Priority.
       * @this {ol.renderer.webgl.Map}
       */
      (function(element) {
        var tileCenter = /** @type {ol.Coordinate} */ (element[1]);
        var tileResolution = /** @type {number} */ (element[2]);
        var deltaX = tileCenter[0] - this.focus_[0];
        var deltaY = tileCenter[1] - this.focus_[1];
        return 65536 * Math.log(tileResolution) +
            Math.sqrt(deltaX * deltaX + deltaY * deltaY) / tileResolution;
      }).bind(this),
      /**
       * @param {Array.<*>} element Element.
       * @return {string} Key.
       */
      function(element) {
        return /** @type {ol.Tile} */ (element[0]).getKey();
      });


 /**
  * @param {ol.Map} map Map.
  * @param {?olx.FrameState} frameState Frame state.
  * @return {boolean} false.
  * @this {ol.renderer.webgl.Map}
  */
  this.loadNextTileTexture_ =
      function(map, frameState) {
        if (!this.tileTextureQueue_.isEmpty()) {
          this.tileTextureQueue_.reprioritize();
          var element = this.tileTextureQueue_.dequeue();
          var tile = /** @type {ol.Tile} */ (element[0]);
          var tileSize = /** @type {ol.Size} */ (element[3]);
          var tileGutter = /** @type {number} */ (element[4]);
          this.bindTileTexture(
              tile, tileSize, tileGutter, goog.webgl.LINEAR, goog.webgl.LINEAR);
        }
        return false;
      }.bind(this);


  /**
   * @private
   * @type {number}
   */
  this.textureCacheFrameMarkerCount_ = 0;

  this.initializeGL_();

};
goog.inherits(ol.renderer.webgl.Map, ol.renderer.Map);


/**
 * @param {ol.Tile} tile Tile.
 * @param {ol.Size} tileSize Tile size.
 * @param {number} tileGutter Tile gutter.
 * @param {number} magFilter Mag filter.
 * @param {number} minFilter Min filter.
 */
ol.renderer.webgl.Map.prototype.bindTileTexture = function(tile, tileSize, tileGutter, magFilter, minFilter) {
  var gl = this.getGL();
  var tileKey = tile.getKey();
  if (this.textureCache_.containsKey(tileKey)) {
    var textureCacheEntry = this.textureCache_.get(tileKey);
    goog.asserts.assert(textureCacheEntry,
        'a texture cache entry exists for key %s', tileKey);
    gl.bindTexture(goog.webgl.TEXTURE_2D, textureCacheEntry.texture);
    if (textureCacheEntry.magFilter != magFilter) {
      gl.texParameteri(
          goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_MAG_FILTER, magFilter);
      textureCacheEntry.magFilter = magFilter;
    }
    if (textureCacheEntry.minFilter != minFilter) {
      gl.texParameteri(
          goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_MAG_FILTER, minFilter);
      textureCacheEntry.minFilter = minFilter;
    }
  } else {
    var texture = gl.createTexture();
    gl.bindTexture(goog.webgl.TEXTURE_2D, texture);
    if (tileGutter > 0) {
      var clipTileCanvas = this.clipTileContext_.canvas;
      var clipTileContext = this.clipTileContext_;
      if (this.clipTileCanvasWidth_ !== tileSize[0] ||
          this.clipTileCanvasHeight_ !== tileSize[1]) {
        clipTileCanvas.width = tileSize[0];
        clipTileCanvas.height = tileSize[1];
        this.clipTileCanvasWidth_ = tileSize[0];
        this.clipTileCanvasHeight_ = tileSize[1];
      } else {
        clipTileContext.clearRect(0, 0, tileSize[0], tileSize[1]);
      }
      clipTileContext.drawImage(tile.getImage(), tileGutter, tileGutter,
          tileSize[0], tileSize[1], 0, 0, tileSize[0], tileSize[1]);
      gl.texImage2D(goog.webgl.TEXTURE_2D, 0,
          goog.webgl.RGBA, goog.webgl.RGBA,
          goog.webgl.UNSIGNED_BYTE, clipTileCanvas);
    } else {
      gl.texImage2D(goog.webgl.TEXTURE_2D, 0,
          goog.webgl.RGBA, goog.webgl.RGBA,
          goog.webgl.UNSIGNED_BYTE, tile.getImage());
    }
    gl.texParameteri(
        goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_MAG_FILTER, magFilter);
    gl.texParameteri(
        goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_MIN_FILTER, minFilter);
    gl.texParameteri(goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_WRAP_S,
        goog.webgl.CLAMP_TO_EDGE);
    gl.texParameteri(goog.webgl.TEXTURE_2D, goog.webgl.TEXTURE_WRAP_T,
        goog.webgl.CLAMP_TO_EDGE);
    this.textureCache_.set(tileKey, {
      texture: texture,
      magFilter: magFilter,
      minFilter: minFilter
    });
  }
};


/**
 * @inheritDoc
 */
ol.renderer.webgl.Map.prototype.createLayerRenderer = function(layer) {
  if (ol.ENABLE_IMAGE && layer instanceof ol.layer.Image) {
    return new ol.renderer.webgl.ImageLayer(this, layer);
  } else if (ol.ENABLE_TILE && layer instanceof ol.layer.Tile) {
    return new ol.renderer.webgl.TileLayer(this, layer);
  } else if (ol.ENABLE_VECTOR && layer instanceof ol.layer.Vector) {
    return new ol.renderer.webgl.VectorLayer(this, layer);
  } else {
    goog.asserts.fail('unexpected layer configuration');
    return null;
  }
};


/**
 * @param {ol.render.EventType} type Event type.
 * @param {olx.FrameState} frameState Frame state.
 * @private
 */
ol.renderer.webgl.Map.prototype.dispatchComposeEvent_ = function(type, frameState) {
  var map = this.getMap();
  if (map.hasListener(type)) {
    var context = this.context_;

    var extent = frameState.extent;
    var size = frameState.size;
    var viewState = frameState.viewState;
    var pixelRatio = frameState.pixelRatio;

    var resolution = viewState.resolution;
    var center = viewState.center;
    var rotation = viewState.rotation;

    var vectorContext = new ol.render.webgl.Immediate(context,
        center, resolution, rotation, size, extent, pixelRatio);
    var composeEvent = new ol.render.Event(type, map, vectorContext,
        frameState, null, context);
    map.dispatchEvent(composeEvent);

    vectorContext.flush();
  }
};


/**
 * @inheritDoc
 */
ol.renderer.webgl.Map.prototype.disposeInternal = function() {
  var gl = this.getGL();
  if (!gl.isContextLost()) {
    this.textureCache_.forEach(
        /**
         * @param {?ol.renderer.webgl.TextureCacheEntry} textureCacheEntry
         *     Texture cache entry.
         */
        function(textureCacheEntry) {
          if (textureCacheEntry) {
            gl.deleteTexture(textureCacheEntry.texture);
          }
        });
  }
  goog.dispose(this.context_);
  goog.base(this, 'disposeInternal');
};


/**
 * @param {ol.Map} map Map.
 * @param {olx.FrameState} frameState Frame state.
 * @private
 */
ol.renderer.webgl.Map.prototype.expireCache_ = function(map, frameState) {
  var gl = this.getGL();
  var textureCacheEntry;
  while (this.textureCache_.getCount() - this.textureCacheFrameMarkerCount_ >
      ol.WEBGL_TEXTURE_CACHE_HIGH_WATER_MARK) {
    textureCacheEntry = this.textureCache_.peekLast();
    if (!textureCacheEntry) {
      if (+this.textureCache_.peekLastKey() == frameState.index) {
        break;
      } else {
        --this.textureCacheFrameMarkerCount_;
      }
    } else {
      gl.deleteTexture(textureCacheEntry.texture);
    }
    this.textureCache_.pop();
  }
};


/**
 * @return {ol.webgl.Context} The context.
 */
ol.renderer.webgl.Map.prototype.getContext = function() {
  return this.context_;
};


/**
 * @return {WebGLRenderingContext} GL.
 */
ol.renderer.webgl.Map.prototype.getGL = function() {
  return this.gl_;
};


/**
 * @return {ol.structs.PriorityQueue.<Array>} Tile texture queue.
 */
ol.renderer.webgl.Map.prototype.getTileTextureQueue = function() {
  return this.tileTextureQueue_;
};


/**
 * @inheritDoc
 */
ol.renderer.webgl.Map.prototype.getType = function() {
  return ol.RendererType.WEBGL;
};


/**
 * @param {goog.events.Event} event Event.
 * @protected
 */
ol.renderer.webgl.Map.prototype.handleWebGLContextLost = function(event) {
  event.preventDefault();
  this.textureCache_.clear();
  this.textureCacheFrameMarkerCount_ = 0;
  goog.object.forEach(this.getLayerRenderers(),
      /**
       * @param {ol.renderer.Layer} layerRenderer Layer renderer.
       * @param {string} key Key.
       * @param {Object.<string, ol.renderer.Layer>} object Object.
       */
      function(layerRenderer, key, object) {
        goog.asserts.assertInstanceof(layerRenderer, ol.renderer.webgl.Layer,
            'renderer is an instance of ol.renderer.webgl.Layer');
        layerRenderer.handleWebGLContextLost();
      });
};


/**
 * @protected
 */
ol.renderer.webgl.Map.prototype.handleWebGLContextRestored = function() {
  this.initializeGL_();
  this.getMap().render();
};


/**
 * @private
 */
ol.renderer.webgl.Map.prototype.initializeGL_ = function() {
  var gl = this.gl_;
  gl.activeTexture(goog.webgl.TEXTURE0);
  gl.blendFuncSeparate(
      goog.webgl.SRC_ALPHA, goog.webgl.ONE_MINUS_SRC_ALPHA,
      goog.webgl.ONE, goog.webgl.ONE_MINUS_SRC_ALPHA);
  gl.disable(goog.webgl.CULL_FACE);
  gl.disable(goog.webgl.DEPTH_TEST);
  gl.disable(goog.webgl.SCISSOR_TEST);
  gl.disable(goog.webgl.STENCIL_TEST);
};


/**
 * @param {ol.Tile} tile Tile.
 * @return {boolean} Is tile texture loaded.
 */
ol.renderer.webgl.Map.prototype.isTileTextureLoaded = function(tile) {
  return this.textureCache_.containsKey(tile.getKey());
};


/**
 * @private
 * @type {goog.log.Logger}
 */
ol.renderer.webgl.Map.prototype.logger_ =
    goog.log.getLogger('ol.renderer.webgl.Map');


/**
 * @inheritDoc
 */
ol.renderer.webgl.Map.prototype.renderFrame = function(frameState) {

  var context = this.getContext();
  var gl = this.getGL();

  if (gl.isContextLost()) {
    return false;
  }

  if (!frameState) {
    if (this.renderedVisible_) {
      goog.style.setElementShown(this.canvas_, false);
      this.renderedVisible_ = false;
    }
    return false;
  }

  this.focus_ = frameState.focus;

  this.textureCache_.set((-frameState.index).toString(), null);
  ++this.textureCacheFrameMarkerCount_;

  this.dispatchComposeEvent_(ol.render.EventType.PRECOMPOSE, frameState);

  /** @type {Array.<ol.layer.LayerState>} */
  var layerStatesToDraw = [];
  var layerStatesArray = frameState.layerStatesArray;
  goog.array.stableSort(layerStatesArray, ol.renderer.Map.sortByZIndex);

  var viewResolution = frameState.viewState.resolution;
  var i, ii, layerRenderer, layerState;
  for (i = 0, ii = layerStatesArray.length; i < ii; ++i) {
    layerState = layerStatesArray[i];
    if (ol.layer.Layer.visibleAtResolution(layerState, viewResolution) &&
        layerState.sourceState == ol.source.State.READY) {
      layerRenderer = this.getLayerRenderer(layerState.layer);
      goog.asserts.assertInstanceof(layerRenderer, ol.renderer.webgl.Layer,
          'renderer is an instance of ol.renderer.webgl.Layer');
      if (layerRenderer.prepareFrame(frameState, layerState, context)) {
        layerStatesToDraw.push(layerState);
      }
    }
  }

  var width = frameState.size[0] * frameState.pixelRatio;
  var height = frameState.size[1] * frameState.pixelRatio;
  if (this.canvas_.width != width || this.canvas_.height != height) {
    this.canvas_.width = width;
    this.canvas_.height = height;
  }

  gl.bindFramebuffer(goog.webgl.FRAMEBUFFER, null);

  gl.clearColor(0, 0, 0, 0);
  gl.clear(goog.webgl.COLOR_BUFFER_BIT);
  gl.enable(goog.webgl.BLEND);
  gl.viewport(0, 0, this.canvas_.width, this.canvas_.height);

  for (i = 0, ii = layerStatesToDraw.length; i < ii; ++i) {
    layerState = layerStatesToDraw[i];
    layerRenderer = this.getLayerRenderer(layerState.layer);
    goog.asserts.assertInstanceof(layerRenderer, ol.renderer.webgl.Layer,
        'renderer is an instance of ol.renderer.webgl.Layer');
    layerRenderer.composeFrame(frameState, layerState, context);
  }

  if (!this.renderedVisible_) {
    goog.style.setElementShown(this.canvas_, true);
    this.renderedVisible_ = true;
  }

  this.calculateMatrices2D(frameState);

  if (this.textureCache_.getCount() - this.textureCacheFrameMarkerCount_ >
      ol.WEBGL_TEXTURE_CACHE_HIGH_WATER_MARK) {
    frameState.postRenderFunctions.push(this.expireCache_.bind(this));
  }

  if (!this.tileTextureQueue_.isEmpty()) {
    frameState.postRenderFunctions.push(this.loadNextTileTexture_);
    frameState.animate = true;
  }

  this.dispatchComposeEvent_(ol.render.EventType.POSTCOMPOSE, frameState);

  this.scheduleRemoveUnusedLayerRenderers(frameState);
  this.scheduleExpireIconCache(frameState);

};


/**
 * @inheritDoc
 */
ol.renderer.webgl.Map.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, callback, thisArg,
        layerFilter, thisArg2) {
  var result;

  if (this.getGL().isContextLost()) {
    return false;
  }

  var viewState = frameState.viewState;

  var layerStates = frameState.layerStatesArray;
  var numLayers = layerStates.length;
  var i;
  for (i = numLayers - 1; i >= 0; --i) {
    var layerState = layerStates[i];
    var layer = layerState.layer;
    if (ol.layer.Layer.visibleAtResolution(layerState, viewState.resolution) &&
        layerFilter.call(thisArg2, layer)) {
      var layerRenderer = this.getLayerRenderer(layer);
      result = layerRenderer.forEachFeatureAtCoordinate(
          coordinate, frameState, callback, thisArg);
      if (result) {
        return result;
      }
    }
  }
  return undefined;
};


/**
 * @inheritDoc
 */
ol.renderer.webgl.Map.prototype.hasFeatureAtCoordinate = function(coordinate, frameState, layerFilter, thisArg) {
  var hasFeature = false;

  if (this.getGL().isContextLost()) {
    return false;
  }

  var viewState = frameState.viewState;

  var layerStates = frameState.layerStatesArray;
  var numLayers = layerStates.length;
  var i;
  for (i = numLayers - 1; i >= 0; --i) {
    var layerState = layerStates[i];
    var layer = layerState.layer;
    if (ol.layer.Layer.visibleAtResolution(layerState, viewState.resolution) &&
        layerFilter.call(thisArg, layer)) {
      var layerRenderer = this.getLayerRenderer(layer);
      hasFeature =
          layerRenderer.hasFeatureAtCoordinate(coordinate, frameState);
      if (hasFeature) {
        return true;
      }
    }
  }
  return hasFeature;
};


/**
 * @inheritDoc
 */
ol.renderer.webgl.Map.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg,
        layerFilter, thisArg2) {
  if (this.getGL().isContextLost()) {
    return false;
  }

  var viewState = frameState.viewState;
  var result;

  var layerStates = frameState.layerStatesArray;
  var numLayers = layerStates.length;
  var i;
  for (i = numLayers - 1; i >= 0; --i) {
    var layerState = layerStates[i];
    var layer = layerState.layer;
    if (ol.layer.Layer.visibleAtResolution(layerState, viewState.resolution) &&
        layerFilter.call(thisArg, layer)) {
      var layerRenderer = this.getLayerRenderer(layer);
      result = layerRenderer.forEachLayerAtPixel(
          pixel, frameState, callback, thisArg);
      if (result) {
        return result;
      }
    }
  }
  return undefined;
};

// FIXME recheck layer/map projection compatibility when projection changes
// FIXME layer renderers should skip when they can't reproject
// FIXME add tilt and height?

goog.provide('ol.Map');
goog.provide('ol.MapProperty');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.async.AnimationDelay');
goog.require('goog.async.nextTick');
goog.require('goog.debug.Console');
goog.require('goog.dom');
goog.require('goog.dom.ViewportSizeMonitor');
goog.require('goog.dom.classlist');
goog.require('goog.events');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.Event');
goog.require('goog.events.EventType');
goog.require('goog.events.KeyHandler');
goog.require('goog.events.KeyHandler.EventType');
goog.require('goog.events.MouseWheelHandler');
goog.require('goog.events.MouseWheelHandler.EventType');
goog.require('goog.functions');
goog.require('goog.log');
goog.require('goog.log.Level');
goog.require('goog.object');
goog.require('goog.style');
goog.require('goog.vec.Mat4');
goog.require('ol.Collection');
goog.require('ol.CollectionEventType');
goog.require('ol.MapBrowserEvent');
goog.require('ol.MapBrowserEvent.EventType');
goog.require('ol.MapBrowserEventHandler');
goog.require('ol.MapEvent');
goog.require('ol.MapEventType');
goog.require('ol.Object');
goog.require('ol.ObjectEvent');
goog.require('ol.ObjectEventType');
goog.require('ol.Pixel');
goog.require('ol.PostRenderFunction');
goog.require('ol.PreRenderFunction');
goog.require('ol.RendererType');
goog.require('ol.Size');
goog.require('ol.TileQueue');
goog.require('ol.View');
goog.require('ol.ViewHint');
goog.require('ol.control');
goog.require('ol.extent');
goog.require('ol.has');
goog.require('ol.interaction');
goog.require('ol.layer.Base');
goog.require('ol.layer.Group');
goog.require('ol.proj');
goog.require('ol.proj.common');
goog.require('ol.renderer.Map');
goog.require('ol.renderer.canvas.Map');
goog.require('ol.renderer.dom.Map');
goog.require('ol.renderer.webgl.Map');
goog.require('ol.size');
goog.require('ol.structs.PriorityQueue');
goog.require('ol.vec.Mat4');


/**
 * @const
 * @type {string}
 */
ol.OL3_URL = 'http://openlayers.org/';


/**
 * @const
 * @type {string}
 */
ol.OL3_LOGO_URL = 'data:image/png;base64,' +
    'iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAA3NCSVQICAjb4U/gAAAACXBI' +
    'WXMAAAHGAAABxgEXwfpGAAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAA' +
    'AhNQTFRF////AP//AICAgP//AFVVQECA////K1VVSbbbYL/fJ05idsTYJFtbbcjbJllmZszW' +
    'WMTOIFhoHlNiZszTa9DdUcHNHlNlV8XRIVdiasrUHlZjIVZjaMnVH1RlIFRkH1RkH1ZlasvY' +
    'asvXVsPQH1VkacnVa8vWIVZjIFRjVMPQa8rXIVVkXsXRsNveIFVkIFZlIVVj3eDeh6GmbMvX' +
    'H1ZkIFRka8rWbMvXIFVkIFVjIFVkbMvWH1VjbMvWIFVlbcvWIFVla8vVIFVkbMvWbMvVH1Vk' +
    'bMvWIFVlbcvWIFVkbcvVbMvWjNPbIFVkU8LPwMzNIFVkbczWIFVkbsvWbMvXIFVkRnB8bcvW' +
    '2+TkW8XRIFVkIlZlJVloJlpoKlxrLl9tMmJwOWd0Omh1RXF8TneCT3iDUHiDU8LPVMLPVcLP' +
    'VcPQVsPPVsPQV8PQWMTQWsTQW8TQXMXSXsXRX4SNX8bSYMfTYcfTYsfTY8jUZcfSZsnUaIqT' +
    'acrVasrVa8jTa8rWbI2VbMvWbcvWdJObdcvUdszUd8vVeJaee87Yfc3WgJyjhqGnitDYjaar' +
    'ldPZnrK2oNbborW5o9bbo9fbpLa6q9ndrL3ArtndscDDutzfu8fJwN7gwt7gxc/QyuHhy+Hi' +
    'zeHi0NfX0+Pj19zb1+Tj2uXk29/e3uLg3+Lh3+bl4uXj4ufl4+fl5Ofl5ufl5ujm5+jmySDn' +
    'BAAAAFp0Uk5TAAECAgMEBAYHCA0NDg4UGRogIiMmKSssLzU7PkJJT1JTVFliY2hrdHZ3foSF' +
    'hYeJjY2QkpugqbG1tre5w8zQ09XY3uXn6+zx8vT09vf4+Pj5+fr6/P39/f3+gz7SsAAAAVVJ' +
    'REFUOMtjYKA7EBDnwCPLrObS1BRiLoJLnte6CQy8FLHLCzs2QUG4FjZ5GbcmBDDjxJBXDWxC' +
    'Brb8aM4zbkIDzpLYnAcE9VXlJSWlZRU13koIeW57mGx5XjoMZEUqwxWYQaQbSzLSkYGfKFSe' +
    '0QMsX5WbjgY0YS4MBplemI4BdGBW+DQ11eZiymfqQuXZIjqwyadPNoSZ4L+0FVM6e+oGI6g8' +
    'a9iKNT3o8kVzNkzRg5lgl7p4wyRUL9Yt2jAxVh6mQCogae6GmflI8p0r13VFWTHBQ0rWPW7a' +
    'hgWVcPm+9cuLoyy4kCJDzCm6d8PSFoh0zvQNC5OjDJhQopPPJqph1doJBUD5tnkbZiUEqaCn' +
    'B3bTqLTFG1bPn71kw4b+GFdpLElKIzRxxgYgWNYc5SCENVHKeUaltHdXx0dZ8uBI1hJ2UUDg' +
    'q82CM2MwKeibqAvSO7MCABq0wXEPiqWEAAAAAElFTkSuQmCC';


/**
 * @type {Array.<ol.RendererType>}
 * @const
 */
ol.DEFAULT_RENDERER_TYPES = [
  ol.RendererType.CANVAS,
  ol.RendererType.WEBGL,
  ol.RendererType.DOM
];


/**
 * @enum {string}
 */
ol.MapProperty = {
  LAYERGROUP: 'layergroup',
  SIZE: 'size',
  TARGET: 'target',
  VIEW: 'view'
};


/**
 * @classdesc
 * The map is the core component of OpenLayers. For a map to render, a view,
 * one or more layers, and a target container are needed:
 *
 *     var map = new ol.Map({
 *       view: new ol.View({
 *         center: [0, 0],
 *         zoom: 1
 *       }),
 *       layers: [
 *         new ol.layer.Tile({
 *           source: new ol.source.MapQuest({layer: 'osm'})
 *         })
 *       ],
 *       target: 'map'
 *     });
 *
 * The above snippet creates a map using a {@link ol.layer.Tile} to display
 * {@link ol.source.MapQuest} OSM data and render it to a DOM element with the
 * id `map`.
 *
 * The constructor places a viewport container (with CSS class name
 * `ol-viewport`) in the target element (see `getViewport()`), and then two
 * further elements within the viewport: one with CSS class name
 * `ol-overlaycontainer-stopevent` for controls and some overlays, and one with
 * CSS class name `ol-overlaycontainer` for other overlays (see the `stopEvent`
 * option of {@link ol.Overlay} for the difference). The map itself is placed in
 * a further element within the viewport, either DOM or Canvas, depending on the
 * renderer.
 *
 * Layers are stored as a `ol.Collection` in layerGroups. A top-level group is
 * provided by the library. This is what is accessed by `getLayerGroup` and
 * `setLayerGroup`. Layers entered in the options are added to this group, and
 * `addLayer` and `removeLayer` change the layer collection in the group.
 * `getLayers` is a convenience function for `getLayerGroup().getLayers()`.
 * Note that `ol.layer.Group` is a subclass of `ol.layer.Base`, so layers
 * entered in the options or added with `addLayer` can be groups, which can
 * contain further groups, and so on.
 *
 * @constructor
 * @extends {ol.Object}
 * @param {olx.MapOptions} options Map options.
 * @fires ol.MapBrowserEvent
 * @fires ol.MapEvent
 * @fires ol.render.Event#postcompose
 * @fires ol.render.Event#precompose
 * @api stable
 */
ol.Map = function(options) {

  goog.base(this);

  var optionsInternal = ol.Map.createOptionsInternal(options);

  /**
   * @type {boolean}
   * @private
   */
  this.loadTilesWhileAnimating_ =
      options.loadTilesWhileAnimating !== undefined ?
          options.loadTilesWhileAnimating : false;

  /**
   * @type {boolean}
   * @private
   */
  this.loadTilesWhileInteracting_ =
      options.loadTilesWhileInteracting !== undefined ?
          options.loadTilesWhileInteracting : false;

  /**
   * @private
   * @type {number}
   */
  this.pixelRatio_ = options.pixelRatio !== undefined ?
      options.pixelRatio : ol.has.DEVICE_PIXEL_RATIO;

  /**
   * @private
   * @type {Object.<string, string>}
   */
  this.logos_ = optionsInternal.logos;

  /**
   * @private
   * @type {goog.async.AnimationDelay}
   */
  this.animationDelay_ =
      new goog.async.AnimationDelay(this.renderFrame_, undefined, this);
  this.registerDisposable(this.animationDelay_);

  /**
   * @private
   * @type {goog.vec.Mat4.Number}
   */
  this.coordinateToPixelMatrix_ = goog.vec.Mat4.createNumber();

  /**
   * @private
   * @type {goog.vec.Mat4.Number}
   */
  this.pixelToCoordinateMatrix_ = goog.vec.Mat4.createNumber();

  /**
   * @private
   * @type {number}
   */
  this.frameIndex_ = 0;

  /**
   * @private
   * @type {?olx.FrameState}
   */
  this.frameState_ = null;

  /**
   * The extent at the previous 'moveend' event.
   * @private
   * @type {ol.Extent}
   */
  this.previousExtent_ = ol.extent.createEmpty();

  /**
   * @private
   * @type {goog.events.Key}
   */
  this.viewPropertyListenerKey_ = null;

  /**
   * @private
   * @type {Array.<goog.events.Key>}
   */
  this.layerGroupPropertyListenerKeys_ = null;

  /**
   * @private
   * @type {Element}
   */
  this.viewport_ = document.createElement('DIV');
  this.viewport_.className = 'ol-viewport';
  this.viewport_.style.position = 'relative';
  this.viewport_.style.overflow = 'hidden';
  this.viewport_.style.width = '100%';
  this.viewport_.style.height = '100%';
  // prevent page zoom on IE >= 10 browsers
  this.viewport_.style.msTouchAction = 'none';
  this.viewport_.style.touchAction = 'none';
  if (ol.has.TOUCH) {
    goog.dom.classlist.add(this.viewport_, 'ol-touch');
  }

  /**
   * @private
   * @type {Element}
   */
  this.overlayContainer_ = document.createElement('DIV');
  this.overlayContainer_.className = 'ol-overlaycontainer';
  this.viewport_.appendChild(this.overlayContainer_);

  /**
   * @private
   * @type {Element}
   */
  this.overlayContainerStopEvent_ = document.createElement('DIV');
  this.overlayContainerStopEvent_.className = 'ol-overlaycontainer-stopevent';
  goog.events.listen(this.overlayContainerStopEvent_, [
    goog.events.EventType.CLICK,
    goog.events.EventType.DBLCLICK,
    goog.events.EventType.MOUSEDOWN,
    goog.events.EventType.TOUCHSTART,
    goog.events.EventType.MSPOINTERDOWN,
    ol.MapBrowserEvent.EventType.POINTERDOWN,
    goog.userAgent.GECKO ? 'DOMMouseScroll' : 'mousewheel'
  ], goog.events.Event.stopPropagation);
  this.viewport_.appendChild(this.overlayContainerStopEvent_);

  var mapBrowserEventHandler = new ol.MapBrowserEventHandler(this);
  goog.events.listen(mapBrowserEventHandler,
      goog.object.getValues(ol.MapBrowserEvent.EventType),
      this.handleMapBrowserEvent, false, this);
  this.registerDisposable(mapBrowserEventHandler);

  /**
   * @private
   * @type {Element|Document}
   */
  this.keyboardEventTarget_ = optionsInternal.keyboardEventTarget;

  /**
   * @private
   * @type {goog.events.KeyHandler}
   */
  this.keyHandler_ = new goog.events.KeyHandler();
  goog.events.listen(this.keyHandler_, goog.events.KeyHandler.EventType.KEY,
      this.handleBrowserEvent, false, this);
  this.registerDisposable(this.keyHandler_);

  var mouseWheelHandler = new goog.events.MouseWheelHandler(this.viewport_);
  goog.events.listen(mouseWheelHandler,
      goog.events.MouseWheelHandler.EventType.MOUSEWHEEL,
      this.handleBrowserEvent, false, this);
  this.registerDisposable(mouseWheelHandler);

  /**
   * @type {ol.Collection.<ol.control.Control>}
   * @private
   */
  this.controls_ = optionsInternal.controls;

  /**
   * @type {ol.Collection.<ol.interaction.Interaction>}
   * @private
   */
  this.interactions_ = optionsInternal.interactions;

  /**
   * @type {ol.Collection.<ol.Overlay>}
   * @private
   */
  this.overlays_ = optionsInternal.overlays;

  /**
   * A lookup of overlays by id.
   * @private
   * @type {Object.<string, ol.Overlay>}
   */
  this.overlayIdIndex_ = {};

  /**
   * @type {ol.renderer.Map}
   * @private
   */
  this.renderer_ =
      new optionsInternal.rendererConstructor(this.viewport_, this);
  this.registerDisposable(this.renderer_);

  /**
   * @type {goog.dom.ViewportSizeMonitor}
   * @private
   */
  this.viewportSizeMonitor_ = new goog.dom.ViewportSizeMonitor();
  this.registerDisposable(this.viewportSizeMonitor_);

  /**
   * @type {goog.events.Key}
   * @private
   */
  this.viewportResizeListenerKey_ = null;

  /**
   * @private
   * @type {ol.Coordinate}
   */
  this.focus_ = null;

  /**
   * @private
   * @type {Array.<ol.PreRenderFunction>}
   */
  this.preRenderFunctions_ = [];

  /**
   * @private
   * @type {Array.<ol.PostRenderFunction>}
   */
  this.postRenderFunctions_ = [];

  /**
   * @private
   * @type {ol.TileQueue}
   */
  this.tileQueue_ = new ol.TileQueue(
      this.getTilePriority.bind(this),
      this.handleTileChange_.bind(this));

  /**
   * Uids of features to skip at rendering time.
   * @type {Object.<string, boolean>}
   * @private
   */
  this.skippedFeatureUids_ = {};

  goog.events.listen(
      this, ol.Object.getChangeEventType(ol.MapProperty.LAYERGROUP),
      this.handleLayerGroupChanged_, false, this);
  goog.events.listen(this, ol.Object.getChangeEventType(ol.MapProperty.VIEW),
      this.handleViewChanged_, false, this);
  goog.events.listen(this, ol.Object.getChangeEventType(ol.MapProperty.SIZE),
      this.handleSizeChanged_, false, this);
  goog.events.listen(this, ol.Object.getChangeEventType(ol.MapProperty.TARGET),
      this.handleTargetChanged_, false, this);

  // setProperties will trigger the rendering of the map if the map
  // is "defined" already.
  this.setProperties(optionsInternal.values);

  this.controls_.forEach(
      /**
       * @param {ol.control.Control} control Control.
       * @this {ol.Map}
       */
      function(control) {
        control.setMap(this);
      }, this);

  goog.events.listen(this.controls_, ol.CollectionEventType.ADD,
      /**
       * @param {ol.CollectionEvent} event Collection event.
       */
      function(event) {
        event.element.setMap(this);
      }, false, this);

  goog.events.listen(this.controls_, ol.CollectionEventType.REMOVE,
      /**
       * @param {ol.CollectionEvent} event Collection event.
       */
      function(event) {
        event.element.setMap(null);
      }, false, this);

  this.interactions_.forEach(
      /**
       * @param {ol.interaction.Interaction} interaction Interaction.
       * @this {ol.Map}
       */
      function(interaction) {
        interaction.setMap(this);
      }, this);

  goog.events.listen(this.interactions_, ol.CollectionEventType.ADD,
      /**
       * @param {ol.CollectionEvent} event Collection event.
       */
      function(event) {
        event.element.setMap(this);
      }, false, this);

  goog.events.listen(this.interactions_, ol.CollectionEventType.REMOVE,
      /**
       * @param {ol.CollectionEvent} event Collection event.
       */
      function(event) {
        event.element.setMap(null);
      }, false, this);

  this.overlays_.forEach(this.addOverlayInternal_, this);

  goog.events.listen(this.overlays_, ol.CollectionEventType.ADD,
      /**
       * @param {ol.CollectionEvent} event Collection event.
       */
      function(event) {
        this.addOverlayInternal_(/** @type {ol.Overlay} */ (event.element));
      }, false, this);

  goog.events.listen(this.overlays_, ol.CollectionEventType.REMOVE,
      /**
       * @param {ol.CollectionEvent} event Collection event.
       */
      function(event) {
        var id = event.element.getId();
        if (id !== undefined) {
          delete this.overlayIdIndex_[id.toString()];
        }
        event.element.setMap(null);
      }, false, this);

};
goog.inherits(ol.Map, ol.Object);


/**
 * Add the given control to the map.
 * @param {ol.control.Control} control Control.
 * @api stable
 */
ol.Map.prototype.addControl = function(control) {
  var controls = this.getControls();
  goog.asserts.assert(controls !== undefined, 'controls should be defined');
  controls.push(control);
};


/**
 * Add the given interaction to the map.
 * @param {ol.interaction.Interaction} interaction Interaction to add.
 * @api stable
 */
ol.Map.prototype.addInteraction = function(interaction) {
  var interactions = this.getInteractions();
  goog.asserts.assert(interactions !== undefined,
      'interactions should be defined');
  interactions.push(interaction);
};


/**
 * Adds the given layer to the top of this map. If you want to add a layer
 * elsewhere in the stack, use `getLayers()` and the methods available on
 * {@link ol.Collection}.
 * @param {ol.layer.Base} layer Layer.
 * @api stable
 */
ol.Map.prototype.addLayer = function(layer) {
  var layers = this.getLayerGroup().getLayers();
  layers.push(layer);
};


/**
 * Add the given overlay to the map.
 * @param {ol.Overlay} overlay Overlay.
 * @api stable
 */
ol.Map.prototype.addOverlay = function(overlay) {
  var overlays = this.getOverlays();
  goog.asserts.assert(overlays !== undefined, 'overlays should be defined');
  overlays.push(overlay);
};


/**
 * This deals with map's overlay collection changes.
 * @param {ol.Overlay} overlay Overlay.
 * @private
 */
ol.Map.prototype.addOverlayInternal_ = function(overlay) {
  var id = overlay.getId();
  if (id !== undefined) {
    this.overlayIdIndex_[id.toString()] = overlay;
  }
  overlay.setMap(this);
};


/**
 * Add functions to be called before rendering. This can be used for attaching
 * animations before updating the map's view.  The {@link ol.animation}
 * namespace provides several static methods for creating prerender functions.
 * @param {...ol.PreRenderFunction} var_args Any number of pre-render functions.
 * @api
 */
ol.Map.prototype.beforeRender = function(var_args) {
  this.render();
  Array.prototype.push.apply(this.preRenderFunctions_, arguments);
};


/**
 * @param {ol.PreRenderFunction} preRenderFunction Pre-render function.
 * @return {boolean} Whether the preRenderFunction has been found and removed.
 */
ol.Map.prototype.removePreRenderFunction = function(preRenderFunction) {
  return goog.array.remove(this.preRenderFunctions_, preRenderFunction);
};


/**
 *
 * @inheritDoc
 */
ol.Map.prototype.disposeInternal = function() {
  goog.dom.removeNode(this.viewport_);
  goog.base(this, 'disposeInternal');
};


/**
 * Detect features that intersect a pixel on the viewport, and execute a
 * callback with each intersecting feature. Layers included in the detection can
 * be configured through `opt_layerFilter`.
 * @param {ol.Pixel} pixel Pixel.
 * @param {function(this: S, (ol.Feature|ol.render.Feature),
 *     ol.layer.Layer): T} callback Feature callback. The callback will be
 *     called with two arguments. The first argument is one
 *     {@link ol.Feature feature} or
 *     {@link ol.render.Feature render feature} at the pixel, the second is
 *     the {@link ol.layer.Layer layer} of the feature and will be null for
 *     unmanaged layers. To stop detection, callback functions can return a
 *     truthy value.
 * @param {S=} opt_this Value to use as `this` when executing `callback`.
 * @param {(function(this: U, ol.layer.Layer): boolean)=} opt_layerFilter Layer
 *     filter function. The filter function will receive one argument, the
 *     {@link ol.layer.Layer layer-candidate} and it should return a boolean
 *     value. Only layers which are visible and for which this function returns
 *     `true` will be tested for features. By default, all visible layers will
 *     be tested.
 * @param {U=} opt_this2 Value to use as `this` when executing `layerFilter`.
 * @return {T|undefined} Callback result, i.e. the return value of last
 * callback execution, or the first truthy callback return value.
 * @template S,T,U
 * @api stable
 */
ol.Map.prototype.forEachFeatureAtPixel = function(pixel, callback, opt_this, opt_layerFilter, opt_this2) {
  if (!this.frameState_) {
    return;
  }
  var coordinate = this.getCoordinateFromPixel(pixel);
  var thisArg = opt_this !== undefined ? opt_this : null;
  var layerFilter = opt_layerFilter !== undefined ?
      opt_layerFilter : goog.functions.TRUE;
  var thisArg2 = opt_this2 !== undefined ? opt_this2 : null;
  return this.renderer_.forEachFeatureAtCoordinate(
      coordinate, this.frameState_, callback, thisArg,
      layerFilter, thisArg2);
};


/**
 * Detect layers that have a color value at a pixel on the viewport, and
 * execute a callback with each matching layer. Layers included in the
 * detection can be configured through `opt_layerFilter`.
 * @param {ol.Pixel} pixel Pixel.
 * @param {function(this: S, ol.layer.Layer): T} callback Layer
 *     callback. Will receive one argument, the {@link ol.layer.Layer layer}
 *     that contains the color pixel. To stop detection, callback functions can
 *     return a truthy value.
 * @param {S=} opt_this Value to use as `this` when executing `callback`.
 * @param {(function(this: U, ol.layer.Layer): boolean)=} opt_layerFilter Layer
 *     filter function. The filter function will receive one argument, the
 *     {@link ol.layer.Layer layer-candidate} and it should return a boolean
 *     value. Only layers which are visible and for which this function returns
 *     `true` will be tested for features. By default, all visible layers will
 *     be tested.
 * @param {U=} opt_this2 Value to use as `this` when executing `layerFilter`.
 * @return {T|undefined} Callback result, i.e. the return value of last
 * callback execution, or the first truthy callback return value.
 * @template S,T,U
 * @api stable
 */
ol.Map.prototype.forEachLayerAtPixel = function(pixel, callback, opt_this, opt_layerFilter, opt_this2) {
  if (!this.frameState_) {
    return;
  }
  var thisArg = opt_this !== undefined ? opt_this : null;
  var layerFilter = opt_layerFilter !== undefined ?
      opt_layerFilter : goog.functions.TRUE;
  var thisArg2 = opt_this2 !== undefined ? opt_this2 : null;
  return this.renderer_.forEachLayerAtPixel(
      pixel, this.frameState_, callback, thisArg,
      layerFilter, thisArg2);
};


/**
 * Detect if features intersect a pixel on the viewport. Layers included in the
 * detection can be configured through `opt_layerFilter`.
 * @param {ol.Pixel} pixel Pixel.
 * @param {(function(this: U, ol.layer.Layer): boolean)=} opt_layerFilter Layer
 *     filter function. The filter function will receive one argument, the
 *     {@link ol.layer.Layer layer-candidate} and it should return a boolean
 *     value. Only layers which are visible and for which this function returns
 *     `true` will be tested for features. By default, all visible layers will
 *     be tested.
 * @param {U=} opt_this Value to use as `this` when executing `layerFilter`.
 * @return {boolean} Is there a feature at the given pixel?
 * @template U
 * @api
 */
ol.Map.prototype.hasFeatureAtPixel = function(pixel, opt_layerFilter, opt_this) {
  if (!this.frameState_) {
    return false;
  }
  var coordinate = this.getCoordinateFromPixel(pixel);
  var layerFilter = opt_layerFilter !== undefined ?
      opt_layerFilter : goog.functions.TRUE;
  var thisArg = opt_this !== undefined ? opt_this : null;
  return this.renderer_.hasFeatureAtCoordinate(
      coordinate, this.frameState_, layerFilter, thisArg);
};


/**
 * Returns the geographical coordinate for a browser event.
 * @param {Event} event Event.
 * @return {ol.Coordinate} Coordinate.
 * @api stable
 */
ol.Map.prototype.getEventCoordinate = function(event) {
  return this.getCoordinateFromPixel(this.getEventPixel(event));
};


/**
 * Returns the map pixel position for a browser event relative to the viewport.
 * @param {Event} event Event.
 * @return {ol.Pixel} Pixel.
 * @api stable
 */
ol.Map.prototype.getEventPixel = function(event) {
  var eventPosition = goog.style.getRelativePosition(event, this.viewport_);
  return [eventPosition.x, eventPosition.y];
};


/**
 * Get the target in which this map is rendered.
 * Note that this returns what is entered as an option or in setTarget:
 * if that was an element, it returns an element; if a string, it returns that.
 * @return {Element|string|undefined} The Element or id of the Element that the
 *     map is rendered in.
 * @observable
 * @api stable
 */
ol.Map.prototype.getTarget = function() {
  return /** @type {Element|string|undefined} */ (
      this.get(ol.MapProperty.TARGET));
};


/**
 * Get the DOM element into which this map is rendered. In contrast to
 * `getTarget` this method always return an `Element`, or `null` if the
 * map has no target.
 * @return {Element} The element that the map is rendered in.
 * @api
 */
ol.Map.prototype.getTargetElement = function() {
  var target = this.getTarget();
  return target !== undefined ? goog.dom.getElement(target) : null;
};


/**
 * Get the coordinate for a given pixel.  This returns a coordinate in the
 * map view projection.
 * @param {ol.Pixel} pixel Pixel position in the map viewport.
 * @return {ol.Coordinate} The coordinate for the pixel position.
 * @api stable
 */
ol.Map.prototype.getCoordinateFromPixel = function(pixel) {
  var frameState = this.frameState_;
  if (!frameState) {
    return null;
  } else {
    var vec2 = pixel.slice();
    return ol.vec.Mat4.multVec2(frameState.pixelToCoordinateMatrix, vec2, vec2);
  }
};


/**
 * Get the map controls. Modifying this collection changes the controls
 * associated with the map.
 * @return {ol.Collection.<ol.control.Control>} Controls.
 * @api stable
 */
ol.Map.prototype.getControls = function() {
  return this.controls_;
};


/**
 * Get the map overlays. Modifying this collection changes the overlays
 * associated with the map.
 * @return {ol.Collection.<ol.Overlay>} Overlays.
 * @api stable
 */
ol.Map.prototype.getOverlays = function() {
  return this.overlays_;
};


/**
 * Get an overlay by its identifier (the value returned by overlay.getId()).
 * Note that the index treats string and numeric identifiers as the same. So
 * `map.getOverlayById(2)` will return an overlay with id `'2'` or `2`.
 * @param {string|number} id Overlay identifier.
 * @return {ol.Overlay} Overlay.
 * @api
 */
ol.Map.prototype.getOverlayById = function(id) {
  var overlay = this.overlayIdIndex_[id.toString()];
  return overlay !== undefined ? overlay : null;
};


/**
 * Get the map interactions. Modifying this collection changes the interactions
 * associated with the map.
 *
 * Interactions are used for e.g. pan, zoom and rotate.
 * @return {ol.Collection.<ol.interaction.Interaction>} Interactions.
 * @api stable
 */
ol.Map.prototype.getInteractions = function() {
  return this.interactions_;
};


/**
 * Get the layergroup associated with this map.
 * @return {ol.layer.Group} A layer group containing the layers in this map.
 * @observable
 * @api stable
 */
ol.Map.prototype.getLayerGroup = function() {
  return /** @type {ol.layer.Group} */ (this.get(ol.MapProperty.LAYERGROUP));
};


/**
 * Get the collection of layers associated with this map.
 * @return {!ol.Collection.<ol.layer.Base>} Layers.
 * @api stable
 */
ol.Map.prototype.getLayers = function() {
  var layers = this.getLayerGroup().getLayers();
  return layers;
};


/**
 * Get the pixel for a coordinate.  This takes a coordinate in the map view
 * projection and returns the corresponding pixel.
 * @param {ol.Coordinate} coordinate A map coordinate.
 * @return {ol.Pixel} A pixel position in the map viewport.
 * @api stable
 */
ol.Map.prototype.getPixelFromCoordinate = function(coordinate) {
  var frameState = this.frameState_;
  if (!frameState) {
    return null;
  } else {
    var vec2 = coordinate.slice(0, 2);
    return ol.vec.Mat4.multVec2(frameState.coordinateToPixelMatrix, vec2, vec2);
  }
};


/**
 * Get the map renderer.
 * @return {ol.renderer.Map} Renderer
 */
ol.Map.prototype.getRenderer = function() {
  return this.renderer_;
};


/**
 * Get the size of this map.
 * @return {ol.Size|undefined} The size in pixels of the map in the DOM.
 * @observable
 * @api stable
 */
ol.Map.prototype.getSize = function() {
  return /** @type {ol.Size|undefined} */ (this.get(ol.MapProperty.SIZE));
};


/**
 * Get the view associated with this map. A view manages properties such as
 * center and resolution.
 * @return {ol.View} The view that controls this map.
 * @observable
 * @api stable
 */
ol.Map.prototype.getView = function() {
  return /** @type {ol.View} */ (this.get(ol.MapProperty.VIEW));
};


/**
 * Get the element that serves as the map viewport.
 * @return {Element} Viewport.
 * @api stable
 */
ol.Map.prototype.getViewport = function() {
  return this.viewport_;
};


/**
 * Get the element that serves as the container for overlays.  Elements added to
 * this container will let mousedown and touchstart events through to the map,
 * so clicks and gestures on an overlay will trigger {@link ol.MapBrowserEvent}
 * events.
 * @return {Element} The map's overlay container.
 */
ol.Map.prototype.getOverlayContainer = function() {
  return this.overlayContainer_;
};


/**
 * Get the element that serves as a container for overlays that don't allow
 * event propagation. Elements added to this container won't let mousedown and
 * touchstart events through to the map, so clicks and gestures on an overlay
 * don't trigger any {@link ol.MapBrowserEvent}.
 * @return {Element} The map's overlay container that stops events.
 */
ol.Map.prototype.getOverlayContainerStopEvent = function() {
  return this.overlayContainerStopEvent_;
};


/**
 * @param {ol.Tile} tile Tile.
 * @param {string} tileSourceKey Tile source key.
 * @param {ol.Coordinate} tileCenter Tile center.
 * @param {number} tileResolution Tile resolution.
 * @return {number} Tile priority.
 */
ol.Map.prototype.getTilePriority = function(tile, tileSourceKey, tileCenter, tileResolution) {
  // Filter out tiles at higher zoom levels than the current zoom level, or that
  // are outside the visible extent.
  var frameState = this.frameState_;
  if (!frameState || !(tileSourceKey in frameState.wantedTiles)) {
    return ol.structs.PriorityQueue.DROP;
  }
  var coordKey = tile.tileCoord.toString();
  if (!frameState.wantedTiles[tileSourceKey][coordKey]) {
    return ol.structs.PriorityQueue.DROP;
  }
  // Prioritize the highest zoom level tiles closest to the focus.
  // Tiles at higher zoom levels are prioritized using Math.log(tileResolution).
  // Within a zoom level, tiles are prioritized by the distance in pixels
  // between the center of the tile and the focus.  The factor of 65536 means
  // that the prioritization should behave as desired for tiles up to
  // 65536 * Math.log(2) = 45426 pixels from the focus.
  var deltaX = tileCenter[0] - frameState.focus[0];
  var deltaY = tileCenter[1] - frameState.focus[1];
  return 65536 * Math.log(tileResolution) +
      Math.sqrt(deltaX * deltaX + deltaY * deltaY) / tileResolution;
};


/**
 * @param {goog.events.BrowserEvent} browserEvent Browser event.
 * @param {string=} opt_type Type.
 */
ol.Map.prototype.handleBrowserEvent = function(browserEvent, opt_type) {
  var type = opt_type || browserEvent.type;
  var mapBrowserEvent = new ol.MapBrowserEvent(type, this, browserEvent);
  this.handleMapBrowserEvent(mapBrowserEvent);
};


/**
 * @param {ol.MapBrowserEvent} mapBrowserEvent The event to handle.
 */
ol.Map.prototype.handleMapBrowserEvent = function(mapBrowserEvent) {
  if (!this.frameState_) {
    // With no view defined, we cannot translate pixels into geographical
    // coordinates so interactions cannot be used.
    return;
  }
  this.focus_ = mapBrowserEvent.coordinate;
  mapBrowserEvent.frameState = this.frameState_;
  var interactions = this.getInteractions();
  goog.asserts.assert(interactions !== undefined,
      'interactions should be defined');
  var interactionsArray = interactions.getArray();
  var i;
  if (this.dispatchEvent(mapBrowserEvent) !== false) {
    for (i = interactionsArray.length - 1; i >= 0; i--) {
      var interaction = interactionsArray[i];
      if (!interaction.getActive()) {
        continue;
      }
      var cont = interaction.handleEvent(mapBrowserEvent);
      if (!cont) {
        break;
      }
    }
  }
};


/**
 * @protected
 */
ol.Map.prototype.handlePostRender = function() {

  var frameState = this.frameState_;

  // Manage the tile queue
  // Image loads are expensive and a limited resource, so try to use them
  // efficiently:
  // * When the view is static we allow a large number of parallel tile loads
  //   to complete the frame as quickly as possible.
  // * When animating or interacting, image loads can cause janks, so we reduce
  //   the maximum number of loads per frame and limit the number of parallel
  //   tile loads to remain reactive to view changes and to reduce the chance of
  //   loading tiles that will quickly disappear from view.
  var tileQueue = this.tileQueue_;
  if (!tileQueue.isEmpty()) {
    var maxTotalLoading = 16;
    var maxNewLoads = maxTotalLoading;
    var tileSourceCount = 0;
    if (frameState) {
      var hints = frameState.viewHints;
      if (hints[ol.ViewHint.ANIMATING]) {
        maxTotalLoading = this.loadTilesWhileAnimating_ ? 8 : 0;
        maxNewLoads = 2;
      }
      if (hints[ol.ViewHint.INTERACTING]) {
        maxTotalLoading = this.loadTilesWhileInteracting_ ? 8 : 0;
        maxNewLoads = 2;
      }
      tileSourceCount = goog.object.getCount(frameState.wantedTiles);
    }
    maxTotalLoading *= tileSourceCount;
    maxNewLoads *= tileSourceCount;
    if (tileQueue.getTilesLoading() < maxTotalLoading) {
      tileQueue.reprioritize(); // FIXME only call if view has changed
      tileQueue.loadMoreTiles(maxTotalLoading, maxNewLoads);
    }
  }

  var postRenderFunctions = this.postRenderFunctions_;
  var i, ii;
  for (i = 0, ii = postRenderFunctions.length; i < ii; ++i) {
    postRenderFunctions[i](this, frameState);
  }
  postRenderFunctions.length = 0;
};


/**
 * @private
 */
ol.Map.prototype.handleSizeChanged_ = function() {
  this.render();
};


/**
 * @private
 */
ol.Map.prototype.handleTargetChanged_ = function() {
  // target may be undefined, null, a string or an Element.
  // If it's a string we convert it to an Element before proceeding.
  // If it's not now an Element we remove the viewport from the DOM.
  // If it's an Element we append the viewport element to it.

  var targetElement = this.getTargetElement();

  this.keyHandler_.detach();

  if (!targetElement) {
    goog.dom.removeNode(this.viewport_);
    if (this.viewportResizeListenerKey_) {
      goog.events.unlistenByKey(this.viewportResizeListenerKey_);
      this.viewportResizeListenerKey_ = null;
    }
  } else {
    targetElement.appendChild(this.viewport_);

    var keyboardEventTarget = !this.keyboardEventTarget_ ?
        targetElement : this.keyboardEventTarget_;
    this.keyHandler_.attach(keyboardEventTarget);

    if (!this.viewportResizeListenerKey_) {
      this.viewportResizeListenerKey_ = goog.events.listen(
          this.viewportSizeMonitor_, goog.events.EventType.RESIZE,
          this.updateSize, false, this);
    }
  }

  this.updateSize();
  // updateSize calls setSize, so no need to call this.render
  // ourselves here.
};


/**
 * @private
 */
ol.Map.prototype.handleTileChange_ = function() {
  this.render();
};


/**
 * @private
 */
ol.Map.prototype.handleViewPropertyChanged_ = function() {
  this.render();
};


/**
 * @private
 */
ol.Map.prototype.handleViewChanged_ = function() {
  if (this.viewPropertyListenerKey_) {
    goog.events.unlistenByKey(this.viewPropertyListenerKey_);
    this.viewPropertyListenerKey_ = null;
  }
  var view = this.getView();
  if (view) {
    this.viewPropertyListenerKey_ = goog.events.listen(
        view, ol.ObjectEventType.PROPERTYCHANGE,
        this.handleViewPropertyChanged_, false, this);
  }
  this.render();
};


/**
 * @param {goog.events.Event} event Event.
 * @private
 */
ol.Map.prototype.handleLayerGroupMemberChanged_ = function(event) {
  goog.asserts.assertInstanceof(event, goog.events.Event,
      'event should be an Event');
  this.render();
};


/**
 * @param {ol.ObjectEvent} event Event.
 * @private
 */
ol.Map.prototype.handleLayerGroupPropertyChanged_ = function(event) {
  goog.asserts.assertInstanceof(event, ol.ObjectEvent,
      'event should be an ol.ObjectEvent');
  this.render();
};


/**
 * @private
 */
ol.Map.prototype.handleLayerGroupChanged_ = function() {
  if (this.layerGroupPropertyListenerKeys_) {
    this.layerGroupPropertyListenerKeys_.forEach(goog.events.unlistenByKey);
    this.layerGroupPropertyListenerKeys_ = null;
  }
  var layerGroup = this.getLayerGroup();
  if (layerGroup) {
    this.layerGroupPropertyListenerKeys_ = [
      goog.events.listen(
          layerGroup, ol.ObjectEventType.PROPERTYCHANGE,
          this.handleLayerGroupPropertyChanged_, false, this),
      goog.events.listen(
          layerGroup, goog.events.EventType.CHANGE,
          this.handleLayerGroupMemberChanged_, false, this)
    ];
  }
  this.render();
};


/**
 * Returns `true` if the map is defined, `false` otherwise. The map is defined
 * if it is contained in `document`, visible, has non-zero height and width, and
 * has a defined view.
 * @return {boolean} Is defined.
 */
ol.Map.prototype.isDef = function() {
  if (!goog.dom.contains(document, this.viewport_)) {
    return false;
  }
  if (!goog.style.isElementShown(this.viewport_)) {
    return false;
  }
  var size = this.getSize();
  if (!size || size[0] <= 0 || size[1] <= 0) {
    return false;
  }
  var view = this.getView();
  if (!view || !view.isDef()) {
    return false;
  }
  return true;
};


/**
 * @return {boolean} Is rendered.
 */
ol.Map.prototype.isRendered = function() {
  return !!this.frameState_;
};


/**
 * Requests an immediate render in a synchronous manner.
 * @api stable
 */
ol.Map.prototype.renderSync = function() {
  this.animationDelay_.fire();
};


/**
 * Request a map rendering (at the next animation frame).
 * @api stable
 */
ol.Map.prototype.render = function() {
  if (!this.animationDelay_.isActive()) {
    this.animationDelay_.start();
  }
};


/**
 * Remove the given control from the map.
 * @param {ol.control.Control} control Control.
 * @return {ol.control.Control|undefined} The removed control (or undefined
 *     if the control was not found).
 * @api stable
 */
ol.Map.prototype.removeControl = function(control) {
  var controls = this.getControls();
  goog.asserts.assert(controls !== undefined, 'controls should be defined');
  return controls.remove(control);
};


/**
 * Remove the given interaction from the map.
 * @param {ol.interaction.Interaction} interaction Interaction to remove.
 * @return {ol.interaction.Interaction|undefined} The removed interaction (or
 *     undefined if the interaction was not found).
 * @api stable
 */
ol.Map.prototype.removeInteraction = function(interaction) {
  var interactions = this.getInteractions();
  goog.asserts.assert(interactions !== undefined,
      'interactions should be defined');
  return interactions.remove(interaction);
};


/**
 * Removes the given layer from the map.
 * @param {ol.layer.Base} layer Layer.
 * @return {ol.layer.Base|undefined} The removed layer (or undefined if the
 *     layer was not found).
 * @api stable
 */
ol.Map.prototype.removeLayer = function(layer) {
  var layers = this.getLayerGroup().getLayers();
  return layers.remove(layer);
};


/**
 * Remove the given overlay from the map.
 * @param {ol.Overlay} overlay Overlay.
 * @return {ol.Overlay|undefined} The removed overlay (or undefined
 *     if the overlay was not found).
 * @api stable
 */
ol.Map.prototype.removeOverlay = function(overlay) {
  var overlays = this.getOverlays();
  goog.asserts.assert(overlays !== undefined, 'overlays should be defined');
  return overlays.remove(overlay);
};


/**
 * @param {number} time Time.
 * @private
 */
ol.Map.prototype.renderFrame_ = function(time) {

  var i, ii, viewState;

  var size = this.getSize();
  var view = this.getView();
  /** @type {?olx.FrameState} */
  var frameState = null;
  if (size !== undefined && ol.size.hasArea(size) &&
      view && view.isDef()) {
    var viewHints = view.getHints();
    var layerStatesArray = this.getLayerGroup().getLayerStatesArray();
    var layerStates = {};
    for (i = 0, ii = layerStatesArray.length; i < ii; ++i) {
      layerStates[goog.getUid(layerStatesArray[i].layer)] = layerStatesArray[i];
    }
    viewState = view.getState();
    frameState = /** @type {olx.FrameState} */ ({
      animate: false,
      attributions: {},
      coordinateToPixelMatrix: this.coordinateToPixelMatrix_,
      extent: null,
      focus: !this.focus_ ? viewState.center : this.focus_,
      index: this.frameIndex_++,
      layerStates: layerStates,
      layerStatesArray: layerStatesArray,
      logos: goog.object.clone(this.logos_),
      pixelRatio: this.pixelRatio_,
      pixelToCoordinateMatrix: this.pixelToCoordinateMatrix_,
      postRenderFunctions: [],
      size: size,
      skippedFeatureUids: this.skippedFeatureUids_,
      tileQueue: this.tileQueue_,
      time: time,
      usedTiles: {},
      viewState: viewState,
      viewHints: viewHints,
      wantedTiles: {}
    });
  }

  if (frameState) {
    var preRenderFunctions = this.preRenderFunctions_;
    var n = 0, preRenderFunction;
    for (i = 0, ii = preRenderFunctions.length; i < ii; ++i) {
      preRenderFunction = preRenderFunctions[i];
      if (preRenderFunction(this, frameState)) {
        preRenderFunctions[n++] = preRenderFunction;
      }
    }
    preRenderFunctions.length = n;

    frameState.extent = ol.extent.getForViewAndSize(viewState.center,
        viewState.resolution, viewState.rotation, frameState.size);
  }

  this.frameState_ = frameState;
  this.renderer_.renderFrame(frameState);

  if (frameState) {
    if (frameState.animate) {
      this.render();
    }
    Array.prototype.push.apply(
        this.postRenderFunctions_, frameState.postRenderFunctions);

    var idle = this.preRenderFunctions_.length === 0 &&
        !frameState.viewHints[ol.ViewHint.ANIMATING] &&
        !frameState.viewHints[ol.ViewHint.INTERACTING] &&
        !ol.extent.equals(frameState.extent, this.previousExtent_);

    if (idle) {
      this.dispatchEvent(
          new ol.MapEvent(ol.MapEventType.MOVEEND, this, frameState));
      ol.extent.clone(frameState.extent, this.previousExtent_);
    }
  }

  this.dispatchEvent(
      new ol.MapEvent(ol.MapEventType.POSTRENDER, this, frameState));

  goog.async.nextTick(this.handlePostRender, this);

};


/**
 * Sets the layergroup of this map.
 * @param {ol.layer.Group} layerGroup A layer group containing the layers in
 *     this map.
 * @observable
 * @api stable
 */
ol.Map.prototype.setLayerGroup = function(layerGroup) {
  this.set(ol.MapProperty.LAYERGROUP, layerGroup);
};


/**
 * Set the size of this map.
 * @param {ol.Size|undefined} size The size in pixels of the map in the DOM.
 * @observable
 * @api
 */
ol.Map.prototype.setSize = function(size) {
  this.set(ol.MapProperty.SIZE, size);
};


/**
 * Set the target element to render this map into.
 * @param {Element|string|undefined} target The Element or id of the Element
 *     that the map is rendered in.
 * @observable
 * @api stable
 */
ol.Map.prototype.setTarget = function(target) {
  this.set(ol.MapProperty.TARGET, target);
};


/**
 * Set the view for this map.
 * @param {ol.View} view The view that controls this map.
 * @observable
 * @api stable
 */
ol.Map.prototype.setView = function(view) {
  this.set(ol.MapProperty.VIEW, view);
};


/**
 * @param {ol.Feature} feature Feature.
 */
ol.Map.prototype.skipFeature = function(feature) {
  var featureUid = goog.getUid(feature).toString();
  this.skippedFeatureUids_[featureUid] = true;
  this.render();
};


/**
 * Force a recalculation of the map viewport size.  This should be called when
 * third-party code changes the size of the map viewport.
 * @api stable
 */
ol.Map.prototype.updateSize = function() {
  var targetElement = this.getTargetElement();

  if (!targetElement) {
    this.setSize(undefined);
  } else {
    var size = goog.style.getContentBoxSize(targetElement);
    this.setSize([size.width, size.height]);
  }
};


/**
 * @param {ol.Feature} feature Feature.
 */
ol.Map.prototype.unskipFeature = function(feature) {
  var featureUid = goog.getUid(feature).toString();
  delete this.skippedFeatureUids_[featureUid];
  this.render();
};


/**
 * @typedef {{controls: ol.Collection.<ol.control.Control>,
 *            interactions: ol.Collection.<ol.interaction.Interaction>,
 *            keyboardEventTarget: (Element|Document),
 *            logos: Object.<string, string>,
 *            overlays: ol.Collection.<ol.Overlay>,
 *            rendererConstructor:
 *                function(new: ol.renderer.Map, Element, ol.Map),
 *            values: Object.<string, *>}}
 */
ol.MapOptionsInternal;


/**
 * @param {olx.MapOptions} options Map options.
 * @return {ol.MapOptionsInternal} Internal map options.
 */
ol.Map.createOptionsInternal = function(options) {

  /**
   * @type {Element|Document}
   */
  var keyboardEventTarget = null;
  if (options.keyboardEventTarget !== undefined) {
    // cannot use goog.dom.getElement because its argument cannot be
    // of type Document
    keyboardEventTarget = goog.isString(options.keyboardEventTarget) ?
        document.getElementById(options.keyboardEventTarget) :
        options.keyboardEventTarget;
  }

  /**
   * @type {Object.<string, *>}
   */
  var values = {};

  var logos = {};
  if (options.logo === undefined ||
      (goog.isBoolean(options.logo) && options.logo)) {
    logos[ol.OL3_LOGO_URL] = ol.OL3_URL;
  } else {
    var logo = options.logo;
    if (goog.isString(logo)) {
      logos[logo] = '';
    } else if (goog.isObject(logo)) {
      goog.asserts.assertString(logo.href, 'logo.href should be a string');
      goog.asserts.assertString(logo.src, 'logo.src should be a string');
      logos[logo.src] = logo.href;
    }
  }

  var layerGroup = (options.layers instanceof ol.layer.Group) ?
      options.layers : new ol.layer.Group({layers: options.layers});
  values[ol.MapProperty.LAYERGROUP] = layerGroup;

  values[ol.MapProperty.TARGET] = options.target;

  values[ol.MapProperty.VIEW] = options.view !== undefined ?
      options.view : new ol.View();

  /**
   * @type {function(new: ol.renderer.Map, Element, ol.Map)}
   */
  var rendererConstructor = ol.renderer.Map;

  /**
   * @type {Array.<ol.RendererType>}
   */
  var rendererTypes;
  if (options.renderer !== undefined) {
    if (goog.isArray(options.renderer)) {
      rendererTypes = options.renderer;
    } else if (goog.isString(options.renderer)) {
      rendererTypes = [options.renderer];
    } else {
      goog.asserts.fail('Incorrect format for renderer option');
    }
  } else {
    rendererTypes = ol.DEFAULT_RENDERER_TYPES;
  }

  var i, ii;
  for (i = 0, ii = rendererTypes.length; i < ii; ++i) {
    /** @type {ol.RendererType} */
    var rendererType = rendererTypes[i];
    if (ol.ENABLE_CANVAS && rendererType == ol.RendererType.CANVAS) {
      if (ol.has.CANVAS) {
        rendererConstructor = ol.renderer.canvas.Map;
        break;
      }
    } else if (ol.ENABLE_DOM && rendererType == ol.RendererType.DOM) {
      if (ol.has.DOM) {
        rendererConstructor = ol.renderer.dom.Map;
        break;
      }
    } else if (ol.ENABLE_WEBGL && rendererType == ol.RendererType.WEBGL) {
      if (ol.has.WEBGL) {
        rendererConstructor = ol.renderer.webgl.Map;
        break;
      }
    }
  }

  var controls;
  if (options.controls !== undefined) {
    if (goog.isArray(options.controls)) {
      controls = new ol.Collection(options.controls.slice());
    } else {
      goog.asserts.assertInstanceof(options.controls, ol.Collection,
          'options.controls should be an ol.Collection');
      controls = options.controls;
    }
  } else {
    controls = ol.control.defaults();
  }

  var interactions;
  if (options.interactions !== undefined) {
    if (goog.isArray(options.interactions)) {
      interactions = new ol.Collection(options.interactions.slice());
    } else {
      goog.asserts.assertInstanceof(options.interactions, ol.Collection,
          'options.interactions should be an ol.Collection');
      interactions = options.interactions;
    }
  } else {
    interactions = ol.interaction.defaults();
  }

  var overlays;
  if (options.overlays !== undefined) {
    if (goog.isArray(options.overlays)) {
      overlays = new ol.Collection(options.overlays.slice());
    } else {
      goog.asserts.assertInstanceof(options.overlays, ol.Collection,
          'options.overlays should be an ol.Collection');
      overlays = options.overlays;
    }
  } else {
    overlays = new ol.Collection();
  }

  return {
    controls: controls,
    interactions: interactions,
    keyboardEventTarget: keyboardEventTarget,
    logos: logos,
    overlays: overlays,
    rendererConstructor: rendererConstructor,
    values: values
  };

};


ol.proj.common.add();


if (goog.DEBUG) {
  (function() {
    goog.debug.Console.autoInstall();
    var logger = goog.log.getLogger('ol');
    logger.setLevel(goog.log.Level.FINEST);
  })();
}

goog.provide('ol.Overlay');
goog.provide('ol.OverlayPositioning');
goog.provide('ol.OverlayProperty');

goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.style');
goog.require('ol.Coordinate');
goog.require('ol.Map');
goog.require('ol.MapEventType');
goog.require('ol.Object');
goog.require('ol.animation');
goog.require('ol.dom');
goog.require('ol.extent');


/**
 * @enum {string}
 */
ol.OverlayProperty = {
  ELEMENT: 'element',
  MAP: 'map',
  OFFSET: 'offset',
  POSITION: 'position',
  POSITIONING: 'positioning'
};


/**
 * Overlay position: `'bottom-left'`, `'bottom-center'`,  `'bottom-right'`,
 * `'center-left'`, `'center-center'`, `'center-right'`, `'top-left'`,
 * `'top-center'`, `'top-right'`
 * @enum {string}
 * @api stable
 */
ol.OverlayPositioning = {
  BOTTOM_LEFT: 'bottom-left',
  BOTTOM_CENTER: 'bottom-center',
  BOTTOM_RIGHT: 'bottom-right',
  CENTER_LEFT: 'center-left',
  CENTER_CENTER: 'center-center',
  CENTER_RIGHT: 'center-right',
  TOP_LEFT: 'top-left',
  TOP_CENTER: 'top-center',
  TOP_RIGHT: 'top-right'
};


/**
 * @classdesc
 * An element to be displayed over the map and attached to a single map
 * location.  Like {@link ol.control.Control}, Overlays are visible widgets.
 * Unlike Controls, they are not in a fixed position on the screen, but are tied
 * to a geographical coordinate, so panning the map will move an Overlay but not
 * a Control.
 *
 * Example:
 *
 *     var popup = new ol.Overlay({
 *       element: document.getElementById('popup')
 *     });
 *     popup.setPosition(coordinate);
 *     map.addOverlay(popup);
 *
 * @constructor
 * @extends {ol.Object}
 * @param {olx.OverlayOptions} options Overlay options.
 * @api stable
 */
ol.Overlay = function(options) {

  goog.base(this);

  /**
   * @private
   * @type {number|string|undefined}
   */
  this.id_ = options.id;

  /**
   * @private
   * @type {boolean}
   */
  this.insertFirst_ = options.insertFirst !== undefined ?
      options.insertFirst : true;

  /**
   * @private
   * @type {boolean}
   */
  this.stopEvent_ = options.stopEvent !== undefined ? options.stopEvent : true;

  /**
   * @private
   * @type {Element}
   */
  this.element_ = document.createElement('DIV');
  this.element_.className = 'ol-overlay-container';
  this.element_.style.position = 'absolute';

  /**
   * @protected
   * @type {boolean}
   */
  this.autoPan = options.autoPan !== undefined ? options.autoPan : false;

  /**
   * @private
   * @type {olx.animation.PanOptions}
   */
  this.autoPanAnimation_ = options.autoPanAnimation !== undefined ?
      options.autoPanAnimation : /** @type {olx.animation.PanOptions} */ ({});

  /**
   * @private
   * @type {number}
   */
  this.autoPanMargin_ = options.autoPanMargin !== undefined ?
      options.autoPanMargin : 20;

  /**
   * @private
   * @type {{bottom_: string,
   *         left_: string,
   *         right_: string,
   *         top_: string,
   *         visible: boolean}}
   */
  this.rendered_ = {
    bottom_: '',
    left_: '',
    right_: '',
    top_: '',
    visible: true
  };

  /**
   * @private
   * @type {goog.events.Key}
   */
  this.mapPostrenderListenerKey_ = null;

  goog.events.listen(
      this, ol.Object.getChangeEventType(ol.OverlayProperty.ELEMENT),
      this.handleElementChanged, false, this);

  goog.events.listen(
      this, ol.Object.getChangeEventType(ol.OverlayProperty.MAP),
      this.handleMapChanged, false, this);

  goog.events.listen(
      this, ol.Object.getChangeEventType(ol.OverlayProperty.OFFSET),
      this.handleOffsetChanged, false, this);

  goog.events.listen(
      this, ol.Object.getChangeEventType(ol.OverlayProperty.POSITION),
      this.handlePositionChanged, false, this);

  goog.events.listen(
      this, ol.Object.getChangeEventType(ol.OverlayProperty.POSITIONING),
      this.handlePositioningChanged, false, this);

  if (options.element !== undefined) {
    this.setElement(options.element);
  }

  this.setOffset(options.offset !== undefined ? options.offset : [0, 0]);

  this.setPositioning(options.positioning !== undefined ?
      /** @type {ol.OverlayPositioning} */ (options.positioning) :
      ol.OverlayPositioning.TOP_LEFT);

  if (options.position !== undefined) {
    this.setPosition(options.position);
  }

};
goog.inherits(ol.Overlay, ol.Object);


/**
 * Get the DOM element of this overlay.
 * @return {Element|undefined} The Element containing the overlay.
 * @observable
 * @api stable
 */
ol.Overlay.prototype.getElement = function() {
  return /** @type {Element|undefined} */ (
      this.get(ol.OverlayProperty.ELEMENT));
};


/**
 * Get the overlay identifier which is set on constructor.
 * @return {number|string|undefined} Id.
 * @api
 */
ol.Overlay.prototype.getId = function() {
  return this.id_;
};


/**
 * Get the map associated with this overlay.
 * @return {ol.Map|undefined} The map that the overlay is part of.
 * @observable
 * @api stable
 */
ol.Overlay.prototype.getMap = function() {
  return /** @type {ol.Map|undefined} */ (
      this.get(ol.OverlayProperty.MAP));
};


/**
 * Get the offset of this overlay.
 * @return {Array.<number>} The offset.
 * @observable
 * @api stable
 */
ol.Overlay.prototype.getOffset = function() {
  return /** @type {Array.<number>} */ (
      this.get(ol.OverlayProperty.OFFSET));
};


/**
 * Get the current position of this overlay.
 * @return {ol.Coordinate|undefined} The spatial point that the overlay is
 *     anchored at.
 * @observable
 * @api stable
 */
ol.Overlay.prototype.getPosition = function() {
  return /** @type {ol.Coordinate|undefined} */ (
      this.get(ol.OverlayProperty.POSITION));
};


/**
 * Get the current positioning of this overlay.
 * @return {ol.OverlayPositioning} How the overlay is positioned
 *     relative to its point on the map.
 * @observable
 * @api stable
 */
ol.Overlay.prototype.getPositioning = function() {
  return /** @type {ol.OverlayPositioning} */ (
      this.get(ol.OverlayProperty.POSITIONING));
};


/**
 * @protected
 */
ol.Overlay.prototype.handleElementChanged = function() {
  goog.dom.removeChildren(this.element_);
  var element = this.getElement();
  if (element) {
    this.element_.appendChild(element);
  }
};


/**
 * @protected
 */
ol.Overlay.prototype.handleMapChanged = function() {
  if (this.mapPostrenderListenerKey_) {
    goog.dom.removeNode(this.element_);
    goog.events.unlistenByKey(this.mapPostrenderListenerKey_);
    this.mapPostrenderListenerKey_ = null;
  }
  var map = this.getMap();
  if (map) {
    this.mapPostrenderListenerKey_ = goog.events.listen(map,
        ol.MapEventType.POSTRENDER, this.render, false, this);
    this.updatePixelPosition();
    var container = this.stopEvent_ ?
        map.getOverlayContainerStopEvent() : map.getOverlayContainer();
    if (this.insertFirst_) {
      goog.dom.insertChildAt(/** @type {!Element} */ (
          container), this.element_, 0);
    } else {
      container.appendChild(this.element_);
    }
  }
};


/**
 * @protected
 */
ol.Overlay.prototype.render = function() {
  this.updatePixelPosition();
};


/**
 * @protected
 */
ol.Overlay.prototype.handleOffsetChanged = function() {
  this.updatePixelPosition();
};


/**
 * @protected
 */
ol.Overlay.prototype.handlePositionChanged = function() {
  this.updatePixelPosition();
  if (this.get(ol.OverlayProperty.POSITION) !== undefined && this.autoPan) {
    this.panIntoView_();
  }
};


/**
 * @protected
 */
ol.Overlay.prototype.handlePositioningChanged = function() {
  this.updatePixelPosition();
};


/**
 * Set the DOM element to be associated with this overlay.
 * @param {Element|undefined} element The Element containing the overlay.
 * @observable
 * @api stable
 */
ol.Overlay.prototype.setElement = function(element) {
  this.set(ol.OverlayProperty.ELEMENT, element);
};


/**
 * Set the map to be associated with this overlay.
 * @param {ol.Map|undefined} map The map that the overlay is part of.
 * @observable
 * @api stable
 */
ol.Overlay.prototype.setMap = function(map) {
  this.set(ol.OverlayProperty.MAP, map);
};


/**
 * Set the offset for this overlay.
 * @param {Array.<number>} offset Offset.
 * @observable
 * @api stable
 */
ol.Overlay.prototype.setOffset = function(offset) {
  this.set(ol.OverlayProperty.OFFSET, offset);
};


/**
 * Set the position for this overlay. If the position is `undefined` the
 * overlay is hidden.
 * @param {ol.Coordinate|undefined} position The spatial point that the overlay
 *     is anchored at.
 * @observable
 * @api stable
 */
ol.Overlay.prototype.setPosition = function(position) {
  this.set(ol.OverlayProperty.POSITION, position);
};


/**
 * Pan the map so that the overlay is entirely visible in the current viewport
 * (if necessary).
 * @private
 */
ol.Overlay.prototype.panIntoView_ = function() {
  goog.asserts.assert(this.autoPan, 'this.autoPan should be true');
  var map = this.getMap();

  if (map === undefined || !map.getTargetElement()) {
    return;
  }

  var mapRect = this.getRect_(map.getTargetElement(), map.getSize());
  var element = this.getElement();
  goog.asserts.assert(element, 'element should be defined');
  var overlayRect = this.getRect_(element,
      [ol.dom.outerWidth(element), ol.dom.outerHeight(element)]);

  var margin = this.autoPanMargin_;
  if (!ol.extent.containsExtent(mapRect, overlayRect)) {
    // the overlay is not completely inside the viewport, so pan the map
    var offsetLeft = overlayRect[0] - mapRect[0];
    var offsetRight = mapRect[2] - overlayRect[2];
    var offsetTop = overlayRect[1] - mapRect[1];
    var offsetBottom = mapRect[3] - overlayRect[3];

    var delta = [0, 0];
    if (offsetLeft < 0) {
      // move map to the left
      delta[0] = offsetLeft - margin;
    } else if (offsetRight < 0) {
      // move map to the right
      delta[0] = Math.abs(offsetRight) + margin;
    }
    if (offsetTop < 0) {
      // move map up
      delta[1] = offsetTop - margin;
    } else if (offsetBottom < 0) {
      // move map down
      delta[1] = Math.abs(offsetBottom) + margin;
    }

    if (delta[0] !== 0 || delta[1] !== 0) {
      var center = map.getView().getCenter();
      goog.asserts.assert(center !== undefined, 'center should be defined');
      var centerPx = map.getPixelFromCoordinate(center);
      var newCenterPx = [
        centerPx[0] + delta[0],
        centerPx[1] + delta[1]
      ];

      if (this.autoPanAnimation_) {
        this.autoPanAnimation_.source = center;
        map.beforeRender(ol.animation.pan(this.autoPanAnimation_));
      }
      map.getView().setCenter(map.getCoordinateFromPixel(newCenterPx));
    }
  }
};


/**
 * Get the extent of an element relative to the document
 * @param {Element|undefined} element The element.
 * @param {ol.Size|undefined} size The size of the element.
 * @return {ol.Extent} The extent.
 * @private
 */
ol.Overlay.prototype.getRect_ = function(element, size) {
  goog.asserts.assert(element, 'element should be defined');
  goog.asserts.assert(size !== undefined, 'size should be defined');

  var offset = goog.style.getPageOffset(element);
  return [
    offset.x,
    offset.y,
    offset.x + size[0],
    offset.y + size[1]
  ];
};


/**
 * Set the positioning for this overlay.
 * @param {ol.OverlayPositioning} positioning how the overlay is
 *     positioned relative to its point on the map.
 * @observable
 * @api stable
 */
ol.Overlay.prototype.setPositioning = function(positioning) {
  this.set(ol.OverlayProperty.POSITIONING, positioning);
};


/**
 * Modify the visibility of the element.
 * @param {boolean} visible Element visibility.
 * @protected
 */
ol.Overlay.prototype.setVisible = function(visible) {
  if (this.rendered_.visible !== visible) {
    goog.style.setElementShown(this.element_, visible);
    this.rendered_.visible = visible;
  }
};


/**
 * Update pixel position.
 * @protected
 */
ol.Overlay.prototype.updatePixelPosition = function() {
  var map = this.getMap();
  var position = this.getPosition();
  if (map === undefined || !map.isRendered() || position === undefined) {
    this.setVisible(false);
    return;
  }

  var pixel = map.getPixelFromCoordinate(position);
  var mapSize = map.getSize();
  this.updateRenderedPosition(pixel, mapSize);
};


/**
 * @param {ol.Pixel} pixel The pixel location.
 * @param {ol.Size|undefined} mapSize The map size.
 * @protected
 */
ol.Overlay.prototype.updateRenderedPosition = function(pixel, mapSize) {
  goog.asserts.assert(pixel, 'pixel should not be null');
  goog.asserts.assert(mapSize !== undefined, 'mapSize should be defined');
  var style = this.element_.style;
  var offset = this.getOffset();
  goog.asserts.assert(goog.isArray(offset), 'offset should be an array');

  var positioning = this.getPositioning();
  goog.asserts.assert(positioning !== undefined,
      'positioning should be defined');

  var offsetX = offset[0];
  var offsetY = offset[1];
  if (positioning == ol.OverlayPositioning.BOTTOM_RIGHT ||
      positioning == ol.OverlayPositioning.CENTER_RIGHT ||
      positioning == ol.OverlayPositioning.TOP_RIGHT) {
    if (this.rendered_.left_ !== '') {
      this.rendered_.left_ = style.left = '';
    }
    var right = Math.round(mapSize[0] - pixel[0] - offsetX) + 'px';
    if (this.rendered_.right_ != right) {
      this.rendered_.right_ = style.right = right;
    }
  } else {
    if (this.rendered_.right_ !== '') {
      this.rendered_.right_ = style.right = '';
    }
    if (positioning == ol.OverlayPositioning.BOTTOM_CENTER ||
        positioning == ol.OverlayPositioning.CENTER_CENTER ||
        positioning == ol.OverlayPositioning.TOP_CENTER) {
      offsetX -= goog.style.getSize(this.element_).width / 2;
    }
    var left = Math.round(pixel[0] + offsetX) + 'px';
    if (this.rendered_.left_ != left) {
      this.rendered_.left_ = style.left = left;
    }
  }
  if (positioning == ol.OverlayPositioning.BOTTOM_LEFT ||
      positioning == ol.OverlayPositioning.BOTTOM_CENTER ||
      positioning == ol.OverlayPositioning.BOTTOM_RIGHT) {
    if (this.rendered_.top_ !== '') {
      this.rendered_.top_ = style.top = '';
    }
    var bottom = Math.round(mapSize[1] - pixel[1] - offsetY) + 'px';
    if (this.rendered_.bottom_ != bottom) {
      this.rendered_.bottom_ = style.bottom = bottom;
    }
  } else {
    if (this.rendered_.bottom_ !== '') {
      this.rendered_.bottom_ = style.bottom = '';
    }
    if (positioning == ol.OverlayPositioning.CENTER_LEFT ||
        positioning == ol.OverlayPositioning.CENTER_CENTER ||
        positioning == ol.OverlayPositioning.CENTER_RIGHT) {
      offsetY -= goog.style.getSize(this.element_).height / 2;
    }
    var top = Math.round(pixel[1] + offsetY) + 'px';
    if (this.rendered_.top_ != top) {
      this.rendered_.top_ = style.top = top;
    }
  }

  this.setVisible(true);
};

goog.provide('ol.control.OverviewMap');

goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.dom.classlist');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.math.Size');
goog.require('goog.style');
goog.require('ol');
goog.require('ol.Collection');
goog.require('ol.Map');
goog.require('ol.MapEventType');
goog.require('ol.Object');
goog.require('ol.ObjectEventType');
goog.require('ol.Overlay');
goog.require('ol.OverlayPositioning');
goog.require('ol.View');
goog.require('ol.ViewProperty');
goog.require('ol.control.Control');
goog.require('ol.coordinate');
goog.require('ol.css');
goog.require('ol.extent');


/**
 * Create a new control with a map acting as an overview map for an other
 * defined map.
 * @constructor
 * @extends {ol.control.Control}
 * @param {olx.control.OverviewMapOptions=} opt_options OverviewMap options.
 * @api
 */
ol.control.OverviewMap = function(opt_options) {

  var options = opt_options ? opt_options : {};

  /**
   * @type {boolean}
   * @private
   */
  this.collapsed_ = options.collapsed !== undefined ? options.collapsed : true;

  /**
   * @private
   * @type {boolean}
   */
  this.collapsible_ = options.collapsible !== undefined ?
      options.collapsible : true;

  if (!this.collapsible_) {
    this.collapsed_ = false;
  }

  var className = options.className ? options.className : 'ol-overviewmap';

  var tipLabel = options.tipLabel ? options.tipLabel : 'Overview map';

  var collapseLabel = options.collapseLabel ? options.collapseLabel : '\u00AB';

  /**
   * @private
   * @type {Node}
   */
  this.collapseLabel_ = goog.isString(collapseLabel) ?
      goog.dom.createDom('SPAN', {}, collapseLabel) :
      collapseLabel;

  var label = options.label ? options.label : '\u00BB';

  /**
   * @private
   * @type {Node}
   */
  this.label_ = goog.isString(label) ?
      goog.dom.createDom('SPAN', {}, label) :
      label;

  var activeLabel = (this.collapsible_ && !this.collapsed_) ?
      this.collapseLabel_ : this.label_;
  var button = goog.dom.createDom('BUTTON', {
    'type': 'button',
    'title': tipLabel
  }, activeLabel);

  goog.events.listen(button, goog.events.EventType.CLICK,
      this.handleClick_, false, this);

  var ovmapDiv = goog.dom.createDom('DIV', 'ol-overviewmap-map');

  /**
   * @type {ol.Map}
   * @private
   */
  this.ovmap_ = new ol.Map({
    controls: new ol.Collection(),
    interactions: new ol.Collection(),
    target: ovmapDiv,
    view: options.view
  });
  var ovmap = this.ovmap_;

  if (options.layers) {
    options.layers.forEach(
        /**
       * @param {ol.layer.Layer} layer Layer.
       */
        function(layer) {
          ovmap.addLayer(layer);
        }, this);
  }

  var box = goog.dom.createDom('DIV', 'ol-overviewmap-box');

  /**
   * @type {ol.Overlay}
   * @private
   */
  this.boxOverlay_ = new ol.Overlay({
    position: [0, 0],
    positioning: ol.OverlayPositioning.BOTTOM_LEFT,
    element: box
  });
  this.ovmap_.addOverlay(this.boxOverlay_);

  var cssClasses = className + ' ' + ol.css.CLASS_UNSELECTABLE + ' ' +
      ol.css.CLASS_CONTROL +
      (this.collapsed_ && this.collapsible_ ? ' ol-collapsed' : '') +
      (this.collapsible_ ? '' : ' ol-uncollapsible');
  var element = goog.dom.createDom('DIV',
      cssClasses, ovmapDiv, button);

  var render = options.render ? options.render : ol.control.OverviewMap.render;

  goog.base(this, {
    element: element,
    render: render,
    target: options.target
  });
};
goog.inherits(ol.control.OverviewMap, ol.control.Control);


/**
 * @inheritDoc
 * @api
 */
ol.control.OverviewMap.prototype.setMap = function(map) {
  var oldMap = this.getMap();
  if (map === oldMap) {
    return;
  }
  if (oldMap) {
    var oldView = oldMap.getView();
    if (oldView) {
      this.unbindView_(oldView);
    }
  }
  goog.base(this, 'setMap', map);

  if (map) {
    this.listenerKeys.push(goog.events.listen(
        map, ol.ObjectEventType.PROPERTYCHANGE,
        this.handleMapPropertyChange_, false, this));

    // TODO: to really support map switching, this would need to be reworked
    if (this.ovmap_.getLayers().getLength() === 0) {
      this.ovmap_.setLayerGroup(map.getLayerGroup());
    }

    var view = map.getView();
    if (view) {
      this.bindView_(view);
      if (view.isDef()) {
        this.ovmap_.updateSize();
        this.resetExtent_();
      }
    }
  }
};


/**
 * Handle map property changes.  This only deals with changes to the map's view.
 * @param {ol.ObjectEvent} event The propertychange event.
 * @private
 */
ol.control.OverviewMap.prototype.handleMapPropertyChange_ = function(event) {
  if (event.key === ol.MapProperty.VIEW) {
    var oldView = /** @type {ol.View} */ (event.oldValue);
    if (oldView) {
      this.unbindView_(oldView);
    }
    var newView = this.getMap().getView();
    this.bindView_(newView);
  }
};


/**
 * Register listeners for view property changes.
 * @param {ol.View} view The view.
 * @private
 */
ol.control.OverviewMap.prototype.bindView_ = function(view) {
  goog.events.listen(view,
      ol.Object.getChangeEventType(ol.ViewProperty.ROTATION),
      this.handleRotationChanged_, false, this);
};


/**
 * Unregister listeners for view property changes.
 * @param {ol.View} view The view.
 * @private
 */
ol.control.OverviewMap.prototype.unbindView_ = function(view) {
  goog.events.unlisten(view,
      ol.Object.getChangeEventType(ol.ViewProperty.ROTATION),
      this.handleRotationChanged_, false, this);
};


/**
 * Handle rotation changes to the main map.
 * TODO: This should rotate the extent rectrangle instead of the
 * overview map's view.
 * @private
 */
ol.control.OverviewMap.prototype.handleRotationChanged_ = function() {
  this.ovmap_.getView().setRotation(this.getMap().getView().getRotation());
};


/**
 * Update the overview map element.
 * @param {ol.MapEvent} mapEvent Map event.
 * @this {ol.control.OverviewMap}
 * @api
 */
ol.control.OverviewMap.render = function(mapEvent) {
  this.validateExtent_();
  this.updateBox_();
};


/**
 * Reset the overview map extent if the box size (width or
 * height) is less than the size of the overview map size times minRatio
 * or is greater than the size of the overview size times maxRatio.
 *
 * If the map extent was not reset, the box size can fits in the defined
 * ratio sizes. This method then checks if is contained inside the overview
 * map current extent. If not, recenter the overview map to the current
 * main map center location.
 * @private
 */
ol.control.OverviewMap.prototype.validateExtent_ = function() {
  var map = this.getMap();
  var ovmap = this.ovmap_;

  if (!map.isRendered() || !ovmap.isRendered()) {
    return;
  }

  var mapSize = map.getSize();
  goog.asserts.assertArray(mapSize, 'mapSize should be an array');

  var view = map.getView();
  goog.asserts.assert(view, 'view should be defined');
  var extent = view.calculateExtent(mapSize);

  var ovmapSize = ovmap.getSize();
  goog.asserts.assertArray(ovmapSize, 'ovmapSize should be an array');

  var ovview = ovmap.getView();
  goog.asserts.assert(ovview, 'ovview should be defined');
  var ovextent = ovview.calculateExtent(ovmapSize);

  var topLeftPixel =
      ovmap.getPixelFromCoordinate(ol.extent.getTopLeft(extent));
  var bottomRightPixel =
      ovmap.getPixelFromCoordinate(ol.extent.getBottomRight(extent));
  var boxSize = new goog.math.Size(
      Math.abs(topLeftPixel[0] - bottomRightPixel[0]),
      Math.abs(topLeftPixel[1] - bottomRightPixel[1]));

  var ovmapWidth = ovmapSize[0];
  var ovmapHeight = ovmapSize[1];

  if (boxSize.width < ovmapWidth * ol.OVERVIEWMAP_MIN_RATIO ||
      boxSize.height < ovmapHeight * ol.OVERVIEWMAP_MIN_RATIO ||
      boxSize.width > ovmapWidth * ol.OVERVIEWMAP_MAX_RATIO ||
      boxSize.height > ovmapHeight * ol.OVERVIEWMAP_MAX_RATIO) {
    this.resetExtent_();
  } else if (!ol.extent.containsExtent(ovextent, extent)) {
    this.recenter_();
  }
};


/**
 * Reset the overview map extent to half calculated min and max ratio times
 * the extent of the main map.
 * @private
 */
ol.control.OverviewMap.prototype.resetExtent_ = function() {
  if (ol.OVERVIEWMAP_MAX_RATIO === 0 || ol.OVERVIEWMAP_MIN_RATIO === 0) {
    return;
  }

  var map = this.getMap();
  var ovmap = this.ovmap_;

  var mapSize = map.getSize();
  goog.asserts.assertArray(mapSize, 'mapSize should be an array');

  var view = map.getView();
  goog.asserts.assert(view, 'view should be defined');
  var extent = view.calculateExtent(mapSize);

  var ovmapSize = ovmap.getSize();
  goog.asserts.assertArray(ovmapSize, 'ovmapSize should be an array');

  var ovview = ovmap.getView();
  goog.asserts.assert(ovview, 'ovview should be defined');

  // get how many times the current map overview could hold different
  // box sizes using the min and max ratio, pick the step in the middle used
  // to calculate the extent from the main map to set it to the overview map,
  var steps = Math.log(
      ol.OVERVIEWMAP_MAX_RATIO / ol.OVERVIEWMAP_MIN_RATIO) / Math.LN2;
  var ratio = 1 / (Math.pow(2, steps / 2) * ol.OVERVIEWMAP_MIN_RATIO);
  ol.extent.scaleFromCenter(extent, ratio);
  ovview.fit(extent, ovmapSize);
};


/**
 * Set the center of the overview map to the map center without changing its
 * resolution.
 * @private
 */
ol.control.OverviewMap.prototype.recenter_ = function() {
  var map = this.getMap();
  var ovmap = this.ovmap_;

  var view = map.getView();
  goog.asserts.assert(view, 'view should be defined');

  var ovview = ovmap.getView();
  goog.asserts.assert(ovview, 'ovview should be defined');

  ovview.setCenter(view.getCenter());
};


/**
 * Update the box using the main map extent
 * @private
 */
ol.control.OverviewMap.prototype.updateBox_ = function() {
  var map = this.getMap();
  var ovmap = this.ovmap_;

  if (!map.isRendered() || !ovmap.isRendered()) {
    return;
  }

  var mapSize = map.getSize();
  goog.asserts.assertArray(mapSize, 'mapSize should be an array');

  var view = map.getView();
  goog.asserts.assert(view, 'view should be defined');

  var ovview = ovmap.getView();
  goog.asserts.assert(ovview, 'ovview should be defined');

  var ovmapSize = ovmap.getSize();
  goog.asserts.assertArray(ovmapSize, 'ovmapSize should be an array');

  var rotation = view.getRotation();
  goog.asserts.assert(rotation !== undefined, 'rotation should be defined');

  var overlay = this.boxOverlay_;
  var box = this.boxOverlay_.getElement();
  var extent = view.calculateExtent(mapSize);
  var ovresolution = ovview.getResolution();
  var bottomLeft = ol.extent.getBottomLeft(extent);
  var topRight = ol.extent.getTopRight(extent);

  // set position using bottom left coordinates
  var rotateBottomLeft = this.calculateCoordinateRotate_(rotation, bottomLeft);
  overlay.setPosition(rotateBottomLeft);

  // set box size calculated from map extent size and overview map resolution
  if (box) {
    var boxWidth = Math.abs((bottomLeft[0] - topRight[0]) / ovresolution);
    var boxHeight = Math.abs((topRight[1] - bottomLeft[1]) / ovresolution);
    goog.style.setBorderBoxSize(box, new goog.math.Size(
        boxWidth, boxHeight));
  }
};


/**
 * @param {number} rotation Target rotation.
 * @param {ol.Coordinate} coordinate Coordinate.
 * @return {ol.Coordinate|undefined} Coordinate for rotation and center anchor.
 * @private
 */
ol.control.OverviewMap.prototype.calculateCoordinateRotate_ = function(
    rotation, coordinate) {
  var coordinateRotate;

  var map = this.getMap();
  var view = map.getView();
  goog.asserts.assert(view, 'view should be defined');

  var currentCenter = view.getCenter();

  if (currentCenter) {
    coordinateRotate = [
      coordinate[0] - currentCenter[0],
      coordinate[1] - currentCenter[1]
    ];
    ol.coordinate.rotate(coordinateRotate, rotation);
    ol.coordinate.add(coordinateRotate, currentCenter);
  }
  return coordinateRotate;
};


/**
 * @param {goog.events.BrowserEvent} event The event to handle
 * @private
 */
ol.control.OverviewMap.prototype.handleClick_ = function(event) {
  event.preventDefault();
  this.handleToggle_();
};


/**
 * @private
 */
ol.control.OverviewMap.prototype.handleToggle_ = function() {
  goog.dom.classlist.toggle(this.element, 'ol-collapsed');
  if (this.collapsed_) {
    goog.dom.replaceNode(this.collapseLabel_, this.label_);
  } else {
    goog.dom.replaceNode(this.label_, this.collapseLabel_);
  }
  this.collapsed_ = !this.collapsed_;

  // manage overview map if it had not been rendered before and control
  // is expanded
  var ovmap = this.ovmap_;
  if (!this.collapsed_ && !ovmap.isRendered()) {
    ovmap.updateSize();
    this.resetExtent_();
    goog.events.listenOnce(ovmap, ol.MapEventType.POSTRENDER,
        function(event) {
          this.updateBox_();
        },
        false, this);
  }
};


/**
 * Return `true` if the overview map is collapsible, `false` otherwise.
 * @return {boolean} True if the widget is collapsible.
 * @api stable
 */
ol.control.OverviewMap.prototype.getCollapsible = function() {
  return this.collapsible_;
};


/**
 * Set whether the overview map should be collapsible.
 * @param {boolean} collapsible True if the widget is collapsible.
 * @api stable
 */
ol.control.OverviewMap.prototype.setCollapsible = function(collapsible) {
  if (this.collapsible_ === collapsible) {
    return;
  }
  this.collapsible_ = collapsible;
  goog.dom.classlist.toggle(this.element, 'ol-uncollapsible');
  if (!collapsible && this.collapsed_) {
    this.handleToggle_();
  }
};


/**
 * Collapse or expand the overview map according to the passed parameter. Will
 * not do anything if the overview map isn't collapsible or if the current
 * collapsed state is already the one requested.
 * @param {boolean} collapsed True if the widget is collapsed.
 * @api stable
 */
ol.control.OverviewMap.prototype.setCollapsed = function(collapsed) {
  if (!this.collapsible_ || this.collapsed_ === collapsed) {
    return;
  }
  this.handleToggle_();
};


/**
 * Determine if the overview map is collapsed.
 * @return {boolean} The overview map is collapsed.
 * @api stable
 */
ol.control.OverviewMap.prototype.getCollapsed = function() {
  return this.collapsed_;
};


/**
 * Return the overview map.
 * @return {ol.Map} Overview map.
 * @api
 */
ol.control.OverviewMap.prototype.getOverviewMap = function() {
  return this.ovmap_;
};

goog.provide('ol.control.ScaleLine');
goog.provide('ol.control.ScaleLineProperty');
goog.provide('ol.control.ScaleLineUnits');

goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.style');
goog.require('ol');
goog.require('ol.Object');
goog.require('ol.control.Control');
goog.require('ol.css');
goog.require('ol.proj.METERS_PER_UNIT');
goog.require('ol.proj.Units');


/**
 * @enum {string}
 */
ol.control.ScaleLineProperty = {
  UNITS: 'units'
};


/**
 * Units for the scale line. Supported values are `'degrees'`, `'imperial'`,
 * `'nautical'`, `'metric'`, `'us'`.
 * @enum {string}
 * @api stable
 */
ol.control.ScaleLineUnits = {
  DEGREES: 'degrees',
  IMPERIAL: 'imperial',
  NAUTICAL: 'nautical',
  METRIC: 'metric',
  US: 'us'
};


/**
 * @classdesc
 * A control displaying rough x-axis distances, calculated for the center of the
 * viewport.
 * No scale line will be shown when the x-axis distance cannot be calculated in
 * the view projection (e.g. at or beyond the poles in EPSG:4326).
 * By default the scale line will show in the bottom left portion of the map,
 * but this can be changed by using the css selector `.ol-scale-line`.
 *
 * @constructor
 * @extends {ol.control.Control}
 * @param {olx.control.ScaleLineOptions=} opt_options Scale line options.
 * @api stable
 */
ol.control.ScaleLine = function(opt_options) {

  var options = opt_options ? opt_options : {};

  var className = options.className ? options.className : 'ol-scale-line';

  /**
   * @private
   * @type {Element}
   */
  this.innerElement_ = goog.dom.createDom('DIV',
      className + '-inner');

  /**
   * @private
   * @type {Element}
   */
  this.element_ = goog.dom.createDom('DIV',
      className + ' ' + ol.css.CLASS_UNSELECTABLE, this.innerElement_);

  /**
   * @private
   * @type {?olx.ViewState}
   */
  this.viewState_ = null;

  /**
   * @private
   * @type {number}
   */
  this.minWidth_ = options.minWidth !== undefined ? options.minWidth : 64;

  /**
   * @private
   * @type {boolean}
   */
  this.renderedVisible_ = false;

  /**
   * @private
   * @type {number|undefined}
   */
  this.renderedWidth_ = undefined;

  /**
   * @private
   * @type {string}
   */
  this.renderedHTML_ = '';

  var render = options.render ? options.render : ol.control.ScaleLine.render;

  goog.base(this, {
    element: this.element_,
    render: render,
    target: options.target
  });

  goog.events.listen(
      this, ol.Object.getChangeEventType(ol.control.ScaleLineProperty.UNITS),
      this.handleUnitsChanged_, false, this);

  this.setUnits(/** @type {ol.control.ScaleLineUnits} */ (options.units) ||
      ol.control.ScaleLineUnits.METRIC);

};
goog.inherits(ol.control.ScaleLine, ol.control.Control);


/**
 * @const
 * @type {Array.<number>}
 */
ol.control.ScaleLine.LEADING_DIGITS = [1, 2, 5];


/**
 * Return the units to use in the scale line.
 * @return {ol.control.ScaleLineUnits|undefined} The units to use in the scale
 *     line.
 * @observable
 * @api stable
 */
ol.control.ScaleLine.prototype.getUnits = function() {
  return /** @type {ol.control.ScaleLineUnits|undefined} */ (
      this.get(ol.control.ScaleLineProperty.UNITS));
};


/**
 * Update the scale line element.
 * @param {ol.MapEvent} mapEvent Map event.
 * @this {ol.control.ScaleLine}
 * @api
 */
ol.control.ScaleLine.render = function(mapEvent) {
  var frameState = mapEvent.frameState;
  if (!frameState) {
    this.viewState_ = null;
  } else {
    this.viewState_ = frameState.viewState;
  }
  this.updateElement_();
};


/**
 * @private
 */
ol.control.ScaleLine.prototype.handleUnitsChanged_ = function() {
  this.updateElement_();
};


/**
 * Set the units to use in the scale line.
 * @param {ol.control.ScaleLineUnits} units The units to use in the scale line.
 * @observable
 * @api stable
 */
ol.control.ScaleLine.prototype.setUnits = function(units) {
  this.set(ol.control.ScaleLineProperty.UNITS, units);
};


/**
 * @private
 */
ol.control.ScaleLine.prototype.updateElement_ = function() {
  var viewState = this.viewState_;

  if (!viewState) {
    if (this.renderedVisible_) {
      goog.style.setElementShown(this.element_, false);
      this.renderedVisible_ = false;
    }
    return;
  }

  var center = viewState.center;
  var projection = viewState.projection;
  var metersPerUnit = projection.getMetersPerUnit();
  var pointResolution =
      projection.getPointResolution(viewState.resolution, center) *
      metersPerUnit;

  var nominalCount = this.minWidth_ * pointResolution;
  var suffix = '';
  var units = this.getUnits();
  if (units == ol.control.ScaleLineUnits.DEGREES) {
    var metersPerDegree = ol.proj.METERS_PER_UNIT[ol.proj.Units.DEGREES];
    pointResolution /= metersPerDegree;
    if (nominalCount < metersPerDegree / 60) {
      suffix = '\u2033'; // seconds
      pointResolution *= 3600;
    } else if (nominalCount < metersPerDegree) {
      suffix = '\u2032'; // minutes
      pointResolution *= 60;
    } else {
      suffix = '\u00b0'; // degrees
    }
  } else if (units == ol.control.ScaleLineUnits.IMPERIAL) {
    if (nominalCount < 0.9144) {
      suffix = 'in';
      pointResolution /= 0.0254;
    } else if (nominalCount < 1609.344) {
      suffix = 'ft';
      pointResolution /= 0.3048;
    } else {
      suffix = 'mi';
      pointResolution /= 1609.344;
    }
  } else if (units == ol.control.ScaleLineUnits.NAUTICAL) {
    pointResolution /= 1852;
    suffix = 'nm';
  } else if (units == ol.control.ScaleLineUnits.METRIC) {
    if (nominalCount < 1) {
      suffix = 'mm';
      pointResolution *= 1000;
    } else if (nominalCount < 1000) {
      suffix = 'm';
    } else {
      suffix = 'km';
      pointResolution /= 1000;
    }
  } else if (units == ol.control.ScaleLineUnits.US) {
    if (nominalCount < 0.9144) {
      suffix = 'in';
      pointResolution *= 39.37;
    } else if (nominalCount < 1609.344) {
      suffix = 'ft';
      pointResolution /= 0.30480061;
    } else {
      suffix = 'mi';
      pointResolution /= 1609.3472;
    }
  } else {
    goog.asserts.fail('Scale line element cannot be updated');
  }

  var i = 3 * Math.floor(
      Math.log(this.minWidth_ * pointResolution) / Math.log(10));
  var count, width;
  while (true) {
    count = ol.control.ScaleLine.LEADING_DIGITS[((i % 3) + 3) % 3] *
        Math.pow(10, Math.floor(i / 3));
    width = Math.round(count / pointResolution);
    if (isNaN(width)) {
      goog.style.setElementShown(this.element_, false);
      this.renderedVisible_ = false;
      return;
    } else if (width >= this.minWidth_) {
      break;
    }
    ++i;
  }

  var html = count + ' ' + suffix;
  if (this.renderedHTML_ != html) {
    this.innerElement_.innerHTML = html;
    this.renderedHTML_ = html;
  }

  if (this.renderedWidth_ != width) {
    this.innerElement_.style.width = width + 'px';
    this.renderedWidth_ = width;
  }

  if (!this.renderedVisible_) {
    goog.style.setElementShown(this.element_, true);
    this.renderedVisible_ = true;
  }

};

// Copyright 2005 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Class to create objects which want to handle multiple events
 * and have their listeners easily cleaned up via a dispose method.
 *
 * Example:
 * <pre>
 * function Something() {
 *   Something.base(this);
 *
 *   ... set up object ...
 *
 *   // Add event listeners
 *   this.listen(this.starEl, goog.events.EventType.CLICK, this.handleStar);
 *   this.listen(this.headerEl, goog.events.EventType.CLICK, this.expand);
 *   this.listen(this.collapseEl, goog.events.EventType.CLICK, this.collapse);
 *   this.listen(this.infoEl, goog.events.EventType.MOUSEOVER, this.showHover);
 *   this.listen(this.infoEl, goog.events.EventType.MOUSEOUT, this.hideHover);
 * }
 * goog.inherits(Something, goog.events.EventHandler);
 *
 * Something.prototype.disposeInternal = function() {
 *   Something.base(this, 'disposeInternal');
 *   goog.dom.removeNode(this.container);
 * };
 *
 *
 * // Then elsewhere:
 *
 * var activeSomething = null;
 * function openSomething() {
 *   activeSomething = new Something();
 * }
 *
 * function closeSomething() {
 *   if (activeSomething) {
 *     activeSomething.dispose();  // Remove event listeners
 *     activeSomething = null;
 *   }
 * }
 * </pre>
 *
 */

goog.provide('goog.events.EventHandler');

goog.require('goog.Disposable');
goog.require('goog.events');
goog.require('goog.object');

goog.forwardDeclare('goog.events.EventWrapper');



/**
 * Super class for objects that want to easily manage a number of event
 * listeners.  It allows a short cut to listen and also provides a quick way
 * to remove all events listeners belonging to this object.
 * @param {SCOPE=} opt_scope Object in whose scope to call the listeners.
 * @constructor
 * @extends {goog.Disposable}
 * @template SCOPE
 */
goog.events.EventHandler = function(opt_scope) {
  goog.Disposable.call(this);
  // TODO(mknichel): Rename this to this.scope_ and fix the classes in google3
  // that access this private variable. :(
  this.handler_ = opt_scope;

  /**
   * Keys for events that are being listened to.
   * @type {!Object<!goog.events.Key>}
   * @private
   */
  this.keys_ = {};
};
goog.inherits(goog.events.EventHandler, goog.Disposable);


/**
 * Utility array used to unify the cases of listening for an array of types
 * and listening for a single event, without using recursion or allocating
 * an array each time.
 * @type {!Array<string>}
 * @const
 * @private
 */
goog.events.EventHandler.typeArray_ = [];


/**
 * Listen to an event on a Listenable.  If the function is omitted then the
 * EventHandler's handleEvent method will be used.
 * @param {goog.events.ListenableType} src Event source.
 * @param {string|Array<string>|
 *     !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
 *     type Event type to listen for or array of event types.
 * @param {function(this:SCOPE, EVENTOBJ):?|{handleEvent:function(?):?}|null=}
 *     opt_fn Optional callback function to be used as the listener or an object
 *     with handleEvent function.
 * @param {boolean=} opt_capture Optional whether to use capture phase.
 * @return {!goog.events.EventHandler<SCOPE>} This object, allowing for
 *     chaining of calls.
 * @template EVENTOBJ
 */
goog.events.EventHandler.prototype.listen = function(
    src, type, opt_fn, opt_capture) {
  return this.listen_(src, type, opt_fn, opt_capture);
};


/**
 * Listen to an event on a Listenable.  If the function is omitted then the
 * EventHandler's handleEvent method will be used.
 * @param {goog.events.ListenableType} src Event source.
 * @param {string|Array<string>|
 *     !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
 *     type Event type to listen for or array of event types.
 * @param {function(this:T, EVENTOBJ):?|{handleEvent:function(this:T, ?):?}|
 *     null|undefined} fn Optional callback function to be used as the
 *     listener or an object with handleEvent function.
 * @param {boolean|undefined} capture Optional whether to use capture phase.
 * @param {T} scope Object in whose scope to call the listener.
 * @return {!goog.events.EventHandler<SCOPE>} This object, allowing for
 *     chaining of calls.
 * @template T,EVENTOBJ
 */
goog.events.EventHandler.prototype.listenWithScope = function(
    src, type, fn, capture, scope) {
  // TODO(mknichel): Deprecate this function.
  return this.listen_(src, type, fn, capture, scope);
};


/**
 * Listen to an event on a Listenable.  If the function is omitted then the
 * EventHandler's handleEvent method will be used.
 * @param {goog.events.ListenableType} src Event source.
 * @param {string|Array<string>|
 *     !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
 *     type Event type to listen for or array of event types.
 * @param {function(EVENTOBJ):?|{handleEvent:function(?):?}|null=} opt_fn
 *     Optional callback function to be used as the listener or an object with
 *     handleEvent function.
 * @param {boolean=} opt_capture Optional whether to use capture phase.
 * @param {Object=} opt_scope Object in whose scope to call the listener.
 * @return {!goog.events.EventHandler<SCOPE>} This object, allowing for
 *     chaining of calls.
 * @template EVENTOBJ
 * @private
 */
goog.events.EventHandler.prototype.listen_ = function(src, type, opt_fn,
                                                      opt_capture,
                                                      opt_scope) {
  if (!goog.isArray(type)) {
    if (type) {
      goog.events.EventHandler.typeArray_[0] = type.toString();
    }
    type = goog.events.EventHandler.typeArray_;
  }
  for (var i = 0; i < type.length; i++) {
    var listenerObj = goog.events.listen(
        src, type[i], opt_fn || this.handleEvent,
        opt_capture || false,
        opt_scope || this.handler_ || this);

    if (!listenerObj) {
      // When goog.events.listen run on OFF_AND_FAIL or OFF_AND_SILENT
      // (goog.events.CaptureSimulationMode) in IE8-, it will return null
      // value.
      return this;
    }

    var key = listenerObj.key;
    this.keys_[key] = listenerObj;
  }

  return this;
};


/**
 * Listen to an event on a Listenable.  If the function is omitted, then the
 * EventHandler's handleEvent method will be used. After the event has fired the
 * event listener is removed from the target. If an array of event types is
 * provided, each event type will be listened to once.
 * @param {goog.events.ListenableType} src Event source.
 * @param {string|Array<string>|
 *     !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
 *     type Event type to listen for or array of event types.
 * @param {function(this:SCOPE, EVENTOBJ):?|{handleEvent:function(?):?}|null=} opt_fn
 *    Optional callback function to be used as the listener or an object with
 *    handleEvent function.
 * @param {boolean=} opt_capture Optional whether to use capture phase.
 * @return {!goog.events.EventHandler<SCOPE>} This object, allowing for
 *     chaining of calls.
 * @template EVENTOBJ
 */
goog.events.EventHandler.prototype.listenOnce = function(
    src, type, opt_fn, opt_capture) {
  return this.listenOnce_(src, type, opt_fn, opt_capture);
};


/**
 * Listen to an event on a Listenable.  If the function is omitted, then the
 * EventHandler's handleEvent method will be used. After the event has fired the
 * event listener is removed from the target. If an array of event types is
 * provided, each event type will be listened to once.
 * @param {goog.events.ListenableType} src Event source.
 * @param {string|Array<string>|
 *     !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
 *     type Event type to listen for or array of event types.
 * @param {function(this:T, EVENTOBJ):?|{handleEvent:function(this:T, ?):?}|
 *     null|undefined} fn Optional callback function to be used as the
 *     listener or an object with handleEvent function.
 * @param {boolean|undefined} capture Optional whether to use capture phase.
 * @param {T} scope Object in whose scope to call the listener.
 * @return {!goog.events.EventHandler<SCOPE>} This object, allowing for
 *     chaining of calls.
 * @template T,EVENTOBJ
 */
goog.events.EventHandler.prototype.listenOnceWithScope = function(
    src, type, fn, capture, scope) {
  // TODO(mknichel): Deprecate this function.
  return this.listenOnce_(src, type, fn, capture, scope);
};


/**
 * Listen to an event on a Listenable.  If the function is omitted, then the
 * EventHandler's handleEvent method will be used. After the event has fired
 * the event listener is removed from the target. If an array of event types is
 * provided, each event type will be listened to once.
 * @param {goog.events.ListenableType} src Event source.
 * @param {string|Array<string>|
 *     !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
 *     type Event type to listen for or array of event types.
 * @param {function(EVENTOBJ):?|{handleEvent:function(?):?}|null=} opt_fn
 *    Optional callback function to be used as the listener or an object with
 *    handleEvent function.
 * @param {boolean=} opt_capture Optional whether to use capture phase.
 * @param {Object=} opt_scope Object in whose scope to call the listener.
 * @return {!goog.events.EventHandler<SCOPE>} This object, allowing for
 *     chaining of calls.
 * @template EVENTOBJ
 * @private
 */
goog.events.EventHandler.prototype.listenOnce_ = function(
    src, type, opt_fn, opt_capture, opt_scope) {
  if (goog.isArray(type)) {
    for (var i = 0; i < type.length; i++) {
      this.listenOnce_(src, type[i], opt_fn, opt_capture, opt_scope);
    }
  } else {
    var listenerObj = goog.events.listenOnce(
        src, type, opt_fn || this.handleEvent, opt_capture,
        opt_scope || this.handler_ || this);
    if (!listenerObj) {
      // When goog.events.listen run on OFF_AND_FAIL or OFF_AND_SILENT
      // (goog.events.CaptureSimulationMode) in IE8-, it will return null
      // value.
      return this;
    }

    var key = listenerObj.key;
    this.keys_[key] = listenerObj;
  }

  return this;
};


/**
 * Adds an event listener with a specific event wrapper on a DOM Node or an
 * object that has implemented {@link goog.events.EventTarget}. A listener can
 * only be added once to an object.
 *
 * @param {EventTarget|goog.events.EventTarget} src The node to listen to
 *     events on.
 * @param {goog.events.EventWrapper} wrapper Event wrapper to use.
 * @param {function(this:SCOPE, ?):?|{handleEvent:function(?):?}|null} listener
 *     Callback method, or an object with a handleEvent function.
 * @param {boolean=} opt_capt Whether to fire in capture phase (defaults to
 *     false).
 * @return {!goog.events.EventHandler<SCOPE>} This object, allowing for
 *     chaining of calls.
 */
goog.events.EventHandler.prototype.listenWithWrapper = function(
    src, wrapper, listener, opt_capt) {
  // TODO(mknichel): Remove the opt_scope from this function and then
  // templatize it.
  return this.listenWithWrapper_(src, wrapper, listener, opt_capt);
};


/**
 * Adds an event listener with a specific event wrapper on a DOM Node or an
 * object that has implemented {@link goog.events.EventTarget}. A listener can
 * only be added once to an object.
 *
 * @param {EventTarget|goog.events.EventTarget} src The node to listen to
 *     events on.
 * @param {goog.events.EventWrapper} wrapper Event wrapper to use.
 * @param {function(this:T, ?):?|{handleEvent:function(this:T, ?):?}|null}
 *     listener Optional callback function to be used as the
 *     listener or an object with handleEvent function.
 * @param {boolean|undefined} capture Optional whether to use capture phase.
 * @param {T} scope Object in whose scope to call the listener.
 * @return {!goog.events.EventHandler<SCOPE>} This object, allowing for
 *     chaining of calls.
 * @template T
 */
goog.events.EventHandler.prototype.listenWithWrapperAndScope = function(
    src, wrapper, listener, capture, scope) {
  // TODO(mknichel): Deprecate this function.
  return this.listenWithWrapper_(src, wrapper, listener, capture, scope);
};


/**
 * Adds an event listener with a specific event wrapper on a DOM Node or an
 * object that has implemented {@link goog.events.EventTarget}. A listener can
 * only be added once to an object.
 *
 * @param {EventTarget|goog.events.EventTarget} src The node to listen to
 *     events on.
 * @param {goog.events.EventWrapper} wrapper Event wrapper to use.
 * @param {function(?):?|{handleEvent:function(?):?}|null} listener Callback
 *     method, or an object with a handleEvent function.
 * @param {boolean=} opt_capt Whether to fire in capture phase (defaults to
 *     false).
 * @param {Object=} opt_scope Element in whose scope to call the listener.
 * @return {!goog.events.EventHandler<SCOPE>} This object, allowing for
 *     chaining of calls.
 * @private
 */
goog.events.EventHandler.prototype.listenWithWrapper_ = function(
    src, wrapper, listener, opt_capt, opt_scope) {
  wrapper.listen(src, listener, opt_capt, opt_scope || this.handler_ || this,
                 this);
  return this;
};


/**
 * @return {number} Number of listeners registered by this handler.
 */
goog.events.EventHandler.prototype.getListenerCount = function() {
  var count = 0;
  for (var key in this.keys_) {
    if (Object.prototype.hasOwnProperty.call(this.keys_, key)) {
      count++;
    }
  }
  return count;
};


/**
 * Unlistens on an event.
 * @param {goog.events.ListenableType} src Event source.
 * @param {string|Array<string>|
 *     !goog.events.EventId<EVENTOBJ>|!Array<!goog.events.EventId<EVENTOBJ>>}
 *     type Event type or array of event types to unlisten to.
 * @param {function(EVENTOBJ):?|{handleEvent:function(?):?}|null=} opt_fn
 *     Optional callback function to be used as the listener or an object with
 *     handleEvent function.
 * @param {boolean=} opt_capture Optional whether to use capture phase.
 * @param {Object=} opt_scope Object in whose scope to call the listener.
 * @return {!goog.events.EventHandler} This object, allowing for chaining of
 *     calls.
 * @template EVENTOBJ
 */
goog.events.EventHandler.prototype.unlisten = function(src, type, opt_fn,
                                                       opt_capture,
                                                       opt_scope) {
  if (goog.isArray(type)) {
    for (var i = 0; i < type.length; i++) {
      this.unlisten(src, type[i], opt_fn, opt_capture, opt_scope);
    }
  } else {
    var listener = goog.events.getListener(src, type,
        opt_fn || this.handleEvent,
        opt_capture, opt_scope || this.handler_ || this);

    if (listener) {
      goog.events.unlistenByKey(listener);
      delete this.keys_[listener.key];
    }
  }

  return this;
};


/**
 * Removes an event listener which was added with listenWithWrapper().
 *
 * @param {EventTarget|goog.events.EventTarget} src The target to stop
 *     listening to events on.
 * @param {goog.events.EventWrapper} wrapper Event wrapper to use.
 * @param {function(?):?|{handleEvent:function(?):?}|null} listener The
 *     listener function to remove.
 * @param {boolean=} opt_capt In DOM-compliant browsers, this determines
 *     whether the listener is fired during the capture or bubble phase of the
 *     event.
 * @param {Object=} opt_scope Element in whose scope to call the listener.
 * @return {!goog.events.EventHandler} This object, allowing for chaining of
 *     calls.
 */
goog.events.EventHandler.prototype.unlistenWithWrapper = function(src, wrapper,
    listener, opt_capt, opt_scope) {
  wrapper.unlisten(src, listener, opt_capt,
                   opt_scope || this.handler_ || this, this);
  return this;
};


/**
 * Unlistens to all events.
 */
goog.events.EventHandler.prototype.removeAll = function() {
  goog.object.forEach(this.keys_, function(listenerObj, key) {
    if (this.keys_.hasOwnProperty(key)) {
      goog.events.unlistenByKey(listenerObj);
    }
  }, this);

  this.keys_ = {};
};


/**
 * Disposes of this EventHandler and removes all listeners that it registered.
 * @override
 * @protected
 */
goog.events.EventHandler.prototype.disposeInternal = function() {
  goog.events.EventHandler.superClass_.disposeInternal.call(this);
  this.removeAll();
};


/**
 * Default event handler
 * @param {goog.events.Event} e Event object.
 */
goog.events.EventHandler.prototype.handleEvent = function(e) {
  throw Error('EventHandler.handleEvent not implemented');
};

// Copyright 2012 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Bidi utility functions.
 *
 */

goog.provide('goog.style.bidi');

goog.require('goog.dom');
goog.require('goog.style');
goog.require('goog.userAgent');


/**
 * Returns the normalized scrollLeft position for a scrolled element.
 * @param {Element} element The scrolled element.
 * @return {number} The number of pixels the element is scrolled. 0 indicates
 *     that the element is not scrolled at all (which, in general, is the
 *     left-most position in ltr and the right-most position in rtl).
 */
goog.style.bidi.getScrollLeft = function(element) {
  var isRtl = goog.style.isRightToLeft(element);
  if (isRtl && goog.userAgent.GECKO) {
    // ScrollLeft starts at 0 and then goes negative as the element is scrolled
    // towards the left.
    return -element.scrollLeft;
  } else if (isRtl &&
             !(goog.userAgent.IE && goog.userAgent.isVersionOrHigher('8'))) {
    // ScrollLeft starts at the maximum positive value and decreases towards
    // 0 as the element is scrolled towards the left. However, for overflow
    // visible, there is no scrollLeft and the value always stays correctly at 0
    var overflowX = goog.style.getComputedOverflowX(element);
    if (overflowX == 'visible') {
      return element.scrollLeft;
    } else {
      return element.scrollWidth - element.clientWidth - element.scrollLeft;
    }
  }
  // ScrollLeft behavior is identical in rtl and ltr, it starts at 0 and
  // increases as the element is scrolled away from the start.
  return element.scrollLeft;
};


/**
 * Returns the "offsetStart" of an element, analagous to offsetLeft but
 * normalized for right-to-left environments and various browser
 * inconsistencies. This value returned can always be passed to setScrollOffset
 * to scroll to an element's left edge in a left-to-right offsetParent or
 * right edge in a right-to-left offsetParent.
 *
 * For example, here offsetStart is 10px in an LTR environment and 5px in RTL:
 *
 * <pre>
 * |          xxxxxxxxxx     |
 *  ^^^^^^^^^^   ^^^^   ^^^^^
 *     10px      elem    5px
 * </pre>
 *
 * If an element is positioned before the start of its offsetParent, the
 * startOffset may be negative.  This can be used with setScrollOffset to
 * reliably scroll to an element:
 *
 * <pre>
 * var scrollOffset = goog.style.bidi.getOffsetStart(element);
 * goog.style.bidi.setScrollOffset(element.offsetParent, scrollOffset);
 * </pre>
 *
 * @see setScrollOffset
 *
 * @param {Element} element The element for which we need to determine the
 *     offsetStart position.
 * @return {number} The offsetStart for that element.
 */
goog.style.bidi.getOffsetStart = function(element) {
  element = /** @type {!HTMLElement} */ (element);
  var offsetLeftForReal = element.offsetLeft;

  // The element might not have an offsetParent.
  // For example, the node might not be attached to the DOM tree,
  // and position:fixed children do not have an offset parent.
  // Just try to do the best we can with what we have.
  var bestParent = element.offsetParent;

  if (!bestParent && goog.style.getComputedPosition(element) == 'fixed') {
    bestParent = goog.dom.getOwnerDocument(element).documentElement;
  }

  // Just give up in this case.
  if (!bestParent) {
    return offsetLeftForReal;
  }

  if (goog.userAgent.GECKO) {
    // When calculating an element's offsetLeft, Firefox erroneously subtracts
    // the border width from the actual distance.  So we need to add it back.
    var borderWidths = goog.style.getBorderBox(bestParent);
    offsetLeftForReal += borderWidths.left;
  } else if (goog.userAgent.isDocumentModeOrHigher(8) &&
             !goog.userAgent.isDocumentModeOrHigher(9)) {
    // When calculating an element's offsetLeft, IE8/9-Standards Mode
    // erroneously adds the border width to the actual distance.  So we need to
    // subtract it.
    var borderWidths = goog.style.getBorderBox(bestParent);
    offsetLeftForReal -= borderWidths.left;
  }

  if (goog.style.isRightToLeft(bestParent)) {
    // Right edge of the element relative to the left edge of its parent.
    var elementRightOffset = offsetLeftForReal + element.offsetWidth;

    // Distance from the parent's right edge to the element's right edge.
    return bestParent.clientWidth - elementRightOffset;
  }

  return offsetLeftForReal;
};


/**
 * Sets the element's scrollLeft attribute so it is correctly scrolled by
 * offsetStart pixels.  This takes into account whether the element is RTL and
 * the nuances of different browsers.  To scroll to the "beginning" of an
 * element use getOffsetStart to obtain the element's offsetStart value and then
 * pass the value to setScrollOffset.
 * @see getOffsetStart
 * @param {Element} element The element to set scrollLeft on.
 * @param {number} offsetStart The number of pixels to scroll the element.
 *     If this value is < 0, 0 is used.
 */
goog.style.bidi.setScrollOffset = function(element, offsetStart) {
  offsetStart = Math.max(offsetStart, 0);
  // In LTR and in "mirrored" browser RTL (such as IE), we set scrollLeft to
  // the number of pixels to scroll.
  // Otherwise, in RTL, we need to account for different browser behavior.
  if (!goog.style.isRightToLeft(element)) {
    element.scrollLeft = offsetStart;
  } else if (goog.userAgent.GECKO) {
    // Negative scroll-left positions in RTL.
    element.scrollLeft = -offsetStart;
  } else if (!(goog.userAgent.IE && goog.userAgent.isVersionOrHigher('8'))) {
    // Take the current scrollLeft value and move to the right by the
    // offsetStart to get to the left edge of the element, and then by
    // the clientWidth of the element to get to the right edge.
    element.scrollLeft =
        element.scrollWidth - offsetStart - element.clientWidth;
  } else {
    element.scrollLeft = offsetStart;
  }
};


/**
 * Sets the element's left style attribute in LTR or right style attribute in
 * RTL.  Also clears the left attribute in RTL and the right attribute in LTR.
 * @param {Element} elem The element to position.
 * @param {number} left The left position in LTR; will be set as right in RTL.
 * @param {?number} top The top position.  If null only the left/right is set.
 * @param {boolean} isRtl Whether we are in RTL mode.
 */
goog.style.bidi.setPosition = function(elem, left, top, isRtl) {
  if (!goog.isNull(top)) {
    elem.style.top = top + 'px';
  }
  if (isRtl) {
    elem.style.right = left + 'px';
    elem.style.left = '';
  } else {
    elem.style.left = left + 'px';
    elem.style.right = '';
  }
};

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Drag Utilities.
 *
 * Provides extensible functionality for drag & drop behaviour.
 *
 * @see ../demos/drag.html
 * @see ../demos/dragger.html
 */


goog.provide('goog.fx.DragEvent');
goog.provide('goog.fx.Dragger');
goog.provide('goog.fx.Dragger.EventType');

goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.math.Coordinate');
goog.require('goog.math.Rect');
goog.require('goog.style');
goog.require('goog.style.bidi');
goog.require('goog.userAgent');



/**
 * A class that allows mouse or touch-based dragging (moving) of an element
 *
 * @param {Element} target The element that will be dragged.
 * @param {Element=} opt_handle An optional handle to control the drag, if null
 *     the target is used.
 * @param {goog.math.Rect=} opt_limits Object containing left, top, width,
 *     and height.
 *
 * @extends {goog.events.EventTarget}
 * @constructor
 * @struct
 */
goog.fx.Dragger = function(target, opt_handle, opt_limits) {
  goog.fx.Dragger.base(this, 'constructor');

  /**
   * Reference to drag target element.
   * @type {?Element}
   */
  this.target = target;

  /**
   * Reference to the handler that initiates the drag.
   * @type {?Element}
   */
  this.handle = opt_handle || target;

  /**
   * Object representing the limits of the drag region.
   * @type {goog.math.Rect}
   */
  this.limits = opt_limits || new goog.math.Rect(NaN, NaN, NaN, NaN);

  /**
   * Reference to a document object to use for the events.
   * @private {Document}
   */
  this.document_ = goog.dom.getOwnerDocument(target);

  /** @private {!goog.events.EventHandler} */
  this.eventHandler_ = new goog.events.EventHandler(this);
  this.registerDisposable(this.eventHandler_);

  /**
   * Whether the element is rendered right-to-left. We initialize this lazily.
   * @private {boolean|undefined}}
   */
  this.rightToLeft_;

  /**
   * Current x position of mouse or touch relative to viewport.
   * @type {number}
   */
  this.clientX = 0;

  /**
   * Current y position of mouse or touch relative to viewport.
   * @type {number}
   */
  this.clientY = 0;

  /**
   * Current x position of mouse or touch relative to screen. Deprecated because
   * it doesn't take into affect zoom level or pixel density.
   * @type {number}
   * @deprecated Consider switching to clientX instead.
   */
  this.screenX = 0;

  /**
   * Current y position of mouse or touch relative to screen. Deprecated because
   * it doesn't take into affect zoom level or pixel density.
   * @type {number}
   * @deprecated Consider switching to clientY instead.
   */
  this.screenY = 0;

  /**
   * The x position where the first mousedown or touchstart occurred.
   * @type {number}
   */
  this.startX = 0;

  /**
   * The y position where the first mousedown or touchstart occurred.
   * @type {number}
   */
  this.startY = 0;

  /**
   * Current x position of drag relative to target's parent.
   * @type {number}
   */
  this.deltaX = 0;

  /**
   * Current y position of drag relative to target's parent.
   * @type {number}
   */
  this.deltaY = 0;

  /**
   * The current page scroll value.
   * @type {?goog.math.Coordinate}
   */
  this.pageScroll;

  /**
   * Whether dragging is currently enabled.
   * @private {boolean}
   */
  this.enabled_ = true;

  /**
   * Whether object is currently being dragged.
   * @private {boolean}
   */
  this.dragging_ = false;

  /**
   * Whether mousedown should be default prevented.
   * @private {boolean}
   **/
  this.preventMouseDown_ = true;

  /**
   * The amount of distance, in pixels, after which a mousedown or touchstart is
   * considered a drag.
   * @private {number}
   */
  this.hysteresisDistanceSquared_ = 0;

  /**
   * The SCROLL event target used to make drag element follow scrolling.
   * @private {?EventTarget}
   */
  this.scrollTarget_;

  /**
   * Whether IE drag events cancelling is on.
   * @private {boolean}
   */
  this.ieDragStartCancellingOn_ = false;

  /**
   * Whether the dragger implements the changes described in http://b/6324964,
   * making it truly RTL.  This is a temporary flag to allow clients to
   * transition to the new behavior at their convenience.  At some point it will
   * be the default.
   * @private {boolean}
   */
  this.useRightPositioningForRtl_ = false;

  // Add listener. Do not use the event handler here since the event handler is
  // used for listeners added and removed during the drag operation.
  goog.events.listen(this.handle,
      [goog.events.EventType.TOUCHSTART, goog.events.EventType.MOUSEDOWN],
      this.startDrag, false, this);
};
goog.inherits(goog.fx.Dragger, goog.events.EventTarget);
// Dragger is meant to be extended, but defines most properties on its
// prototype, thus making it unsuitable for sealing.
goog.tagUnsealableClass(goog.fx.Dragger);


/**
 * Whether setCapture is supported by the browser.
 * @type {boolean}
 * @private
 */
goog.fx.Dragger.HAS_SET_CAPTURE_ =
    // IE and Gecko after 1.9.3 has setCapture
    // WebKit does not yet: https://bugs.webkit.org/show_bug.cgi?id=27330
    goog.userAgent.IE ||
    goog.userAgent.GECKO && goog.userAgent.isVersionOrHigher('1.9.3');


/**
 * Creates copy of node being dragged.  This is a utility function to be used
 * wherever it is inappropriate for the original source to follow the mouse
 * cursor itself.
 *
 * @param {Element} sourceEl Element to copy.
 * @return {!Element} The clone of {@code sourceEl}.
 */
goog.fx.Dragger.cloneNode = function(sourceEl) {
  var clonedEl = /** @type {Element} */ (sourceEl.cloneNode(true)),
      origTexts = sourceEl.getElementsByTagName(goog.dom.TagName.TEXTAREA),
      dragTexts = clonedEl.getElementsByTagName(goog.dom.TagName.TEXTAREA);
  // Cloning does not copy the current value of textarea elements, so correct
  // this manually.
  for (var i = 0; i < origTexts.length; i++) {
    dragTexts[i].value = origTexts[i].value;
  }
  switch (sourceEl.tagName) {
    case goog.dom.TagName.TR:
      return goog.dom.createDom(goog.dom.TagName.TABLE, null,
                                goog.dom.createDom(goog.dom.TagName.TBODY,
                                                   null, clonedEl));
    case goog.dom.TagName.TD:
    case goog.dom.TagName.TH:
      return goog.dom.createDom(
          goog.dom.TagName.TABLE, null, goog.dom.createDom(
              goog.dom.TagName.TBODY, null, goog.dom.createDom(
                  goog.dom.TagName.TR, null, clonedEl)));
    case goog.dom.TagName.TEXTAREA:
      clonedEl.value = sourceEl.value;
    default:
      return clonedEl;
  }
};


/**
 * Constants for event names.
 * @enum {string}
 */
goog.fx.Dragger.EventType = {
  // The drag action was canceled before the START event. Possible reasons:
  // disabled dragger, dragging with the right mouse button or releasing the
  // button before reaching the hysteresis distance.
  EARLY_CANCEL: 'earlycancel',
  START: 'start',
  BEFOREDRAG: 'beforedrag',
  DRAG: 'drag',
  END: 'end'
};


/**
 * Turns on/off true RTL behavior.  This should be called immediately after
 * construction.  This is a temporary flag to allow clients to transition
 * to the new component at their convenience.  At some point true will be the
 * default.
 * @param {boolean} useRightPositioningForRtl True if "right" should be used for
 *     positioning, false if "left" should be used for positioning.
 */
goog.fx.Dragger.prototype.enableRightPositioningForRtl =
    function(useRightPositioningForRtl) {
  this.useRightPositioningForRtl_ = useRightPositioningForRtl;
};


/**
 * Returns the event handler, intended for subclass use.
 * @return {!goog.events.EventHandler<T>} The event handler.
 * @this {T}
 * @template T
 */
goog.fx.Dragger.prototype.getHandler = function() {
  // TODO(user): templated "this" values currently result in "this" being
  // "unknown" in the body of the function.
  var self = /** @type {goog.fx.Dragger} */ (this);
  return self.eventHandler_;
};


/**
 * Sets (or reset) the Drag limits after a Dragger is created.
 * @param {goog.math.Rect?} limits Object containing left, top, width,
 *     height for new Dragger limits. If target is right-to-left and
 *     enableRightPositioningForRtl(true) is called, then rect is interpreted as
 *     right, top, width, and height.
 */
goog.fx.Dragger.prototype.setLimits = function(limits) {
  this.limits = limits || new goog.math.Rect(NaN, NaN, NaN, NaN);
};


/**
 * Sets the distance the user has to drag the element before a drag operation is
 * started.
 * @param {number} distance The number of pixels after which a mousedown and
 *     move is considered a drag.
 */
goog.fx.Dragger.prototype.setHysteresis = function(distance) {
  this.hysteresisDistanceSquared_ = Math.pow(distance, 2);
};


/**
 * Gets the distance the user has to drag the element before a drag operation is
 * started.
 * @return {number} distance The number of pixels after which a mousedown and
 *     move is considered a drag.
 */
goog.fx.Dragger.prototype.getHysteresis = function() {
  return Math.sqrt(this.hysteresisDistanceSquared_);
};


/**
 * Sets the SCROLL event target to make drag element follow scrolling.
 *
 * @param {EventTarget} scrollTarget The event target that dispatches SCROLL
 *     events.
 */
goog.fx.Dragger.prototype.setScrollTarget = function(scrollTarget) {
  this.scrollTarget_ = scrollTarget;
};


/**
 * Enables cancelling of built-in IE drag events.
 * @param {boolean} cancelIeDragStart Whether to enable cancelling of IE
 *     dragstart event.
 */
goog.fx.Dragger.prototype.setCancelIeDragStart = function(cancelIeDragStart) {
  this.ieDragStartCancellingOn_ = cancelIeDragStart;
};


/**
 * @return {boolean} Whether the dragger is enabled.
 */
goog.fx.Dragger.prototype.getEnabled = function() {
  return this.enabled_;
};


/**
 * Set whether dragger is enabled
 * @param {boolean} enabled Whether dragger is enabled.
 */
goog.fx.Dragger.prototype.setEnabled = function(enabled) {
  this.enabled_ = enabled;
};


/**
 * Set whether mousedown should be default prevented.
 * @param {boolean} preventMouseDown Whether mousedown should be default
 *     prevented.
 */
goog.fx.Dragger.prototype.setPreventMouseDown = function(preventMouseDown) {
  this.preventMouseDown_ = preventMouseDown;
};


/** @override */
goog.fx.Dragger.prototype.disposeInternal = function() {
  goog.fx.Dragger.superClass_.disposeInternal.call(this);
  goog.events.unlisten(this.handle,
      [goog.events.EventType.TOUCHSTART, goog.events.EventType.MOUSEDOWN],
      this.startDrag, false, this);
  this.cleanUpAfterDragging_();

  this.target = null;
  this.handle = null;
};


/**
 * Whether the DOM element being manipulated is rendered right-to-left.
 * @return {boolean} True if the DOM element is rendered right-to-left, false
 *     otherwise.
 * @private
 */
goog.fx.Dragger.prototype.isRightToLeft_ = function() {
  if (!goog.isDef(this.rightToLeft_)) {
    this.rightToLeft_ = goog.style.isRightToLeft(this.target);
  }
  return this.rightToLeft_;
};


/**
 * Event handler that is used to start the drag
 * @param {goog.events.BrowserEvent} e Event object.
 */
goog.fx.Dragger.prototype.startDrag = function(e) {
  var isMouseDown = e.type == goog.events.EventType.MOUSEDOWN;

  // Dragger.startDrag() can be called by AbstractDragDrop with a mousemove
  // event and IE does not report pressed mouse buttons on mousemove. Also,
  // it does not make sense to check for the button if the user is already
  // dragging.

  if (this.enabled_ && !this.dragging_ &&
      (!isMouseDown || e.isMouseActionButton())) {
    if (this.hysteresisDistanceSquared_ == 0) {
      if (this.fireDragStart_(e)) {
        this.dragging_ = true;
        if (this.preventMouseDown_) {
          e.preventDefault();
        }
      } else {
        // If the start drag is cancelled, don't setup for a drag.
        return;
      }
    } else if (this.preventMouseDown_) {
      // Need to preventDefault for hysteresis to prevent page getting selected.
      e.preventDefault();
    }
    this.setupDragHandlers();

    this.clientX = this.startX = e.clientX;
    this.clientY = this.startY = e.clientY;
    this.screenX = e.screenX;
    this.screenY = e.screenY;
    this.computeInitialPosition();
    this.pageScroll = goog.dom.getDomHelper(this.document_).getDocumentScroll();
  } else {
    this.dispatchEvent(goog.fx.Dragger.EventType.EARLY_CANCEL);
  }
};


/**
 * Sets up event handlers when dragging starts.
 * @protected
 */
goog.fx.Dragger.prototype.setupDragHandlers = function() {
  var doc = this.document_;
  var docEl = doc.documentElement;
  // Use bubbling when we have setCapture since we got reports that IE has
  // problems with the capturing events in combination with setCapture.
  var useCapture = !goog.fx.Dragger.HAS_SET_CAPTURE_;

  this.eventHandler_.listen(doc,
      [goog.events.EventType.TOUCHMOVE, goog.events.EventType.MOUSEMOVE],
      this.handleMove_, useCapture);
  this.eventHandler_.listen(doc,
      [goog.events.EventType.TOUCHEND, goog.events.EventType.MOUSEUP],
      this.endDrag, useCapture);

  if (goog.fx.Dragger.HAS_SET_CAPTURE_) {
    docEl.setCapture(false);
    this.eventHandler_.listen(docEl,
                              goog.events.EventType.LOSECAPTURE,
                              this.endDrag);
  } else {
    // Make sure we stop the dragging if the window loses focus.
    // Don't use capture in this listener because we only want to end the drag
    // if the actual window loses focus. Since blur events do not bubble we use
    // a bubbling listener on the window.
    this.eventHandler_.listen(goog.dom.getWindow(doc),
                              goog.events.EventType.BLUR,
                              this.endDrag);
  }

  if (goog.userAgent.IE && this.ieDragStartCancellingOn_) {
    // Cancel IE's 'ondragstart' event.
    this.eventHandler_.listen(doc, goog.events.EventType.DRAGSTART,
                              goog.events.Event.preventDefault);
  }

  if (this.scrollTarget_) {
    this.eventHandler_.listen(this.scrollTarget_, goog.events.EventType.SCROLL,
                              this.onScroll_, useCapture);
  }
};


/**
 * Fires a goog.fx.Dragger.EventType.START event.
 * @param {goog.events.BrowserEvent} e Browser event that triggered the drag.
 * @return {boolean} False iff preventDefault was called on the DragEvent.
 * @private
 */
goog.fx.Dragger.prototype.fireDragStart_ = function(e) {
  return this.dispatchEvent(new goog.fx.DragEvent(
      goog.fx.Dragger.EventType.START, this, e.clientX, e.clientY, e));
};


/**
 * Unregisters the event handlers that are only active during dragging, and
 * releases mouse capture.
 * @private
 */
goog.fx.Dragger.prototype.cleanUpAfterDragging_ = function() {
  this.eventHandler_.removeAll();
  if (goog.fx.Dragger.HAS_SET_CAPTURE_) {
    this.document_.releaseCapture();
  }
};


/**
 * Event handler that is used to end the drag.
 * @param {goog.events.BrowserEvent} e Event object.
 * @param {boolean=} opt_dragCanceled Whether the drag has been canceled.
 */
goog.fx.Dragger.prototype.endDrag = function(e, opt_dragCanceled) {
  this.cleanUpAfterDragging_();

  if (this.dragging_) {
    this.dragging_ = false;

    var x = this.limitX(this.deltaX);
    var y = this.limitY(this.deltaY);
    var dragCanceled = opt_dragCanceled ||
        e.type == goog.events.EventType.TOUCHCANCEL;
    this.dispatchEvent(new goog.fx.DragEvent(
        goog.fx.Dragger.EventType.END, this, e.clientX, e.clientY, e, x, y,
        dragCanceled));
  } else {
    this.dispatchEvent(goog.fx.Dragger.EventType.EARLY_CANCEL);
  }
};


/**
 * Event handler that is used to end the drag by cancelling it.
 * @param {goog.events.BrowserEvent} e Event object.
 */
goog.fx.Dragger.prototype.endDragCancel = function(e) {
  this.endDrag(e, true);
};


/**
 * Event handler that is used on mouse / touch move to update the drag
 * @param {goog.events.BrowserEvent} e Event object.
 * @private
 */
goog.fx.Dragger.prototype.handleMove_ = function(e) {
  if (this.enabled_) {
    // dx in right-to-left cases is relative to the right.
    var sign = this.useRightPositioningForRtl_ &&
        this.isRightToLeft_() ? -1 : 1;
    var dx = sign * (e.clientX - this.clientX);
    var dy = e.clientY - this.clientY;
    this.clientX = e.clientX;
    this.clientY = e.clientY;
    this.screenX = e.screenX;
    this.screenY = e.screenY;

    if (!this.dragging_) {
      var diffX = this.startX - this.clientX;
      var diffY = this.startY - this.clientY;
      var distance = diffX * diffX + diffY * diffY;
      if (distance > this.hysteresisDistanceSquared_) {
        if (this.fireDragStart_(e)) {
          this.dragging_ = true;
        } else {
          // DragListGroup disposes of the dragger if BEFOREDRAGSTART is
          // canceled.
          if (!this.isDisposed()) {
            this.endDrag(e);
          }
          return;
        }
      }
    }

    var pos = this.calculatePosition_(dx, dy);
    var x = pos.x;
    var y = pos.y;

    if (this.dragging_) {

      var rv = this.dispatchEvent(new goog.fx.DragEvent(
          goog.fx.Dragger.EventType.BEFOREDRAG, this, e.clientX, e.clientY,
          e, x, y));

      // Only do the defaultAction and dispatch drag event if predrag didn't
      // prevent default
      if (rv) {
        this.doDrag(e, x, y, false);
        e.preventDefault();
      }
    }
  }
};


/**
 * Calculates the drag position.
 *
 * @param {number} dx The horizontal movement delta.
 * @param {number} dy The vertical movement delta.
 * @return {!goog.math.Coordinate} The newly calculated drag element position.
 * @private
 */
goog.fx.Dragger.prototype.calculatePosition_ = function(dx, dy) {
  // Update the position for any change in body scrolling
  var pageScroll = goog.dom.getDomHelper(this.document_).getDocumentScroll();
  dx += pageScroll.x - this.pageScroll.x;
  dy += pageScroll.y - this.pageScroll.y;
  this.pageScroll = pageScroll;

  this.deltaX += dx;
  this.deltaY += dy;

  var x = this.limitX(this.deltaX);
  var y = this.limitY(this.deltaY);
  return new goog.math.Coordinate(x, y);
};


/**
 * Event handler for scroll target scrolling.
 * @param {goog.events.BrowserEvent} e The event.
 * @private
 */
goog.fx.Dragger.prototype.onScroll_ = function(e) {
  var pos = this.calculatePosition_(0, 0);
  e.clientX = this.clientX;
  e.clientY = this.clientY;
  this.doDrag(e, pos.x, pos.y, true);
};


/**
 * @param {goog.events.BrowserEvent} e The closure object
 *     representing the browser event that caused a drag event.
 * @param {number} x The new horizontal position for the drag element.
 * @param {number} y The new vertical position for the drag element.
 * @param {boolean} dragFromScroll Whether dragging was caused by scrolling
 *     the associated scroll target.
 * @protected
 */
goog.fx.Dragger.prototype.doDrag = function(e, x, y, dragFromScroll) {
  this.defaultAction(x, y);
  this.dispatchEvent(new goog.fx.DragEvent(
      goog.fx.Dragger.EventType.DRAG, this, e.clientX, e.clientY, e, x, y));
};


/**
 * Returns the 'real' x after limits are applied (allows for some
 * limits to be undefined).
 * @param {number} x X-coordinate to limit.
 * @return {number} The 'real' X-coordinate after limits are applied.
 */
goog.fx.Dragger.prototype.limitX = function(x) {
  var rect = this.limits;
  var left = !isNaN(rect.left) ? rect.left : null;
  var width = !isNaN(rect.width) ? rect.width : 0;
  var maxX = left != null ? left + width : Infinity;
  var minX = left != null ? left : -Infinity;
  return Math.min(maxX, Math.max(minX, x));
};


/**
 * Returns the 'real' y after limits are applied (allows for some
 * limits to be undefined).
 * @param {number} y Y-coordinate to limit.
 * @return {number} The 'real' Y-coordinate after limits are applied.
 */
goog.fx.Dragger.prototype.limitY = function(y) {
  var rect = this.limits;
  var top = !isNaN(rect.top) ? rect.top : null;
  var height = !isNaN(rect.height) ? rect.height : 0;
  var maxY = top != null ? top + height : Infinity;
  var minY = top != null ? top : -Infinity;
  return Math.min(maxY, Math.max(minY, y));
};


/**
 * Overridable function for computing the initial position of the target
 * before dragging begins.
 * @protected
 */
goog.fx.Dragger.prototype.computeInitialPosition = function() {
  this.deltaX = this.useRightPositioningForRtl_ ?
      goog.style.bidi.getOffsetStart(this.target) :
      /** @type {!HTMLElement} */ (this.target).offsetLeft;
  this.deltaY = /** @type {!HTMLElement} */ (this.target).offsetTop;
};


/**
 * Overridable function for handling the default action of the drag behaviour.
 * Normally this is simply moving the element to x,y though in some cases it
 * might be used to resize the layer.  This is basically a shortcut to
 * implementing a default ondrag event handler.
 * @param {number} x X-coordinate for target element. In right-to-left, x this
 *     is the number of pixels the target should be moved to from the right.
 * @param {number} y Y-coordinate for target element.
 */
goog.fx.Dragger.prototype.defaultAction = function(x, y) {
  if (this.useRightPositioningForRtl_ && this.isRightToLeft_()) {
    this.target.style.right = x + 'px';
  } else {
    this.target.style.left = x + 'px';
  }
  this.target.style.top = y + 'px';
};


/**
 * @return {boolean} Whether the dragger is currently in the midst of a drag.
 */
goog.fx.Dragger.prototype.isDragging = function() {
  return this.dragging_;
};



/**
 * Object representing a drag event
 * @param {string} type Event type.
 * @param {goog.fx.Dragger} dragobj Drag object initiating event.
 * @param {number} clientX X-coordinate relative to the viewport.
 * @param {number} clientY Y-coordinate relative to the viewport.
 * @param {goog.events.BrowserEvent} browserEvent The closure object
 *   representing the browser event that caused this drag event.
 * @param {number=} opt_actX Optional actual x for drag if it has been limited.
 * @param {number=} opt_actY Optional actual y for drag if it has been limited.
 * @param {boolean=} opt_dragCanceled Whether the drag has been canceled.
 * @constructor
 * @struct
 * @extends {goog.events.Event}
 */
goog.fx.DragEvent = function(type, dragobj, clientX, clientY, browserEvent,
                             opt_actX, opt_actY, opt_dragCanceled) {
  goog.events.Event.call(this, type);

  /**
   * X-coordinate relative to the viewport
   * @type {number}
   */
  this.clientX = clientX;

  /**
   * Y-coordinate relative to the viewport
   * @type {number}
   */
  this.clientY = clientY;

  /**
   * The closure object representing the browser event that caused this drag
   * event.
   * @type {goog.events.BrowserEvent}
   */
  this.browserEvent = browserEvent;

  /**
   * The real x-position of the drag if it has been limited
   * @type {number}
   */
  this.left = goog.isDef(opt_actX) ? opt_actX : dragobj.deltaX;

  /**
   * The real y-position of the drag if it has been limited
   * @type {number}
   */
  this.top = goog.isDef(opt_actY) ? opt_actY : dragobj.deltaY;

  /**
   * Reference to the drag object for this event
   * @type {goog.fx.Dragger}
   */
  this.dragger = dragobj;

  /**
   * Whether drag was canceled with this event. Used to differentiate between
   * a legitimate drag END that can result in an action and a drag END which is
   * a result of a drag cancelation. For now it can happen 1) with drag END
   * event on FireFox when user drags the mouse out of the window, 2) with
   * drag END event on IE7 which is generated on MOUSEMOVE event when user
   * moves the mouse into the document after the mouse button has been
   * released, 3) when TOUCHCANCEL is raised instead of TOUCHEND (on touch
   * events).
   * @type {boolean}
   */
  this.dragCanceled = !!opt_dragCanceled;
};
goog.inherits(goog.fx.DragEvent, goog.events.Event);

// FIXME should possibly show tooltip when dragging?

goog.provide('ol.control.ZoomSlider');

goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.EventType');
goog.require('goog.fx.DragEvent');
goog.require('goog.fx.Dragger');
goog.require('goog.fx.Dragger.EventType');
goog.require('goog.math.Rect');
goog.require('goog.style');
goog.require('ol.Size');
goog.require('ol.ViewHint');
goog.require('ol.animation');
goog.require('ol.control.Control');
goog.require('ol.css');
goog.require('ol.easing');
goog.require('ol.math');


/**
 * @classdesc
 * A slider type of control for zooming.
 *
 * Example:
 *
 *     map.addControl(new ol.control.ZoomSlider());
 *
 * @constructor
 * @extends {ol.control.Control}
 * @param {olx.control.ZoomSliderOptions=} opt_options Zoom slider options.
 * @api stable
 */
ol.control.ZoomSlider = function(opt_options) {

  var options = opt_options ? opt_options : {};

  /**
   * Will hold the current resolution of the view.
   *
   * @type {number|undefined}
   * @private
   */
  this.currentResolution_ = undefined;

  /**
   * The direction of the slider. Will be determined from actual display of the
   * container and defaults to ol.control.ZoomSlider.direction.VERTICAL.
   *
   * @type {ol.control.ZoomSlider.direction}
   * @private
   */
  this.direction_ = ol.control.ZoomSlider.direction.VERTICAL;

  /**
   * The calculated thumb size (border box plus margins).  Set when initSlider_
   * is called.
   * @type {ol.Size}
   * @private
   */
  this.thumbSize_ = null;

  /**
   * Whether the slider is initialized.
   * @type {boolean}
   * @private
   */
  this.sliderInitialized_ = false;

  /**
   * @private
   * @type {number}
   */
  this.duration_ = options.duration !== undefined ? options.duration : 200;

  var className = options.className ? options.className : 'ol-zoomslider';
  var thumbElement = goog.dom.createDom('BUTTON', {
    'type': 'button',
    'class': className + '-thumb ' + ol.css.CLASS_UNSELECTABLE
  });
  var containerElement = goog.dom.createDom('DIV',
      [className, ol.css.CLASS_UNSELECTABLE, ol.css.CLASS_CONTROL],
      thumbElement);

  /**
   * @type {goog.fx.Dragger}
   * @private
   */
  this.dragger_ = new goog.fx.Dragger(thumbElement);
  this.registerDisposable(this.dragger_);

  goog.events.listen(this.dragger_, goog.fx.Dragger.EventType.START,
      this.handleDraggerStart_, false, this);
  goog.events.listen(this.dragger_, goog.fx.Dragger.EventType.DRAG,
      this.handleDraggerDrag_, false, this);
  goog.events.listen(this.dragger_, goog.fx.Dragger.EventType.END,
      this.handleDraggerEnd_, false, this);

  goog.events.listen(containerElement, goog.events.EventType.CLICK,
      this.handleContainerClick_, false, this);
  goog.events.listen(thumbElement, goog.events.EventType.CLICK,
      goog.events.Event.stopPropagation);

  var render = options.render ? options.render : ol.control.ZoomSlider.render;

  goog.base(this, {
    element: containerElement,
    render: render
  });
};
goog.inherits(ol.control.ZoomSlider, ol.control.Control);


/**
 * The enum for available directions.
 *
 * @enum {number}
 */
ol.control.ZoomSlider.direction = {
  VERTICAL: 0,
  HORIZONTAL: 1
};


/**
 * @inheritDoc
 */
ol.control.ZoomSlider.prototype.setMap = function(map) {
  goog.base(this, 'setMap', map);
  if (map) {
    map.render();
  }
};


/**
 * Initializes the slider element. This will determine and set this controls
 * direction_ and also constrain the dragging of the thumb to always be within
 * the bounds of the container.
 *
 * @private
 */
ol.control.ZoomSlider.prototype.initSlider_ = function() {
  var container = this.element;
  var containerSize = goog.style.getSize(container);

  var thumb = goog.dom.getFirstElementChild(container);
  var thumbMargins = goog.style.getMarginBox(thumb);
  var thumbBorderBoxSize = goog.style.getBorderBoxSize(thumb);
  var thumbWidth = thumbBorderBoxSize.width +
      thumbMargins.right + thumbMargins.left;
  var thumbHeight = thumbBorderBoxSize.height +
      thumbMargins.top + thumbMargins.bottom;
  this.thumbSize_ = [thumbWidth, thumbHeight];

  var width = containerSize.width - thumbWidth;
  var height = containerSize.height - thumbHeight;

  var limits;
  if (containerSize.width > containerSize.height) {
    this.direction_ = ol.control.ZoomSlider.direction.HORIZONTAL;
    limits = new goog.math.Rect(0, 0, width, 0);
  } else {
    this.direction_ = ol.control.ZoomSlider.direction.VERTICAL;
    limits = new goog.math.Rect(0, 0, 0, height);
  }
  this.dragger_.setLimits(limits);
  this.sliderInitialized_ = true;
};


/**
 * Update the zoomslider element.
 * @param {ol.MapEvent} mapEvent Map event.
 * @this {ol.control.ZoomSlider}
 * @api
 */
ol.control.ZoomSlider.render = function(mapEvent) {
  if (!mapEvent.frameState) {
    return;
  }
  goog.asserts.assert(mapEvent.frameState.viewState,
      'viewState should be defined');
  if (!this.sliderInitialized_) {
    this.initSlider_();
  }
  var res = mapEvent.frameState.viewState.resolution;
  if (res !== this.currentResolution_) {
    this.currentResolution_ = res;
    this.setThumbPosition_(res);
  }
};


/**
 * @param {goog.events.BrowserEvent} browserEvent The browser event to handle.
 * @private
 */
ol.control.ZoomSlider.prototype.handleContainerClick_ = function(browserEvent) {
  var map = this.getMap();
  var view = map.getView();
  var currentResolution = view.getResolution();
  goog.asserts.assert(currentResolution,
      'currentResolution should be defined');
  map.beforeRender(ol.animation.zoom({
    resolution: currentResolution,
    duration: this.duration_,
    easing: ol.easing.easeOut
  }));
  var relativePosition = this.getRelativePosition_(
      browserEvent.offsetX - this.thumbSize_[0] / 2,
      browserEvent.offsetY - this.thumbSize_[1] / 2);
  var resolution = this.getResolutionForPosition_(relativePosition);
  view.setResolution(view.constrainResolution(resolution));
};


/**
 * Handle dragger start events.
 * @param {goog.fx.DragEvent} event The drag event.
 * @private
 */
ol.control.ZoomSlider.prototype.handleDraggerStart_ = function(event) {
  this.getMap().getView().setHint(ol.ViewHint.INTERACTING, 1);
};


/**
 * Handle dragger drag events.
 *
 * @param {goog.fx.DragEvent} event The drag event.
 * @private
 */
ol.control.ZoomSlider.prototype.handleDraggerDrag_ = function(event) {
  var relativePosition = this.getRelativePosition_(event.left, event.top);
  this.currentResolution_ = this.getResolutionForPosition_(relativePosition);
  this.getMap().getView().setResolution(this.currentResolution_);
};


/**
 * Handle dragger end events.
 * @param {goog.fx.DragEvent} event The drag event.
 * @private
 */
ol.control.ZoomSlider.prototype.handleDraggerEnd_ = function(event) {
  var map = this.getMap();
  var view = map.getView();
  view.setHint(ol.ViewHint.INTERACTING, -1);
  goog.asserts.assert(this.currentResolution_,
      'this.currentResolution_ should be defined');
  map.beforeRender(ol.animation.zoom({
    resolution: this.currentResolution_,
    duration: this.duration_,
    easing: ol.easing.easeOut
  }));
  var resolution = view.constrainResolution(this.currentResolution_);
  view.setResolution(resolution);
};


/**
 * Positions the thumb inside its container according to the given resolution.
 *
 * @param {number} res The res.
 * @private
 */
ol.control.ZoomSlider.prototype.setThumbPosition_ = function(res) {
  var position = this.getPositionForResolution_(res);
  var dragger = this.dragger_;
  var thumb = goog.dom.getFirstElementChild(this.element);

  if (this.direction_ == ol.control.ZoomSlider.direction.HORIZONTAL) {
    var left = dragger.limits.left + dragger.limits.width * position;
    goog.style.setPosition(thumb, left);
  } else {
    var top = dragger.limits.top + dragger.limits.height * position;
    goog.style.setPosition(thumb, dragger.limits.left, top);
  }
};


/**
 * Calculates the relative position of the thumb given x and y offsets.  The
 * relative position scales from 0 to 1.  The x and y offsets are assumed to be
 * in pixel units within the dragger limits.
 *
 * @param {number} x Pixel position relative to the left of the slider.
 * @param {number} y Pixel position relative to the top of the slider.
 * @return {number} The relative position of the thumb.
 * @private
 */
ol.control.ZoomSlider.prototype.getRelativePosition_ = function(x, y) {
  var draggerLimits = this.dragger_.limits;
  var amount;
  if (this.direction_ === ol.control.ZoomSlider.direction.HORIZONTAL) {
    amount = (x - draggerLimits.left) / draggerLimits.width;
  } else {
    amount = (y - draggerLimits.top) / draggerLimits.height;
  }
  return ol.math.clamp(amount, 0, 1);
};


/**
 * Calculates the corresponding resolution of the thumb given its relative
 * position (where 0 is the minimum and 1 is the maximum).
 *
 * @param {number} position The relative position of the thumb.
 * @return {number} The corresponding resolution.
 * @private
 */
ol.control.ZoomSlider.prototype.getResolutionForPosition_ = function(position) {
  var fn = this.getMap().getView().getResolutionForValueFunction();
  return fn(1 - position);
};


/**
 * Determines the relative position of the slider for the given resolution.  A
 * relative position of 0 corresponds to the minimum view resolution.  A
 * relative position of 1 corresponds to the maximum view resolution.
 *
 * @param {number} res The resolution.
 * @return {number} The relative position value (between 0 and 1).
 * @private
 */
ol.control.ZoomSlider.prototype.getPositionForResolution_ = function(res) {
  var fn = this.getMap().getView().getValueForResolutionFunction();
  return 1 - fn(res);
};

goog.provide('ol.control.ZoomToExtent');

goog.require('goog.asserts');
goog.require('goog.dom');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('ol.control.Control');
goog.require('ol.css');


/**
 * @classdesc
 * A button control which, when pressed, changes the map view to a specific
 * extent. To style this control use the css selector `.ol-zoom-extent`.
 *
 * @constructor
 * @extends {ol.control.Control}
 * @param {olx.control.ZoomToExtentOptions=} opt_options Options.
 * @api stable
 */
ol.control.ZoomToExtent = function(opt_options) {
  var options = opt_options ? opt_options : {};

  /**
   * @type {ol.Extent}
   * @private
   */
  this.extent_ = options.extent ? options.extent : null;

  var className = options.className ? options.className :
      'ol-zoom-extent';

  var label = options.label ? options.label : 'E';
  var tipLabel = options.tipLabel ?
      options.tipLabel : 'Fit to extent';
  var button = goog.dom.createDom('BUTTON', {
    'type': 'button',
    'title': tipLabel
  }, label);

  goog.events.listen(button, goog.events.EventType.CLICK,
      this.handleClick_, false, this);

  var cssClasses = className + ' ' + ol.css.CLASS_UNSELECTABLE + ' ' +
      ol.css.CLASS_CONTROL;
  var element = goog.dom.createDom('DIV', cssClasses, button);

  goog.base(this, {
    element: element,
    target: options.target
  });
};
goog.inherits(ol.control.ZoomToExtent, ol.control.Control);


/**
 * @param {goog.events.BrowserEvent} event The event to handle
 * @private
 */
ol.control.ZoomToExtent.prototype.handleClick_ = function(event) {
  event.preventDefault();
  this.handleZoomToExtent_();
};


/**
 * @private
 */
ol.control.ZoomToExtent.prototype.handleZoomToExtent_ = function() {
  var map = this.getMap();
  var view = map.getView();
  var extent = !this.extent_ ?
      view.getProjection().getExtent() : this.extent_;
  var size = map.getSize();
  goog.asserts.assert(size, 'size should be defined');
  view.fit(extent, size);
};

goog.provide('ol.DeviceOrientation');
goog.provide('ol.DeviceOrientationProperty');

goog.require('goog.events');
goog.require('ol');
goog.require('ol.Object');
goog.require('ol.has');
goog.require('ol.math');


/**
 * @enum {string}
 */
ol.DeviceOrientationProperty = {
  ALPHA: 'alpha',
  BETA: 'beta',
  GAMMA: 'gamma',
  HEADING: 'heading',
  TRACKING: 'tracking'
};


/**
 * @classdesc
 * The ol.DeviceOrientation class provides access to information from
 * DeviceOrientation events.  See the [HTML 5 DeviceOrientation Specification](
 * http://www.w3.org/TR/orientation-event/) for more details.
 *
 * Many new computers, and especially mobile phones
 * and tablets, provide hardware support for device orientation. Web
 * developers targeting mobile devices will be especially interested in this
 * class.
 *
 * Device orientation data are relative to a common starting point. For mobile
 * devices, the starting point is to lay your phone face up on a table with the
 * top of the phone pointing north. This represents the zero state. All
 * angles are then relative to this state. For computers, it is the same except
 * the screen is open at 90 degrees.
 *
 * Device orientation is reported as three angles - `alpha`, `beta`, and
 * `gamma` - relative to the starting position along the three planar axes X, Y
 * and Z. The X axis runs from the left edge to the right edge through the
 * middle of the device. Similarly, the Y axis runs from the bottom to the top
 * of the device through the middle. The Z axis runs from the back to the front
 * through the middle. In the starting position, the X axis points to the
 * right, the Y axis points away from you and the Z axis points straight up
 * from the device lying flat.
 *
 * The three angles representing the device orientation are relative to the
 * three axes. `alpha` indicates how much the device has been rotated around the
 * Z axis, which is commonly interpreted as the compass heading (see note
 * below). `beta` indicates how much the device has been rotated around the X
 * axis, or how much it is tilted from front to back.  `gamma` indicates how
 * much the device has been rotated around the Y axis, or how much it is tilted
 * from left to right.
 *
 * For most browsers, the `alpha` value returns the compass heading so if the
 * device points north, it will be 0.  With Safari on iOS, the 0 value of
 * `alpha` is calculated from when device orientation was first requested.
 * ol.DeviceOrientation provides the `heading` property which normalizes this
 * behavior across all browsers for you.
 *
 * It is important to note that the HTML 5 DeviceOrientation specification
 * indicates that `alpha`, `beta` and `gamma` are in degrees while the
 * equivalent properties in ol.DeviceOrientation are in radians for consistency
 * with all other uses of angles throughout OpenLayers.
 *
 * @see http://www.w3.org/TR/orientation-event/
 *
 * To get notified of device orientation changes, register a listener for the
 * generic `change` event on your `ol.DeviceOrientation` instance.
 *
 * @constructor
 * @extends {ol.Object}
 * @param {olx.DeviceOrientationOptions=} opt_options Options.
 * @api
 */
ol.DeviceOrientation = function(opt_options) {

  goog.base(this);

  var options = opt_options ? opt_options : {};

  /**
   * @private
   * @type {goog.events.Key}
   */
  this.listenerKey_ = null;

  goog.events.listen(this,
      ol.Object.getChangeEventType(ol.DeviceOrientationProperty.TRACKING),
      this.handleTrackingChanged_, false, this);

  this.setTracking(options.tracking !== undefined ? options.tracking : false);

};
goog.inherits(ol.DeviceOrientation, ol.Object);


/**
 * @inheritDoc
 */
ol.DeviceOrientation.prototype.disposeInternal = function() {
  this.setTracking(false);
  goog.base(this, 'disposeInternal');
};


/**
 * @private
 * @param {goog.events.BrowserEvent} browserEvent Event.
 */
ol.DeviceOrientation.prototype.orientationChange_ = function(browserEvent) {
  var event = /** @type {DeviceOrientationEvent} */
      (browserEvent.getBrowserEvent());
  if (event.alpha !== null) {
    var alpha = ol.math.toRadians(event.alpha);
    this.set(ol.DeviceOrientationProperty.ALPHA, alpha);
    // event.absolute is undefined in iOS.
    if (goog.isBoolean(event.absolute) && event.absolute) {
      this.set(ol.DeviceOrientationProperty.HEADING, alpha);
    } else if (goog.isNumber(event.webkitCompassHeading) &&
               event.webkitCompassAccuracy != -1) {
      var heading = ol.math.toRadians(event.webkitCompassHeading);
      this.set(ol.DeviceOrientationProperty.HEADING, heading);
    }
  }
  if (event.beta !== null) {
    this.set(ol.DeviceOrientationProperty.BETA,
        ol.math.toRadians(event.beta));
  }
  if (event.gamma !== null) {
    this.set(ol.DeviceOrientationProperty.GAMMA,
        ol.math.toRadians(event.gamma));
  }
  this.changed();
};


/**
 * Rotation around the device z-axis (in radians).
 * @return {number|undefined} The euler angle in radians of the device from the
 *     standard Z axis.
 * @observable
 * @api
 */
ol.DeviceOrientation.prototype.getAlpha = function() {
  return /** @type {number|undefined} */ (
      this.get(ol.DeviceOrientationProperty.ALPHA));
};


/**
 * Rotation around the device x-axis (in radians).
 * @return {number|undefined} The euler angle in radians of the device from the
 *     planar X axis.
 * @observable
 * @api
 */
ol.DeviceOrientation.prototype.getBeta = function() {
  return /** @type {number|undefined} */ (
      this.get(ol.DeviceOrientationProperty.BETA));
};


/**
 * Rotation around the device y-axis (in radians).
 * @return {number|undefined} The euler angle in radians of the device from the
 *     planar Y axis.
 * @observable
 * @api
 */
ol.DeviceOrientation.prototype.getGamma = function() {
  return /** @type {number|undefined} */ (
      this.get(ol.DeviceOrientationProperty.GAMMA));
};


/**
 * The heading of the device relative to north (in radians).
 * @return {number|undefined} The heading of the device relative to north, in
 *     radians, normalizing for different browser behavior.
 * @observable
 * @api
 */
ol.DeviceOrientation.prototype.getHeading = function() {
  return /** @type {number|undefined} */ (
      this.get(ol.DeviceOrientationProperty.HEADING));
};


/**
 * Determine if orientation is being tracked.
 * @return {boolean} Changes in device orientation are being tracked.
 * @observable
 * @api
 */
ol.DeviceOrientation.prototype.getTracking = function() {
  return /** @type {boolean} */ (
      this.get(ol.DeviceOrientationProperty.TRACKING));
};


/**
 * @private
 */
ol.DeviceOrientation.prototype.handleTrackingChanged_ = function() {
  if (ol.has.DEVICE_ORIENTATION) {
    var tracking = this.getTracking();
    if (tracking && !this.listenerKey_) {
      this.listenerKey_ = goog.events.listen(goog.global, 'deviceorientation',
          this.orientationChange_, false, this);
    } else if (!tracking && this.listenerKey_) {
      goog.events.unlistenByKey(this.listenerKey_);
      this.listenerKey_ = null;
    }
  }
};


/**
 * Enable or disable tracking of device orientation events.
 * @param {boolean} tracking The status of tracking changes to alpha, beta and
 *     gamma. If true, changes are tracked and reported immediately.
 * @observable
 * @api
 */
ol.DeviceOrientation.prototype.setTracking = function(tracking) {
  this.set(ol.DeviceOrientationProperty.TRACKING, tracking);
};

goog.provide('ol.format.Feature');

goog.require('ol.geom.Geometry');
goog.require('ol.proj');


/**
 * @classdesc
 * Abstract base class; normally only used for creating subclasses and not
 * instantiated in apps.
 * Base class for feature formats.
 * {ol.format.Feature} subclasses provide the ability to decode and encode
 * {@link ol.Feature} objects from a variety of commonly used geospatial
 * file formats.  See the documentation for each format for more details.
 *
 * @constructor
 * @api stable
 */
ol.format.Feature = function() {

  /**
   * @protected
   * @type {ol.proj.Projection}
   */
  this.defaultDataProjection = null;

};


/**
 * @return {Array.<string>} Extensions.
 */
ol.format.Feature.prototype.getExtensions = goog.abstractMethod;


/**
 * Adds the data projection to the read options.
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Options.
 * @return {olx.format.ReadOptions|undefined} Options.
 * @protected
 */
ol.format.Feature.prototype.getReadOptions = function(source, opt_options) {
  var options;
  if (opt_options) {
    options = {
      dataProjection: opt_options.dataProjection ?
          opt_options.dataProjection : this.readProjection(source),
      featureProjection: opt_options.featureProjection
    };
  }
  return this.adaptOptions(options);
};


/**
 * Sets the `defaultDataProjection` on the options, if no `dataProjection`
 * is set.
 * @param {olx.format.WriteOptions|olx.format.ReadOptions|undefined} options
 *     Options.
 * @protected
 * @return {olx.format.WriteOptions|olx.format.ReadOptions|undefined}
 *     Updated options.
 */
ol.format.Feature.prototype.adaptOptions = function(options) {
  var updatedOptions;
  if (options) {
    updatedOptions = {
      featureProjection: options.featureProjection,
      dataProjection: options.dataProjection ?
          options.dataProjection : this.defaultDataProjection,
      rightHanded: options.rightHanded
    };
  }
  return updatedOptions;
};


/**
 * @return {ol.format.FormatType} Format.
 */
ol.format.Feature.prototype.getType = goog.abstractMethod;


/**
 * Read a single feature from a source.
 *
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {ol.Feature} Feature.
 */
ol.format.Feature.prototype.readFeature = goog.abstractMethod;


/**
 * Read all features from a source.
 *
 * @param {Document|Node|ArrayBuffer|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<ol.Feature>} Features.
 */
ol.format.Feature.prototype.readFeatures = goog.abstractMethod;


/**
 * Read a single geometry from a source.
 *
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {ol.geom.Geometry} Geometry.
 */
ol.format.Feature.prototype.readGeometry = goog.abstractMethod;


/**
 * Read the projection from a source.
 *
 * @param {Document|Node|Object|string} source Source.
 * @return {ol.proj.Projection} Projection.
 */
ol.format.Feature.prototype.readProjection = goog.abstractMethod;


/**
 * Encode a feature in this format.
 *
 * @param {ol.Feature} feature Feature.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {string} Result.
 */
ol.format.Feature.prototype.writeFeature = goog.abstractMethod;


/**
 * Encode an array of features in this format.
 *
 * @param {Array.<ol.Feature>} features Features.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {string} Result.
 */
ol.format.Feature.prototype.writeFeatures = goog.abstractMethod;


/**
 * Write a single geometry in this format.
 *
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {string} Result.
 */
ol.format.Feature.prototype.writeGeometry = goog.abstractMethod;


/**
 * @param {ol.geom.Geometry|ol.Extent} geometry Geometry.
 * @param {boolean} write Set to true for writing, false for reading.
 * @param {(olx.format.WriteOptions|olx.format.ReadOptions)=} opt_options
 *     Options.
 * @return {ol.geom.Geometry|ol.Extent} Transformed geometry.
 * @protected
 */
ol.format.Feature.transformWithOptions = function(
    geometry, write, opt_options) {
  var featureProjection = opt_options ?
      ol.proj.get(opt_options.featureProjection) : null;
  var dataProjection = opt_options ?
      ol.proj.get(opt_options.dataProjection) : null;
  if (featureProjection && dataProjection &&
      !ol.proj.equivalent(featureProjection, dataProjection)) {
    if (geometry instanceof ol.geom.Geometry) {
      return (write ? geometry.clone() : geometry).transform(
          write ? featureProjection : dataProjection,
          write ? dataProjection : featureProjection);
    } else {
      // FIXME this is necessary because ol.format.GML treats extents
      // as geometries
      return ol.proj.transformExtent(
          write ? geometry.slice() : geometry,
          write ? featureProjection : dataProjection,
          write ? dataProjection : featureProjection);
    }
  } else {
    return geometry;
  }
};

goog.provide('ol.format.JSONFeature');

goog.require('goog.asserts');
goog.require('goog.json');
goog.require('ol.format.Feature');
goog.require('ol.format.FormatType');


/**
 * @classdesc
 * Abstract base class; normally only used for creating subclasses and not
 * instantiated in apps.
 * Base class for JSON feature formats.
 *
 * @constructor
 * @extends {ol.format.Feature}
 */
ol.format.JSONFeature = function() {
  goog.base(this);
};
goog.inherits(ol.format.JSONFeature, ol.format.Feature);


/**
 * @param {Document|Node|Object|string} source Source.
 * @private
 * @return {Object} Object.
 */
ol.format.JSONFeature.prototype.getObject_ = function(source) {
  if (goog.isObject(source)) {
    return source;
  } else if (goog.isString(source)) {
    var object = goog.json.parse(source);
    return object ? object : null;
  } else {
    goog.asserts.fail();
    return null;
  }
};


/**
 * @inheritDoc
 */
ol.format.JSONFeature.prototype.getType = function() {
  return ol.format.FormatType.JSON;
};


/**
 * @inheritDoc
 */
ol.format.JSONFeature.prototype.readFeature = function(source, opt_options) {
  return this.readFeatureFromObject(
      this.getObject_(source), this.getReadOptions(source, opt_options));
};


/**
 * @inheritDoc
 */
ol.format.JSONFeature.prototype.readFeatures = function(source, opt_options) {
  return this.readFeaturesFromObject(
      this.getObject_(source), this.getReadOptions(source, opt_options));
};


/**
 * @param {Object} object Object.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @protected
 * @return {ol.Feature} Feature.
 */
ol.format.JSONFeature.prototype.readFeatureFromObject = goog.abstractMethod;


/**
 * @param {Object} object Object.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @protected
 * @return {Array.<ol.Feature>} Features.
 */
ol.format.JSONFeature.prototype.readFeaturesFromObject = goog.abstractMethod;


/**
 * @inheritDoc
 */
ol.format.JSONFeature.prototype.readGeometry = function(source, opt_options) {
  return this.readGeometryFromObject(
      this.getObject_(source), this.getReadOptions(source, opt_options));
};


/**
 * @param {Object} object Object.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @protected
 * @return {ol.geom.Geometry} Geometry.
 */
ol.format.JSONFeature.prototype.readGeometryFromObject = goog.abstractMethod;


/**
 * @inheritDoc
 */
ol.format.JSONFeature.prototype.readProjection = function(source) {
  return this.readProjectionFromObject(this.getObject_(source));
};


/**
 * @param {Object} object Object.
 * @protected
 * @return {ol.proj.Projection} Projection.
 */
ol.format.JSONFeature.prototype.readProjectionFromObject = goog.abstractMethod;


/**
 * @inheritDoc
 */
ol.format.JSONFeature.prototype.writeFeature = function(feature, opt_options) {
  return goog.json.serialize(this.writeFeatureObject(feature, opt_options));
};


/**
 * @param {ol.Feature} feature Feature.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {Object} Object.
 */
ol.format.JSONFeature.prototype.writeFeatureObject = goog.abstractMethod;


/**
 * @inheritDoc
 */
ol.format.JSONFeature.prototype.writeFeatures = function(
    features, opt_options) {
  return goog.json.serialize(this.writeFeaturesObject(features, opt_options));
};


/**
 * @param {Array.<ol.Feature>} features Features.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {Object} Object.
 */
ol.format.JSONFeature.prototype.writeFeaturesObject = goog.abstractMethod;


/**
 * @inheritDoc
 */
ol.format.JSONFeature.prototype.writeGeometry = function(
    geometry, opt_options) {
  return goog.json.serialize(this.writeGeometryObject(geometry, opt_options));
};


/**
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {Object} Object.
 */
ol.format.JSONFeature.prototype.writeGeometryObject = goog.abstractMethod;

goog.provide('ol.geom.flat.interpolate');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.math');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {number} fraction Fraction.
 * @param {Array.<number>=} opt_dest Destination.
 * @return {Array.<number>} Destination.
 */
ol.geom.flat.interpolate.lineString = function(flatCoordinates, offset, end, stride, fraction, opt_dest) {
  // FIXME interpolate extra dimensions
  goog.asserts.assert(0 <= fraction && fraction <= 1,
      'fraction should be in between 0 and 1');
  var pointX = NaN;
  var pointY = NaN;
  var n = (end - offset) / stride;
  if (n === 0) {
    goog.asserts.fail('n cannot be 0');
  } else if (n == 1) {
    pointX = flatCoordinates[offset];
    pointY = flatCoordinates[offset + 1];
  } else if (n == 2) {
    pointX = (1 - fraction) * flatCoordinates[offset] +
        fraction * flatCoordinates[offset + stride];
    pointY = (1 - fraction) * flatCoordinates[offset + 1] +
        fraction * flatCoordinates[offset + stride + 1];
  } else {
    var x1 = flatCoordinates[offset];
    var y1 = flatCoordinates[offset + 1];
    var length = 0;
    var cumulativeLengths = [0];
    var i;
    for (i = offset + stride; i < end; i += stride) {
      var x2 = flatCoordinates[i];
      var y2 = flatCoordinates[i + 1];
      length += Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
      cumulativeLengths.push(length);
      x1 = x2;
      y1 = y2;
    }
    var target = fraction * length;
    var index = goog.array.binarySearch(cumulativeLengths, target);
    if (index < 0) {
      var t = (target - cumulativeLengths[-index - 2]) /
          (cumulativeLengths[-index - 1] - cumulativeLengths[-index - 2]);
      var o = offset + (-index - 2) * stride;
      pointX = goog.math.lerp(
          flatCoordinates[o], flatCoordinates[o + stride], t);
      pointY = goog.math.lerp(
          flatCoordinates[o + 1], flatCoordinates[o + stride + 1], t);
    } else {
      pointX = flatCoordinates[offset + index * stride];
      pointY = flatCoordinates[offset + index * stride + 1];
    }
  }
  if (opt_dest) {
    opt_dest[0] = pointX;
    opt_dest[1] = pointY;
    return opt_dest;
  } else {
    return [pointX, pointY];
  }
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {number} m M.
 * @param {boolean} extrapolate Extrapolate.
 * @return {ol.Coordinate} Coordinate.
 */
ol.geom.flat.lineStringCoordinateAtM = function(flatCoordinates, offset, end, stride, m, extrapolate) {
  if (end == offset) {
    return null;
  }
  var coordinate;
  if (m < flatCoordinates[offset + stride - 1]) {
    if (extrapolate) {
      coordinate = flatCoordinates.slice(offset, offset + stride);
      coordinate[stride - 1] = m;
      return coordinate;
    } else {
      return null;
    }
  } else if (flatCoordinates[end - 1] < m) {
    if (extrapolate) {
      coordinate = flatCoordinates.slice(end - stride, end);
      coordinate[stride - 1] = m;
      return coordinate;
    } else {
      return null;
    }
  }
  // FIXME use O(1) search
  if (m == flatCoordinates[offset + stride - 1]) {
    return flatCoordinates.slice(offset, offset + stride);
  }
  var lo = offset / stride;
  var hi = end / stride;
  while (lo < hi) {
    var mid = (lo + hi) >> 1;
    if (m < flatCoordinates[(mid + 1) * stride - 1]) {
      hi = mid;
    } else {
      lo = mid + 1;
    }
  }
  var m0 = flatCoordinates[lo * stride - 1];
  if (m == m0) {
    return flatCoordinates.slice((lo - 1) * stride, (lo - 1) * stride + stride);
  }
  var m1 = flatCoordinates[(lo + 1) * stride - 1];
  goog.asserts.assert(m0 < m, 'm0 should be less than m');
  goog.asserts.assert(m <= m1, 'm should be less than or equal to m1');
  var t = (m - m0) / (m1 - m0);
  coordinate = [];
  var i;
  for (i = 0; i < stride - 1; ++i) {
    coordinate.push(goog.math.lerp(flatCoordinates[(lo - 1) * stride + i],
        flatCoordinates[lo * stride + i], t));
  }
  coordinate.push(m);
  goog.asserts.assert(coordinate.length == stride,
      'length of coordinate array should match stride');
  return coordinate;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<number>} ends Ends.
 * @param {number} stride Stride.
 * @param {number} m M.
 * @param {boolean} extrapolate Extrapolate.
 * @param {boolean} interpolate Interpolate.
 * @return {ol.Coordinate} Coordinate.
 */
ol.geom.flat.lineStringsCoordinateAtM = function(
    flatCoordinates, offset, ends, stride, m, extrapolate, interpolate) {
  if (interpolate) {
    return ol.geom.flat.lineStringCoordinateAtM(
        flatCoordinates, offset, ends[ends.length - 1], stride, m, extrapolate);
  }
  var coordinate;
  if (m < flatCoordinates[stride - 1]) {
    if (extrapolate) {
      coordinate = flatCoordinates.slice(0, stride);
      coordinate[stride - 1] = m;
      return coordinate;
    } else {
      return null;
    }
  }
  if (flatCoordinates[flatCoordinates.length - 1] < m) {
    if (extrapolate) {
      coordinate = flatCoordinates.slice(flatCoordinates.length - stride);
      coordinate[stride - 1] = m;
      return coordinate;
    } else {
      return null;
    }
  }
  var i, ii;
  for (i = 0, ii = ends.length; i < ii; ++i) {
    var end = ends[i];
    if (offset == end) {
      continue;
    }
    if (m < flatCoordinates[offset + stride - 1]) {
      return null;
    } else if (m <= flatCoordinates[end - 1]) {
      return ol.geom.flat.lineStringCoordinateAtM(
          flatCoordinates, offset, end, stride, m, false);
    }
    offset = end;
  }
  goog.asserts.fail(
      'ol.geom.flat.lineStringsCoordinateAtM should have returned');
  return null;
};

goog.provide('ol.geom.flat.length');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @return {number} Length.
 */
ol.geom.flat.length.lineString = function(flatCoordinates, offset, end, stride) {
  var x1 = flatCoordinates[offset];
  var y1 = flatCoordinates[offset + 1];
  var length = 0;
  var i;
  for (i = offset + stride; i < end; i += stride) {
    var x2 = flatCoordinates[i];
    var y2 = flatCoordinates[i + 1];
    length += Math.sqrt((x2 - x1) * (x2 - x1) + (y2 - y1) * (y2 - y1));
    x1 = x2;
    y1 = y2;
  }
  return length;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @return {number} Perimeter.
 */
ol.geom.flat.length.linearRing = function(flatCoordinates, offset, end, stride) {
  var perimeter =
      ol.geom.flat.length.lineString(flatCoordinates, offset, end, stride);
  var dx = flatCoordinates[end - stride] - flatCoordinates[offset];
  var dy = flatCoordinates[end - stride + 1] - flatCoordinates[offset + 1];
  perimeter += Math.sqrt(dx * dx + dy * dy);
  return perimeter;
};

goog.provide('ol.geom.LineString');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('ol');
goog.require('ol.extent');
goog.require('ol.geom.GeometryLayout');
goog.require('ol.geom.GeometryType');
goog.require('ol.geom.SimpleGeometry');
goog.require('ol.geom.flat.closest');
goog.require('ol.geom.flat.deflate');
goog.require('ol.geom.flat.inflate');
goog.require('ol.geom.flat.interpolate');
goog.require('ol.geom.flat.intersectsextent');
goog.require('ol.geom.flat.length');
goog.require('ol.geom.flat.segments');
goog.require('ol.geom.flat.simplify');


/**
 * @classdesc
 * Linestring geometry.
 *
 * @constructor
 * @extends {ol.geom.SimpleGeometry}
 * @param {Array.<ol.Coordinate>} coordinates Coordinates.
 * @param {ol.geom.GeometryLayout=} opt_layout Layout.
 * @api stable
 */
ol.geom.LineString = function(coordinates, opt_layout) {

  goog.base(this);

  /**
   * @private
   * @type {ol.Coordinate}
   */
  this.flatMidpoint_ = null;

  /**
   * @private
   * @type {number}
   */
  this.flatMidpointRevision_ = -1;

  /**
   * @private
   * @type {number}
   */
  this.maxDelta_ = -1;

  /**
   * @private
   * @type {number}
   */
  this.maxDeltaRevision_ = -1;

  this.setCoordinates(coordinates, opt_layout);

};
goog.inherits(ol.geom.LineString, ol.geom.SimpleGeometry);


/**
 * Append the passed coordinate to the coordinates of the linestring.
 * @param {ol.Coordinate} coordinate Coordinate.
 * @api stable
 */
ol.geom.LineString.prototype.appendCoordinate = function(coordinate) {
  goog.asserts.assert(coordinate.length == this.stride,
      'length of coordinate array should match stride');
  if (!this.flatCoordinates) {
    this.flatCoordinates = coordinate.slice();
  } else {
    goog.array.extend(this.flatCoordinates, coordinate);
  }
  this.changed();
};


/**
 * Make a complete copy of the geometry.
 * @return {!ol.geom.LineString} Clone.
 * @api stable
 */
ol.geom.LineString.prototype.clone = function() {
  var lineString = new ol.geom.LineString(null);
  lineString.setFlatCoordinates(this.layout, this.flatCoordinates.slice());
  return lineString;
};


/**
 * @inheritDoc
 */
ol.geom.LineString.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
  if (minSquaredDistance <
      ol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
    return minSquaredDistance;
  }
  if (this.maxDeltaRevision_ != this.getRevision()) {
    this.maxDelta_ = Math.sqrt(ol.geom.flat.closest.getMaxSquaredDelta(
        this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, 0));
    this.maxDeltaRevision_ = this.getRevision();
  }
  return ol.geom.flat.closest.getClosestPoint(
      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
      this.maxDelta_, false, x, y, closestPoint, minSquaredDistance);
};


/**
 * Iterate over each segment, calling the provided callback.
 * If the callback returns a truthy value the function returns that
 * value immediately. Otherwise the function returns `false`.
 *
 * @param {function(this: S, ol.Coordinate, ol.Coordinate): T} callback Function
 *     called for each segment.
 * @param {S=} opt_this The object to be used as the value of 'this'
 *     within callback.
 * @return {T|boolean} Value.
 * @template T,S
 * @api
 */
ol.geom.LineString.prototype.forEachSegment = function(callback, opt_this) {
  return ol.geom.flat.segments.forEach(this.flatCoordinates, 0,
      this.flatCoordinates.length, this.stride, callback, opt_this);
};


/**
 * Returns the coordinate at `m` using linear interpolation, or `null` if no
 * such coordinate exists.
 *
 * `opt_extrapolate` controls extrapolation beyond the range of Ms in the
 * MultiLineString. If `opt_extrapolate` is `true` then Ms less than the first
 * M will return the first coordinate and Ms greater than the last M will
 * return the last coordinate.
 *
 * @param {number} m M.
 * @param {boolean=} opt_extrapolate Extrapolate. Default is `false`.
 * @return {ol.Coordinate} Coordinate.
 * @api stable
 */
ol.geom.LineString.prototype.getCoordinateAtM = function(m, opt_extrapolate) {
  if (this.layout != ol.geom.GeometryLayout.XYM &&
      this.layout != ol.geom.GeometryLayout.XYZM) {
    return null;
  }
  var extrapolate = opt_extrapolate !== undefined ? opt_extrapolate : false;
  return ol.geom.flat.lineStringCoordinateAtM(this.flatCoordinates, 0,
      this.flatCoordinates.length, this.stride, m, extrapolate);
};


/**
 * Return the coordinates of the linestring.
 * @return {Array.<ol.Coordinate>} Coordinates.
 * @api stable
 */
ol.geom.LineString.prototype.getCoordinates = function() {
  return ol.geom.flat.inflate.coordinates(
      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride);
};


/**
 * Return the coordinate at the provided fraction along the linestring.
 * The `fraction` is a number between 0 and 1, where 0 is the start of the
 * linestring and 1 is the end.
 * @param {number} fraction Fraction.
 * @param {ol.Coordinate=} opt_dest Optional coordinate whose values will
 *     be modified. If not provided, a new coordinate will be returned.
 * @return {ol.Coordinate} Coordinate of the interpolated point.
 * @api
 */
ol.geom.LineString.prototype.getCoordinateAt = function(fraction, opt_dest) {
  return ol.geom.flat.interpolate.lineString(
      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
      fraction, opt_dest);
};


/**
 * Return the length of the linestring on projected plane.
 * @return {number} Length (on projected plane).
 * @api stable
 */
ol.geom.LineString.prototype.getLength = function() {
  return ol.geom.flat.length.lineString(
      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride);
};


/**
 * @return {Array.<number>} Flat midpoint.
 */
ol.geom.LineString.prototype.getFlatMidpoint = function() {
  if (this.flatMidpointRevision_ != this.getRevision()) {
    this.flatMidpoint_ = this.getCoordinateAt(0.5, this.flatMidpoint_);
    this.flatMidpointRevision_ = this.getRevision();
  }
  return this.flatMidpoint_;
};


/**
 * @inheritDoc
 */
ol.geom.LineString.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) {
  var simplifiedFlatCoordinates = [];
  simplifiedFlatCoordinates.length = ol.geom.flat.simplify.douglasPeucker(
      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
      squaredTolerance, simplifiedFlatCoordinates, 0);
  var simplifiedLineString = new ol.geom.LineString(null);
  simplifiedLineString.setFlatCoordinates(
      ol.geom.GeometryLayout.XY, simplifiedFlatCoordinates);
  return simplifiedLineString;
};


/**
 * @inheritDoc
 * @api stable
 */
ol.geom.LineString.prototype.getType = function() {
  return ol.geom.GeometryType.LINE_STRING;
};


/**
 * @inheritDoc
 * @api stable
 */
ol.geom.LineString.prototype.intersectsExtent = function(extent) {
  return ol.geom.flat.intersectsextent.lineString(
      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
      extent);
};


/**
 * Set the coordinates of the linestring.
 * @param {Array.<ol.Coordinate>} coordinates Coordinates.
 * @param {ol.geom.GeometryLayout=} opt_layout Layout.
 * @api stable
 */
ol.geom.LineString.prototype.setCoordinates = function(coordinates, opt_layout) {
  if (!coordinates) {
    this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null);
  } else {
    this.setLayout(opt_layout, coordinates, 1);
    if (!this.flatCoordinates) {
      this.flatCoordinates = [];
    }
    this.flatCoordinates.length = ol.geom.flat.deflate.coordinates(
        this.flatCoordinates, 0, coordinates, this.stride);
    this.changed();
  }
};


/**
 * @param {ol.geom.GeometryLayout} layout Layout.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 */
ol.geom.LineString.prototype.setFlatCoordinates = function(layout, flatCoordinates) {
  this.setFlatCoordinatesInternal(layout, flatCoordinates);
  this.changed();
};

goog.provide('ol.geom.MultiLineString');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('ol');
goog.require('ol.extent');
goog.require('ol.geom.GeometryLayout');
goog.require('ol.geom.GeometryType');
goog.require('ol.geom.LineString');
goog.require('ol.geom.SimpleGeometry');
goog.require('ol.geom.flat.closest');
goog.require('ol.geom.flat.deflate');
goog.require('ol.geom.flat.inflate');
goog.require('ol.geom.flat.interpolate');
goog.require('ol.geom.flat.intersectsextent');
goog.require('ol.geom.flat.simplify');


/**
 * @classdesc
 * Multi-linestring geometry.
 *
 * @constructor
 * @extends {ol.geom.SimpleGeometry}
 * @param {Array.<Array.<ol.Coordinate>>} coordinates Coordinates.
 * @param {ol.geom.GeometryLayout=} opt_layout Layout.
 * @api stable
 */
ol.geom.MultiLineString = function(coordinates, opt_layout) {

  goog.base(this);

  /**
   * @type {Array.<number>}
   * @private
   */
  this.ends_ = [];

  /**
   * @private
   * @type {number}
   */
  this.maxDelta_ = -1;

  /**
   * @private
   * @type {number}
   */
  this.maxDeltaRevision_ = -1;

  this.setCoordinates(coordinates, opt_layout);

};
goog.inherits(ol.geom.MultiLineString, ol.geom.SimpleGeometry);


/**
 * Append the passed linestring to the multilinestring.
 * @param {ol.geom.LineString} lineString LineString.
 * @api stable
 */
ol.geom.MultiLineString.prototype.appendLineString = function(lineString) {
  goog.asserts.assert(lineString.getLayout() == this.layout,
      'layout of lineString should match the layout');
  if (!this.flatCoordinates) {
    this.flatCoordinates = lineString.getFlatCoordinates().slice();
  } else {
    goog.array.extend(
        this.flatCoordinates, lineString.getFlatCoordinates().slice());
  }
  this.ends_.push(this.flatCoordinates.length);
  this.changed();
};


/**
 * Make a complete copy of the geometry.
 * @return {!ol.geom.MultiLineString} Clone.
 * @api stable
 */
ol.geom.MultiLineString.prototype.clone = function() {
  var multiLineString = new ol.geom.MultiLineString(null);
  multiLineString.setFlatCoordinates(
      this.layout, this.flatCoordinates.slice(), this.ends_.slice());
  return multiLineString;
};


/**
 * @inheritDoc
 */
ol.geom.MultiLineString.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
  if (minSquaredDistance <
      ol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
    return minSquaredDistance;
  }
  if (this.maxDeltaRevision_ != this.getRevision()) {
    this.maxDelta_ = Math.sqrt(ol.geom.flat.closest.getsMaxSquaredDelta(
        this.flatCoordinates, 0, this.ends_, this.stride, 0));
    this.maxDeltaRevision_ = this.getRevision();
  }
  return ol.geom.flat.closest.getsClosestPoint(
      this.flatCoordinates, 0, this.ends_, this.stride,
      this.maxDelta_, false, x, y, closestPoint, minSquaredDistance);
};


/**
 * Returns the coordinate at `m` using linear interpolation, or `null` if no
 * such coordinate exists.
 *
 * `opt_extrapolate` controls extrapolation beyond the range of Ms in the
 * MultiLineString. If `opt_extrapolate` is `true` then Ms less than the first
 * M will return the first coordinate and Ms greater than the last M will
 * return the last coordinate.
 *
 * `opt_interpolate` controls interpolation between consecutive LineStrings
 * within the MultiLineString. If `opt_interpolate` is `true` the coordinates
 * will be linearly interpolated between the last coordinate of one LineString
 * and the first coordinate of the next LineString.  If `opt_interpolate` is
 * `false` then the function will return `null` for Ms falling between
 * LineStrings.
 *
 * @param {number} m M.
 * @param {boolean=} opt_extrapolate Extrapolate. Default is `false`.
 * @param {boolean=} opt_interpolate Interpolate. Default is `false`.
 * @return {ol.Coordinate} Coordinate.
 * @api stable
 */
ol.geom.MultiLineString.prototype.getCoordinateAtM = function(m, opt_extrapolate, opt_interpolate) {
  if ((this.layout != ol.geom.GeometryLayout.XYM &&
       this.layout != ol.geom.GeometryLayout.XYZM) ||
      this.flatCoordinates.length === 0) {
    return null;
  }
  var extrapolate = opt_extrapolate !== undefined ? opt_extrapolate : false;
  var interpolate = opt_interpolate !== undefined ? opt_interpolate : false;
  return ol.geom.flat.lineStringsCoordinateAtM(this.flatCoordinates, 0,
      this.ends_, this.stride, m, extrapolate, interpolate);
};


/**
 * Return the coordinates of the multilinestring.
 * @return {Array.<Array.<ol.Coordinate>>} Coordinates.
 * @api stable
 */
ol.geom.MultiLineString.prototype.getCoordinates = function() {
  return ol.geom.flat.inflate.coordinatess(
      this.flatCoordinates, 0, this.ends_, this.stride);
};


/**
 * @return {Array.<number>} Ends.
 */
ol.geom.MultiLineString.prototype.getEnds = function() {
  return this.ends_;
};


/**
 * Return the linestring at the specified index.
 * @param {number} index Index.
 * @return {ol.geom.LineString} LineString.
 * @api stable
 */
ol.geom.MultiLineString.prototype.getLineString = function(index) {
  goog.asserts.assert(0 <= index && index < this.ends_.length,
      'index should be in between 0 and length of the this.ends_ array');
  if (index < 0 || this.ends_.length <= index) {
    return null;
  }
  var lineString = new ol.geom.LineString(null);
  lineString.setFlatCoordinates(this.layout, this.flatCoordinates.slice(
      index === 0 ? 0 : this.ends_[index - 1], this.ends_[index]));
  return lineString;
};


/**
 * Return the linestrings of this multilinestring.
 * @return {Array.<ol.geom.LineString>} LineStrings.
 * @api stable
 */
ol.geom.MultiLineString.prototype.getLineStrings = function() {
  var flatCoordinates = this.flatCoordinates;
  var ends = this.ends_;
  var layout = this.layout;
  /** @type {Array.<ol.geom.LineString>} */
  var lineStrings = [];
  var offset = 0;
  var i, ii;
  for (i = 0, ii = ends.length; i < ii; ++i) {
    var end = ends[i];
    var lineString = new ol.geom.LineString(null);
    lineString.setFlatCoordinates(layout, flatCoordinates.slice(offset, end));
    lineStrings.push(lineString);
    offset = end;
  }
  return lineStrings;
};


/**
 * @return {Array.<number>} Flat midpoints.
 */
ol.geom.MultiLineString.prototype.getFlatMidpoints = function() {
  var midpoints = [];
  var flatCoordinates = this.flatCoordinates;
  var offset = 0;
  var ends = this.ends_;
  var stride = this.stride;
  var i, ii;
  for (i = 0, ii = ends.length; i < ii; ++i) {
    var end = ends[i];
    var midpoint = ol.geom.flat.interpolate.lineString(
        flatCoordinates, offset, end, stride, 0.5);
    goog.array.extend(midpoints, midpoint);
    offset = end;
  }
  return midpoints;
};


/**
 * @inheritDoc
 */
ol.geom.MultiLineString.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) {
  var simplifiedFlatCoordinates = [];
  var simplifiedEnds = [];
  simplifiedFlatCoordinates.length = ol.geom.flat.simplify.douglasPeuckers(
      this.flatCoordinates, 0, this.ends_, this.stride, squaredTolerance,
      simplifiedFlatCoordinates, 0, simplifiedEnds);
  var simplifiedMultiLineString = new ol.geom.MultiLineString(null);
  simplifiedMultiLineString.setFlatCoordinates(
      ol.geom.GeometryLayout.XY, simplifiedFlatCoordinates, simplifiedEnds);
  return simplifiedMultiLineString;
};


/**
 * @inheritDoc
 * @api stable
 */
ol.geom.MultiLineString.prototype.getType = function() {
  return ol.geom.GeometryType.MULTI_LINE_STRING;
};


/**
 * @inheritDoc
 * @api stable
 */
ol.geom.MultiLineString.prototype.intersectsExtent = function(extent) {
  return ol.geom.flat.intersectsextent.lineStrings(
      this.flatCoordinates, 0, this.ends_, this.stride, extent);
};


/**
 * Set the coordinates of the multilinestring.
 * @param {Array.<Array.<ol.Coordinate>>} coordinates Coordinates.
 * @param {ol.geom.GeometryLayout=} opt_layout Layout.
 * @api stable
 */
ol.geom.MultiLineString.prototype.setCoordinates = function(coordinates, opt_layout) {
  if (!coordinates) {
    this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null, this.ends_);
  } else {
    this.setLayout(opt_layout, coordinates, 2);
    if (!this.flatCoordinates) {
      this.flatCoordinates = [];
    }
    var ends = ol.geom.flat.deflate.coordinatess(
        this.flatCoordinates, 0, coordinates, this.stride, this.ends_);
    this.flatCoordinates.length = ends.length === 0 ? 0 : ends[ends.length - 1];
    this.changed();
  }
};


/**
 * @param {ol.geom.GeometryLayout} layout Layout.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {Array.<number>} ends Ends.
 */
ol.geom.MultiLineString.prototype.setFlatCoordinates = function(layout, flatCoordinates, ends) {
  if (!flatCoordinates) {
    goog.asserts.assert(ends && ends.length === 0,
        'ends must be truthy and ends.length should be 0');
  } else if (ends.length === 0) {
    goog.asserts.assert(flatCoordinates.length === 0,
        'flatCoordinates should be an empty array');
  } else {
    goog.asserts.assert(flatCoordinates.length == ends[ends.length - 1],
        'length of flatCoordinates array should match the last value of ends');
  }
  this.setFlatCoordinatesInternal(layout, flatCoordinates);
  this.ends_ = ends;
  this.changed();
};


/**
 * @param {Array.<ol.geom.LineString>} lineStrings LineStrings.
 */
ol.geom.MultiLineString.prototype.setLineStrings = function(lineStrings) {
  var layout = this.getLayout();
  var flatCoordinates = [];
  var ends = [];
  var i, ii;
  for (i = 0, ii = lineStrings.length; i < ii; ++i) {
    var lineString = lineStrings[i];
    if (i === 0) {
      layout = lineString.getLayout();
    } else {
      // FIXME better handle the case of non-matching layouts
      goog.asserts.assert(lineString.getLayout() == layout,
          'layout of lineString should match layout');
    }
    goog.array.extend(flatCoordinates, lineString.getFlatCoordinates());
    ends.push(flatCoordinates.length);
  }
  this.setFlatCoordinates(layout, flatCoordinates, ends);
};

goog.provide('ol.geom.MultiPoint');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('ol.extent');
goog.require('ol.geom.GeometryLayout');
goog.require('ol.geom.GeometryType');
goog.require('ol.geom.Point');
goog.require('ol.geom.SimpleGeometry');
goog.require('ol.geom.flat.deflate');
goog.require('ol.geom.flat.inflate');
goog.require('ol.math');


/**
 * @classdesc
 * Multi-point geometry.
 *
 * @constructor
 * @extends {ol.geom.SimpleGeometry}
 * @param {Array.<ol.Coordinate>} coordinates Coordinates.
 * @param {ol.geom.GeometryLayout=} opt_layout Layout.
 * @api stable
 */
ol.geom.MultiPoint = function(coordinates, opt_layout) {
  goog.base(this);
  this.setCoordinates(coordinates, opt_layout);
};
goog.inherits(ol.geom.MultiPoint, ol.geom.SimpleGeometry);


/**
 * Append the passed point to this multipoint.
 * @param {ol.geom.Point} point Point.
 * @api stable
 */
ol.geom.MultiPoint.prototype.appendPoint = function(point) {
  goog.asserts.assert(point.getLayout() == this.layout,
      'the layout of point should match layout');
  if (!this.flatCoordinates) {
    this.flatCoordinates = point.getFlatCoordinates().slice();
  } else {
    goog.array.extend(this.flatCoordinates, point.getFlatCoordinates());
  }
  this.changed();
};


/**
 * Make a complete copy of the geometry.
 * @return {!ol.geom.MultiPoint} Clone.
 * @api stable
 */
ol.geom.MultiPoint.prototype.clone = function() {
  var multiPoint = new ol.geom.MultiPoint(null);
  multiPoint.setFlatCoordinates(this.layout, this.flatCoordinates.slice());
  return multiPoint;
};


/**
 * @inheritDoc
 */
ol.geom.MultiPoint.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
  if (minSquaredDistance <
      ol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
    return minSquaredDistance;
  }
  var flatCoordinates = this.flatCoordinates;
  var stride = this.stride;
  var i, ii, j;
  for (i = 0, ii = flatCoordinates.length; i < ii; i += stride) {
    var squaredDistance = ol.math.squaredDistance(
        x, y, flatCoordinates[i], flatCoordinates[i + 1]);
    if (squaredDistance < minSquaredDistance) {
      minSquaredDistance = squaredDistance;
      for (j = 0; j < stride; ++j) {
        closestPoint[j] = flatCoordinates[i + j];
      }
      closestPoint.length = stride;
    }
  }
  return minSquaredDistance;
};


/**
 * Return the coordinates of the multipoint.
 * @return {Array.<ol.Coordinate>} Coordinates.
 * @api stable
 */
ol.geom.MultiPoint.prototype.getCoordinates = function() {
  return ol.geom.flat.inflate.coordinates(
      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride);
};


/**
 * Return the point at the specified index.
 * @param {number} index Index.
 * @return {ol.geom.Point} Point.
 * @api stable
 */
ol.geom.MultiPoint.prototype.getPoint = function(index) {
  var n = !this.flatCoordinates ?
      0 : this.flatCoordinates.length / this.stride;
  goog.asserts.assert(0 <= index && index < n,
      'index should be in between 0 and n');
  if (index < 0 || n <= index) {
    return null;
  }
  var point = new ol.geom.Point(null);
  point.setFlatCoordinates(this.layout, this.flatCoordinates.slice(
      index * this.stride, (index + 1) * this.stride));
  return point;
};


/**
 * Return the points of this multipoint.
 * @return {Array.<ol.geom.Point>} Points.
 * @api stable
 */
ol.geom.MultiPoint.prototype.getPoints = function() {
  var flatCoordinates = this.flatCoordinates;
  var layout = this.layout;
  var stride = this.stride;
  /** @type {Array.<ol.geom.Point>} */
  var points = [];
  var i, ii;
  for (i = 0, ii = flatCoordinates.length; i < ii; i += stride) {
    var point = new ol.geom.Point(null);
    point.setFlatCoordinates(layout, flatCoordinates.slice(i, i + stride));
    points.push(point);
  }
  return points;
};


/**
 * @inheritDoc
 * @api stable
 */
ol.geom.MultiPoint.prototype.getType = function() {
  return ol.geom.GeometryType.MULTI_POINT;
};


/**
 * @inheritDoc
 * @api stable
 */
ol.geom.MultiPoint.prototype.intersectsExtent = function(extent) {
  var flatCoordinates = this.flatCoordinates;
  var stride = this.stride;
  var i, ii, x, y;
  for (i = 0, ii = flatCoordinates.length; i < ii; i += stride) {
    x = flatCoordinates[i];
    y = flatCoordinates[i + 1];
    if (ol.extent.containsXY(extent, x, y)) {
      return true;
    }
  }
  return false;
};


/**
 * Set the coordinates of the multipoint.
 * @param {Array.<ol.Coordinate>} coordinates Coordinates.
 * @param {ol.geom.GeometryLayout=} opt_layout Layout.
 * @api stable
 */
ol.geom.MultiPoint.prototype.setCoordinates = function(coordinates, opt_layout) {
  if (!coordinates) {
    this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null);
  } else {
    this.setLayout(opt_layout, coordinates, 1);
    if (!this.flatCoordinates) {
      this.flatCoordinates = [];
    }
    this.flatCoordinates.length = ol.geom.flat.deflate.coordinates(
        this.flatCoordinates, 0, coordinates, this.stride);
    this.changed();
  }
};


/**
 * @param {ol.geom.GeometryLayout} layout Layout.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 */
ol.geom.MultiPoint.prototype.setFlatCoordinates = function(layout, flatCoordinates) {
  this.setFlatCoordinatesInternal(layout, flatCoordinates);
  this.changed();
};

goog.provide('ol.geom.flat.center');

goog.require('ol.extent');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<Array.<number>>} endss Endss.
 * @param {number} stride Stride.
 * @return {Array.<number>} Flat centers.
 */
ol.geom.flat.center.linearRingss = function(flatCoordinates, offset, endss, stride) {
  var flatCenters = [];
  var i, ii;
  var extent = ol.extent.createEmpty();
  for (i = 0, ii = endss.length; i < ii; ++i) {
    var ends = endss[i];
    extent = ol.extent.createOrUpdateFromFlatCoordinates(
        flatCoordinates, offset, ends[0], stride);
    flatCenters.push((extent[0] + extent[2]) / 2, (extent[1] + extent[3]) / 2);
    offset = ends[ends.length - 1];
  }
  return flatCenters;
};

goog.provide('ol.geom.MultiPolygon');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.object');
goog.require('ol');
goog.require('ol.extent');
goog.require('ol.geom.GeometryLayout');
goog.require('ol.geom.GeometryType');
goog.require('ol.geom.MultiPoint');
goog.require('ol.geom.Polygon');
goog.require('ol.geom.SimpleGeometry');
goog.require('ol.geom.flat.area');
goog.require('ol.geom.flat.center');
goog.require('ol.geom.flat.closest');
goog.require('ol.geom.flat.contains');
goog.require('ol.geom.flat.deflate');
goog.require('ol.geom.flat.inflate');
goog.require('ol.geom.flat.interiorpoint');
goog.require('ol.geom.flat.intersectsextent');
goog.require('ol.geom.flat.orient');
goog.require('ol.geom.flat.simplify');


/**
 * @classdesc
 * Multi-polygon geometry.
 *
 * @constructor
 * @extends {ol.geom.SimpleGeometry}
 * @param {Array.<Array.<Array.<ol.Coordinate>>>} coordinates Coordinates.
 * @param {ol.geom.GeometryLayout=} opt_layout Layout.
 * @api stable
 */
ol.geom.MultiPolygon = function(coordinates, opt_layout) {

  goog.base(this);

  /**
   * @type {Array.<Array.<number>>}
   * @private
   */
  this.endss_ = [];

  /**
   * @private
   * @type {number}
   */
  this.flatInteriorPointsRevision_ = -1;

  /**
   * @private
   * @type {Array.<number>}
   */
  this.flatInteriorPoints_ = null;

  /**
   * @private
   * @type {number}
   */
  this.maxDelta_ = -1;

  /**
   * @private
   * @type {number}
   */
  this.maxDeltaRevision_ = -1;

  /**
   * @private
   * @type {number}
   */
  this.orientedRevision_ = -1;

  /**
   * @private
   * @type {Array.<number>}
   */
  this.orientedFlatCoordinates_ = null;

  this.setCoordinates(coordinates, opt_layout);

};
goog.inherits(ol.geom.MultiPolygon, ol.geom.SimpleGeometry);


/**
 * Append the passed polygon to this multipolygon.
 * @param {ol.geom.Polygon} polygon Polygon.
 * @api stable
 */
ol.geom.MultiPolygon.prototype.appendPolygon = function(polygon) {
  goog.asserts.assert(polygon.getLayout() == this.layout,
      'layout of polygon should match layout');
  /** @type {Array.<number>} */
  var ends;
  if (!this.flatCoordinates) {
    this.flatCoordinates = polygon.getFlatCoordinates().slice();
    ends = polygon.getEnds().slice();
    this.endss_.push();
  } else {
    var offset = this.flatCoordinates.length;
    goog.array.extend(this.flatCoordinates, polygon.getFlatCoordinates());
    ends = polygon.getEnds().slice();
    var i, ii;
    for (i = 0, ii = ends.length; i < ii; ++i) {
      ends[i] += offset;
    }
  }
  this.endss_.push(ends);
  this.changed();
};


/**
 * Make a complete copy of the geometry.
 * @return {!ol.geom.MultiPolygon} Clone.
 * @api stable
 */
ol.geom.MultiPolygon.prototype.clone = function() {
  var multiPolygon = new ol.geom.MultiPolygon(null);
  var newEndss = /** @type {Array.<Array.<number>>} */
      (goog.object.unsafeClone(this.endss_));
  multiPolygon.setFlatCoordinates(
      this.layout, this.flatCoordinates.slice(), newEndss);
  return multiPolygon;
};


/**
 * @inheritDoc
 */
ol.geom.MultiPolygon.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
  if (minSquaredDistance <
      ol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
    return minSquaredDistance;
  }
  if (this.maxDeltaRevision_ != this.getRevision()) {
    this.maxDelta_ = Math.sqrt(ol.geom.flat.closest.getssMaxSquaredDelta(
        this.flatCoordinates, 0, this.endss_, this.stride, 0));
    this.maxDeltaRevision_ = this.getRevision();
  }
  return ol.geom.flat.closest.getssClosestPoint(
      this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride,
      this.maxDelta_, true, x, y, closestPoint, minSquaredDistance);
};


/**
 * @inheritDoc
 */
ol.geom.MultiPolygon.prototype.containsXY = function(x, y) {
  return ol.geom.flat.contains.linearRingssContainsXY(
      this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride, x, y);
};


/**
 * Return the area of the multipolygon on projected plane.
 * @return {number} Area (on projected plane).
 * @api stable
 */
ol.geom.MultiPolygon.prototype.getArea = function() {
  return ol.geom.flat.area.linearRingss(
      this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride);
};


/**
 * Get the coordinate array for this geometry.  This array has the structure
 * of a GeoJSON coordinate array for multi-polygons.
 *
 * @param {boolean=} opt_right Orient coordinates according to the right-hand
 *     rule (counter-clockwise for exterior and clockwise for interior rings).
 *     If `false`, coordinates will be oriented according to the left-hand rule
 *     (clockwise for exterior and counter-clockwise for interior rings).
 *     By default, coordinate orientation will depend on how the geometry was
 *     constructed.
 * @return {Array.<Array.<Array.<ol.Coordinate>>>} Coordinates.
 * @api stable
 */
ol.geom.MultiPolygon.prototype.getCoordinates = function(opt_right) {
  var flatCoordinates;
  if (opt_right !== undefined) {
    flatCoordinates = this.getOrientedFlatCoordinates().slice();
    ol.geom.flat.orient.orientLinearRingss(
        flatCoordinates, 0, this.endss_, this.stride, opt_right);
  } else {
    flatCoordinates = this.flatCoordinates;
  }

  return ol.geom.flat.inflate.coordinatesss(
      flatCoordinates, 0, this.endss_, this.stride);
};


/**
 * @return {Array.<Array.<number>>} Endss.
 */
ol.geom.MultiPolygon.prototype.getEndss = function() {
  return this.endss_;
};


/**
 * @return {Array.<number>} Flat interior points.
 */
ol.geom.MultiPolygon.prototype.getFlatInteriorPoints = function() {
  if (this.flatInteriorPointsRevision_ != this.getRevision()) {
    var flatCenters = ol.geom.flat.center.linearRingss(
        this.flatCoordinates, 0, this.endss_, this.stride);
    this.flatInteriorPoints_ = ol.geom.flat.interiorpoint.linearRingss(
        this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride,
        flatCenters);
    this.flatInteriorPointsRevision_ = this.getRevision();
  }
  return this.flatInteriorPoints_;
};


/**
 * Return the interior points as {@link ol.geom.MultiPoint multipoint}.
 * @return {ol.geom.MultiPoint} Interior points.
 * @api stable
 */
ol.geom.MultiPolygon.prototype.getInteriorPoints = function() {
  var interiorPoints = new ol.geom.MultiPoint(null);
  interiorPoints.setFlatCoordinates(ol.geom.GeometryLayout.XY,
      this.getFlatInteriorPoints().slice());
  return interiorPoints;
};


/**
 * @return {Array.<number>} Oriented flat coordinates.
 */
ol.geom.MultiPolygon.prototype.getOrientedFlatCoordinates = function() {
  if (this.orientedRevision_ != this.getRevision()) {
    var flatCoordinates = this.flatCoordinates;
    if (ol.geom.flat.orient.linearRingssAreOriented(
        flatCoordinates, 0, this.endss_, this.stride)) {
      this.orientedFlatCoordinates_ = flatCoordinates;
    } else {
      this.orientedFlatCoordinates_ = flatCoordinates.slice();
      this.orientedFlatCoordinates_.length =
          ol.geom.flat.orient.orientLinearRingss(
              this.orientedFlatCoordinates_, 0, this.endss_, this.stride);
    }
    this.orientedRevision_ = this.getRevision();
  }
  return this.orientedFlatCoordinates_;
};


/**
 * @inheritDoc
 */
ol.geom.MultiPolygon.prototype.getSimplifiedGeometryInternal = function(squaredTolerance) {
  var simplifiedFlatCoordinates = [];
  var simplifiedEndss = [];
  simplifiedFlatCoordinates.length = ol.geom.flat.simplify.quantizess(
      this.flatCoordinates, 0, this.endss_, this.stride,
      Math.sqrt(squaredTolerance),
      simplifiedFlatCoordinates, 0, simplifiedEndss);
  var simplifiedMultiPolygon = new ol.geom.MultiPolygon(null);
  simplifiedMultiPolygon.setFlatCoordinates(
      ol.geom.GeometryLayout.XY, simplifiedFlatCoordinates, simplifiedEndss);
  return simplifiedMultiPolygon;
};


/**
 * Return the polygon at the specified index.
 * @param {number} index Index.
 * @return {ol.geom.Polygon} Polygon.
 * @api stable
 */
ol.geom.MultiPolygon.prototype.getPolygon = function(index) {
  goog.asserts.assert(0 <= index && index < this.endss_.length,
      'index should be in between 0 and the length of this.endss_');
  if (index < 0 || this.endss_.length <= index) {
    return null;
  }
  var offset;
  if (index === 0) {
    offset = 0;
  } else {
    var prevEnds = this.endss_[index - 1];
    offset = prevEnds[prevEnds.length - 1];
  }
  var ends = this.endss_[index].slice();
  var end = ends[ends.length - 1];
  if (offset !== 0) {
    var i, ii;
    for (i = 0, ii = ends.length; i < ii; ++i) {
      ends[i] -= offset;
    }
  }
  var polygon = new ol.geom.Polygon(null);
  polygon.setFlatCoordinates(
      this.layout, this.flatCoordinates.slice(offset, end), ends);
  return polygon;
};


/**
 * Return the polygons of this multipolygon.
 * @return {Array.<ol.geom.Polygon>} Polygons.
 * @api stable
 */
ol.geom.MultiPolygon.prototype.getPolygons = function() {
  var layout = this.layout;
  var flatCoordinates = this.flatCoordinates;
  var endss = this.endss_;
  var polygons = [];
  var offset = 0;
  var i, ii, j, jj;
  for (i = 0, ii = endss.length; i < ii; ++i) {
    var ends = endss[i].slice();
    var end = ends[ends.length - 1];
    if (offset !== 0) {
      for (j = 0, jj = ends.length; j < jj; ++j) {
        ends[j] -= offset;
      }
    }
    var polygon = new ol.geom.Polygon(null);
    polygon.setFlatCoordinates(
        layout, flatCoordinates.slice(offset, end), ends);
    polygons.push(polygon);
    offset = end;
  }
  return polygons;
};


/**
 * @inheritDoc
 * @api stable
 */
ol.geom.MultiPolygon.prototype.getType = function() {
  return ol.geom.GeometryType.MULTI_POLYGON;
};


/**
 * @inheritDoc
 * @api stable
 */
ol.geom.MultiPolygon.prototype.intersectsExtent = function(extent) {
  return ol.geom.flat.intersectsextent.linearRingss(
      this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride, extent);
};


/**
 * Set the coordinates of the multipolygon.
 * @param {Array.<Array.<Array.<ol.Coordinate>>>} coordinates Coordinates.
 * @param {ol.geom.GeometryLayout=} opt_layout Layout.
 * @api stable
 */
ol.geom.MultiPolygon.prototype.setCoordinates = function(coordinates, opt_layout) {
  if (!coordinates) {
    this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null, this.endss_);
  } else {
    this.setLayout(opt_layout, coordinates, 3);
    if (!this.flatCoordinates) {
      this.flatCoordinates = [];
    }
    var endss = ol.geom.flat.deflate.coordinatesss(
        this.flatCoordinates, 0, coordinates, this.stride, this.endss_);
    if (endss.length === 0) {
      this.flatCoordinates.length = 0;
    } else {
      var lastEnds = endss[endss.length - 1];
      this.flatCoordinates.length = lastEnds.length === 0 ?
          0 : lastEnds[lastEnds.length - 1];
    }
    this.changed();
  }
};


/**
 * @param {ol.geom.GeometryLayout} layout Layout.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {Array.<Array.<number>>} endss Endss.
 */
ol.geom.MultiPolygon.prototype.setFlatCoordinates = function(layout, flatCoordinates, endss) {
  goog.asserts.assert(endss, 'endss must be truthy');
  if (!flatCoordinates || flatCoordinates.length === 0) {
    goog.asserts.assert(endss.length === 0, 'the length of endss should be 0');
  } else {
    goog.asserts.assert(endss.length > 0, 'endss cannot be an empty array');
    var ends = endss[endss.length - 1];
    goog.asserts.assert(flatCoordinates.length == ends[ends.length - 1],
        'the length of flatCoordinates should be the last value of ends');
  }
  this.setFlatCoordinatesInternal(layout, flatCoordinates);
  this.endss_ = endss;
  this.changed();
};


/**
 * @param {Array.<ol.geom.Polygon>} polygons Polygons.
 */
ol.geom.MultiPolygon.prototype.setPolygons = function(polygons) {
  var layout = this.getLayout();
  var flatCoordinates = [];
  var endss = [];
  var i, ii, ends;
  for (i = 0, ii = polygons.length; i < ii; ++i) {
    var polygon = polygons[i];
    if (i === 0) {
      layout = polygon.getLayout();
    } else {
      // FIXME better handle the case of non-matching layouts
      goog.asserts.assert(polygon.getLayout() == layout,
          'layout of polygon should be layout');
    }
    var offset = flatCoordinates.length;
    ends = polygon.getEnds();
    var j, jj;
    for (j = 0, jj = ends.length; j < jj; ++j) {
      ends[j] += offset;
    }
    goog.array.extend(flatCoordinates, polygon.getFlatCoordinates());
    endss.push(ends);
  }
  this.setFlatCoordinates(layout, flatCoordinates, endss);
};

goog.provide('ol.format.EsriJSON');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.object');
goog.require('ol.Feature');
goog.require('ol.extent');
goog.require('ol.format.Feature');
goog.require('ol.format.JSONFeature');
goog.require('ol.geom.GeometryLayout');
goog.require('ol.geom.GeometryType');
goog.require('ol.geom.LineString');
goog.require('ol.geom.LinearRing');
goog.require('ol.geom.MultiLineString');
goog.require('ol.geom.MultiPoint');
goog.require('ol.geom.MultiPolygon');
goog.require('ol.geom.Point');
goog.require('ol.geom.Polygon');
goog.require('ol.geom.flat.orient');
goog.require('ol.proj');


/**
 * @classdesc
 * Feature format for reading and writing data in the EsriJSON format.
 *
 * @constructor
 * @extends {ol.format.JSONFeature}
 * @param {olx.format.EsriJSONOptions=} opt_options Options.
 * @api
 */
ol.format.EsriJSON = function(opt_options) {

  var options = opt_options ? opt_options : {};

  goog.base(this);

  /**
   * Name of the geometry attribute for features.
   * @type {string|undefined}
   * @private
   */
  this.geometryName_ = options.geometryName;

};
goog.inherits(ol.format.EsriJSON, ol.format.JSONFeature);


/**
 * @param {EsriJSONGeometry} object Object.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @private
 * @return {ol.geom.Geometry} Geometry.
 */
ol.format.EsriJSON.readGeometry_ = function(object, opt_options) {
  if (!object) {
    return null;
  }
  var type;
  if (goog.isNumber(object.x) && goog.isNumber(object.y)) {
    type = ol.geom.GeometryType.POINT;
  } else if (object.points) {
    type = ol.geom.GeometryType.MULTI_POINT;
  } else if (object.paths) {
    if (object.paths.length === 1) {
      type = ol.geom.GeometryType.LINE_STRING;
    } else {
      type = ol.geom.GeometryType.MULTI_LINE_STRING;
    }
  } else if (object.rings) {
    var layout = ol.format.EsriJSON.getGeometryLayout_(object);
    var rings = ol.format.EsriJSON.convertRings_(object.rings, layout);
    object = /** @type {EsriJSONGeometry} */(goog.object.clone(object));
    if (rings.length === 1) {
      type = ol.geom.GeometryType.POLYGON;
      object.rings = rings[0];
    } else {
      type = ol.geom.GeometryType.MULTI_POLYGON;
      object.rings = rings;
    }
  }
  goog.asserts.assert(type, 'geometry type should be defined');
  var geometryReader = ol.format.EsriJSON.GEOMETRY_READERS_[type];
  goog.asserts.assert(geometryReader,
      'geometryReader should be defined');
  return /** @type {ol.geom.Geometry} */ (
      ol.format.Feature.transformWithOptions(
          geometryReader(object), false, opt_options));
};


/**
 * Determines inner and outer rings.
 * Checks if any polygons in this array contain any other polygons in this
 * array. It is used for checking for holes.
 * Logic inspired by: https://github.com/Esri/terraformer-arcgis-parser
 * @param {Array.<!Array.<!Array.<number>>>} rings Rings.
 * @param {ol.geom.GeometryLayout} layout Geometry layout.
 * @private
 * @return {Array.<!Array.<!Array.<number>>>} Transoformed rings.
 */
ol.format.EsriJSON.convertRings_ = function(rings, layout) {
  var outerRings = [];
  var holes = [];
  var i, ii;
  for (i = 0, ii = rings.length; i < ii; ++i) {
    var flatRing = goog.array.flatten(rings[i]);
    // is this ring an outer ring? is it clockwise?
    var clockwise = ol.geom.flat.orient.linearRingIsClockwise(flatRing, 0,
        flatRing.length, layout.length);
    if (clockwise) {
      outerRings.push([rings[i]]);
    } else {
      holes.push(rings[i]);
    }
  }
  while (holes.length) {
    var hole = holes.shift();
    var matched = false;
    // loop over all outer rings and see if they contain our hole.
    for (i = outerRings.length - 1; i >= 0; i--) {
      var outerRing = outerRings[i][0];
      if (ol.extent.containsExtent(new ol.geom.LinearRing(
          outerRing).getExtent(),
          new ol.geom.LinearRing(hole).getExtent())) {
        // the hole is contained push it into our polygon
        outerRings[i].push(hole);
        matched = true;
        break;
      }
    }
    if (!matched) {
      // no outer rings contain this hole turn it into and outer
      // ring (reverse it)
      outerRings.push([hole.reverse()]);
    }
  }
  return outerRings;
};


/**
 * @param {EsriJSONGeometry} object Object.
 * @private
 * @return {ol.geom.Geometry} Point.
 */
ol.format.EsriJSON.readPointGeometry_ = function(object) {
  goog.asserts.assert(goog.isNumber(object.x), 'object.x should be number');
  goog.asserts.assert(goog.isNumber(object.y), 'object.y should be number');
  var point;
  if (object.m !== undefined && object.z !== undefined) {
    point = new ol.geom.Point([object.x, object.y, object.z, object.m],
        ol.geom.GeometryLayout.XYZM);
  } else if (object.z !== undefined) {
    point = new ol.geom.Point([object.x, object.y, object.z],
        ol.geom.GeometryLayout.XYZ);
  } else if (object.m !== undefined) {
    point = new ol.geom.Point([object.x, object.y, object.m],
        ol.geom.GeometryLayout.XYM);
  } else {
    point = new ol.geom.Point([object.x, object.y]);
  }
  return point;
};


/**
 * @param {EsriJSONGeometry} object Object.
 * @private
 * @return {ol.geom.Geometry} LineString.
 */
ol.format.EsriJSON.readLineStringGeometry_ = function(object) {
  goog.asserts.assert(goog.isArray(object.paths),
      'object.paths should be an array');
  goog.asserts.assert(object.paths.length === 1,
      'object.paths array length should be 1');
  var layout = ol.format.EsriJSON.getGeometryLayout_(object);
  return new ol.geom.LineString(object.paths[0], layout);
};


/**
 * @param {EsriJSONGeometry} object Object.
 * @private
 * @return {ol.geom.Geometry} MultiLineString.
 */
ol.format.EsriJSON.readMultiLineStringGeometry_ = function(object) {
  goog.asserts.assert(goog.isArray(object.paths),
      'object.paths should be an array');
  goog.asserts.assert(object.paths.length > 1,
      'object.paths array length should be more than 1');
  var layout = ol.format.EsriJSON.getGeometryLayout_(object);
  return new ol.geom.MultiLineString(object.paths, layout);
};


/**
 * @param {EsriJSONGeometry} object Object.
 * @private
 * @return {ol.geom.GeometryLayout} The geometry layout to use.
 */
ol.format.EsriJSON.getGeometryLayout_ = function(object) {
  var layout = ol.geom.GeometryLayout.XY;
  if (object.hasZ === true && object.hasM === true) {
    layout = ol.geom.GeometryLayout.XYZM;
  } else if (object.hasZ === true) {
    layout = ol.geom.GeometryLayout.XYZ;
  } else if (object.hasM === true) {
    layout = ol.geom.GeometryLayout.XYM;
  }
  return layout;
};


/**
 * @param {EsriJSONGeometry} object Object.
 * @private
 * @return {ol.geom.Geometry} MultiPoint.
 */
ol.format.EsriJSON.readMultiPointGeometry_ = function(object) {
  goog.asserts.assert(object.points, 'object.points should be defined');
  var layout = ol.format.EsriJSON.getGeometryLayout_(object);
  return new ol.geom.MultiPoint(object.points, layout);
};


/**
 * @param {EsriJSONGeometry} object Object.
 * @private
 * @return {ol.geom.Geometry} MultiPolygon.
 */
ol.format.EsriJSON.readMultiPolygonGeometry_ = function(object) {
  goog.asserts.assert(object.rings);
  goog.asserts.assert(object.rings.length > 1,
      'object.rings should have length larger than 1');
  var layout = ol.format.EsriJSON.getGeometryLayout_(object);
  return new ol.geom.MultiPolygon(
      /** @type {Array.<Array.<Array.<Array.<number>>>>} */(object.rings),
      layout);
};


/**
 * @param {EsriJSONGeometry} object Object.
 * @private
 * @return {ol.geom.Geometry} Polygon.
 */
ol.format.EsriJSON.readPolygonGeometry_ = function(object) {
  goog.asserts.assert(object.rings);
  var layout = ol.format.EsriJSON.getGeometryLayout_(object);
  return new ol.geom.Polygon(object.rings, layout);
};


/**
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {EsriJSONGeometry} EsriJSON geometry.
 */
ol.format.EsriJSON.writePointGeometry_ = function(geometry, opt_options) {
  goog.asserts.assertInstanceof(geometry, ol.geom.Point,
      'geometry should be an ol.geom.Point');
  var coordinates = geometry.getCoordinates();
  var layout = geometry.getLayout();
  if (layout === ol.geom.GeometryLayout.XYZ) {
    return /** @type {EsriJSONPoint} */ ({
      x: coordinates[0],
      y: coordinates[1],
      z: coordinates[2]
    });
  } else if (layout === ol.geom.GeometryLayout.XYM) {
    return /** @type {EsriJSONPoint} */ ({
      x: coordinates[0],
      y: coordinates[1],
      m: coordinates[2]
    });
  } else if (layout === ol.geom.GeometryLayout.XYZM) {
    return /** @type {EsriJSONPoint} */ ({
      x: coordinates[0],
      y: coordinates[1],
      z: coordinates[2],
      m: coordinates[3]
    });
  } else if (layout === ol.geom.GeometryLayout.XY) {
    return /** @type {EsriJSONPoint} */ ({
      x: coordinates[0],
      y: coordinates[1]
    });
  } else {
    goog.asserts.fail('Unknown geometry layout');
  }
};


/**
 * @param {ol.geom.SimpleGeometry} geometry Geometry.
 * @private
 * @return {Object} Object with boolean hasZ and hasM keys.
 */
ol.format.EsriJSON.getHasZM_ = function(geometry) {
  var layout = geometry.getLayout();
  return {
    hasZ: (layout === ol.geom.GeometryLayout.XYZ ||
        layout === ol.geom.GeometryLayout.XYZM),
    hasM: (layout === ol.geom.GeometryLayout.XYM ||
        layout === ol.geom.GeometryLayout.XYZM)
  };
};


/**
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {EsriJSONPolyline} EsriJSON geometry.
 */
ol.format.EsriJSON.writeLineStringGeometry_ = function(geometry, opt_options) {
  goog.asserts.assertInstanceof(geometry, ol.geom.LineString,
      'geometry should be an ol.geom.LineString');
  var hasZM = ol.format.EsriJSON.getHasZM_(geometry);
  return /** @type {EsriJSONPolyline} */ ({
    hasZ: hasZM.hasZ,
    hasM: hasZM.hasM,
    paths: [geometry.getCoordinates()]
  });
};


/**
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {EsriJSONPolygon} EsriJSON geometry.
 */
ol.format.EsriJSON.writePolygonGeometry_ = function(geometry, opt_options) {
  goog.asserts.assertInstanceof(geometry, ol.geom.Polygon,
      'geometry should be an ol.geom.Polygon');
  // Esri geometries use the left-hand rule
  var hasZM = ol.format.EsriJSON.getHasZM_(geometry);
  return /** @type {EsriJSONPolygon} */ ({
    hasZ: hasZM.hasZ,
    hasM: hasZM.hasM,
    rings: geometry.getCoordinates(false)
  });
};


/**
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {EsriJSONPolyline} EsriJSON geometry.
 */
ol.format.EsriJSON.writeMultiLineStringGeometry_ = function(geometry, opt_options) {
  goog.asserts.assertInstanceof(geometry, ol.geom.MultiLineString,
      'geometry should be an ol.geom.MultiLineString');
  var hasZM = ol.format.EsriJSON.getHasZM_(geometry);
  return /** @type {EsriJSONPolyline} */ ({
    hasZ: hasZM.hasZ,
    hasM: hasZM.hasM,
    paths: geometry.getCoordinates()
  });
};


/**
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {EsriJSONMultipoint} EsriJSON geometry.
 */
ol.format.EsriJSON.writeMultiPointGeometry_ = function(geometry, opt_options) {
  goog.asserts.assertInstanceof(geometry, ol.geom.MultiPoint,
      'geometry should be an ol.geom.MultiPoint');
  var hasZM = ol.format.EsriJSON.getHasZM_(geometry);
  return /** @type {EsriJSONMultipoint} */ ({
    hasZ: hasZM.hasZ,
    hasM: hasZM.hasM,
    points: geometry.getCoordinates()
  });
};


/**
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {EsriJSONPolygon} EsriJSON geometry.
 */
ol.format.EsriJSON.writeMultiPolygonGeometry_ = function(geometry,
    opt_options) {
  goog.asserts.assertInstanceof(geometry, ol.geom.MultiPolygon,
      'geometry should be an ol.geom.MultiPolygon');
  var hasZM = ol.format.EsriJSON.getHasZM_(geometry);
  var coordinates = geometry.getCoordinates(false);
  var output = [];
  for (var i = 0; i < coordinates.length; i++) {
    for (var x = coordinates[i].length - 1; x >= 0; x--) {
      output.push(coordinates[i][x]);
    }
  }
  return /** @type {EsriJSONPolygon} */ ({
    hasZ: hasZM.hasZ,
    hasM: hasZM.hasM,
    rings: output
  });
};


/**
 * @const
 * @private
 * @type {Object.<ol.geom.GeometryType, function(EsriJSONGeometry): ol.geom.Geometry>}
 */
ol.format.EsriJSON.GEOMETRY_READERS_ = {};
ol.format.EsriJSON.GEOMETRY_READERS_[ol.geom.GeometryType.POINT] =
    ol.format.EsriJSON.readPointGeometry_;
ol.format.EsriJSON.GEOMETRY_READERS_[ol.geom.GeometryType.LINE_STRING] =
    ol.format.EsriJSON.readLineStringGeometry_;
ol.format.EsriJSON.GEOMETRY_READERS_[ol.geom.GeometryType.POLYGON] =
    ol.format.EsriJSON.readPolygonGeometry_;
ol.format.EsriJSON.GEOMETRY_READERS_[ol.geom.GeometryType.MULTI_POINT] =
    ol.format.EsriJSON.readMultiPointGeometry_;
ol.format.EsriJSON.GEOMETRY_READERS_[ol.geom.GeometryType.MULTI_LINE_STRING] =
    ol.format.EsriJSON.readMultiLineStringGeometry_;
ol.format.EsriJSON.GEOMETRY_READERS_[ol.geom.GeometryType.MULTI_POLYGON] =
    ol.format.EsriJSON.readMultiPolygonGeometry_;


/**
 * @const
 * @private
 * @type {Object.<string, function(ol.geom.Geometry, olx.format.WriteOptions=): (EsriJSONGeometry)>}
 */
ol.format.EsriJSON.GEOMETRY_WRITERS_ = {};
ol.format.EsriJSON.GEOMETRY_WRITERS_[ol.geom.GeometryType.POINT] =
    ol.format.EsriJSON.writePointGeometry_;
ol.format.EsriJSON.GEOMETRY_WRITERS_[ol.geom.GeometryType.LINE_STRING] =
    ol.format.EsriJSON.writeLineStringGeometry_;
ol.format.EsriJSON.GEOMETRY_WRITERS_[ol.geom.GeometryType.POLYGON] =
    ol.format.EsriJSON.writePolygonGeometry_;
ol.format.EsriJSON.GEOMETRY_WRITERS_[ol.geom.GeometryType.MULTI_POINT] =
    ol.format.EsriJSON.writeMultiPointGeometry_;
ol.format.EsriJSON.GEOMETRY_WRITERS_[ol.geom.GeometryType.MULTI_LINE_STRING] =
    ol.format.EsriJSON.writeMultiLineStringGeometry_;
ol.format.EsriJSON.GEOMETRY_WRITERS_[ol.geom.GeometryType.MULTI_POLYGON] =
    ol.format.EsriJSON.writeMultiPolygonGeometry_;


/**
 * Read a feature from a EsriJSON Feature source.  Only works for Feature,
 * use `readFeatures` to read FeatureCollection source.
 *
 * @function
 * @param {ArrayBuffer|Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {ol.Feature} Feature.
 * @api
 */
ol.format.EsriJSON.prototype.readFeature;


/**
 * Read all features from a EsriJSON source.  Works with both Feature and
 * FeatureCollection sources.
 *
 * @function
 * @param {ArrayBuffer|Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<ol.Feature>} Features.
 * @api
 */
ol.format.EsriJSON.prototype.readFeatures;


/**
 * @inheritDoc
 */
ol.format.EsriJSON.prototype.readFeatureFromObject = function(
    object, opt_options) {
  var esriJSONFeature = /** @type {EsriJSONFeature} */ (object);
  goog.asserts.assert(esriJSONFeature.geometry ||
      esriJSONFeature.attributes,
      'geometry or attributes should be defined');
  var geometry = ol.format.EsriJSON.readGeometry_(esriJSONFeature.geometry,
      opt_options);
  var feature = new ol.Feature();
  if (this.geometryName_) {
    feature.setGeometryName(this.geometryName_);
  }
  feature.setGeometry(geometry);
  if (opt_options && opt_options.idField &&
      esriJSONFeature.attributes[opt_options.idField]) {
    goog.asserts.assert(
        goog.isNumber(esriJSONFeature.attributes[opt_options.idField]),
        'objectIdFieldName value should be a number');
    feature.setId(/** @type {number} */(
        esriJSONFeature.attributes[opt_options.idField]));
  }
  if (esriJSONFeature.attributes) {
    feature.setProperties(esriJSONFeature.attributes);
  }
  return feature;
};


/**
 * @inheritDoc
 */
ol.format.EsriJSON.prototype.readFeaturesFromObject = function(
    object, opt_options) {
  var esriJSONObject = /** @type {EsriJSONObject} */ (object);
  var options = opt_options ? opt_options : {};
  if (esriJSONObject.features) {
    var esriJSONFeatureCollection = /** @type {EsriJSONFeatureCollection} */
        (object);
    /** @type {Array.<ol.Feature>} */
    var features = [];
    var esriJSONFeatures = esriJSONFeatureCollection.features;
    var i, ii;
    options.idField = object.objectIdFieldName;
    for (i = 0, ii = esriJSONFeatures.length; i < ii; ++i) {
      features.push(this.readFeatureFromObject(esriJSONFeatures[i],
          options));
    }
    return features;
  } else {
    return [this.readFeatureFromObject(object, options)];
  }
};


/**
 * Read a geometry from a EsriJSON source.
 *
 * @function
 * @param {ArrayBuffer|Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {ol.geom.Geometry} Geometry.
 * @api
 */
ol.format.EsriJSON.prototype.readGeometry;


/**
 * @inheritDoc
 */
ol.format.EsriJSON.prototype.readGeometryFromObject = function(
    object, opt_options) {
  return ol.format.EsriJSON.readGeometry_(
      /** @type {EsriJSONGeometry} */ (object), opt_options);
};


/**
 * Read the projection from a EsriJSON source.
 *
 * @function
 * @param {ArrayBuffer|Document|Node|Object|string} source Source.
 * @return {ol.proj.Projection} Projection.
 * @api
 */
ol.format.EsriJSON.prototype.readProjection;


/**
 * @inheritDoc
 */
ol.format.EsriJSON.prototype.readProjectionFromObject = function(object) {
  var esriJSONObject = /** @type {EsriJSONObject} */ (object);
  if (esriJSONObject.spatialReference && esriJSONObject.spatialReference.wkid) {
    var crs = esriJSONObject.spatialReference.wkid;
    return ol.proj.get('EPSG:' + crs);
  } else {
    return null;
  }
};


/**
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {EsriJSONGeometry} EsriJSON geometry.
 */
ol.format.EsriJSON.writeGeometry_ = function(geometry, opt_options) {
  var geometryWriter = ol.format.EsriJSON.GEOMETRY_WRITERS_[geometry.getType()];
  goog.asserts.assert(geometryWriter, 'geometryWriter should be defined');
  return geometryWriter(/** @type {ol.geom.Geometry} */ (
      ol.format.Feature.transformWithOptions(geometry, true, opt_options)),
      opt_options);
};


/**
 * Encode a geometry as a EsriJSON string.
 *
 * @function
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {string} EsriJSON.
 * @api
 */
ol.format.EsriJSON.prototype.writeGeometry;


/**
 * Encode a geometry as a EsriJSON object.
 *
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {EsriJSONGeometry} Object.
 * @api
 */
ol.format.EsriJSON.prototype.writeGeometryObject = function(geometry,
    opt_options) {
  return ol.format.EsriJSON.writeGeometry_(geometry,
      this.adaptOptions(opt_options));
};


/**
 * Encode a feature as a EsriJSON Feature string.
 *
 * @function
 * @param {ol.Feature} feature Feature.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {string} EsriJSON.
 * @api
 */
ol.format.EsriJSON.prototype.writeFeature;


/**
 * Encode a feature as a esriJSON Feature object.
 *
 * @param {ol.Feature} feature Feature.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {Object} Object.
 * @api
 */
ol.format.EsriJSON.prototype.writeFeatureObject = function(
    feature, opt_options) {
  opt_options = this.adaptOptions(opt_options);
  var object = {};
  var geometry = feature.getGeometry();
  if (geometry) {
    object['geometry'] =
        ol.format.EsriJSON.writeGeometry_(geometry, opt_options);
  }
  var properties = feature.getProperties();
  delete properties[feature.getGeometryName()];
  if (!goog.object.isEmpty(properties)) {
    object['attributes'] = properties;
  } else {
    object['attributes'] = {};
  }
  if (opt_options && opt_options.featureProjection) {
    object['spatialReference'] = /** @type {EsriJSONCRS} */({
      wkid: ol.proj.get(
          opt_options.featureProjection).getCode().split(':').pop()
    });
  }
  return object;
};


/**
 * Encode an array of features as EsriJSON.
 *
 * @function
 * @param {Array.<ol.Feature>} features Features.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {string} EsriJSON.
 * @api
 */
ol.format.EsriJSON.prototype.writeFeatures;


/**
 * Encode an array of features as a EsriJSON object.
 *
 * @param {Array.<ol.Feature>} features Features.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {Object} EsriJSON Object.
 * @api
 */
ol.format.EsriJSON.prototype.writeFeaturesObject = function(features, opt_options) {
  opt_options = this.adaptOptions(opt_options);
  var objects = [];
  var i, ii;
  for (i = 0, ii = features.length; i < ii; ++i) {
    objects.push(this.writeFeatureObject(features[i], opt_options));
  }
  return /** @type {EsriJSONFeatureCollection} */ ({
    'features': objects
  });
};

goog.provide('ol.geom.GeometryCollection');

goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.object');
goog.require('ol.extent');
goog.require('ol.geom.Geometry');
goog.require('ol.geom.GeometryType');


/**
 * @classdesc
 * An array of {@link ol.geom.Geometry} objects.
 *
 * @constructor
 * @extends {ol.geom.Geometry}
 * @param {Array.<ol.geom.Geometry>=} opt_geometries Geometries.
 * @api stable
 */
ol.geom.GeometryCollection = function(opt_geometries) {

  goog.base(this);

  /**
   * @private
   * @type {Array.<ol.geom.Geometry>}
   */
  this.geometries_ = opt_geometries ? opt_geometries : null;

  this.listenGeometriesChange_();
};
goog.inherits(ol.geom.GeometryCollection, ol.geom.Geometry);


/**
 * @param {Array.<ol.geom.Geometry>} geometries Geometries.
 * @private
 * @return {Array.<ol.geom.Geometry>} Cloned geometries.
 */
ol.geom.GeometryCollection.cloneGeometries_ = function(geometries) {
  var clonedGeometries = [];
  var i, ii;
  for (i = 0, ii = geometries.length; i < ii; ++i) {
    clonedGeometries.push(geometries[i].clone());
  }
  return clonedGeometries;
};


/**
 * @private
 */
ol.geom.GeometryCollection.prototype.unlistenGeometriesChange_ = function() {
  var i, ii;
  if (!this.geometries_) {
    return;
  }
  for (i = 0, ii = this.geometries_.length; i < ii; ++i) {
    goog.events.unlisten(
        this.geometries_[i], goog.events.EventType.CHANGE,
        this.changed, false, this);
  }
};


/**
 * @private
 */
ol.geom.GeometryCollection.prototype.listenGeometriesChange_ = function() {
  var i, ii;
  if (!this.geometries_) {
    return;
  }
  for (i = 0, ii = this.geometries_.length; i < ii; ++i) {
    goog.events.listen(
        this.geometries_[i], goog.events.EventType.CHANGE,
        this.changed, false, this);
  }
};


/**
 * Make a complete copy of the geometry.
 * @return {!ol.geom.GeometryCollection} Clone.
 * @api stable
 */
ol.geom.GeometryCollection.prototype.clone = function() {
  var geometryCollection = new ol.geom.GeometryCollection(null);
  geometryCollection.setGeometries(this.geometries_);
  return geometryCollection;
};


/**
 * @inheritDoc
 */
ol.geom.GeometryCollection.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
  if (minSquaredDistance <
      ol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
    return minSquaredDistance;
  }
  var geometries = this.geometries_;
  var i, ii;
  for (i = 0, ii = geometries.length; i < ii; ++i) {
    minSquaredDistance = geometries[i].closestPointXY(
        x, y, closestPoint, minSquaredDistance);
  }
  return minSquaredDistance;
};


/**
 * @inheritDoc
 */
ol.geom.GeometryCollection.prototype.containsXY = function(x, y) {
  var geometries = this.geometries_;
  var i, ii;
  for (i = 0, ii = geometries.length; i < ii; ++i) {
    if (geometries[i].containsXY(x, y)) {
      return true;
    }
  }
  return false;
};


/**
 * @inheritDoc
 */
ol.geom.GeometryCollection.prototype.computeExtent = function(extent) {
  ol.extent.createOrUpdateEmpty(extent);
  var geometries = this.geometries_;
  for (var i = 0, ii = geometries.length; i < ii; ++i) {
    ol.extent.extend(extent, geometries[i].getExtent());
  }
  return extent;
};


/**
 * Return the geometries that make up this geometry collection.
 * @return {Array.<ol.geom.Geometry>} Geometries.
 * @api stable
 */
ol.geom.GeometryCollection.prototype.getGeometries = function() {
  return ol.geom.GeometryCollection.cloneGeometries_(this.geometries_);
};


/**
 * @return {Array.<ol.geom.Geometry>} Geometries.
 */
ol.geom.GeometryCollection.prototype.getGeometriesArray = function() {
  return this.geometries_;
};


/**
 * @inheritDoc
 */
ol.geom.GeometryCollection.prototype.getSimplifiedGeometry = function(squaredTolerance) {
  if (this.simplifiedGeometryRevision != this.getRevision()) {
    goog.object.clear(this.simplifiedGeometryCache);
    this.simplifiedGeometryMaxMinSquaredTolerance = 0;
    this.simplifiedGeometryRevision = this.getRevision();
  }
  if (squaredTolerance < 0 ||
      (this.simplifiedGeometryMaxMinSquaredTolerance !== 0 &&
       squaredTolerance < this.simplifiedGeometryMaxMinSquaredTolerance)) {
    return this;
  }
  var key = squaredTolerance.toString();
  if (this.simplifiedGeometryCache.hasOwnProperty(key)) {
    return this.simplifiedGeometryCache[key];
  } else {
    var simplifiedGeometries = [];
    var geometries = this.geometries_;
    var simplified = false;
    var i, ii;
    for (i = 0, ii = geometries.length; i < ii; ++i) {
      var geometry = geometries[i];
      var simplifiedGeometry = geometry.getSimplifiedGeometry(squaredTolerance);
      simplifiedGeometries.push(simplifiedGeometry);
      if (simplifiedGeometry !== geometry) {
        simplified = true;
      }
    }
    if (simplified) {
      var simplifiedGeometryCollection = new ol.geom.GeometryCollection(null);
      simplifiedGeometryCollection.setGeometriesArray(simplifiedGeometries);
      this.simplifiedGeometryCache[key] = simplifiedGeometryCollection;
      return simplifiedGeometryCollection;
    } else {
      this.simplifiedGeometryMaxMinSquaredTolerance = squaredTolerance;
      return this;
    }
  }
};


/**
 * @inheritDoc
 * @api stable
 */
ol.geom.GeometryCollection.prototype.getType = function() {
  return ol.geom.GeometryType.GEOMETRY_COLLECTION;
};


/**
 * @inheritDoc
 * @api stable
 */
ol.geom.GeometryCollection.prototype.intersectsExtent = function(extent) {
  var geometries = this.geometries_;
  var i, ii;
  for (i = 0, ii = geometries.length; i < ii; ++i) {
    if (geometries[i].intersectsExtent(extent)) {
      return true;
    }
  }
  return false;
};


/**
 * @return {boolean} Is empty.
 */
ol.geom.GeometryCollection.prototype.isEmpty = function() {
  return this.geometries_.length === 0;
};


/**
 * Set the geometries that make up this geometry collection.
 * @param {Array.<ol.geom.Geometry>} geometries Geometries.
 * @api stable
 */
ol.geom.GeometryCollection.prototype.setGeometries = function(geometries) {
  this.setGeometriesArray(
      ol.geom.GeometryCollection.cloneGeometries_(geometries));
};


/**
 * @param {Array.<ol.geom.Geometry>} geometries Geometries.
 */
ol.geom.GeometryCollection.prototype.setGeometriesArray = function(geometries) {
  this.unlistenGeometriesChange_();
  this.geometries_ = geometries;
  this.listenGeometriesChange_();
  this.changed();
};


/**
 * @inheritDoc
 * @api stable
 */
ol.geom.GeometryCollection.prototype.applyTransform = function(transformFn) {
  var geometries = this.geometries_;
  var i, ii;
  for (i = 0, ii = geometries.length; i < ii; ++i) {
    geometries[i].applyTransform(transformFn);
  }
  this.changed();
};


/**
 * Translate the geometry.
 * @param {number} deltaX Delta X.
 * @param {number} deltaY Delta Y.
 * @api
 */
ol.geom.GeometryCollection.prototype.translate = function(deltaX, deltaY) {
  var geometries = this.geometries_;
  var i, ii;
  for (i = 0, ii = geometries.length; i < ii; ++i) {
    geometries[i].translate(deltaX, deltaY);
  }
  this.changed();
};


/**
 * @inheritDoc
 */
ol.geom.GeometryCollection.prototype.disposeInternal = function() {
  this.unlistenGeometriesChange_();
  goog.base(this, 'disposeInternal');
};

// TODO: serialize dataProjection as crs member when writing
// see https://github.com/openlayers/ol3/issues/2078

goog.provide('ol.format.GeoJSON');

goog.require('goog.asserts');
goog.require('goog.object');
goog.require('ol.Feature');
goog.require('ol.format.Feature');
goog.require('ol.format.JSONFeature');
goog.require('ol.geom.GeometryCollection');
goog.require('ol.geom.LineString');
goog.require('ol.geom.MultiLineString');
goog.require('ol.geom.MultiPoint');
goog.require('ol.geom.MultiPolygon');
goog.require('ol.geom.Point');
goog.require('ol.geom.Polygon');
goog.require('ol.proj');


/**
 * @classdesc
 * Feature format for reading and writing data in the GeoJSON format.
 *
 * @constructor
 * @extends {ol.format.JSONFeature}
 * @param {olx.format.GeoJSONOptions=} opt_options Options.
 * @api stable
 */
ol.format.GeoJSON = function(opt_options) {

  var options = opt_options ? opt_options : {};

  goog.base(this);

  /**
   * @inheritDoc
   */
  this.defaultDataProjection = ol.proj.get(
      options.defaultDataProjection ?
          options.defaultDataProjection : 'EPSG:4326');


  /**
   * Name of the geometry attribute for features.
   * @type {string|undefined}
   * @private
   */
  this.geometryName_ = options.geometryName;

};
goog.inherits(ol.format.GeoJSON, ol.format.JSONFeature);


/**
 * @const
 * @type {Array.<string>}
 * @private
 */
ol.format.GeoJSON.EXTENSIONS_ = ['.geojson'];


/**
 * @param {GeoJSONGeometry|GeoJSONGeometryCollection} object Object.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @private
 * @return {ol.geom.Geometry} Geometry.
 */
ol.format.GeoJSON.readGeometry_ = function(object, opt_options) {
  if (!object) {
    return null;
  }
  var geometryReader = ol.format.GeoJSON.GEOMETRY_READERS_[object.type];
  goog.asserts.assert(geometryReader, 'geometryReader should be defined');
  return /** @type {ol.geom.Geometry} */ (
      ol.format.Feature.transformWithOptions(
          geometryReader(object), false, opt_options));
};


/**
 * @param {GeoJSONGeometryCollection} object Object.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @private
 * @return {ol.geom.GeometryCollection} Geometry collection.
 */
ol.format.GeoJSON.readGeometryCollectionGeometry_ = function(
    object, opt_options) {
  goog.asserts.assert(object.type == 'GeometryCollection',
      'object.type should be GeometryCollection');
  var geometries = object.geometries.map(
      /**
       * @param {GeoJSONGeometry} geometry Geometry.
       * @return {ol.geom.Geometry} geometry Geometry.
       */
      function(geometry) {
        return ol.format.GeoJSON.readGeometry_(geometry, opt_options);
      });
  return new ol.geom.GeometryCollection(geometries);
};


/**
 * @param {GeoJSONGeometry} object Object.
 * @private
 * @return {ol.geom.Point} Point.
 */
ol.format.GeoJSON.readPointGeometry_ = function(object) {
  goog.asserts.assert(object.type == 'Point',
      'object.type should be Point');
  return new ol.geom.Point(object.coordinates);
};


/**
 * @param {GeoJSONGeometry} object Object.
 * @private
 * @return {ol.geom.LineString} LineString.
 */
ol.format.GeoJSON.readLineStringGeometry_ = function(object) {
  goog.asserts.assert(object.type == 'LineString',
      'object.type should be LineString');
  return new ol.geom.LineString(object.coordinates);
};


/**
 * @param {GeoJSONGeometry} object Object.
 * @private
 * @return {ol.geom.MultiLineString} MultiLineString.
 */
ol.format.GeoJSON.readMultiLineStringGeometry_ = function(object) {
  goog.asserts.assert(object.type == 'MultiLineString',
      'object.type should be MultiLineString');
  return new ol.geom.MultiLineString(object.coordinates);
};


/**
 * @param {GeoJSONGeometry} object Object.
 * @private
 * @return {ol.geom.MultiPoint} MultiPoint.
 */
ol.format.GeoJSON.readMultiPointGeometry_ = function(object) {
  goog.asserts.assert(object.type == 'MultiPoint',
      'object.type should be MultiPoint');
  return new ol.geom.MultiPoint(object.coordinates);
};


/**
 * @param {GeoJSONGeometry} object Object.
 * @private
 * @return {ol.geom.MultiPolygon} MultiPolygon.
 */
ol.format.GeoJSON.readMultiPolygonGeometry_ = function(object) {
  goog.asserts.assert(object.type == 'MultiPolygon',
      'object.type should be MultiPolygon');
  return new ol.geom.MultiPolygon(object.coordinates);
};


/**
 * @param {GeoJSONGeometry} object Object.
 * @private
 * @return {ol.geom.Polygon} Polygon.
 */
ol.format.GeoJSON.readPolygonGeometry_ = function(object) {
  goog.asserts.assert(object.type == 'Polygon',
      'object.type should be Polygon');
  return new ol.geom.Polygon(object.coordinates);
};


/**
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {GeoJSONGeometry|GeoJSONGeometryCollection} GeoJSON geometry.
 */
ol.format.GeoJSON.writeGeometry_ = function(geometry, opt_options) {
  var geometryWriter = ol.format.GeoJSON.GEOMETRY_WRITERS_[geometry.getType()];
  goog.asserts.assert(geometryWriter, 'geometryWriter should be defined');
  return geometryWriter(/** @type {ol.geom.Geometry} */ (
      ol.format.Feature.transformWithOptions(geometry, true, opt_options)),
      opt_options);
};


/**
 * @param {ol.geom.Geometry} geometry Geometry.
 * @private
 * @return {GeoJSONGeometryCollection} Empty GeoJSON geometry collection.
 */
ol.format.GeoJSON.writeEmptyGeometryCollectionGeometry_ = function(geometry) {
  return /** @type {GeoJSONGeometryCollection} */ ({
    type: 'GeometryCollection',
    geometries: []
  });
};


/**
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {GeoJSONGeometryCollection} GeoJSON geometry collection.
 */
ol.format.GeoJSON.writeGeometryCollectionGeometry_ = function(
    geometry, opt_options) {
  goog.asserts.assertInstanceof(geometry, ol.geom.GeometryCollection,
      'geometry should be an ol.geom.GeometryCollection');
  var geometries = geometry.getGeometriesArray().map(function(geometry) {
    return ol.format.GeoJSON.writeGeometry_(geometry, opt_options);
  });
  return /** @type {GeoJSONGeometryCollection} */ ({
    type: 'GeometryCollection',
    geometries: geometries
  });
};


/**
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {GeoJSONGeometry} GeoJSON geometry.
 */
ol.format.GeoJSON.writeLineStringGeometry_ = function(geometry, opt_options) {
  goog.asserts.assertInstanceof(geometry, ol.geom.LineString,
      'geometry should be an ol.geom.LineString');
  return /** @type {GeoJSONGeometry} */ ({
    type: 'LineString',
    coordinates: geometry.getCoordinates()
  });
};


/**
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {GeoJSONGeometry} GeoJSON geometry.
 */
ol.format.GeoJSON.writeMultiLineStringGeometry_ = function(geometry, opt_options) {
  goog.asserts.assertInstanceof(geometry, ol.geom.MultiLineString,
      'geometry should be an ol.geom.MultiLineString');
  return /** @type {GeoJSONGeometry} */ ({
    type: 'MultiLineString',
    coordinates: geometry.getCoordinates()
  });
};


/**
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {GeoJSONGeometry} GeoJSON geometry.
 */
ol.format.GeoJSON.writeMultiPointGeometry_ = function(geometry, opt_options) {
  goog.asserts.assertInstanceof(geometry, ol.geom.MultiPoint,
      'geometry should be an ol.geom.MultiPoint');
  return /** @type {GeoJSONGeometry} */ ({
    type: 'MultiPoint',
    coordinates: geometry.getCoordinates()
  });
};


/**
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {GeoJSONGeometry} GeoJSON geometry.
 */
ol.format.GeoJSON.writeMultiPolygonGeometry_ = function(geometry, opt_options) {
  goog.asserts.assertInstanceof(geometry, ol.geom.MultiPolygon,
      'geometry should be an ol.geom.MultiPolygon');
  var right;
  if (opt_options) {
    right = opt_options.rightHanded;
  }
  return /** @type {GeoJSONGeometry} */ ({
    type: 'MultiPolygon',
    coordinates: geometry.getCoordinates(right)
  });
};


/**
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {GeoJSONGeometry} GeoJSON geometry.
 */
ol.format.GeoJSON.writePointGeometry_ = function(geometry, opt_options) {
  goog.asserts.assertInstanceof(geometry, ol.geom.Point,
      'geometry should be an ol.geom.Point');
  return /** @type {GeoJSONGeometry} */ ({
    type: 'Point',
    coordinates: geometry.getCoordinates()
  });
};


/**
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {GeoJSONGeometry} GeoJSON geometry.
 */
ol.format.GeoJSON.writePolygonGeometry_ = function(geometry, opt_options) {
  goog.asserts.assertInstanceof(geometry, ol.geom.Polygon,
      'geometry should be an ol.geom.Polygon');
  var right;
  if (opt_options) {
    right = opt_options.rightHanded;
  }
  return /** @type {GeoJSONGeometry} */ ({
    type: 'Polygon',
    coordinates: geometry.getCoordinates(right)
  });
};


/**
 * @const
 * @private
 * @type {Object.<string, function(GeoJSONObject): ol.geom.Geometry>}
 */
ol.format.GeoJSON.GEOMETRY_READERS_ = {
  'Point': ol.format.GeoJSON.readPointGeometry_,
  'LineString': ol.format.GeoJSON.readLineStringGeometry_,
  'Polygon': ol.format.GeoJSON.readPolygonGeometry_,
  'MultiPoint': ol.format.GeoJSON.readMultiPointGeometry_,
  'MultiLineString': ol.format.GeoJSON.readMultiLineStringGeometry_,
  'MultiPolygon': ol.format.GeoJSON.readMultiPolygonGeometry_,
  'GeometryCollection': ol.format.GeoJSON.readGeometryCollectionGeometry_
};


/**
 * @const
 * @private
 * @type {Object.<string, function(ol.geom.Geometry, olx.format.WriteOptions=): (GeoJSONGeometry|GeoJSONGeometryCollection)>}
 */
ol.format.GeoJSON.GEOMETRY_WRITERS_ = {
  'Point': ol.format.GeoJSON.writePointGeometry_,
  'LineString': ol.format.GeoJSON.writeLineStringGeometry_,
  'Polygon': ol.format.GeoJSON.writePolygonGeometry_,
  'MultiPoint': ol.format.GeoJSON.writeMultiPointGeometry_,
  'MultiLineString': ol.format.GeoJSON.writeMultiLineStringGeometry_,
  'MultiPolygon': ol.format.GeoJSON.writeMultiPolygonGeometry_,
  'GeometryCollection': ol.format.GeoJSON.writeGeometryCollectionGeometry_,
  'Circle': ol.format.GeoJSON.writeEmptyGeometryCollectionGeometry_
};


/**
 * @inheritDoc
 */
ol.format.GeoJSON.prototype.getExtensions = function() {
  return ol.format.GeoJSON.EXTENSIONS_;
};


/**
 * Read a feature from a GeoJSON Feature source.  Only works for Feature,
 * use `readFeatures` to read FeatureCollection source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {ol.Feature} Feature.
 * @api stable
 */
ol.format.GeoJSON.prototype.readFeature;


/**
 * Read all features from a GeoJSON source.  Works with both Feature and
 * FeatureCollection sources.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<ol.Feature>} Features.
 * @api stable
 */
ol.format.GeoJSON.prototype.readFeatures;


/**
 * @inheritDoc
 */
ol.format.GeoJSON.prototype.readFeatureFromObject = function(
    object, opt_options) {
  var geoJSONFeature = /** @type {GeoJSONFeature} */ (object);
  goog.asserts.assert(geoJSONFeature.type == 'Feature',
      'geoJSONFeature.type should be Feature');
  var geometry = ol.format.GeoJSON.readGeometry_(geoJSONFeature.geometry,
      opt_options);
  var feature = new ol.Feature();
  if (this.geometryName_) {
    feature.setGeometryName(this.geometryName_);
  }
  feature.setGeometry(geometry);
  if (geoJSONFeature.id !== undefined) {
    feature.setId(geoJSONFeature.id);
  }
  if (geoJSONFeature.properties) {
    feature.setProperties(geoJSONFeature.properties);
  }
  return feature;
};


/**
 * @inheritDoc
 */
ol.format.GeoJSON.prototype.readFeaturesFromObject = function(
    object, opt_options) {
  var geoJSONObject = /** @type {GeoJSONObject} */ (object);
  if (geoJSONObject.type == 'Feature') {
    return [this.readFeatureFromObject(object, opt_options)];
  } else if (geoJSONObject.type == 'FeatureCollection') {
    var geoJSONFeatureCollection = /** @type {GeoJSONFeatureCollection} */
        (object);
    /** @type {Array.<ol.Feature>} */
    var features = [];
    var geoJSONFeatures = geoJSONFeatureCollection.features;
    var i, ii;
    for (i = 0, ii = geoJSONFeatures.length; i < ii; ++i) {
      features.push(this.readFeatureFromObject(geoJSONFeatures[i],
          opt_options));
    }
    return features;
  } else {
    goog.asserts.fail('Unknown geoJSONObject.type: ' + geoJSONObject.type);
    return [];
  }
};


/**
 * Read a geometry from a GeoJSON source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {ol.geom.Geometry} Geometry.
 * @api stable
 */
ol.format.GeoJSON.prototype.readGeometry;


/**
 * @inheritDoc
 */
ol.format.GeoJSON.prototype.readGeometryFromObject = function(
    object, opt_options) {
  return ol.format.GeoJSON.readGeometry_(
      /** @type {GeoJSONGeometry} */ (object), opt_options);
};


/**
 * Read the projection from a GeoJSON source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @return {ol.proj.Projection} Projection.
 * @api stable
 */
ol.format.GeoJSON.prototype.readProjection;


/**
 * @inheritDoc
 */
ol.format.GeoJSON.prototype.readProjectionFromObject = function(object) {
  var geoJSONObject = /** @type {GeoJSONObject} */ (object);
  var crs = geoJSONObject.crs;
  if (crs) {
    if (crs.type == 'name') {
      return ol.proj.get(crs.properties.name);
    } else if (crs.type == 'EPSG') {
      // 'EPSG' is not part of the GeoJSON specification, but is generated by
      // GeoServer.
      // TODO: remove this when http://jira.codehaus.org/browse/GEOS-5996
      // is fixed and widely deployed.
      return ol.proj.get('EPSG:' + crs.properties.code);
    } else {
      goog.asserts.fail('Unknown crs.type: ' + crs.type);
      return null;
    }
  } else {
    return this.defaultDataProjection;
  }
};


/**
 * Encode a feature as a GeoJSON Feature string.
 *
 * @function
 * @param {ol.Feature} feature Feature.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {string} GeoJSON.
 * @api stable
 */
ol.format.GeoJSON.prototype.writeFeature;


/**
 * Encode a feature as a GeoJSON Feature object.
 *
 * @param {ol.Feature} feature Feature.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {GeoJSONObject} Object.
 * @api stable
 */
ol.format.GeoJSON.prototype.writeFeatureObject = function(
    feature, opt_options) {
  opt_options = this.adaptOptions(opt_options);

  var object = /** @type {GeoJSONObject} */ ({
    'type': 'Feature'
  });
  var id = feature.getId();
  if (id !== undefined) {
    object.id = id;
  }
  var geometry = feature.getGeometry();
  if (geometry) {
    object.geometry =
        ol.format.GeoJSON.writeGeometry_(geometry, opt_options);
  } else {
    object.geometry = null;
  }
  var properties = feature.getProperties();
  delete properties[feature.getGeometryName()];
  if (!goog.object.isEmpty(properties)) {
    object.properties = properties;
  } else {
    object.properties = null;
  }
  return object;
};


/**
 * Encode an array of features as GeoJSON.
 *
 * @function
 * @param {Array.<ol.Feature>} features Features.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {string} GeoJSON.
 * @api stable
 */
ol.format.GeoJSON.prototype.writeFeatures;


/**
 * Encode an array of features as a GeoJSON object.
 *
 * @param {Array.<ol.Feature>} features Features.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {GeoJSONFeatureCollection} GeoJSON Object.
 * @api stable
 */
ol.format.GeoJSON.prototype.writeFeaturesObject = function(features, opt_options) {
  opt_options = this.adaptOptions(opt_options);
  var objects = [];
  var i, ii;
  for (i = 0, ii = features.length; i < ii; ++i) {
    objects.push(this.writeFeatureObject(features[i], opt_options));
  }
  return /** @type {GeoJSONFeatureCollection} */ ({
    type: 'FeatureCollection',
    features: objects
  });
};


/**
 * Encode a geometry as a GeoJSON string.
 *
 * @function
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {string} GeoJSON.
 * @api stable
 */
ol.format.GeoJSON.prototype.writeGeometry;


/**
 * Encode a geometry as a GeoJSON object.
 *
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {GeoJSONGeometry|GeoJSONGeometryCollection} Object.
 * @api stable
 */
ol.format.GeoJSON.prototype.writeGeometryObject = function(geometry,
    opt_options) {
  return ol.format.GeoJSON.writeGeometry_(geometry,
      this.adaptOptions(opt_options));
};

goog.provide('ol.format.XMLFeature');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom.NodeType');
goog.require('goog.dom.xml');
goog.require('ol.format.Feature');
goog.require('ol.format.FormatType');
goog.require('ol.proj');
goog.require('ol.xml');


/**
 * @classdesc
 * Abstract base class; normally only used for creating subclasses and not
 * instantiated in apps.
 * Base class for XML feature formats.
 *
 * @constructor
 * @extends {ol.format.Feature}
 */
ol.format.XMLFeature = function() {
  goog.base(this);
};
goog.inherits(ol.format.XMLFeature, ol.format.Feature);


/**
 * @inheritDoc
 */
ol.format.XMLFeature.prototype.getType = function() {
  return ol.format.FormatType.XML;
};


/**
 * @inheritDoc
 */
ol.format.XMLFeature.prototype.readFeature = function(source, opt_options) {
  if (ol.xml.isDocument(source)) {
    return this.readFeatureFromDocument(
        /** @type {Document} */ (source), opt_options);
  } else if (ol.xml.isNode(source)) {
    return this.readFeatureFromNode(/** @type {Node} */ (source), opt_options);
  } else if (goog.isString(source)) {
    var doc = ol.xml.parse(source);
    return this.readFeatureFromDocument(doc, opt_options);
  } else {
    goog.asserts.fail('Unknown source type');
    return null;
  }
};


/**
 * @param {Document} doc Document.
 * @param {olx.format.ReadOptions=} opt_options Options.
 * @return {ol.Feature} Feature.
 */
ol.format.XMLFeature.prototype.readFeatureFromDocument = function(
    doc, opt_options) {
  var features = this.readFeaturesFromDocument(doc, opt_options);
  if (features.length > 0) {
    return features[0];
  } else {
    return null;
  }
};


/**
 * @param {Node} node Node.
 * @param {olx.format.ReadOptions=} opt_options Options.
 * @return {ol.Feature} Feature.
 */
ol.format.XMLFeature.prototype.readFeatureFromNode = goog.abstractMethod;


/**
 * @inheritDoc
 */
ol.format.XMLFeature.prototype.readFeatures = function(source, opt_options) {
  if (ol.xml.isDocument(source)) {
    return this.readFeaturesFromDocument(
        /** @type {Document} */ (source), opt_options);
  } else if (ol.xml.isNode(source)) {
    return this.readFeaturesFromNode(/** @type {Node} */ (source), opt_options);
  } else if (goog.isString(source)) {
    var doc = ol.xml.parse(source);
    return this.readFeaturesFromDocument(doc, opt_options);
  } else {
    goog.asserts.fail('Unknown source type');
    return [];
  }
};


/**
 * @param {Document} doc Document.
 * @param {olx.format.ReadOptions=} opt_options Options.
 * @protected
 * @return {Array.<ol.Feature>} Features.
 */
ol.format.XMLFeature.prototype.readFeaturesFromDocument = function(
    doc, opt_options) {
  /** @type {Array.<ol.Feature>} */
  var features = [];
  var n;
  for (n = doc.firstChild; n; n = n.nextSibling) {
    if (n.nodeType == goog.dom.NodeType.ELEMENT) {
      goog.array.extend(features, this.readFeaturesFromNode(n, opt_options));
    }
  }
  return features;
};


/**
 * @param {Node} node Node.
 * @param {olx.format.ReadOptions=} opt_options Options.
 * @protected
 * @return {Array.<ol.Feature>} Features.
 */
ol.format.XMLFeature.prototype.readFeaturesFromNode = goog.abstractMethod;


/**
 * @inheritDoc
 */
ol.format.XMLFeature.prototype.readGeometry = function(source, opt_options) {
  if (ol.xml.isDocument(source)) {
    return this.readGeometryFromDocument(
        /** @type {Document} */ (source), opt_options);
  } else if (ol.xml.isNode(source)) {
    return this.readGeometryFromNode(/** @type {Node} */ (source), opt_options);
  } else if (goog.isString(source)) {
    var doc = ol.xml.parse(source);
    return this.readGeometryFromDocument(doc, opt_options);
  } else {
    goog.asserts.fail('Unknown source type');
    return null;
  }
};


/**
 * @param {Document} doc Document.
 * @param {olx.format.ReadOptions=} opt_options Options.
 * @protected
 * @return {ol.geom.Geometry} Geometry.
 */
ol.format.XMLFeature.prototype.readGeometryFromDocument = goog.abstractMethod;


/**
 * @param {Node} node Node.
 * @param {olx.format.ReadOptions=} opt_options Options.
 * @protected
 * @return {ol.geom.Geometry} Geometry.
 */
ol.format.XMLFeature.prototype.readGeometryFromNode = goog.abstractMethod;


/**
 * @inheritDoc
 */
ol.format.XMLFeature.prototype.readProjection = function(source) {
  if (ol.xml.isDocument(source)) {
    return this.readProjectionFromDocument(/** @type {Document} */ (source));
  } else if (ol.xml.isNode(source)) {
    return this.readProjectionFromNode(/** @type {Node} */ (source));
  } else if (goog.isString(source)) {
    var doc = ol.xml.parse(source);
    return this.readProjectionFromDocument(doc);
  } else {
    goog.asserts.fail('Unknown source type');
    return null;
  }
};


/**
 * @param {Document} doc Document.
 * @protected
 * @return {ol.proj.Projection} Projection.
 */
ol.format.XMLFeature.prototype.readProjectionFromDocument = function(doc) {
  return this.defaultDataProjection;
};


/**
 * @param {Node} node Node.
 * @protected
 * @return {ol.proj.Projection} Projection.
 */
ol.format.XMLFeature.prototype.readProjectionFromNode = function(node) {
  return this.defaultDataProjection;
};


/**
 * @inheritDoc
 */
ol.format.XMLFeature.prototype.writeFeature = function(feature, opt_options) {
  var node = this.writeFeatureNode(feature, opt_options);
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  return goog.dom.xml.serialize(/** @type {Element} */(node));
};


/**
 * @param {ol.Feature} feature Feature.
 * @param {olx.format.WriteOptions=} opt_options Options.
 * @protected
 * @return {Node} Node.
 */
ol.format.XMLFeature.prototype.writeFeatureNode = goog.abstractMethod;


/**
 * @inheritDoc
 */
ol.format.XMLFeature.prototype.writeFeatures = function(features, opt_options) {
  var node = this.writeFeaturesNode(features, opt_options);
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  return goog.dom.xml.serialize(/** @type {Element} */(node));
};


/**
 * @param {Array.<ol.Feature>} features Features.
 * @param {olx.format.WriteOptions=} opt_options Options.
 * @return {Node} Node.
 */
ol.format.XMLFeature.prototype.writeFeaturesNode = goog.abstractMethod;


/**
 * @inheritDoc
 */
ol.format.XMLFeature.prototype.writeGeometry = function(geometry, opt_options) {
  var node = this.writeGeometryNode(geometry, opt_options);
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  return goog.dom.xml.serialize(/** @type {Element} */(node));
};


/**
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Options.
 * @return {Node} Node.
 */
ol.format.XMLFeature.prototype.writeGeometryNode = goog.abstractMethod;

// FIXME Envelopes should not be treated as geometries! readEnvelope_ is part
// of GEOMETRY_PARSERS_ and methods using GEOMETRY_PARSERS_ do not expect
// envelopes/extents, only geometries!
goog.provide('ol.format.GMLBase');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom.NodeType');
goog.require('goog.object');
goog.require('goog.string');
goog.require('ol.Feature');
goog.require('ol.format.Feature');
goog.require('ol.format.XMLFeature');
goog.require('ol.geom.Geometry');
goog.require('ol.geom.GeometryLayout');
goog.require('ol.geom.LineString');
goog.require('ol.geom.LinearRing');
goog.require('ol.geom.MultiLineString');
goog.require('ol.geom.MultiPoint');
goog.require('ol.geom.MultiPolygon');
goog.require('ol.geom.Point');
goog.require('ol.geom.Polygon');
goog.require('ol.proj');
goog.require('ol.xml');


/**
 * @classdesc
 * Abstract base class; normally only used for creating subclasses and not
 * instantiated in apps.
 * Feature base format for reading and writing data in the GML format.
 * This class cannot be instantiated, it contains only base content that
 * is shared with versioned format classes ol.format.GML2 and
 * ol.format.GML3.
 *
 * @constructor
 * @param {olx.format.GMLOptions=} opt_options
 *     Optional configuration object.
 * @extends {ol.format.XMLFeature}
 */
ol.format.GMLBase = function(opt_options) {
  var options = /** @type {olx.format.GMLOptions} */
      (opt_options ? opt_options : {});

  /**
   * @protected
   * @type {Array.<string>|string|undefined}
   */
  this.featureType = options.featureType;

  /**
   * @protected
   * @type {Object.<string, string>|string|undefined}
   */
  this.featureNS = options.featureNS;

  /**
   * @protected
   * @type {string}
   */
  this.srsName = options.srsName;

  /**
   * @protected
   * @type {string}
   */
  this.schemaLocation = '';

  /**
   * @type {Object.<string, Object.<string, Object>>}
   */
  this.FEATURE_COLLECTION_PARSERS = {};
  this.FEATURE_COLLECTION_PARSERS[ol.format.GMLBase.GMLNS] = {
    'featureMember': ol.xml.makeReplacer(
        ol.format.GMLBase.prototype.readFeaturesInternal),
    'featureMembers': ol.xml.makeReplacer(
        ol.format.GMLBase.prototype.readFeaturesInternal)
  };

  goog.base(this);
};
goog.inherits(ol.format.GMLBase, ol.format.XMLFeature);


/**
 * @const
 * @type {string}
 */
ol.format.GMLBase.GMLNS = 'http://www.opengis.net/gml';


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Array.<ol.Feature>} Features.
 */
ol.format.GMLBase.prototype.readFeaturesInternal = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  var localName = ol.xml.getLocalName(node);
  var features;
  if (localName == 'FeatureCollection') {
    if (node.namespaceURI === 'http://www.opengis.net/wfs') {
      features = ol.xml.pushParseAndPop([],
          this.FEATURE_COLLECTION_PARSERS, node,
          objectStack, this);
    } else {
      features = ol.xml.pushParseAndPop(null,
          this.FEATURE_COLLECTION_PARSERS, node,
          objectStack, this);
    }
  } else if (localName == 'featureMembers' || localName == 'featureMember') {
    var context = objectStack[0];
    goog.asserts.assert(goog.isObject(context), 'context should be an Object');
    var featureType = context['featureType'];
    var featureNS = context['featureNS'];
    var i, ii, prefix = 'p', defaultPrefix = 'p0';
    if (!featureType && node.childNodes) {
      featureType = [], featureNS = {};
      for (i = 0, ii = node.childNodes.length; i < ii; ++i) {
        var child = node.childNodes[i];
        if (child.nodeType === 1) {
          var ft = child.nodeName.split(':').pop();
          if (featureType.indexOf(ft) === -1) {
            var key;
            if (!goog.object.contains(featureNS, child.namespaceURI)) {
              key = prefix + goog.object.getCount(featureNS);
              featureNS[key] = child.namespaceURI;
            } else {
              key = goog.object.findKey(featureNS, function(value) {
                return value === child.namespaceURI;
              });
            }
            featureType.push(key + ':' + ft);
          }
        }
      }
      context['featureType'] = featureType;
      context['featureNS'] = featureNS;
    }
    if (goog.isString(featureNS)) {
      var ns = featureNS;
      featureNS = {};
      featureNS[defaultPrefix] = ns;
    }
    var parsersNS = {};
    var featureTypes = goog.isArray(featureType) ? featureType : [featureType];
    for (var p in featureNS) {
      var parsers = {};
      for (i = 0, ii = featureTypes.length; i < ii; ++i) {
        var featurePrefix = featureTypes[i].indexOf(':') === -1 ?
            defaultPrefix : featureTypes[i].split(':')[0];
        if (featurePrefix === p) {
          parsers[featureTypes[i].split(':').pop()] =
              (localName == 'featureMembers') ?
              ol.xml.makeArrayPusher(this.readFeatureElement, this) :
              ol.xml.makeReplacer(this.readFeatureElement, this);
        }
      }
      parsersNS[featureNS[p]] = parsers;
    }
    features = ol.xml.pushParseAndPop([], parsersNS, node, objectStack);
  }
  if (!features) {
    features = [];
  }
  return features;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {ol.geom.Geometry|undefined} Geometry.
 */
ol.format.GMLBase.prototype.readGeometryElement = function(node, objectStack) {
  var context = objectStack[0];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  context['srsName'] = node.firstElementChild.getAttribute('srsName');
  var geometry = ol.xml.pushParseAndPop(/** @type {ol.geom.Geometry} */(null),
      this.GEOMETRY_PARSERS_, node, objectStack, this);
  if (geometry) {
    return /** @type {ol.geom.Geometry} */ (
        ol.format.Feature.transformWithOptions(geometry, false, context));
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {ol.Feature} Feature.
 */
ol.format.GMLBase.prototype.readFeatureElement = function(node, objectStack) {
  var n;
  var fid = node.getAttribute('fid') ||
      ol.xml.getAttributeNS(node, ol.format.GMLBase.GMLNS, 'id');
  var values = {}, geometryName;
  for (n = node.firstElementChild; n; n = n.nextElementSibling) {
    var localName = ol.xml.getLocalName(n);
    // Assume attribute elements have one child node and that the child
    // is a text or CDATA node (to be treated as text).
    // Otherwise assume it is a geometry node.
    if (n.childNodes.length === 0 ||
        (n.childNodes.length === 1 &&
        (n.firstChild.nodeType === 3 || n.firstChild.nodeType === 4))) {
      var value = ol.xml.getAllTextContent(n, false);
      if (goog.string.isEmpty(value)) {
        value = undefined;
      }
      values[localName] = value;
    } else {
      // boundedBy is an extent and must not be considered as a geometry
      if (localName !== 'boundedBy') {
        geometryName = localName;
      }
      values[localName] = this.readGeometryElement(n, objectStack);
    }
  }
  var feature = new ol.Feature(values);
  if (geometryName) {
    feature.setGeometryName(geometryName);
  }
  if (fid) {
    feature.setId(fid);
  }
  return feature;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {ol.geom.Point|undefined} Point.
 */
ol.format.GMLBase.prototype.readPoint = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Point', 'localName should be Point');
  var flatCoordinates =
      this.readFlatCoordinatesFromNode_(node, objectStack);
  if (flatCoordinates) {
    var point = new ol.geom.Point(null);
    goog.asserts.assert(flatCoordinates.length == 3,
        'flatCoordinates should have a length of 3');
    point.setFlatCoordinates(ol.geom.GeometryLayout.XYZ, flatCoordinates);
    return point;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {ol.geom.MultiPoint|undefined} MultiPoint.
 */
ol.format.GMLBase.prototype.readMultiPoint = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'MultiPoint',
      'localName should be MultiPoint');
  var coordinates = ol.xml.pushParseAndPop(
      /** @type {Array.<Array.<number>>} */ ([]),
      this.MULTIPOINT_PARSERS_, node, objectStack, this);
  if (coordinates) {
    return new ol.geom.MultiPoint(coordinates);
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {ol.geom.MultiLineString|undefined} MultiLineString.
 */
ol.format.GMLBase.prototype.readMultiLineString = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'MultiLineString',
      'localName should be MultiLineString');
  var lineStrings = ol.xml.pushParseAndPop(
      /** @type {Array.<ol.geom.LineString>} */ ([]),
      this.MULTILINESTRING_PARSERS_, node, objectStack, this);
  if (lineStrings) {
    var multiLineString = new ol.geom.MultiLineString(null);
    multiLineString.setLineStrings(lineStrings);
    return multiLineString;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {ol.geom.MultiPolygon|undefined} MultiPolygon.
 */
ol.format.GMLBase.prototype.readMultiPolygon = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'MultiPolygon',
      'localName should be MultiPolygon');
  var polygons = ol.xml.pushParseAndPop(
      /** @type {Array.<ol.geom.Polygon>} */ ([]),
      this.MULTIPOLYGON_PARSERS_, node, objectStack, this);
  if (polygons) {
    var multiPolygon = new ol.geom.MultiPolygon(null);
    multiPolygon.setPolygons(polygons);
    return multiPolygon;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.GMLBase.prototype.pointMemberParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'pointMember' ||
      node.localName == 'pointMembers',
      'localName should be pointMember or pointMembers');
  ol.xml.parseNode(this.POINTMEMBER_PARSERS_,
      node, objectStack, this);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.GMLBase.prototype.lineStringMemberParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'lineStringMember' ||
      node.localName == 'lineStringMembers',
      'localName should be LineStringMember or LineStringMembers');
  ol.xml.parseNode(this.LINESTRINGMEMBER_PARSERS_,
      node, objectStack, this);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.GMLBase.prototype.polygonMemberParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'polygonMember' ||
      node.localName == 'polygonMembers',
      'localName should be polygonMember or polygonMembers');
  ol.xml.parseNode(this.POLYGONMEMBER_PARSERS_, node,
      objectStack, this);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {ol.geom.LineString|undefined} LineString.
 */
ol.format.GMLBase.prototype.readLineString = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'LineString',
      'localName should be LineString');
  var flatCoordinates =
      this.readFlatCoordinatesFromNode_(node, objectStack);
  if (flatCoordinates) {
    var lineString = new ol.geom.LineString(null);
    lineString.setFlatCoordinates(ol.geom.GeometryLayout.XYZ, flatCoordinates);
    return lineString;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<number>|undefined} LinearRing flat coordinates.
 */
ol.format.GMLBase.prototype.readFlatLinearRing_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'LinearRing',
      'localName should be LinearRing');
  var ring = ol.xml.pushParseAndPop(/** @type {Array.<number>} */(null),
      this.GEOMETRY_FLAT_COORDINATES_PARSERS_, node,
      objectStack, this);
  if (ring) {
    return ring;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {ol.geom.LinearRing|undefined} LinearRing.
 */
ol.format.GMLBase.prototype.readLinearRing = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'LinearRing',
      'localName should be LinearRing');
  var flatCoordinates =
      this.readFlatCoordinatesFromNode_(node, objectStack);
  if (flatCoordinates) {
    var ring = new ol.geom.LinearRing(null);
    ring.setFlatCoordinates(ol.geom.GeometryLayout.XYZ, flatCoordinates);
    return ring;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {ol.geom.Polygon|undefined} Polygon.
 */
ol.format.GMLBase.prototype.readPolygon = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Polygon',
      'localName should be Polygon');
  var flatLinearRings = ol.xml.pushParseAndPop(
      /** @type {Array.<Array.<number>>} */ ([null]),
      this.FLAT_LINEAR_RINGS_PARSERS_, node, objectStack, this);
  if (flatLinearRings && flatLinearRings[0]) {
    var polygon = new ol.geom.Polygon(null);
    var flatCoordinates = flatLinearRings[0];
    var ends = [flatCoordinates.length];
    var i, ii;
    for (i = 1, ii = flatLinearRings.length; i < ii; ++i) {
      goog.array.extend(flatCoordinates, flatLinearRings[i]);
      ends.push(flatCoordinates.length);
    }
    polygon.setFlatCoordinates(
        ol.geom.GeometryLayout.XYZ, flatCoordinates, ends);
    return polygon;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<number>} Flat coordinates.
 */
ol.format.GMLBase.prototype.readFlatCoordinatesFromNode_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  return /** @type {Array.<number>} */ (ol.xml.pushParseAndPop(
      null,
      this.GEOMETRY_FLAT_COORDINATES_PARSERS_, node,
      objectStack, this));
};


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GMLBase.prototype.MULTIPOINT_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'pointMember': ol.xml.makeArrayPusher(
        ol.format.GMLBase.prototype.pointMemberParser_),
    'pointMembers': ol.xml.makeArrayPusher(
        ol.format.GMLBase.prototype.pointMemberParser_)
  }
});


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GMLBase.prototype.MULTILINESTRING_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'lineStringMember': ol.xml.makeArrayPusher(
        ol.format.GMLBase.prototype.lineStringMemberParser_),
    'lineStringMembers': ol.xml.makeArrayPusher(
        ol.format.GMLBase.prototype.lineStringMemberParser_)
  }
});


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GMLBase.prototype.MULTIPOLYGON_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'polygonMember': ol.xml.makeArrayPusher(
        ol.format.GMLBase.prototype.polygonMemberParser_),
    'polygonMembers': ol.xml.makeArrayPusher(
        ol.format.GMLBase.prototype.polygonMemberParser_)
  }
});


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GMLBase.prototype.POINTMEMBER_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'Point': ol.xml.makeArrayPusher(
        ol.format.GMLBase.prototype.readFlatCoordinatesFromNode_)
  }
});


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GMLBase.prototype.LINESTRINGMEMBER_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'LineString': ol.xml.makeArrayPusher(
        ol.format.GMLBase.prototype.readLineString)
  }
});


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GMLBase.prototype.POLYGONMEMBER_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'Polygon': ol.xml.makeArrayPusher(
        ol.format.GMLBase.prototype.readPolygon)
  }
});


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @protected
 */
ol.format.GMLBase.prototype.RING_PARSERS = Object({
  'http://www.opengis.net/gml' : {
    'LinearRing': ol.xml.makeReplacer(
        ol.format.GMLBase.prototype.readFlatLinearRing_)
  }
});


/**
 * @inheritDoc
 */
ol.format.GMLBase.prototype.readGeometryFromNode = function(node, opt_options) {
  var geometry = this.readGeometryElement(node,
      [this.getReadOptions(node, opt_options ? opt_options : {})]);
  return geometry ? geometry : null;
};


/**
 * Read all features from a GML FeatureCollection.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Options.
 * @return {Array.<ol.Feature>} Features.
 * @api stable
 */
ol.format.GMLBase.prototype.readFeatures;


/**
 * @inheritDoc
 */
ol.format.GMLBase.prototype.readFeaturesFromNode = function(node, opt_options) {
  var options = {
    featureType: this.featureType,
    featureNS: this.featureNS
  };
  if (opt_options) {
    goog.object.extend(options, this.getReadOptions(node, opt_options));
  }
  return this.readFeaturesInternal(node, [options]);
};


/**
 * @inheritDoc
 */
ol.format.GMLBase.prototype.readProjectionFromNode = function(node) {
  return ol.proj.get(this.srsName_ ? this.srsName_ :
      node.firstElementChild.getAttribute('srsName'));
};

goog.provide('ol.format.XSD');

goog.require('goog.asserts');
goog.require('goog.string');
goog.require('ol');
goog.require('ol.xml');


/**
 * @const
 * @type {string}
 */
ol.format.XSD.NAMESPACE_URI = 'http://www.w3.org/2001/XMLSchema';


/**
 * @param {Node} node Node.
 * @return {boolean|undefined} Boolean.
 */
ol.format.XSD.readBoolean = function(node) {
  var s = ol.xml.getAllTextContent(node, false);
  return ol.format.XSD.readBooleanString(s);
};


/**
 * @param {string} string String.
 * @return {boolean|undefined} Boolean.
 */
ol.format.XSD.readBooleanString = function(string) {
  var m = /^\s*(true|1)|(false|0)\s*$/.exec(string);
  if (m) {
    return m[1] !== undefined || false;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @return {number|undefined} DateTime in seconds.
 */
ol.format.XSD.readDateTime = function(node) {
  var s = ol.xml.getAllTextContent(node, false);
  var re =
      /^\s*(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(Z|(?:([+\-])(\d{2})(?::(\d{2}))?))\s*$/;
  var m = re.exec(s);
  if (m) {
    var year = parseInt(m[1], 10);
    var month = parseInt(m[2], 10) - 1;
    var day = parseInt(m[3], 10);
    var hour = parseInt(m[4], 10);
    var minute = parseInt(m[5], 10);
    var second = parseInt(m[6], 10);
    var dateTime = Date.UTC(year, month, day, hour, minute, second) / 1000;
    if (m[7] != 'Z') {
      var sign = m[8] == '-' ? -1 : 1;
      dateTime += sign * 60 * parseInt(m[9], 10);
      if (m[10] !== undefined) {
        dateTime += sign * 60 * 60 * parseInt(m[10], 10);
      }
    }
    return dateTime;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @return {number|undefined} Decimal.
 */
ol.format.XSD.readDecimal = function(node) {
  var s = ol.xml.getAllTextContent(node, false);
  return ol.format.XSD.readDecimalString(s);
};


/**
 * @param {string} string String.
 * @return {number|undefined} Decimal.
 */
ol.format.XSD.readDecimalString = function(string) {
  // FIXME check spec
  var m = /^\s*([+\-]?\d*\.?\d+(?:e[+\-]?\d+)?)\s*$/i.exec(string);
  if (m) {
    return parseFloat(m[1]);
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @return {number|undefined} Non negative integer.
 */
ol.format.XSD.readNonNegativeInteger = function(node) {
  var s = ol.xml.getAllTextContent(node, false);
  return ol.format.XSD.readNonNegativeIntegerString(s);
};


/**
 * @param {string} string String.
 * @return {number|undefined} Non negative integer.
 */
ol.format.XSD.readNonNegativeIntegerString = function(string) {
  var m = /^\s*(\d+)\s*$/.exec(string);
  if (m) {
    return parseInt(m[1], 10);
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @return {string|undefined} String.
 */
ol.format.XSD.readString = function(node) {
  return ol.xml.getAllTextContent(node, false).trim();
};


/**
 * @param {Node} node Node to append a TextNode with the boolean to.
 * @param {boolean} bool Boolean.
 */
ol.format.XSD.writeBooleanTextNode = function(node, bool) {
  ol.format.XSD.writeStringTextNode(node, (bool) ? '1' : '0');
};


/**
 * @param {Node} node Node to append a TextNode with the dateTime to.
 * @param {number} dateTime DateTime in seconds.
 */
ol.format.XSD.writeDateTimeTextNode = function(node, dateTime) {
  var date = new Date(dateTime * 1000);
  var string = date.getUTCFullYear() + '-' +
      goog.string.padNumber(date.getUTCMonth() + 1, 2) + '-' +
      goog.string.padNumber(date.getUTCDate(), 2) + 'T' +
      goog.string.padNumber(date.getUTCHours(), 2) + ':' +
      goog.string.padNumber(date.getUTCMinutes(), 2) + ':' +
      goog.string.padNumber(date.getUTCSeconds(), 2) + 'Z';
  node.appendChild(ol.xml.DOCUMENT.createTextNode(string));
};


/**
 * @param {Node} node Node to append a TextNode with the decimal to.
 * @param {number} decimal Decimal.
 */
ol.format.XSD.writeDecimalTextNode = function(node, decimal) {
  var string = decimal.toPrecision();
  node.appendChild(ol.xml.DOCUMENT.createTextNode(string));
};


/**
 * @param {Node} node Node to append a TextNode with the decimal to.
 * @param {number} nonNegativeInteger Non negative integer.
 */
ol.format.XSD.writeNonNegativeIntegerTextNode = function(node, nonNegativeInteger) {
  goog.asserts.assert(nonNegativeInteger >= 0, 'value should be more than 0');
  goog.asserts.assert(nonNegativeInteger == (nonNegativeInteger | 0),
      'value should be an integer value');
  var string = nonNegativeInteger.toString();
  node.appendChild(ol.xml.DOCUMENT.createTextNode(string));
};


/**
 * @param {Node} node Node to append a TextNode with the string to.
 * @param {string} string String.
 */
ol.format.XSD.writeStringTextNode = function(node, string) {
  node.appendChild(ol.xml.DOCUMENT.createTextNode(string));
};

goog.provide('ol.format.GML2');

goog.require('goog.asserts');
goog.require('goog.dom.NodeType');
goog.require('ol.extent');
goog.require('ol.format.GMLBase');
goog.require('ol.format.XSD');
goog.require('ol.proj');
goog.require('ol.xml');


/**
 * @classdesc
 * Feature format for reading and writing data in the GML format,
 * version 2.1.2.
 *
 * @constructor
 * @param {olx.format.GMLOptions=} opt_options Optional configuration object.
 * @extends {ol.format.GMLBase}
 * @api
 */
ol.format.GML2 = function(opt_options) {
  var options = /** @type {olx.format.GMLOptions} */
      (opt_options ? opt_options : {});

  goog.base(this, options);

  this.FEATURE_COLLECTION_PARSERS[ol.format.GMLBase.GMLNS][
      'featureMember'] =
      ol.xml.makeArrayPusher(ol.format.GMLBase.prototype.readFeaturesInternal);

  /**
   * @inheritDoc
   */
  this.schemaLocation = options.schemaLocation ?
      options.schemaLocation : ol.format.GML2.schemaLocation_;

};
goog.inherits(ol.format.GML2, ol.format.GMLBase);


/**
 * @const
 * @type {string}
 * @private
 */
ol.format.GML2.schemaLocation_ = ol.format.GMLBase.GMLNS +
    ' http://schemas.opengis.net/gml/2.1.2/feature.xsd';


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<number>|undefined} Flat coordinates.
 */
ol.format.GML2.prototype.readFlatCoordinates_ = function(node, objectStack) {
  var s = ol.xml.getAllTextContent(node, false).replace(/^\s*|\s*$/g, '');
  var context = objectStack[0];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var containerSrs = context['srsName'];
  var containerDimension = node.parentNode.getAttribute('srsDimension');
  var axisOrientation = 'enu';
  if (containerSrs) {
    var proj = ol.proj.get(containerSrs);
    if (proj) {
      axisOrientation = proj.getAxisOrientation();
    }
  }
  var coords = s.split(/[\s,]+/);
  // The "dimension" attribute is from the GML 3.0.1 spec.
  var dim = 2;
  if (node.getAttribute('srsDimension')) {
    dim = ol.format.XSD.readNonNegativeIntegerString(
        node.getAttribute('srsDimension'));
  } else if (node.getAttribute('dimension')) {
    dim = ol.format.XSD.readNonNegativeIntegerString(
        node.getAttribute('dimension'));
  } else if (containerDimension) {
    dim = ol.format.XSD.readNonNegativeIntegerString(containerDimension);
  }
  var x, y, z;
  var flatCoordinates = [];
  for (var i = 0, ii = coords.length; i < ii; i += dim) {
    x = parseFloat(coords[i]);
    y = parseFloat(coords[i + 1]);
    z = (dim === 3) ? parseFloat(coords[i + 2]) : 0;
    if (axisOrientation.substr(0, 2) === 'en') {
      flatCoordinates.push(x, y, z);
    } else {
      flatCoordinates.push(y, x, z);
    }
  }
  return flatCoordinates;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {ol.Extent|undefined} Envelope.
 */
ol.format.GML2.prototype.readBox_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Box', 'localName should be Box');
  var flatCoordinates = ol.xml.pushParseAndPop(
      /** @type {Array.<number>} */ ([null]),
      this.BOX_PARSERS_, node, objectStack, this);
  return ol.extent.createOrUpdate(flatCoordinates[1][0],
      flatCoordinates[1][1], flatCoordinates[1][3],
      flatCoordinates[1][4]);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.GML2.prototype.innerBoundaryIsParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'innerBoundaryIs',
      'localName should be innerBoundaryIs');
  var flatLinearRing = ol.xml.pushParseAndPop(
      /** @type {Array.<number>|undefined} */ (undefined),
      this.RING_PARSERS, node, objectStack, this);
  if (flatLinearRing) {
    var flatLinearRings = /** @type {Array.<Array.<number>>} */
        (objectStack[objectStack.length - 1]);
    goog.asserts.assert(goog.isArray(flatLinearRings),
        'flatLinearRings should be an array');
    goog.asserts.assert(flatLinearRings.length > 0,
        'flatLinearRings should have an array length larger than 0');
    flatLinearRings.push(flatLinearRing);
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.GML2.prototype.outerBoundaryIsParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'outerBoundaryIs',
      'localName should be outerBoundaryIs');
  var flatLinearRing = ol.xml.pushParseAndPop(
      /** @type {Array.<number>|undefined} */ (undefined),
      this.RING_PARSERS, node, objectStack, this);
  if (flatLinearRing) {
    var flatLinearRings = /** @type {Array.<Array.<number>>} */
        (objectStack[objectStack.length - 1]);
    goog.asserts.assert(goog.isArray(flatLinearRings),
        'flatLinearRings should be an array');
    goog.asserts.assert(flatLinearRings.length > 0,
        'flatLinearRings should have an array length larger than 0');
    flatLinearRings[0] = flatLinearRing;
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GML2.prototype.GEOMETRY_FLAT_COORDINATES_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'coordinates': ol.xml.makeReplacer(
        ol.format.GML2.prototype.readFlatCoordinates_)
  }
});


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GML2.prototype.FLAT_LINEAR_RINGS_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'innerBoundaryIs': ol.format.GML2.prototype.innerBoundaryIsParser_,
    'outerBoundaryIs': ol.format.GML2.prototype.outerBoundaryIsParser_
  }
});


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GML2.prototype.BOX_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'coordinates': ol.xml.makeArrayPusher(
        ol.format.GML2.prototype.readFlatCoordinates_)
  }
});


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GML2.prototype.GEOMETRY_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'Point': ol.xml.makeReplacer(ol.format.GMLBase.prototype.readPoint),
    'MultiPoint': ol.xml.makeReplacer(
        ol.format.GMLBase.prototype.readMultiPoint),
    'LineString': ol.xml.makeReplacer(
        ol.format.GMLBase.prototype.readLineString),
    'MultiLineString': ol.xml.makeReplacer(
        ol.format.GMLBase.prototype.readMultiLineString),
    'LinearRing' : ol.xml.makeReplacer(
        ol.format.GMLBase.prototype.readLinearRing),
    'Polygon': ol.xml.makeReplacer(ol.format.GMLBase.prototype.readPolygon),
    'MultiPolygon': ol.xml.makeReplacer(
        ol.format.GMLBase.prototype.readMultiPolygon),
    'Box': ol.xml.makeReplacer(ol.format.GML2.prototype.readBox_)
  }
});

goog.provide('ol.format.GML');
goog.provide('ol.format.GML3');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom.NodeType');
goog.require('goog.object');
goog.require('ol');
goog.require('ol.Feature');
goog.require('ol.extent');
goog.require('ol.format.Feature');
goog.require('ol.format.GMLBase');
goog.require('ol.format.XSD');
goog.require('ol.geom.Geometry');
goog.require('ol.geom.GeometryLayout');
goog.require('ol.geom.LineString');
goog.require('ol.geom.LinearRing');
goog.require('ol.geom.MultiLineString');
goog.require('ol.geom.MultiPolygon');
goog.require('ol.geom.Point');
goog.require('ol.geom.Polygon');
goog.require('ol.proj');
goog.require('ol.xml');


/**
 * @classdesc
 * Feature format for reading and writing data in the GML format
 * version 3.1.1.
 * Currently only supports GML 3.1.1 Simple Features profile.
 *
 * @constructor
 * @param {olx.format.GMLOptions=} opt_options
 *     Optional configuration object.
 * @extends {ol.format.GMLBase}
 * @api
 */
ol.format.GML3 = function(opt_options) {
  var options = /** @type {olx.format.GMLOptions} */
      (opt_options ? opt_options : {});

  goog.base(this, options);

  /**
   * @private
   * @type {boolean}
   */
  this.surface_ = options.surface !== undefined ? options.surface : false;

  /**
   * @private
   * @type {boolean}
   */
  this.curve_ = options.curve !== undefined ? options.curve : false;

  /**
   * @private
   * @type {boolean}
   */
  this.multiCurve_ = options.multiCurve !== undefined ?
      options.multiCurve : true;

  /**
   * @private
   * @type {boolean}
   */
  this.multiSurface_ = options.multiSurface !== undefined ?
      options.multiSurface : true;

  /**
   * @inheritDoc
   */
  this.schemaLocation = options.schemaLocation ?
      options.schemaLocation : ol.format.GML3.schemaLocation_;

};
goog.inherits(ol.format.GML3, ol.format.GMLBase);


/**
 * @const
 * @type {string}
 * @private
 */
ol.format.GML3.schemaLocation_ = ol.format.GMLBase.GMLNS +
    ' http://schemas.opengis.net/gml/3.1.1/profiles/gmlsfProfile/' +
    '1.0.0/gmlsf.xsd';


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {ol.geom.MultiLineString|undefined} MultiLineString.
 */
ol.format.GML3.prototype.readMultiCurve_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'MultiCurve',
      'localName should be MultiCurve');
  var lineStrings = ol.xml.pushParseAndPop(
      /** @type {Array.<ol.geom.LineString>} */ ([]),
      this.MULTICURVE_PARSERS_, node, objectStack, this);
  if (lineStrings) {
    var multiLineString = new ol.geom.MultiLineString(null);
    multiLineString.setLineStrings(lineStrings);
    return multiLineString;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {ol.geom.MultiPolygon|undefined} MultiPolygon.
 */
ol.format.GML3.prototype.readMultiSurface_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'MultiSurface',
      'localName should be MultiSurface');
  var polygons = ol.xml.pushParseAndPop(
      /** @type {Array.<ol.geom.Polygon>} */ ([]),
      this.MULTISURFACE_PARSERS_, node, objectStack, this);
  if (polygons) {
    var multiPolygon = new ol.geom.MultiPolygon(null);
    multiPolygon.setPolygons(polygons);
    return multiPolygon;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.GML3.prototype.curveMemberParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'curveMember' ||
      node.localName == 'curveMembers',
      'localName should be curveMember or curveMembers');
  ol.xml.parseNode(this.CURVEMEMBER_PARSERS_, node, objectStack, this);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.GML3.prototype.surfaceMemberParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'surfaceMember' ||
      node.localName == 'surfaceMembers',
      'localName should be surfaceMember or surfaceMembers');
  ol.xml.parseNode(this.SURFACEMEMBER_PARSERS_,
      node, objectStack, this);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<(Array.<number>)>|undefined} flat coordinates.
 */
ol.format.GML3.prototype.readPatch_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'patches',
      'localName should be patches');
  return ol.xml.pushParseAndPop(
      /** @type {Array.<Array.<number>>} */ ([null]),
      this.PATCHES_PARSERS_, node, objectStack, this);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<number>|undefined} flat coordinates.
 */
ol.format.GML3.prototype.readSegment_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'segments',
      'localName should be segments');
  return ol.xml.pushParseAndPop(
      /** @type {Array.<number>} */ ([null]),
      this.SEGMENTS_PARSERS_, node, objectStack, this);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<(Array.<number>)>|undefined} flat coordinates.
 */
ol.format.GML3.prototype.readPolygonPatch_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'npde.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'PolygonPatch',
      'localName should be PolygonPatch');
  return ol.xml.pushParseAndPop(
      /** @type {Array.<Array.<number>>} */ ([null]),
      this.FLAT_LINEAR_RINGS_PARSERS_, node, objectStack, this);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<number>|undefined} flat coordinates.
 */
ol.format.GML3.prototype.readLineStringSegment_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'LineStringSegment',
      'localName should be LineStringSegment');
  return ol.xml.pushParseAndPop(
      /** @type {Array.<number>} */ ([null]),
      this.GEOMETRY_FLAT_COORDINATES_PARSERS_,
      node, objectStack, this);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.GML3.prototype.interiorParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'interior',
      'localName should be interior');
  var flatLinearRing = ol.xml.pushParseAndPop(
      /** @type {Array.<number>|undefined} */ (undefined),
      this.RING_PARSERS, node, objectStack, this);
  if (flatLinearRing) {
    var flatLinearRings = /** @type {Array.<Array.<number>>} */
        (objectStack[objectStack.length - 1]);
    goog.asserts.assert(goog.isArray(flatLinearRings),
        'flatLinearRings should be an array');
    goog.asserts.assert(flatLinearRings.length > 0,
        'flatLinearRings should have an array length of 1 or more');
    flatLinearRings.push(flatLinearRing);
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.GML3.prototype.exteriorParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'exterior',
      'localName should be exterior');
  var flatLinearRing = ol.xml.pushParseAndPop(
      /** @type {Array.<number>|undefined} */ (undefined),
      this.RING_PARSERS, node, objectStack, this);
  if (flatLinearRing) {
    var flatLinearRings = /** @type {Array.<Array.<number>>} */
        (objectStack[objectStack.length - 1]);
    goog.asserts.assert(goog.isArray(flatLinearRings),
        'flatLinearRings should be an array');
    goog.asserts.assert(flatLinearRings.length > 0,
        'flatLinearRings should have an array length of 1 or more');
    flatLinearRings[0] = flatLinearRing;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {ol.geom.Polygon|undefined} Polygon.
 */
ol.format.GML3.prototype.readSurface_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Surface',
      'localName should be Surface');
  var flatLinearRings = ol.xml.pushParseAndPop(
      /** @type {Array.<Array.<number>>} */ ([null]),
      this.SURFACE_PARSERS_, node, objectStack, this);
  if (flatLinearRings && flatLinearRings[0]) {
    var polygon = new ol.geom.Polygon(null);
    var flatCoordinates = flatLinearRings[0];
    var ends = [flatCoordinates.length];
    var i, ii;
    for (i = 1, ii = flatLinearRings.length; i < ii; ++i) {
      goog.array.extend(flatCoordinates, flatLinearRings[i]);
      ends.push(flatCoordinates.length);
    }
    polygon.setFlatCoordinates(
        ol.geom.GeometryLayout.XYZ, flatCoordinates, ends);
    return polygon;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {ol.geom.LineString|undefined} LineString.
 */
ol.format.GML3.prototype.readCurve_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Curve', 'localName should be Curve');
  var flatCoordinates = ol.xml.pushParseAndPop(
      /** @type {Array.<number>} */ ([null]),
      this.CURVE_PARSERS_, node, objectStack, this);
  if (flatCoordinates) {
    var lineString = new ol.geom.LineString(null);
    lineString.setFlatCoordinates(ol.geom.GeometryLayout.XYZ, flatCoordinates);
    return lineString;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {ol.Extent|undefined} Envelope.
 */
ol.format.GML3.prototype.readEnvelope_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Envelope',
      'localName should be Envelope');
  var flatCoordinates = ol.xml.pushParseAndPop(
      /** @type {Array.<number>} */ ([null]),
      this.ENVELOPE_PARSERS_, node, objectStack, this);
  return ol.extent.createOrUpdate(flatCoordinates[1][0],
      flatCoordinates[1][1], flatCoordinates[2][0],
      flatCoordinates[2][1]);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<number>|undefined} Flat coordinates.
 */
ol.format.GML3.prototype.readFlatPos_ = function(node, objectStack) {
  var s = ol.xml.getAllTextContent(node, false);
  var re = /^\s*([+\-]?\d*\.?\d+(?:[eE][+\-]?\d+)?)\s*/;
  /** @type {Array.<number>} */
  var flatCoordinates = [];
  var m;
  while ((m = re.exec(s))) {
    flatCoordinates.push(parseFloat(m[1]));
    s = s.substr(m[0].length);
  }
  if (s !== '') {
    return undefined;
  }
  var context = objectStack[0];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var containerSrs = context['srsName'];
  var axisOrientation = 'enu';
  if (containerSrs) {
    var proj = ol.proj.get(containerSrs);
    axisOrientation = proj.getAxisOrientation();
  }
  if (axisOrientation === 'neu') {
    var i, ii;
    for (i = 0, ii = flatCoordinates.length; i < ii; i += 3) {
      var y = flatCoordinates[i];
      var x = flatCoordinates[i + 1];
      flatCoordinates[i] = x;
      flatCoordinates[i + 1] = y;
    }
  }
  var len = flatCoordinates.length;
  if (len == 2) {
    flatCoordinates.push(0);
  }
  if (len === 0) {
    return undefined;
  }
  return flatCoordinates;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<number>|undefined} Flat coordinates.
 */
ol.format.GML3.prototype.readFlatPosList_ = function(node, objectStack) {
  var s = ol.xml.getAllTextContent(node, false).replace(/^\s*|\s*$/g, '');
  var context = objectStack[0];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var containerSrs = context['srsName'];
  var containerDimension = node.parentNode.getAttribute('srsDimension');
  var axisOrientation = 'enu';
  if (containerSrs) {
    var proj = ol.proj.get(containerSrs);
    axisOrientation = proj.getAxisOrientation();
  }
  var coords = s.split(/\s+/);
  // The "dimension" attribute is from the GML 3.0.1 spec.
  var dim = 2;
  if (node.getAttribute('srsDimension')) {
    dim = ol.format.XSD.readNonNegativeIntegerString(
        node.getAttribute('srsDimension'));
  } else if (node.getAttribute('dimension')) {
    dim = ol.format.XSD.readNonNegativeIntegerString(
        node.getAttribute('dimension'));
  } else if (containerDimension) {
    dim = ol.format.XSD.readNonNegativeIntegerString(containerDimension);
  }
  var x, y, z;
  var flatCoordinates = [];
  for (var i = 0, ii = coords.length; i < ii; i += dim) {
    x = parseFloat(coords[i]);
    y = parseFloat(coords[i + 1]);
    z = (dim === 3) ? parseFloat(coords[i + 2]) : 0;
    if (axisOrientation.substr(0, 2) === 'en') {
      flatCoordinates.push(x, y, z);
    } else {
      flatCoordinates.push(y, x, z);
    }
  }
  return flatCoordinates;
};


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GML3.prototype.GEOMETRY_FLAT_COORDINATES_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'pos': ol.xml.makeReplacer(ol.format.GML3.prototype.readFlatPos_),
    'posList': ol.xml.makeReplacer(ol.format.GML3.prototype.readFlatPosList_)
  }
});


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GML3.prototype.FLAT_LINEAR_RINGS_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'interior': ol.format.GML3.prototype.interiorParser_,
    'exterior': ol.format.GML3.prototype.exteriorParser_
  }
});


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GML3.prototype.GEOMETRY_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'Point': ol.xml.makeReplacer(ol.format.GMLBase.prototype.readPoint),
    'MultiPoint': ol.xml.makeReplacer(
        ol.format.GMLBase.prototype.readMultiPoint),
    'LineString': ol.xml.makeReplacer(
        ol.format.GMLBase.prototype.readLineString),
    'MultiLineString': ol.xml.makeReplacer(
        ol.format.GMLBase.prototype.readMultiLineString),
    'LinearRing' : ol.xml.makeReplacer(
        ol.format.GMLBase.prototype.readLinearRing),
    'Polygon': ol.xml.makeReplacer(ol.format.GMLBase.prototype.readPolygon),
    'MultiPolygon': ol.xml.makeReplacer(
        ol.format.GMLBase.prototype.readMultiPolygon),
    'Surface': ol.xml.makeReplacer(ol.format.GML3.prototype.readSurface_),
    'MultiSurface': ol.xml.makeReplacer(
        ol.format.GML3.prototype.readMultiSurface_),
    'Curve': ol.xml.makeReplacer(ol.format.GML3.prototype.readCurve_),
    'MultiCurve': ol.xml.makeReplacer(
        ol.format.GML3.prototype.readMultiCurve_),
    'Envelope': ol.xml.makeReplacer(ol.format.GML3.prototype.readEnvelope_)
  }
});


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GML3.prototype.MULTICURVE_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'curveMember': ol.xml.makeArrayPusher(
        ol.format.GML3.prototype.curveMemberParser_),
    'curveMembers': ol.xml.makeArrayPusher(
        ol.format.GML3.prototype.curveMemberParser_)
  }
});


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GML3.prototype.MULTISURFACE_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'surfaceMember': ol.xml.makeArrayPusher(
        ol.format.GML3.prototype.surfaceMemberParser_),
    'surfaceMembers': ol.xml.makeArrayPusher(
        ol.format.GML3.prototype.surfaceMemberParser_)
  }
});


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GML3.prototype.CURVEMEMBER_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'LineString': ol.xml.makeArrayPusher(
        ol.format.GMLBase.prototype.readLineString),
    'Curve': ol.xml.makeArrayPusher(ol.format.GML3.prototype.readCurve_)
  }
});


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GML3.prototype.SURFACEMEMBER_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'Polygon': ol.xml.makeArrayPusher(ol.format.GMLBase.prototype.readPolygon),
    'Surface': ol.xml.makeArrayPusher(ol.format.GML3.prototype.readSurface_)
  }
});


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GML3.prototype.SURFACE_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'patches': ol.xml.makeReplacer(ol.format.GML3.prototype.readPatch_)
  }
});


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GML3.prototype.CURVE_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'segments': ol.xml.makeReplacer(ol.format.GML3.prototype.readSegment_)
  }
});


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GML3.prototype.ENVELOPE_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'lowerCorner': ol.xml.makeArrayPusher(
        ol.format.GML3.prototype.readFlatPosList_),
    'upperCorner': ol.xml.makeArrayPusher(
        ol.format.GML3.prototype.readFlatPosList_)
  }
});


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GML3.prototype.PATCHES_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'PolygonPatch': ol.xml.makeReplacer(
        ol.format.GML3.prototype.readPolygonPatch_)
  }
});


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GML3.prototype.SEGMENTS_PARSERS_ = Object({
  'http://www.opengis.net/gml' : {
    'LineStringSegment': ol.xml.makeReplacer(
        ol.format.GML3.prototype.readLineStringSegment_)
  }
});


/**
 * @param {Node} node Node.
 * @param {ol.geom.Point} value Point geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.GML3.prototype.writePos_ = function(node, value, objectStack) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var srsName = context['srsName'];
  var axisOrientation = 'enu';
  if (srsName) {
    axisOrientation = ol.proj.get(srsName).getAxisOrientation();
  }
  var point = value.getCoordinates();
  var coords;
  // only 2d for simple features profile
  if (axisOrientation.substr(0, 2) === 'en') {
    coords = (point[0] + ' ' + point[1]);
  } else {
    coords = (point[1] + ' ' + point[0]);
  }
  ol.format.XSD.writeStringTextNode(node, coords);
};


/**
 * @param {Array.<number>} point Point geometry.
 * @param {string=} opt_srsName Optional srsName
 * @return {string} The coords string.
 * @private
 */
ol.format.GML3.prototype.getCoords_ = function(point, opt_srsName) {
  var axisOrientation = 'enu';
  if (opt_srsName) {
    axisOrientation = ol.proj.get(opt_srsName).getAxisOrientation();
  }
  return ((axisOrientation.substr(0, 2) === 'en') ?
      point[0] + ' ' + point[1] :
      point[1] + ' ' + point[0]);
};


/**
 * @param {Node} node Node.
 * @param {ol.geom.LineString|ol.geom.LinearRing} value Geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.GML3.prototype.writePosList_ = function(node, value, objectStack) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var srsName = context['srsName'];
  // only 2d for simple features profile
  var points = value.getCoordinates();
  var len = points.length;
  var parts = new Array(len);
  var point;
  for (var i = 0; i < len; ++i) {
    point = points[i];
    parts[i] = this.getCoords_(point, srsName);
  }
  ol.format.XSD.writeStringTextNode(node, parts.join(' '));
};


/**
 * @param {Node} node Node.
 * @param {ol.geom.Point} geometry Point geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.GML3.prototype.writePoint_ = function(node, geometry, objectStack) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var srsName = context['srsName'];
  if (srsName) {
    node.setAttribute('srsName', srsName);
  }
  var pos = ol.xml.createElementNS(node.namespaceURI, 'pos');
  node.appendChild(pos);
  this.writePos_(pos, geometry, objectStack);
};


/**
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.GML3.ENVELOPE_SERIALIZERS_ = {
  'http://www.opengis.net/gml': {
    'lowerCorner': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
    'upperCorner': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode)
  }
};


/**
 * @param {Node} node Node.
 * @param {ol.Extent} extent Extent.
 * @param {Array.<*>} objectStack Node stack.
 */
ol.format.GML3.prototype.writeEnvelope = function(node, extent, objectStack) {
  goog.asserts.assert(extent.length == 4, 'extent should have 4 items');
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var srsName = context['srsName'];
  if (srsName) {
    node.setAttribute('srsName', srsName);
  }
  var keys = ['lowerCorner', 'upperCorner'];
  var values = [extent[0] + ' ' + extent[1], extent[2] + ' ' + extent[3]];
  ol.xml.pushSerializeAndPop(/** @type {ol.xml.NodeStackItem} */
      ({node: node}), ol.format.GML3.ENVELOPE_SERIALIZERS_,
      ol.xml.OBJECT_PROPERTY_NODE_FACTORY,
      values,
      objectStack, keys, this);
};


/**
 * @param {Node} node Node.
 * @param {ol.geom.LinearRing} geometry LinearRing geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.GML3.prototype.writeLinearRing_ = function(node, geometry, objectStack) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var srsName = context['srsName'];
  if (srsName) {
    node.setAttribute('srsName', srsName);
  }
  var posList = ol.xml.createElementNS(node.namespaceURI, 'posList');
  node.appendChild(posList);
  this.writePosList_(posList, geometry, objectStack);
};


/**
 * @param {*} value Value.
 * @param {Array.<*>} objectStack Object stack.
 * @param {string=} opt_nodeName Node name.
 * @return {Node} Node.
 * @private
 */
ol.format.GML3.prototype.RING_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) {
  var context = objectStack[objectStack.length - 1];
  var parentNode = context.node;
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var exteriorWritten = context['exteriorWritten'];
  if (exteriorWritten === undefined) {
    context['exteriorWritten'] = true;
  }
  return ol.xml.createElementNS(parentNode.namespaceURI,
      exteriorWritten !== undefined ? 'interior' : 'exterior');
};


/**
 * @param {Node} node Node.
 * @param {ol.geom.Polygon} geometry Polygon geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.GML3.prototype.writeSurfaceOrPolygon_ = function(node, geometry, objectStack) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var srsName = context['srsName'];
  if (node.nodeName !== 'PolygonPatch' && srsName) {
    node.setAttribute('srsName', srsName);
  }
  if (node.nodeName === 'Polygon' || node.nodeName === 'PolygonPatch') {
    var rings = geometry.getLinearRings();
    ol.xml.pushSerializeAndPop(
        {node: node, srsName: srsName},
        ol.format.GML3.RING_SERIALIZERS_,
        this.RING_NODE_FACTORY_,
        rings, objectStack, undefined, this);
  } else if (node.nodeName === 'Surface') {
    var patches = ol.xml.createElementNS(node.namespaceURI, 'patches');
    node.appendChild(patches);
    this.writeSurfacePatches_(
        patches, geometry, objectStack);
  }
};


/**
 * @param {Node} node Node.
 * @param {ol.geom.LineString} geometry LineString geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.GML3.prototype.writeCurveOrLineString_ = function(node, geometry, objectStack) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var srsName = context['srsName'];
  if (node.nodeName !== 'LineStringSegment' && srsName) {
    node.setAttribute('srsName', srsName);
  }
  if (node.nodeName === 'LineString' ||
      node.nodeName === 'LineStringSegment') {
    var posList = ol.xml.createElementNS(node.namespaceURI, 'posList');
    node.appendChild(posList);
    this.writePosList_(posList, geometry, objectStack);
  } else if (node.nodeName === 'Curve') {
    var segments = ol.xml.createElementNS(node.namespaceURI, 'segments');
    node.appendChild(segments);
    this.writeCurveSegments_(segments,
        geometry, objectStack);
  }
};


/**
 * @param {Node} node Node.
 * @param {ol.geom.MultiPolygon} geometry MultiPolygon geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.GML3.prototype.writeMultiSurfaceOrPolygon_ = function(node, geometry, objectStack) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var srsName = context['srsName'];
  var surface = context['surface'];
  if (srsName) {
    node.setAttribute('srsName', srsName);
  }
  var polygons = geometry.getPolygons();
  ol.xml.pushSerializeAndPop({node: node, srsName: srsName, surface: surface},
      ol.format.GML3.SURFACEORPOLYGONMEMBER_SERIALIZERS_,
      this.MULTIGEOMETRY_MEMBER_NODE_FACTORY_, polygons,
      objectStack, undefined, this);
};


/**
 * @param {Node} node Node.
 * @param {ol.geom.MultiPoint} geometry MultiPoint geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.GML3.prototype.writeMultiPoint_ = function(node, geometry,
    objectStack) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var srsName = context['srsName'];
  if (srsName) {
    node.setAttribute('srsName', srsName);
  }
  var points = geometry.getPoints();
  ol.xml.pushSerializeAndPop({node: node, srsName: srsName},
      ol.format.GML3.POINTMEMBER_SERIALIZERS_,
      ol.xml.makeSimpleNodeFactory('pointMember'), points,
      objectStack, undefined, this);
};


/**
 * @param {Node} node Node.
 * @param {ol.geom.MultiLineString} geometry MultiLineString geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.GML3.prototype.writeMultiCurveOrLineString_ = function(node, geometry, objectStack) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var srsName = context['srsName'];
  var curve = context['curve'];
  if (srsName) {
    node.setAttribute('srsName', srsName);
  }
  var lines = geometry.getLineStrings();
  ol.xml.pushSerializeAndPop({node: node, srsName: srsName, curve: curve},
      ol.format.GML3.LINESTRINGORCURVEMEMBER_SERIALIZERS_,
      this.MULTIGEOMETRY_MEMBER_NODE_FACTORY_, lines,
      objectStack, undefined, this);
};


/**
 * @param {Node} node Node.
 * @param {ol.geom.LinearRing} ring LinearRing geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.GML3.prototype.writeRing_ = function(node, ring, objectStack) {
  var linearRing = ol.xml.createElementNS(node.namespaceURI, 'LinearRing');
  node.appendChild(linearRing);
  this.writeLinearRing_(linearRing, ring, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {ol.geom.Polygon} polygon Polygon geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.GML3.prototype.writeSurfaceOrPolygonMember_ = function(node, polygon, objectStack) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var child = this.GEOMETRY_NODE_FACTORY_(
      polygon, objectStack);
  if (child) {
    node.appendChild(child);
    this.writeSurfaceOrPolygon_(child, polygon, objectStack);
  }
};


/**
 * @param {Node} node Node.
 * @param {ol.geom.Point} point Point geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.GML3.prototype.writePointMember_ = function(node, point, objectStack) {
  var child = ol.xml.createElementNS(node.namespaceURI, 'Point');
  node.appendChild(child);
  this.writePoint_(child, point, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {ol.geom.LineString} line LineString geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.GML3.prototype.writeLineStringOrCurveMember_ = function(node, line, objectStack) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var child = this.GEOMETRY_NODE_FACTORY_(line, objectStack);
  if (child) {
    node.appendChild(child);
    this.writeCurveOrLineString_(child, line, objectStack);
  }
};


/**
 * @param {Node} node Node.
 * @param {ol.geom.Polygon} polygon Polygon geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.GML3.prototype.writeSurfacePatches_ = function(node, polygon, objectStack) {
  var child = ol.xml.createElementNS(node.namespaceURI, 'PolygonPatch');
  node.appendChild(child);
  this.writeSurfaceOrPolygon_(child, polygon, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {ol.geom.LineString} line LineString geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.GML3.prototype.writeCurveSegments_ = function(node, line, objectStack) {
  var child = ol.xml.createElementNS(node.namespaceURI,
      'LineStringSegment');
  node.appendChild(child);
  this.writeCurveOrLineString_(child, line, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {ol.geom.Geometry|ol.Extent} geometry Geometry.
 * @param {Array.<*>} objectStack Node stack.
 */
ol.format.GML3.prototype.writeGeometryElement = function(node, geometry, objectStack) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var item = goog.object.clone(context);
  item.node = node;
  var value;
  if (goog.isArray(geometry)) {
    if (context.dataProjection) {
      value = ol.proj.transformExtent(
          geometry, context.featureProjection, context.dataProjection);
    } else {
      value = geometry;
    }
  } else {
    goog.asserts.assertInstanceof(geometry, ol.geom.Geometry,
        'geometry should be an ol.geom.Geometry');
    value =
        ol.format.Feature.transformWithOptions(geometry, true, context);
  }
  ol.xml.pushSerializeAndPop(/** @type {ol.xml.NodeStackItem} */
      (item), ol.format.GML3.GEOMETRY_SERIALIZERS_,
      this.GEOMETRY_NODE_FACTORY_, [value],
      objectStack, undefined, this);
};


/**
 * @param {Node} node Node.
 * @param {ol.Feature} feature Feature.
 * @param {Array.<*>} objectStack Node stack.
 */
ol.format.GML3.prototype.writeFeatureElement = function(node, feature, objectStack) {
  var fid = feature.getId();
  if (fid) {
    node.setAttribute('fid', fid);
  }
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var featureNS = context['featureNS'];
  var geometryName = feature.getGeometryName();
  if (!context.serializers) {
    context.serializers = {};
    context.serializers[featureNS] = {};
  }
  var properties = feature.getProperties();
  var keys = [], values = [];
  for (var key in properties) {
    var value = properties[key];
    if (value !== null) {
      keys.push(key);
      values.push(value);
      if (key == geometryName || value instanceof ol.geom.Geometry) {
        if (!(key in context.serializers[featureNS])) {
          context.serializers[featureNS][key] = ol.xml.makeChildAppender(
              this.writeGeometryElement, this);
        }
      } else {
        if (!(key in context.serializers[featureNS])) {
          context.serializers[featureNS][key] = ol.xml.makeChildAppender(
              ol.format.XSD.writeStringTextNode);
        }
      }
    }
  }
  var item = goog.object.clone(context);
  item.node = node;
  ol.xml.pushSerializeAndPop(/** @type {ol.xml.NodeStackItem} */
      (item), context.serializers,
      ol.xml.makeSimpleNodeFactory(undefined, featureNS),
      values,
      objectStack, keys);
};


/**
 * @param {Node} node Node.
 * @param {Array.<ol.Feature>} features Features.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.GML3.prototype.writeFeatureMembers_ = function(node, features, objectStack) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var featureType = context['featureType'];
  var featureNS = context['featureNS'];
  var serializers = {};
  serializers[featureNS] = {};
  serializers[featureNS][featureType] = ol.xml.makeChildAppender(
      this.writeFeatureElement, this);
  var item = goog.object.clone(context);
  item.node = node;
  ol.xml.pushSerializeAndPop(/** @type {ol.xml.NodeStackItem} */
      (item),
      serializers,
      ol.xml.makeSimpleNodeFactory(featureType, featureNS), features,
      objectStack);
};


/**
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.GML3.SURFACEORPOLYGONMEMBER_SERIALIZERS_ = {
  'http://www.opengis.net/gml': {
    'surfaceMember': ol.xml.makeChildAppender(
        ol.format.GML3.prototype.writeSurfaceOrPolygonMember_),
    'polygonMember': ol.xml.makeChildAppender(
        ol.format.GML3.prototype.writeSurfaceOrPolygonMember_)
  }
};


/**
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.GML3.POINTMEMBER_SERIALIZERS_ = {
  'http://www.opengis.net/gml': {
    'pointMember': ol.xml.makeChildAppender(
        ol.format.GML3.prototype.writePointMember_)
  }
};


/**
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.GML3.LINESTRINGORCURVEMEMBER_SERIALIZERS_ = {
  'http://www.opengis.net/gml': {
    'lineStringMember': ol.xml.makeChildAppender(
        ol.format.GML3.prototype.writeLineStringOrCurveMember_),
    'curveMember': ol.xml.makeChildAppender(
        ol.format.GML3.prototype.writeLineStringOrCurveMember_)
  }
};


/**
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.GML3.RING_SERIALIZERS_ = {
  'http://www.opengis.net/gml': {
    'exterior': ol.xml.makeChildAppender(ol.format.GML3.prototype.writeRing_),
    'interior': ol.xml.makeChildAppender(ol.format.GML3.prototype.writeRing_)
  }
};


/**
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.GML3.GEOMETRY_SERIALIZERS_ = {
  'http://www.opengis.net/gml': {
    'Curve': ol.xml.makeChildAppender(
        ol.format.GML3.prototype.writeCurveOrLineString_),
    'MultiCurve': ol.xml.makeChildAppender(
        ol.format.GML3.prototype.writeMultiCurveOrLineString_),
    'Point': ol.xml.makeChildAppender(ol.format.GML3.prototype.writePoint_),
    'MultiPoint': ol.xml.makeChildAppender(
        ol.format.GML3.prototype.writeMultiPoint_),
    'LineString': ol.xml.makeChildAppender(
        ol.format.GML3.prototype.writeCurveOrLineString_),
    'MultiLineString': ol.xml.makeChildAppender(
        ol.format.GML3.prototype.writeMultiCurveOrLineString_),
    'LinearRing': ol.xml.makeChildAppender(
        ol.format.GML3.prototype.writeLinearRing_),
    'Polygon': ol.xml.makeChildAppender(
        ol.format.GML3.prototype.writeSurfaceOrPolygon_),
    'MultiPolygon': ol.xml.makeChildAppender(
        ol.format.GML3.prototype.writeMultiSurfaceOrPolygon_),
    'Surface': ol.xml.makeChildAppender(
        ol.format.GML3.prototype.writeSurfaceOrPolygon_),
    'MultiSurface': ol.xml.makeChildAppender(
        ol.format.GML3.prototype.writeMultiSurfaceOrPolygon_),
    'Envelope': ol.xml.makeChildAppender(
        ol.format.GML3.prototype.writeEnvelope)
  }
};


/**
 * @const
 * @type {Object.<string, string>}
 * @private
 */
ol.format.GML3.MULTIGEOMETRY_TO_MEMBER_NODENAME_ = {
  'MultiLineString': 'lineStringMember',
  'MultiCurve': 'curveMember',
  'MultiPolygon': 'polygonMember',
  'MultiSurface': 'surfaceMember'
};


/**
 * @const
 * @param {*} value Value.
 * @param {Array.<*>} objectStack Object stack.
 * @param {string=} opt_nodeName Node name.
 * @return {Node|undefined} Node.
 * @private
 */
ol.format.GML3.prototype.MULTIGEOMETRY_MEMBER_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) {
  var parentNode = objectStack[objectStack.length - 1].node;
  goog.asserts.assert(ol.xml.isNode(parentNode),
      'parentNode should be a node');
  return ol.xml.createElementNS('http://www.opengis.net/gml',
      ol.format.GML3.MULTIGEOMETRY_TO_MEMBER_NODENAME_[parentNode.nodeName]);
};


/**
 * @const
 * @param {*} value Value.
 * @param {Array.<*>} objectStack Object stack.
 * @param {string=} opt_nodeName Node name.
 * @return {Node|undefined} Node.
 * @private
 */
ol.format.GML3.prototype.GEOMETRY_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var multiSurface = context['multiSurface'];
  var surface = context['surface'];
  var curve = context['curve'];
  var multiCurve = context['multiCurve'];
  var parentNode = objectStack[objectStack.length - 1].node;
  goog.asserts.assert(ol.xml.isNode(parentNode),
      'parentNode should be a node');
  var nodeName;
  if (!goog.isArray(value)) {
    goog.asserts.assertInstanceof(value, ol.geom.Geometry,
        'value should be an ol.geom.Geometry');
    nodeName = value.getType();
    if (nodeName === 'MultiPolygon' && multiSurface === true) {
      nodeName = 'MultiSurface';
    } else if (nodeName === 'Polygon' && surface === true) {
      nodeName = 'Surface';
    } else if (nodeName === 'LineString' && curve === true) {
      nodeName = 'Curve';
    } else if (nodeName === 'MultiLineString' && multiCurve === true) {
      nodeName = 'MultiCurve';
    }
  } else {
    nodeName = 'Envelope';
  }
  return ol.xml.createElementNS('http://www.opengis.net/gml',
      nodeName);
};


/**
 * Encode a geometry in GML 3.1.1 Simple Features.
 *
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Options.
 * @return {Node} Node.
 * @api
 */
ol.format.GML3.prototype.writeGeometryNode = function(geometry, opt_options) {
  opt_options = this.adaptOptions(opt_options);
  var geom = ol.xml.createElementNS('http://www.opengis.net/gml', 'geom');
  var context = {node: geom, srsName: this.srsName,
    curve: this.curve_, surface: this.surface_,
    multiSurface: this.multiSurface_, multiCurve: this.multiCurve_};
  if (opt_options) {
    goog.object.extend(context, opt_options);
  }
  this.writeGeometryElement(geom, geometry, [context]);
  return geom;
};


/**
 * Encode an array of features in GML 3.1.1 Simple Features.
 *
 * @function
 * @param {Array.<ol.Feature>} features Features.
 * @param {olx.format.WriteOptions=} opt_options Options.
 * @return {string} Result.
 * @api stable
 */
ol.format.GML3.prototype.writeFeatures;


/**
 * Encode an array of features in the GML 3.1.1 format as an XML node.
 *
 * @param {Array.<ol.Feature>} features Features.
 * @param {olx.format.WriteOptions=} opt_options Options.
 * @return {Node} Node.
 * @api
 */
ol.format.GML3.prototype.writeFeaturesNode = function(features, opt_options) {
  opt_options = this.adaptOptions(opt_options);
  var node = ol.xml.createElementNS('http://www.opengis.net/gml',
      'featureMembers');
  ol.xml.setAttributeNS(node, 'http://www.w3.org/2001/XMLSchema-instance',
      'xsi:schemaLocation', this.schemaLocation);
  var context = {
    srsName: this.srsName,
    curve: this.curve_,
    surface: this.surface_,
    multiSurface: this.multiSurface_,
    multiCurve: this.multiCurve_,
    featureNS: this.featureNS,
    featureType: this.featureType
  };
  if (opt_options) {
    goog.object.extend(context, opt_options);
  }
  this.writeFeatureMembers_(node, features, [context]);
  return node;
};


/**
 * @classdesc
 * Feature format for reading and writing data in the GML format
 * version 3.1.1.
 * Currently only supports GML 3.1.1 Simple Features profile.
 *
 * @constructor
 * @param {olx.format.GMLOptions=} opt_options
 *     Optional configuration object.
 * @extends {ol.format.GMLBase}
 * @api stable
 */
ol.format.GML = ol.format.GML3;


/**
 * Encode an array of features in GML 3.1.1 Simple Features.
 *
 * @function
 * @param {Array.<ol.Feature>} features Features.
 * @param {olx.format.WriteOptions=} opt_options Options.
 * @return {string} Result.
 * @api stable
 */
ol.format.GML.prototype.writeFeatures;


/**
 * Encode an array of features in the GML 3.1.1 format as an XML node.
 *
 * @function
 * @param {Array.<ol.Feature>} features Features.
 * @param {olx.format.WriteOptions=} opt_options Options.
 * @return {Node} Node.
 * @api
 */
ol.format.GML.prototype.writeFeaturesNode;

goog.provide('ol.format.GPX');

goog.require('goog.asserts');
goog.require('goog.dom.NodeType');
goog.require('ol.Feature');
goog.require('ol.array');
goog.require('ol.format.Feature');
goog.require('ol.format.XMLFeature');
goog.require('ol.format.XSD');
goog.require('ol.geom.GeometryLayout');
goog.require('ol.geom.LineString');
goog.require('ol.geom.MultiLineString');
goog.require('ol.geom.Point');
goog.require('ol.proj');
goog.require('ol.xml');


/**
 * @classdesc
 * Feature format for reading and writing data in the GPX format.
 *
 * @constructor
 * @extends {ol.format.XMLFeature}
 * @param {olx.format.GPXOptions=} opt_options Options.
 * @api stable
 */
ol.format.GPX = function(opt_options) {

  var options = opt_options ? opt_options : {};

  goog.base(this);

  /**
   * @inheritDoc
   */
  this.defaultDataProjection = ol.proj.get('EPSG:4326');

  /**
   * @type {function(ol.Feature, Node)|undefined}
   * @private
   */
  this.readExtensions_ = options.readExtensions;
};
goog.inherits(ol.format.GPX, ol.format.XMLFeature);


/**
 * @const
 * @private
 * @type {Array.<string>}
 */
ol.format.GPX.NAMESPACE_URIS_ = [
  null,
  'http://www.topografix.com/GPX/1/0',
  'http://www.topografix.com/GPX/1/1'
];


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {Node} node Node.
 * @param {Object} values Values.
 * @private
 * @return {Array.<number>} Flat coordinates.
 */
ol.format.GPX.appendCoordinate_ = function(flatCoordinates, node, values) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  flatCoordinates.push(
      parseFloat(node.getAttribute('lon')),
      parseFloat(node.getAttribute('lat')));
  if ('ele' in values) {
    flatCoordinates.push(/** @type {number} */ (values['ele']));
    delete values['ele'];
  } else {
    flatCoordinates.push(0);
  }
  if ('time' in values) {
    flatCoordinates.push(/** @type {number} */ (values['time']));
    delete values['time'];
  } else {
    flatCoordinates.push(0);
  }
  return flatCoordinates;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.GPX.parseLink_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'link', 'localName should be link');
  var values = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  var href = node.getAttribute('href');
  if (href !== null) {
    values['link'] = href;
  }
  ol.xml.parseNode(ol.format.GPX.LINK_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.GPX.parseExtensions_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'extensions',
      'localName should be extensions');
  var values = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  values['extensionsNode_'] = node;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.GPX.parseRtePt_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'rtept', 'localName should be rtept');
  var values = ol.xml.pushParseAndPop(
      {}, ol.format.GPX.RTEPT_PARSERS_, node, objectStack);
  if (values) {
    var rteValues = /** @type {Object} */ (objectStack[objectStack.length - 1]);
    var flatCoordinates = /** @type {Array.<number>} */
        (rteValues['flatCoordinates']);
    ol.format.GPX.appendCoordinate_(flatCoordinates, node, values);
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.GPX.parseTrkPt_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'trkpt', 'localName should be trkpt');
  var values = ol.xml.pushParseAndPop(
      {}, ol.format.GPX.TRKPT_PARSERS_, node, objectStack);
  if (values) {
    var trkValues = /** @type {Object} */ (objectStack[objectStack.length - 1]);
    var flatCoordinates = /** @type {Array.<number>} */
        (trkValues['flatCoordinates']);
    ol.format.GPX.appendCoordinate_(flatCoordinates, node, values);
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.GPX.parseTrkSeg_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'trkseg',
      'localName should be trkseg');
  var values = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  ol.xml.parseNode(ol.format.GPX.TRKSEG_PARSERS_, node, objectStack);
  var flatCoordinates = /** @type {Array.<number>} */
      (values['flatCoordinates']);
  var ends = /** @type {Array.<number>} */ (values['ends']);
  ends.push(flatCoordinates.length);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {ol.Feature|undefined} Track.
 */
ol.format.GPX.readRte_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'rte', 'localName should be rte');
  var options = /** @type {olx.format.ReadOptions} */ (objectStack[0]);
  var values = ol.xml.pushParseAndPop({
    'flatCoordinates': []
  }, ol.format.GPX.RTE_PARSERS_, node, objectStack);
  if (!values) {
    return undefined;
  }
  var flatCoordinates = /** @type {Array.<number>} */
      (values['flatCoordinates']);
  delete values['flatCoordinates'];
  var geometry = new ol.geom.LineString(null);
  geometry.setFlatCoordinates(ol.geom.GeometryLayout.XYZM, flatCoordinates);
  ol.format.Feature.transformWithOptions(geometry, false, options);
  var feature = new ol.Feature(geometry);
  feature.setProperties(values);
  return feature;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {ol.Feature|undefined} Track.
 */
ol.format.GPX.readTrk_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'trk', 'localName should be trk');
  var options = /** @type {olx.format.ReadOptions} */ (objectStack[0]);
  var values = ol.xml.pushParseAndPop({
    'flatCoordinates': [],
    'ends': []
  }, ol.format.GPX.TRK_PARSERS_, node, objectStack);
  if (!values) {
    return undefined;
  }
  var flatCoordinates = /** @type {Array.<number>} */
      (values['flatCoordinates']);
  delete values['flatCoordinates'];
  var ends = /** @type {Array.<number>} */ (values['ends']);
  delete values['ends'];
  var geometry = new ol.geom.MultiLineString(null);
  geometry.setFlatCoordinates(
      ol.geom.GeometryLayout.XYZM, flatCoordinates, ends);
  ol.format.Feature.transformWithOptions(geometry, false, options);
  var feature = new ol.Feature(geometry);
  feature.setProperties(values);
  return feature;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {ol.Feature|undefined} Waypoint.
 */
ol.format.GPX.readWpt_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'wpt', 'localName should be wpt');
  var options = /** @type {olx.format.ReadOptions} */ (objectStack[0]);
  var values = ol.xml.pushParseAndPop(
      {}, ol.format.GPX.WPT_PARSERS_, node, objectStack);
  if (!values) {
    return undefined;
  }
  var coordinates = ol.format.GPX.appendCoordinate_([], node, values);
  var geometry = new ol.geom.Point(
      coordinates, ol.geom.GeometryLayout.XYZM);
  ol.format.Feature.transformWithOptions(geometry, false, options);
  var feature = new ol.Feature(geometry);
  feature.setProperties(values);
  return feature;
};


/**
 * @const
 * @type {Object.<string, function(Node, Array.<*>): (ol.Feature|undefined)>}
 * @private
 */
ol.format.GPX.FEATURE_READER_ = {
  'rte': ol.format.GPX.readRte_,
  'trk': ol.format.GPX.readTrk_,
  'wpt': ol.format.GPX.readWpt_
};


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GPX.GPX_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.GPX.NAMESPACE_URIS_, {
      'rte': ol.xml.makeArrayPusher(ol.format.GPX.readRte_),
      'trk': ol.xml.makeArrayPusher(ol.format.GPX.readTrk_),
      'wpt': ol.xml.makeArrayPusher(ol.format.GPX.readWpt_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GPX.LINK_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.GPX.NAMESPACE_URIS_, {
      'text':
          ol.xml.makeObjectPropertySetter(ol.format.XSD.readString, 'linkText'),
      'type':
          ol.xml.makeObjectPropertySetter(ol.format.XSD.readString, 'linkType')
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GPX.RTE_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.GPX.NAMESPACE_URIS_, {
      'name': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'cmt': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'desc': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'src': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'link': ol.format.GPX.parseLink_,
      'number':
          ol.xml.makeObjectPropertySetter(ol.format.XSD.readNonNegativeInteger),
      'extensions': ol.format.GPX.parseExtensions_,
      'type': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'rtept': ol.format.GPX.parseRtePt_
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GPX.RTEPT_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.GPX.NAMESPACE_URIS_, {
      'ele': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
      'time': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDateTime)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GPX.TRK_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.GPX.NAMESPACE_URIS_, {
      'name': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'cmt': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'desc': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'src': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'link': ol.format.GPX.parseLink_,
      'number':
          ol.xml.makeObjectPropertySetter(ol.format.XSD.readNonNegativeInteger),
      'type': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'extensions': ol.format.GPX.parseExtensions_,
      'trkseg': ol.format.GPX.parseTrkSeg_
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GPX.TRKSEG_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.GPX.NAMESPACE_URIS_, {
      'trkpt': ol.format.GPX.parseTrkPt_
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GPX.TRKPT_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.GPX.NAMESPACE_URIS_, {
      'ele': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
      'time': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDateTime)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.GPX.WPT_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.GPX.NAMESPACE_URIS_, {
      'ele': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
      'time': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDateTime),
      'magvar': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
      'geoidheight': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
      'name': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'cmt': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'desc': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'src': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'link': ol.format.GPX.parseLink_,
      'sym': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'type': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'fix': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'sat': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readNonNegativeInteger),
      'hdop': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
      'vdop': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
      'pdop': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
      'ageofdgpsdata':
          ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
      'dgpsid':
          ol.xml.makeObjectPropertySetter(ol.format.XSD.readNonNegativeInteger),
      'extensions': ol.format.GPX.parseExtensions_
    });


/**
 * @param {Array.<ol.Feature>} features List of features.
 * @private
 */
ol.format.GPX.prototype.handleReadExtensions_ = function(features) {
  if (!features) {
    features = [];
  }
  for (var i = 0, ii = features.length; i < ii; ++i) {
    var feature = features[i];
    if (this.readExtensions_) {
      var extensionsNode = feature.get('extensionsNode_') || null;
      this.readExtensions_(feature, extensionsNode);
    }
    feature.set('extensionsNode_', undefined);
  }
};


/**
 * Read the first feature from a GPX source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {ol.Feature} Feature.
 * @api stable
 */
ol.format.GPX.prototype.readFeature;


/**
 * @inheritDoc
 */
ol.format.GPX.prototype.readFeatureFromNode = function(node, opt_options) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  if (!ol.array.includes(ol.format.GPX.NAMESPACE_URIS_, node.namespaceURI)) {
    return null;
  }
  var featureReader = ol.format.GPX.FEATURE_READER_[node.localName];
  if (!featureReader) {
    return null;
  }
  var feature = featureReader(node, [this.getReadOptions(node, opt_options)]);
  if (!feature) {
    return null;
  }
  this.handleReadExtensions_([feature]);
  return feature;
};


/**
 * Read all features from a GPX source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<ol.Feature>} Features.
 * @api stable
 */
ol.format.GPX.prototype.readFeatures;


/**
 * @inheritDoc
 */
ol.format.GPX.prototype.readFeaturesFromNode = function(node, opt_options) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  if (!ol.array.includes(ol.format.GPX.NAMESPACE_URIS_, node.namespaceURI)) {
    return [];
  }
  if (node.localName == 'gpx') {
    var features = ol.xml.pushParseAndPop(
        /** @type {Array.<ol.Feature>} */ ([]), ol.format.GPX.GPX_PARSERS_,
        node, [this.getReadOptions(node, opt_options)]);
    if (features) {
      this.handleReadExtensions_(features);
      return features;
    } else {
      return [];
    }
  }
  return [];
};


/**
 * Read the projection from a GPX source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @return {ol.proj.Projection} Projection.
 * @api stable
 */
ol.format.GPX.prototype.readProjection;


/**
 * @param {Node} node Node.
 * @param {string} value Value for the link's `href` attribute.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.GPX.writeLink_ = function(node, value, objectStack) {
  node.setAttribute('href', value);
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var properties = context['properties'];
  var link = [
    properties['linkText'],
    properties['linkType']
  ];
  ol.xml.pushSerializeAndPop(/** @type {ol.xml.NodeStackItem} */ ({node: node}),
      ol.format.GPX.LINK_SERIALIZERS_, ol.xml.OBJECT_PROPERTY_NODE_FACTORY,
      link, objectStack, ol.format.GPX.LINK_SEQUENCE_);
};


/**
 * @param {Node} node Node.
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.GPX.writeWptType_ = function(node, coordinate, objectStack) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var parentNode = context.node;
  goog.asserts.assert(ol.xml.isNode(parentNode),
      'parentNode should be an XML node');
  var namespaceURI = parentNode.namespaceURI;
  var properties = context['properties'];
  //FIXME Projection handling
  ol.xml.setAttributeNS(node, null, 'lat', coordinate[1]);
  ol.xml.setAttributeNS(node, null, 'lon', coordinate[0]);
  var geometryLayout = context['geometryLayout'];
  switch (geometryLayout) {
    case ol.geom.GeometryLayout.XYZM:
      if (coordinate[3] !== 0) {
        properties['time'] = coordinate[3];
      }
      // fall through
    case ol.geom.GeometryLayout.XYZ:
      if (coordinate[2] !== 0) {
        properties['ele'] = coordinate[2];
      }
      break;
    case ol.geom.GeometryLayout.XYM:
      if (coordinate[2] !== 0) {
        properties['time'] = coordinate[2];
      }
      break;
    default:
      // pass
  }
  var orderedKeys = ol.format.GPX.WPT_TYPE_SEQUENCE_[namespaceURI];
  var values = ol.xml.makeSequence(properties, orderedKeys);
  ol.xml.pushSerializeAndPop(/** @type {ol.xml.NodeStackItem} */
      ({node: node, 'properties': properties}),
      ol.format.GPX.WPT_TYPE_SERIALIZERS_, ol.xml.OBJECT_PROPERTY_NODE_FACTORY,
      values, objectStack, orderedKeys);
};


/**
 * @param {Node} node Node.
 * @param {ol.Feature} feature Feature.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.GPX.writeRte_ = function(node, feature, objectStack) {
  var options = /** @type {olx.format.WriteOptions} */ (objectStack[0]);
  var properties = feature.getProperties();
  var context = {node: node, 'properties': properties};
  var geometry = feature.getGeometry();
  if (geometry) {
    goog.asserts.assertInstanceof(geometry, ol.geom.LineString,
        'geometry should be an ol.geom.LineString');
    geometry = /** @type {ol.geom.LineString} */
        (ol.format.Feature.transformWithOptions(geometry, true, options));
    context['geometryLayout'] = geometry.getLayout();
    properties['rtept'] = geometry.getCoordinates();
  }
  var parentNode = objectStack[objectStack.length - 1].node;
  var orderedKeys = ol.format.GPX.RTE_SEQUENCE_[parentNode.namespaceURI];
  var values = ol.xml.makeSequence(properties, orderedKeys);
  ol.xml.pushSerializeAndPop(/** @type {ol.xml.NodeStackItem} */ (context),
      ol.format.GPX.RTE_SERIALIZERS_, ol.xml.OBJECT_PROPERTY_NODE_FACTORY,
      values, objectStack, orderedKeys);
};


/**
 * @param {Node} node Node.
 * @param {ol.Feature} feature Feature.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.GPX.writeTrk_ = function(node, feature, objectStack) {
  var options = /** @type {olx.format.WriteOptions} */ (objectStack[0]);
  var properties = feature.getProperties();
  var context = {node: node, 'properties': properties};
  var geometry = feature.getGeometry();
  if (geometry) {
    goog.asserts.assertInstanceof(geometry, ol.geom.MultiLineString,
        'geometry should be an ol.geom.MultiLineString');
    geometry = /** @type {ol.geom.MultiLineString} */
        (ol.format.Feature.transformWithOptions(geometry, true, options));
    properties['trkseg'] = geometry.getLineStrings();
  }
  var parentNode = objectStack[objectStack.length - 1].node;
  var orderedKeys = ol.format.GPX.TRK_SEQUENCE_[parentNode.namespaceURI];
  var values = ol.xml.makeSequence(properties, orderedKeys);
  ol.xml.pushSerializeAndPop(/** @type {ol.xml.NodeStackItem} */ (context),
      ol.format.GPX.TRK_SERIALIZERS_, ol.xml.OBJECT_PROPERTY_NODE_FACTORY,
      values, objectStack, orderedKeys);
};


/**
 * @param {Node} node Node.
 * @param {ol.geom.LineString} lineString LineString.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.GPX.writeTrkSeg_ = function(node, lineString, objectStack) {
  var context = {node: node, 'geometryLayout': lineString.getLayout(),
    'properties': {}};
  ol.xml.pushSerializeAndPop(/** @type {ol.xml.NodeStackItem} */ (context),
      ol.format.GPX.TRKSEG_SERIALIZERS_, ol.format.GPX.TRKSEG_NODE_FACTORY_,
      lineString.getCoordinates(), objectStack);
};


/**
 * @param {Node} node Node.
 * @param {ol.Feature} feature Feature.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.GPX.writeWpt_ = function(node, feature, objectStack) {
  var options = /** @type {olx.format.WriteOptions} */ (objectStack[0]);
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  context['properties'] = feature.getProperties();
  var geometry = feature.getGeometry();
  if (geometry) {
    goog.asserts.assertInstanceof(geometry, ol.geom.Point,
        'geometry should be an ol.geom.Point');
    geometry = /** @type {ol.geom.Point} */
        (ol.format.Feature.transformWithOptions(geometry, true, options));
    context['geometryLayout'] = geometry.getLayout();
    ol.format.GPX.writeWptType_(node, geometry.getCoordinates(), objectStack);
  }
};


/**
 * @const
 * @type {Array.<string>}
 * @private
 */
ol.format.GPX.LINK_SEQUENCE_ = ['text', 'type'];


/**
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.GPX.LINK_SERIALIZERS_ = ol.xml.makeStructureNS(
    ol.format.GPX.NAMESPACE_URIS_, {
      'text': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
      'type': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode)
    });


/**
 * @const
 * @type {Object.<string, Array.<string>>}
 * @private
 */
ol.format.GPX.RTE_SEQUENCE_ = ol.xml.makeStructureNS(
    ol.format.GPX.NAMESPACE_URIS_, [
      'name', 'cmt', 'desc', 'src', 'link', 'number', 'type', 'rtept'
    ]);


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.GPX.RTE_SERIALIZERS_ = ol.xml.makeStructureNS(
    ol.format.GPX.NAMESPACE_URIS_, {
      'name': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
      'cmt': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
      'desc': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
      'src': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
      'link': ol.xml.makeChildAppender(ol.format.GPX.writeLink_),
      'number': ol.xml.makeChildAppender(
          ol.format.XSD.writeNonNegativeIntegerTextNode),
      'type': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
      'rtept': ol.xml.makeArraySerializer(ol.xml.makeChildAppender(
          ol.format.GPX.writeWptType_))
    });


/**
 * @const
 * @type {Object.<string, Array.<string>>}
 * @private
 */
ol.format.GPX.TRK_SEQUENCE_ = ol.xml.makeStructureNS(
    ol.format.GPX.NAMESPACE_URIS_, [
      'name', 'cmt', 'desc', 'src', 'link', 'number', 'type', 'trkseg'
    ]);


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.GPX.TRK_SERIALIZERS_ = ol.xml.makeStructureNS(
    ol.format.GPX.NAMESPACE_URIS_, {
      'name': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
      'cmt': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
      'desc': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
      'src': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
      'link': ol.xml.makeChildAppender(ol.format.GPX.writeLink_),
      'number': ol.xml.makeChildAppender(
          ol.format.XSD.writeNonNegativeIntegerTextNode),
      'type': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
      'trkseg': ol.xml.makeArraySerializer(ol.xml.makeChildAppender(
          ol.format.GPX.writeTrkSeg_))
    });


/**
 * @const
 * @type {function(*, Array.<*>, string=): (Node|undefined)}
 * @private
 */
ol.format.GPX.TRKSEG_NODE_FACTORY_ = ol.xml.makeSimpleNodeFactory('trkpt');


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.GPX.TRKSEG_SERIALIZERS_ = ol.xml.makeStructureNS(
    ol.format.GPX.NAMESPACE_URIS_, {
      'trkpt': ol.xml.makeChildAppender(ol.format.GPX.writeWptType_)
    });


/**
 * @const
 * @type {Object.<string, Array.<string>>}
 * @private
 */
ol.format.GPX.WPT_TYPE_SEQUENCE_ = ol.xml.makeStructureNS(
    ol.format.GPX.NAMESPACE_URIS_, [
      'ele', 'time', 'magvar', 'geoidheight', 'name', 'cmt', 'desc', 'src',
      'link', 'sym', 'type', 'fix', 'sat', 'hdop', 'vdop', 'pdop',
      'ageofdgpsdata', 'dgpsid'
    ]);


/**
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.GPX.WPT_TYPE_SERIALIZERS_ = ol.xml.makeStructureNS(
    ol.format.GPX.NAMESPACE_URIS_, {
      'ele': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode),
      'time': ol.xml.makeChildAppender(ol.format.XSD.writeDateTimeTextNode),
      'magvar': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode),
      'geoidheight': ol.xml.makeChildAppender(
          ol.format.XSD.writeDecimalTextNode),
      'name': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
      'cmt': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
      'desc': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
      'src': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
      'link': ol.xml.makeChildAppender(ol.format.GPX.writeLink_),
      'sym': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
      'type': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
      'fix': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
      'sat': ol.xml.makeChildAppender(
          ol.format.XSD.writeNonNegativeIntegerTextNode),
      'hdop': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode),
      'vdop': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode),
      'pdop': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode),
      'ageofdgpsdata': ol.xml.makeChildAppender(
          ol.format.XSD.writeDecimalTextNode),
      'dgpsid': ol.xml.makeChildAppender(
          ol.format.XSD.writeNonNegativeIntegerTextNode)
    });


/**
 * @const
 * @type {Object.<string, string>}
 * @private
 */
ol.format.GPX.GEOMETRY_TYPE_TO_NODENAME_ = {
  'Point': 'wpt',
  'LineString': 'rte',
  'MultiLineString': 'trk'
};


/**
 * @const
 * @param {*} value Value.
 * @param {Array.<*>} objectStack Object stack.
 * @param {string=} opt_nodeName Node name.
 * @return {Node|undefined} Node.
 * @private
 */
ol.format.GPX.GPX_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) {
  goog.asserts.assertInstanceof(value, ol.Feature,
      'value should be an ol.Feature');
  var geometry = value.getGeometry();
  if (geometry) {
    var nodeName = ol.format.GPX.GEOMETRY_TYPE_TO_NODENAME_[geometry.getType()];
    if (nodeName) {
      var parentNode = objectStack[objectStack.length - 1].node;
      goog.asserts.assert(ol.xml.isNode(parentNode),
          'parentNode should be an XML node');
      return ol.xml.createElementNS(parentNode.namespaceURI, nodeName);
    }
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.GPX.GPX_SERIALIZERS_ = ol.xml.makeStructureNS(
    ol.format.GPX.NAMESPACE_URIS_, {
      'rte': ol.xml.makeChildAppender(ol.format.GPX.writeRte_),
      'trk': ol.xml.makeChildAppender(ol.format.GPX.writeTrk_),
      'wpt': ol.xml.makeChildAppender(ol.format.GPX.writeWpt_)
    });


/**
 * Encode an array of features in the GPX format.
 *
 * @function
 * @param {Array.<ol.Feature>} features Features.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {string} Result.
 * @api stable
 */
ol.format.GPX.prototype.writeFeatures;


/**
 * Encode an array of features in the GPX format as an XML node.
 *
 * @param {Array.<ol.Feature>} features Features.
 * @param {olx.format.WriteOptions=} opt_options Options.
 * @return {Node} Node.
 * @api
 */
ol.format.GPX.prototype.writeFeaturesNode = function(features, opt_options) {
  opt_options = this.adaptOptions(opt_options);
  //FIXME Serialize metadata
  var gpx = ol.xml.createElementNS('http://www.topografix.com/GPX/1/1', 'gpx');

  ol.xml.pushSerializeAndPop(/** @type {ol.xml.NodeStackItem} */
      ({node: gpx}), ol.format.GPX.GPX_SERIALIZERS_,
      ol.format.GPX.GPX_NODE_FACTORY_, features, [opt_options]);
  return gpx;
};

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Utilities for string newlines.
 * @author nnaze@google.com (Nathan Naze)
 */


/**
 * Namespace for string utilities
 */
goog.provide('goog.string.newlines');
goog.provide('goog.string.newlines.Line');

goog.require('goog.array');


/**
 * Splits a string into lines, properly handling universal newlines.
 * @param {string} str String to split.
 * @param {boolean=} opt_keepNewlines Whether to keep the newlines in the
 *     resulting strings. Defaults to false.
 * @return {!Array<string>} String split into lines.
 */
goog.string.newlines.splitLines = function(str, opt_keepNewlines) {
  var lines = goog.string.newlines.getLines(str);
  return goog.array.map(lines, function(line) {
    return opt_keepNewlines ? line.getFullLine() : line.getContent();
  });
};



/**
 * Line metadata class that records the start/end indicies of lines
 * in a string.  Can be used to implement common newline use cases such as
 * splitLines() or determining line/column of an index in a string.
 * Also implements methods to get line contents.
 *
 * Indexes are expressed as string indicies into string.substring(), inclusive
 * at the start, exclusive at the end.
 *
 * Create an array of these with goog.string.newlines.getLines().
 * @param {string} string The original string.
 * @param {number} startLineIndex The index of the start of the line.
 * @param {number} endContentIndex The index of the end of the line, excluding
 *     newlines.
 * @param {number} endLineIndex The index of the end of the line, index
 *     newlines.
 * @constructor
 * @struct
 * @final
 */
goog.string.newlines.Line = function(string, startLineIndex,
                                     endContentIndex, endLineIndex) {
  /**
   * The original string.
   * @type {string}
   */
  this.string = string;

  /**
   * Index of the start of the line.
   * @type {number}
   */
  this.startLineIndex = startLineIndex;

  /**
   * Index of the end of the line, excluding any newline characters.
   * Index is the first character after the line, suitable for
   * String.substring().
   * @type {number}
   */
  this.endContentIndex = endContentIndex;

  /**
   * Index of the end of the line, excluding any newline characters.
   * Index is the first character after the line, suitable for
   * String.substring().
   * @type {number}
   */

  this.endLineIndex = endLineIndex;
};


/**
 * @return {string} The content of the line, excluding any newline characters.
 */
goog.string.newlines.Line.prototype.getContent = function() {
  return this.string.substring(this.startLineIndex, this.endContentIndex);
};


/**
 * @return {string} The full line, including any newline characters.
 */
goog.string.newlines.Line.prototype.getFullLine = function() {
  return this.string.substring(this.startLineIndex, this.endLineIndex);
};


/**
 * @return {string} The newline characters, if any ('\n', \r', '\r\n', '', etc).
 */
goog.string.newlines.Line.prototype.getNewline = function() {
  return this.string.substring(this.endContentIndex, this.endLineIndex);
};


/**
 * Splits a string into an array of line metadata.
 * @param {string} str String to split.
 * @return {!Array<!goog.string.newlines.Line>} Array of line metadata.
 */
goog.string.newlines.getLines = function(str) {
  // We use the constructor because literals are evaluated only once in
  // < ES 3.1.
  // See http://www.mail-archive.com/es-discuss@mozilla.org/msg01796.html
  var re = RegExp('\r\n|\r|\n', 'g');
  var sliceIndex = 0;
  var result;
  var lines = [];

  while (result = re.exec(str)) {
    var line = new goog.string.newlines.Line(
        str, sliceIndex, result.index, result.index + result[0].length);
    lines.push(line);

    // remember where to start the slice from
    sliceIndex = re.lastIndex;
  }

  // If the string does not end with a newline, add the last line.
  if (sliceIndex < str.length) {
    var line = new goog.string.newlines.Line(
        str, sliceIndex, str.length, str.length);
    lines.push(line);
  }

  return lines;
};

goog.provide('ol.format.TextFeature');

goog.require('goog.asserts');
goog.require('ol.format.Feature');
goog.require('ol.format.FormatType');


/**
 * @classdesc
 * Abstract base class; normally only used for creating subclasses and not
 * instantiated in apps.
 * Base class for text feature formats.
 *
 * @constructor
 * @extends {ol.format.Feature}
 */
ol.format.TextFeature = function() {
  goog.base(this);
};
goog.inherits(ol.format.TextFeature, ol.format.Feature);


/**
 * @param {Document|Node|Object|string} source Source.
 * @private
 * @return {string} Text.
 */
ol.format.TextFeature.prototype.getText_ = function(source) {
  if (goog.isString(source)) {
    return source;
  } else {
    goog.asserts.fail();
    return '';
  }
};


/**
 * @inheritDoc
 */
ol.format.TextFeature.prototype.getType = function() {
  return ol.format.FormatType.TEXT;
};


/**
 * @inheritDoc
 */
ol.format.TextFeature.prototype.readFeature = function(source, opt_options) {
  return this.readFeatureFromText(
      this.getText_(source), this.adaptOptions(opt_options));
};


/**
 * @param {string} text Text.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @protected
 * @return {ol.Feature} Feature.
 */
ol.format.TextFeature.prototype.readFeatureFromText = goog.abstractMethod;


/**
 * @inheritDoc
 */
ol.format.TextFeature.prototype.readFeatures = function(source, opt_options) {
  return this.readFeaturesFromText(
      this.getText_(source), this.adaptOptions(opt_options));
};


/**
 * @param {string} text Text.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @protected
 * @return {Array.<ol.Feature>} Features.
 */
ol.format.TextFeature.prototype.readFeaturesFromText = goog.abstractMethod;


/**
 * @inheritDoc
 */
ol.format.TextFeature.prototype.readGeometry = function(source, opt_options) {
  return this.readGeometryFromText(
      this.getText_(source), this.adaptOptions(opt_options));
};


/**
 * @param {string} text Text.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @protected
 * @return {ol.geom.Geometry} Geometry.
 */
ol.format.TextFeature.prototype.readGeometryFromText = goog.abstractMethod;


/**
 * @inheritDoc
 */
ol.format.TextFeature.prototype.readProjection = function(source) {
  return this.readProjectionFromText(this.getText_(source));
};


/**
 * @param {string} text Text.
 * @protected
 * @return {ol.proj.Projection} Projection.
 */
ol.format.TextFeature.prototype.readProjectionFromText = function(text) {
  return this.defaultDataProjection;
};


/**
 * @inheritDoc
 */
ol.format.TextFeature.prototype.writeFeature = function(feature, opt_options) {
  return this.writeFeatureText(feature, this.adaptOptions(opt_options));
};


/**
 * @param {ol.Feature} feature Features.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @protected
 * @return {string} Text.
 */
ol.format.TextFeature.prototype.writeFeatureText = goog.abstractMethod;


/**
 * @inheritDoc
 */
ol.format.TextFeature.prototype.writeFeatures = function(
    features, opt_options) {
  return this.writeFeaturesText(features, this.adaptOptions(opt_options));
};


/**
 * @param {Array.<ol.Feature>} features Features.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @protected
 * @return {string} Text.
 */
ol.format.TextFeature.prototype.writeFeaturesText = goog.abstractMethod;


/**
 * @inheritDoc
 */
ol.format.TextFeature.prototype.writeGeometry = function(
    geometry, opt_options) {
  return this.writeGeometryText(geometry, this.adaptOptions(opt_options));
};


/**
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @protected
 * @return {string} Text.
 */
ol.format.TextFeature.prototype.writeGeometryText = goog.abstractMethod;

goog.provide('ol.format.IGC');
goog.provide('ol.format.IGCZ');

goog.require('goog.asserts');
goog.require('goog.string.newlines');
goog.require('ol.Feature');
goog.require('ol.format.Feature');
goog.require('ol.format.TextFeature');
goog.require('ol.geom.GeometryLayout');
goog.require('ol.geom.LineString');
goog.require('ol.proj');


/**
 * IGC altitude/z. One of 'barometric', 'gps', 'none'.
 * @enum {string}
 * @api
 */
ol.format.IGCZ = {
  BAROMETRIC: 'barometric',
  GPS: 'gps',
  NONE: 'none'
};


/**
 * @classdesc
 * Feature format for `*.igc` flight recording files.
 *
 * @constructor
 * @extends {ol.format.TextFeature}
 * @param {olx.format.IGCOptions=} opt_options Options.
 * @api
 */
ol.format.IGC = function(opt_options) {

  var options = opt_options ? opt_options : {};

  goog.base(this);

  /**
   * @inheritDoc
   */
  this.defaultDataProjection = ol.proj.get('EPSG:4326');

  /**
   * @private
   * @type {ol.format.IGCZ}
   */
  this.altitudeMode_ = options.altitudeMode ?
      options.altitudeMode : ol.format.IGCZ.NONE;

};
goog.inherits(ol.format.IGC, ol.format.TextFeature);


/**
 * @const
 * @type {Array.<string>}
 * @private
 */
ol.format.IGC.EXTENSIONS_ = ['.igc'];


/**
 * @const
 * @type {RegExp}
 * @private
 */
ol.format.IGC.B_RECORD_RE_ =
    /^B(\d{2})(\d{2})(\d{2})(\d{2})(\d{5})([NS])(\d{3})(\d{5})([EW])([AV])(\d{5})(\d{5})/;


/**
 * @const
 * @type {RegExp}
 * @private
 */
ol.format.IGC.H_RECORD_RE_ = /^H.([A-Z]{3}).*?:(.*)/;


/**
 * @const
 * @type {RegExp}
 * @private
 */
ol.format.IGC.HFDTE_RECORD_RE_ = /^HFDTE(\d{2})(\d{2})(\d{2})/;


/**
 * @inheritDoc
 */
ol.format.IGC.prototype.getExtensions = function() {
  return ol.format.IGC.EXTENSIONS_;
};


/**
 * Read the feature from the IGC source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {ol.Feature} Feature.
 * @api
 */
ol.format.IGC.prototype.readFeature;


/**
 * @inheritDoc
 */
ol.format.IGC.prototype.readFeatureFromText = function(text, opt_options) {
  var altitudeMode = this.altitudeMode_;
  var lines = goog.string.newlines.splitLines(text);
  /** @type {Object.<string, string>} */
  var properties = {};
  var flatCoordinates = [];
  var year = 2000;
  var month = 0;
  var day = 1;
  var i, ii;
  for (i = 0, ii = lines.length; i < ii; ++i) {
    var line = lines[i];
    var m;
    if (line.charAt(0) == 'B') {
      m = ol.format.IGC.B_RECORD_RE_.exec(line);
      if (m) {
        var hour = parseInt(m[1], 10);
        var minute = parseInt(m[2], 10);
        var second = parseInt(m[3], 10);
        var y = parseInt(m[4], 10) + parseInt(m[5], 10) / 60000;
        if (m[6] == 'S') {
          y = -y;
        }
        var x = parseInt(m[7], 10) + parseInt(m[8], 10) / 60000;
        if (m[9] == 'W') {
          x = -x;
        }
        flatCoordinates.push(x, y);
        if (altitudeMode != ol.format.IGCZ.NONE) {
          var z;
          if (altitudeMode == ol.format.IGCZ.GPS) {
            z = parseInt(m[11], 10);
          } else if (altitudeMode == ol.format.IGCZ.BAROMETRIC) {
            z = parseInt(m[12], 10);
          } else {
            goog.asserts.fail();
            z = 0;
          }
          flatCoordinates.push(z);
        }
        var dateTime = Date.UTC(year, month, day, hour, minute, second);
        flatCoordinates.push(dateTime / 1000);
      }
    } else if (line.charAt(0) == 'H') {
      m = ol.format.IGC.HFDTE_RECORD_RE_.exec(line);
      if (m) {
        day = parseInt(m[1], 10);
        month = parseInt(m[2], 10) - 1;
        year = 2000 + parseInt(m[3], 10);
      } else {
        m = ol.format.IGC.H_RECORD_RE_.exec(line);
        if (m) {
          properties[m[1]] = m[2].trim();
          m = ol.format.IGC.HFDTE_RECORD_RE_.exec(line);
        }
      }
    }
  }
  if (flatCoordinates.length === 0) {
    return null;
  }
  var lineString = new ol.geom.LineString(null);
  var layout = altitudeMode == ol.format.IGCZ.NONE ?
      ol.geom.GeometryLayout.XYM : ol.geom.GeometryLayout.XYZM;
  lineString.setFlatCoordinates(layout, flatCoordinates);
  var feature = new ol.Feature(ol.format.Feature.transformWithOptions(
      lineString, false, opt_options));
  feature.setProperties(properties);
  return feature;
};


/**
 * Read the feature from the source. As IGC sources contain a single
 * feature, this will return the feature in an array.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<ol.Feature>} Features.
 * @api
 */
ol.format.IGC.prototype.readFeatures;


/**
 * @inheritDoc
 */
ol.format.IGC.prototype.readFeaturesFromText = function(text, opt_options) {
  var feature = this.readFeatureFromText(text, opt_options);
  if (feature) {
    return [feature];
  } else {
    return [];
  }
};


/**
 * Read the projection from the IGC source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @return {ol.proj.Projection} Projection.
 * @api
 */
ol.format.IGC.prototype.readProjection;

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Class for parsing and formatting URIs.
 *
 * Use goog.Uri(string) to parse a URI string.  Use goog.Uri.create(...) to
 * create a new instance of the goog.Uri object from Uri parts.
 *
 * e.g: <code>var myUri = new goog.Uri(window.location);</code>
 *
 * Implements RFC 3986 for parsing/formatting URIs.
 * http://www.ietf.org/rfc/rfc3986.txt
 *
 * Some changes have been made to the interface (more like .NETs), though the
 * internal representation is now of un-encoded parts, this will change the
 * behavior slightly.
 *
 */

goog.provide('goog.Uri');
goog.provide('goog.Uri.QueryData');

goog.require('goog.array');
goog.require('goog.string');
goog.require('goog.structs');
goog.require('goog.structs.Map');
goog.require('goog.uri.utils');
goog.require('goog.uri.utils.ComponentIndex');
goog.require('goog.uri.utils.StandardQueryParam');



/**
 * This class contains setters and getters for the parts of the URI.
 * The <code>getXyz</code>/<code>setXyz</code> methods return the decoded part
 * -- so<code>goog.Uri.parse('/foo%20bar').getPath()</code> will return the
 * decoded path, <code>/foo bar</code>.
 *
 * Reserved characters (see RFC 3986 section 2.2) can be present in
 * their percent-encoded form in scheme, domain, and path URI components and
 * will not be auto-decoded. For example:
 * <code>goog.Uri.parse('rel%61tive/path%2fto/resource').getPath()</code> will
 * return <code>relative/path%2fto/resource</code>.
 *
 * The constructor accepts an optional unparsed, raw URI string.  The parser
 * is relaxed, so special characters that aren't escaped but don't cause
 * ambiguities will not cause parse failures.
 *
 * All setters return <code>this</code> and so may be chained, a la
 * <code>goog.Uri.parse('/foo').setFragment('part').toString()</code>.
 *
 * @param {*=} opt_uri Optional string URI to parse
 *        (use goog.Uri.create() to create a URI from parts), or if
 *        a goog.Uri is passed, a clone is created.
 * @param {boolean=} opt_ignoreCase If true, #getParameterValue will ignore
 * the case of the parameter name.
 *
 * @throws URIError If opt_uri is provided and URI is malformed (that is,
 *     if decodeURIComponent fails on any of the URI components).
 * @constructor
 * @struct
 */
goog.Uri = function(opt_uri, opt_ignoreCase) {
  /**
   * Scheme such as "http".
   * @private {string}
   */
  this.scheme_ = '';

  /**
   * User credentials in the form "username:password".
   * @private {string}
   */
  this.userInfo_ = '';

  /**
   * Domain part, e.g. "www.google.com".
   * @private {string}
   */
  this.domain_ = '';

  /**
   * Port, e.g. 8080.
   * @private {?number}
   */
  this.port_ = null;

  /**
   * Path, e.g. "/tests/img.png".
   * @private {string}
   */
  this.path_ = '';

  /**
   * The fragment without the #.
   * @private {string}
   */
  this.fragment_ = '';

  /**
   * Whether or not this Uri should be treated as Read Only.
   * @private {boolean}
   */
  this.isReadOnly_ = false;

  /**
   * Whether or not to ignore case when comparing query params.
   * @private {boolean}
   */
  this.ignoreCase_ = false;

  /**
   * Object representing query data.
   * @private {!goog.Uri.QueryData}
   */
  this.queryData_;

  // Parse in the uri string
  var m;
  if (opt_uri instanceof goog.Uri) {
    this.ignoreCase_ = goog.isDef(opt_ignoreCase) ?
        opt_ignoreCase : opt_uri.getIgnoreCase();
    this.setScheme(opt_uri.getScheme());
    this.setUserInfo(opt_uri.getUserInfo());
    this.setDomain(opt_uri.getDomain());
    this.setPort(opt_uri.getPort());
    this.setPath(opt_uri.getPath());
    this.setQueryData(opt_uri.getQueryData().clone());
    this.setFragment(opt_uri.getFragment());
  } else if (opt_uri && (m = goog.uri.utils.split(String(opt_uri)))) {
    this.ignoreCase_ = !!opt_ignoreCase;

    // Set the parts -- decoding as we do so.
    // COMPATABILITY NOTE - In IE, unmatched fields may be empty strings,
    // whereas in other browsers they will be undefined.
    this.setScheme(m[goog.uri.utils.ComponentIndex.SCHEME] || '', true);
    this.setUserInfo(m[goog.uri.utils.ComponentIndex.USER_INFO] || '', true);
    this.setDomain(m[goog.uri.utils.ComponentIndex.DOMAIN] || '', true);
    this.setPort(m[goog.uri.utils.ComponentIndex.PORT]);
    this.setPath(m[goog.uri.utils.ComponentIndex.PATH] || '', true);
    this.setQueryData(m[goog.uri.utils.ComponentIndex.QUERY_DATA] || '', true);
    this.setFragment(m[goog.uri.utils.ComponentIndex.FRAGMENT] || '', true);

  } else {
    this.ignoreCase_ = !!opt_ignoreCase;
    this.queryData_ = new goog.Uri.QueryData(null, null, this.ignoreCase_);
  }
};


/**
 * If true, we preserve the type of query parameters set programmatically.
 *
 * This means that if you set a parameter to a boolean, and then call
 * getParameterValue, you will get a boolean back.
 *
 * If false, we will coerce parameters to strings, just as they would
 * appear in real URIs.
 *
 * TODO(nicksantos): Remove this once people have time to fix all tests.
 *
 * @type {boolean}
 */
goog.Uri.preserveParameterTypesCompatibilityFlag = false;


/**
 * Parameter name added to stop caching.
 * @type {string}
 */
goog.Uri.RANDOM_PARAM = goog.uri.utils.StandardQueryParam.RANDOM;


/**
 * @return {string} The string form of the url.
 * @override
 */
goog.Uri.prototype.toString = function() {
  var out = [];

  var scheme = this.getScheme();
  if (scheme) {
    out.push(goog.Uri.encodeSpecialChars_(
        scheme, goog.Uri.reDisallowedInSchemeOrUserInfo_, true), ':');
  }

  var domain = this.getDomain();
  if (domain || scheme == 'file') {
    out.push('//');

    var userInfo = this.getUserInfo();
    if (userInfo) {
      out.push(goog.Uri.encodeSpecialChars_(
          userInfo, goog.Uri.reDisallowedInSchemeOrUserInfo_, true), '@');
    }

    out.push(goog.Uri.removeDoubleEncoding_(goog.string.urlEncode(domain)));

    var port = this.getPort();
    if (port != null) {
      out.push(':', String(port));
    }
  }

  var path = this.getPath();
  if (path) {
    if (this.hasDomain() && path.charAt(0) != '/') {
      out.push('/');
    }
    out.push(goog.Uri.encodeSpecialChars_(
        path,
        path.charAt(0) == '/' ?
            goog.Uri.reDisallowedInAbsolutePath_ :
            goog.Uri.reDisallowedInRelativePath_,
        true));
  }

  var query = this.getEncodedQuery();
  if (query) {
    out.push('?', query);
  }

  var fragment = this.getFragment();
  if (fragment) {
    out.push('#', goog.Uri.encodeSpecialChars_(
        fragment, goog.Uri.reDisallowedInFragment_));
  }
  return out.join('');
};


/**
 * Resolves the given relative URI (a goog.Uri object), using the URI
 * represented by this instance as the base URI.
 *
 * There are several kinds of relative URIs:<br>
 * 1. foo - replaces the last part of the path, the whole query and fragment<br>
 * 2. /foo - replaces the the path, the query and fragment<br>
 * 3. //foo - replaces everything from the domain on.  foo is a domain name<br>
 * 4. ?foo - replace the query and fragment<br>
 * 5. #foo - replace the fragment only
 *
 * Additionally, if relative URI has a non-empty path, all ".." and "."
 * segments will be resolved, as described in RFC 3986.
 *
 * @param {!goog.Uri} relativeUri The relative URI to resolve.
 * @return {!goog.Uri} The resolved URI.
 */
goog.Uri.prototype.resolve = function(relativeUri) {

  var absoluteUri = this.clone();

  // we satisfy these conditions by looking for the first part of relativeUri
  // that is not blank and applying defaults to the rest

  var overridden = relativeUri.hasScheme();

  if (overridden) {
    absoluteUri.setScheme(relativeUri.getScheme());
  } else {
    overridden = relativeUri.hasUserInfo();
  }

  if (overridden) {
    absoluteUri.setUserInfo(relativeUri.getUserInfo());
  } else {
    overridden = relativeUri.hasDomain();
  }

  if (overridden) {
    absoluteUri.setDomain(relativeUri.getDomain());
  } else {
    overridden = relativeUri.hasPort();
  }

  var path = relativeUri.getPath();
  if (overridden) {
    absoluteUri.setPort(relativeUri.getPort());
  } else {
    overridden = relativeUri.hasPath();
    if (overridden) {
      // resolve path properly
      if (path.charAt(0) != '/') {
        // path is relative
        if (this.hasDomain() && !this.hasPath()) {
          // RFC 3986, section 5.2.3, case 1
          path = '/' + path;
        } else {
          // RFC 3986, section 5.2.3, case 2
          var lastSlashIndex = absoluteUri.getPath().lastIndexOf('/');
          if (lastSlashIndex != -1) {
            path = absoluteUri.getPath().substr(0, lastSlashIndex + 1) + path;
          }
        }
      }
      path = goog.Uri.removeDotSegments(path);
    }
  }

  if (overridden) {
    absoluteUri.setPath(path);
  } else {
    overridden = relativeUri.hasQuery();
  }

  if (overridden) {
    absoluteUri.setQueryData(relativeUri.getDecodedQuery());
  } else {
    overridden = relativeUri.hasFragment();
  }

  if (overridden) {
    absoluteUri.setFragment(relativeUri.getFragment());
  }

  return absoluteUri;
};


/**
 * Clones the URI instance.
 * @return {!goog.Uri} New instance of the URI object.
 */
goog.Uri.prototype.clone = function() {
  return new goog.Uri(this);
};


/**
 * @return {string} The encoded scheme/protocol for the URI.
 */
goog.Uri.prototype.getScheme = function() {
  return this.scheme_;
};


/**
 * Sets the scheme/protocol.
 * @throws URIError If opt_decode is true and newScheme is malformed (that is,
 *     if decodeURIComponent fails).
 * @param {string} newScheme New scheme value.
 * @param {boolean=} opt_decode Optional param for whether to decode new value.
 * @return {!goog.Uri} Reference to this URI object.
 */
goog.Uri.prototype.setScheme = function(newScheme, opt_decode) {
  this.enforceReadOnly();
  this.scheme_ = opt_decode ? goog.Uri.decodeOrEmpty_(newScheme, true) :
      newScheme;

  // remove an : at the end of the scheme so somebody can pass in
  // window.location.protocol
  if (this.scheme_) {
    this.scheme_ = this.scheme_.replace(/:$/, '');
  }
  return this;
};


/**
 * @return {boolean} Whether the scheme has been set.
 */
goog.Uri.prototype.hasScheme = function() {
  return !!this.scheme_;
};


/**
 * @return {string} The decoded user info.
 */
goog.Uri.prototype.getUserInfo = function() {
  return this.userInfo_;
};


/**
 * Sets the userInfo.
 * @throws URIError If opt_decode is true and newUserInfo is malformed (that is,
 *     if decodeURIComponent fails).
 * @param {string} newUserInfo New userInfo value.
 * @param {boolean=} opt_decode Optional param for whether to decode new value.
 * @return {!goog.Uri} Reference to this URI object.
 */
goog.Uri.prototype.setUserInfo = function(newUserInfo, opt_decode) {
  this.enforceReadOnly();
  this.userInfo_ = opt_decode ? goog.Uri.decodeOrEmpty_(newUserInfo) :
                   newUserInfo;
  return this;
};


/**
 * @return {boolean} Whether the user info has been set.
 */
goog.Uri.prototype.hasUserInfo = function() {
  return !!this.userInfo_;
};


/**
 * @return {string} The decoded domain.
 */
goog.Uri.prototype.getDomain = function() {
  return this.domain_;
};


/**
 * Sets the domain.
 * @throws URIError If opt_decode is true and newDomain is malformed (that is,
 *     if decodeURIComponent fails).
 * @param {string} newDomain New domain value.
 * @param {boolean=} opt_decode Optional param for whether to decode new value.
 * @return {!goog.Uri} Reference to this URI object.
 */
goog.Uri.prototype.setDomain = function(newDomain, opt_decode) {
  this.enforceReadOnly();
  this.domain_ = opt_decode ? goog.Uri.decodeOrEmpty_(newDomain, true) :
      newDomain;
  return this;
};


/**
 * @return {boolean} Whether the domain has been set.
 */
goog.Uri.prototype.hasDomain = function() {
  return !!this.domain_;
};


/**
 * @return {?number} The port number.
 */
goog.Uri.prototype.getPort = function() {
  return this.port_;
};


/**
 * Sets the port number.
 * @param {*} newPort Port number. Will be explicitly casted to a number.
 * @return {!goog.Uri} Reference to this URI object.
 */
goog.Uri.prototype.setPort = function(newPort) {
  this.enforceReadOnly();

  if (newPort) {
    newPort = Number(newPort);
    if (isNaN(newPort) || newPort < 0) {
      throw Error('Bad port number ' + newPort);
    }
    this.port_ = newPort;
  } else {
    this.port_ = null;
  }

  return this;
};


/**
 * @return {boolean} Whether the port has been set.
 */
goog.Uri.prototype.hasPort = function() {
  return this.port_ != null;
};


/**
  * @return {string} The decoded path.
 */
goog.Uri.prototype.getPath = function() {
  return this.path_;
};


/**
 * Sets the path.
 * @throws URIError If opt_decode is true and newPath is malformed (that is,
 *     if decodeURIComponent fails).
 * @param {string} newPath New path value.
 * @param {boolean=} opt_decode Optional param for whether to decode new value.
 * @return {!goog.Uri} Reference to this URI object.
 */
goog.Uri.prototype.setPath = function(newPath, opt_decode) {
  this.enforceReadOnly();
  this.path_ = opt_decode ? goog.Uri.decodeOrEmpty_(newPath, true) : newPath;
  return this;
};


/**
 * @return {boolean} Whether the path has been set.
 */
goog.Uri.prototype.hasPath = function() {
  return !!this.path_;
};


/**
 * @return {boolean} Whether the query string has been set.
 */
goog.Uri.prototype.hasQuery = function() {
  return this.queryData_.toString() !== '';
};


/**
 * Sets the query data.
 * @param {goog.Uri.QueryData|string|undefined} queryData QueryData object.
 * @param {boolean=} opt_decode Optional param for whether to decode new value.
 *     Applies only if queryData is a string.
 * @return {!goog.Uri} Reference to this URI object.
 */
goog.Uri.prototype.setQueryData = function(queryData, opt_decode) {
  this.enforceReadOnly();

  if (queryData instanceof goog.Uri.QueryData) {
    this.queryData_ = queryData;
    this.queryData_.setIgnoreCase(this.ignoreCase_);
  } else {
    if (!opt_decode) {
      // QueryData accepts encoded query string, so encode it if
      // opt_decode flag is not true.
      queryData = goog.Uri.encodeSpecialChars_(queryData,
                                               goog.Uri.reDisallowedInQuery_);
    }
    this.queryData_ = new goog.Uri.QueryData(queryData, null, this.ignoreCase_);
  }

  return this;
};


/**
 * Sets the URI query.
 * @param {string} newQuery New query value.
 * @param {boolean=} opt_decode Optional param for whether to decode new value.
 * @return {!goog.Uri} Reference to this URI object.
 */
goog.Uri.prototype.setQuery = function(newQuery, opt_decode) {
  return this.setQueryData(newQuery, opt_decode);
};


/**
 * @return {string} The encoded URI query, not including the ?.
 */
goog.Uri.prototype.getEncodedQuery = function() {
  return this.queryData_.toString();
};


/**
 * @return {string} The decoded URI query, not including the ?.
 */
goog.Uri.prototype.getDecodedQuery = function() {
  return this.queryData_.toDecodedString();
};


/**
 * Returns the query data.
 * @return {!goog.Uri.QueryData} QueryData object.
 */
goog.Uri.prototype.getQueryData = function() {
  return this.queryData_;
};


/**
 * @return {string} The encoded URI query, not including the ?.
 *
 * Warning: This method, unlike other getter methods, returns encoded
 * value, instead of decoded one.
 */
goog.Uri.prototype.getQuery = function() {
  return this.getEncodedQuery();
};


/**
 * Sets the value of the named query parameters, clearing previous values for
 * that key.
 *
 * @param {string} key The parameter to set.
 * @param {*} value The new value.
 * @return {!goog.Uri} Reference to this URI object.
 */
goog.Uri.prototype.setParameterValue = function(key, value) {
  this.enforceReadOnly();
  this.queryData_.set(key, value);
  return this;
};


/**
 * Sets the values of the named query parameters, clearing previous values for
 * that key.  Not new values will currently be moved to the end of the query
 * string.
 *
 * So, <code>goog.Uri.parse('foo?a=b&c=d&e=f').setParameterValues('c', ['new'])
 * </code> yields <tt>foo?a=b&e=f&c=new</tt>.</p>
 *
 * @param {string} key The parameter to set.
 * @param {*} values The new values. If values is a single
 *     string then it will be treated as the sole value.
 * @return {!goog.Uri} Reference to this URI object.
 */
goog.Uri.prototype.setParameterValues = function(key, values) {
  this.enforceReadOnly();

  if (!goog.isArray(values)) {
    values = [String(values)];
  }

  this.queryData_.setValues(key, values);

  return this;
};


/**
 * Returns the value<b>s</b> for a given cgi parameter as a list of decoded
 * query parameter values.
 * @param {string} name The parameter to get values for.
 * @return {!Array<?>} The values for a given cgi parameter as a list of
 *     decoded query parameter values.
 */
goog.Uri.prototype.getParameterValues = function(name) {
  return this.queryData_.getValues(name);
};


/**
 * Returns the first value for a given cgi parameter or undefined if the given
 * parameter name does not appear in the query string.
 * @param {string} paramName Unescaped parameter name.
 * @return {string|undefined} The first value for a given cgi parameter or
 *     undefined if the given parameter name does not appear in the query
 *     string.
 */
goog.Uri.prototype.getParameterValue = function(paramName) {
  // NOTE(nicksantos): This type-cast is a lie when
  // preserveParameterTypesCompatibilityFlag is set to true.
  // But this should only be set to true in tests.
  return /** @type {string|undefined} */ (this.queryData_.get(paramName));
};


/**
 * @return {string} The URI fragment, not including the #.
 */
goog.Uri.prototype.getFragment = function() {
  return this.fragment_;
};


/**
 * Sets the URI fragment.
 * @throws URIError If opt_decode is true and newFragment is malformed (that is,
 *     if decodeURIComponent fails).
 * @param {string} newFragment New fragment value.
 * @param {boolean=} opt_decode Optional param for whether to decode new value.
 * @return {!goog.Uri} Reference to this URI object.
 */
goog.Uri.prototype.setFragment = function(newFragment, opt_decode) {
  this.enforceReadOnly();
  this.fragment_ = opt_decode ? goog.Uri.decodeOrEmpty_(newFragment) :
                   newFragment;
  return this;
};


/**
 * @return {boolean} Whether the URI has a fragment set.
 */
goog.Uri.prototype.hasFragment = function() {
  return !!this.fragment_;
};


/**
 * Returns true if this has the same domain as that of uri2.
 * @param {!goog.Uri} uri2 The URI object to compare to.
 * @return {boolean} true if same domain; false otherwise.
 */
goog.Uri.prototype.hasSameDomainAs = function(uri2) {
  return ((!this.hasDomain() && !uri2.hasDomain()) ||
          this.getDomain() == uri2.getDomain()) &&
      ((!this.hasPort() && !uri2.hasPort()) ||
          this.getPort() == uri2.getPort());
};


/**
 * Adds a random parameter to the Uri.
 * @return {!goog.Uri} Reference to this Uri object.
 */
goog.Uri.prototype.makeUnique = function() {
  this.enforceReadOnly();
  this.setParameterValue(goog.Uri.RANDOM_PARAM, goog.string.getRandomString());

  return this;
};


/**
 * Removes the named query parameter.
 *
 * @param {string} key The parameter to remove.
 * @return {!goog.Uri} Reference to this URI object.
 */
goog.Uri.prototype.removeParameter = function(key) {
  this.enforceReadOnly();
  this.queryData_.remove(key);
  return this;
};


/**
 * Sets whether Uri is read only. If this goog.Uri is read-only,
 * enforceReadOnly_ will be called at the start of any function that may modify
 * this Uri.
 * @param {boolean} isReadOnly whether this goog.Uri should be read only.
 * @return {!goog.Uri} Reference to this Uri object.
 */
goog.Uri.prototype.setReadOnly = function(isReadOnly) {
  this.isReadOnly_ = isReadOnly;
  return this;
};


/**
 * @return {boolean} Whether the URI is read only.
 */
goog.Uri.prototype.isReadOnly = function() {
  return this.isReadOnly_;
};


/**
 * Checks if this Uri has been marked as read only, and if so, throws an error.
 * This should be called whenever any modifying function is called.
 */
goog.Uri.prototype.enforceReadOnly = function() {
  if (this.isReadOnly_) {
    throw Error('Tried to modify a read-only Uri');
  }
};


/**
 * Sets whether to ignore case.
 * NOTE: If there are already key/value pairs in the QueryData, and
 * ignoreCase_ is set to false, the keys will all be lower-cased.
 * @param {boolean} ignoreCase whether this goog.Uri should ignore case.
 * @return {!goog.Uri} Reference to this Uri object.
 */
goog.Uri.prototype.setIgnoreCase = function(ignoreCase) {
  this.ignoreCase_ = ignoreCase;
  if (this.queryData_) {
    this.queryData_.setIgnoreCase(ignoreCase);
  }
  return this;
};


/**
 * @return {boolean} Whether to ignore case.
 */
goog.Uri.prototype.getIgnoreCase = function() {
  return this.ignoreCase_;
};


//==============================================================================
// Static members
//==============================================================================


/**
 * Creates a uri from the string form.  Basically an alias of new goog.Uri().
 * If a Uri object is passed to parse then it will return a clone of the object.
 *
 * @throws URIError If parsing the URI is malformed. The passed URI components
 *     should all be parseable by decodeURIComponent.
 * @param {*} uri Raw URI string or instance of Uri
 *     object.
 * @param {boolean=} opt_ignoreCase Whether to ignore the case of parameter
 * names in #getParameterValue.
 * @return {!goog.Uri} The new URI object.
 */
goog.Uri.parse = function(uri, opt_ignoreCase) {
  return uri instanceof goog.Uri ?
         uri.clone() : new goog.Uri(uri, opt_ignoreCase);
};


/**
 * Creates a new goog.Uri object from unencoded parts.
 *
 * @param {?string=} opt_scheme Scheme/protocol or full URI to parse.
 * @param {?string=} opt_userInfo username:password.
 * @param {?string=} opt_domain www.google.com.
 * @param {?number=} opt_port 9830.
 * @param {?string=} opt_path /some/path/to/a/file.html.
 * @param {string|goog.Uri.QueryData=} opt_query a=1&b=2.
 * @param {?string=} opt_fragment The fragment without the #.
 * @param {boolean=} opt_ignoreCase Whether to ignore parameter name case in
 *     #getParameterValue.
 *
 * @return {!goog.Uri} The new URI object.
 */
goog.Uri.create = function(opt_scheme, opt_userInfo, opt_domain, opt_port,
                           opt_path, opt_query, opt_fragment, opt_ignoreCase) {

  var uri = new goog.Uri(null, opt_ignoreCase);

  // Only set the parts if they are defined and not empty strings.
  opt_scheme && uri.setScheme(opt_scheme);
  opt_userInfo && uri.setUserInfo(opt_userInfo);
  opt_domain && uri.setDomain(opt_domain);
  opt_port && uri.setPort(opt_port);
  opt_path && uri.setPath(opt_path);
  opt_query && uri.setQueryData(opt_query);
  opt_fragment && uri.setFragment(opt_fragment);

  return uri;
};


/**
 * Resolves a relative Uri against a base Uri, accepting both strings and
 * Uri objects.
 *
 * @param {*} base Base Uri.
 * @param {*} rel Relative Uri.
 * @return {!goog.Uri} Resolved uri.
 */
goog.Uri.resolve = function(base, rel) {
  if (!(base instanceof goog.Uri)) {
    base = goog.Uri.parse(base);
  }

  if (!(rel instanceof goog.Uri)) {
    rel = goog.Uri.parse(rel);
  }

  return base.resolve(rel);
};


/**
 * Removes dot segments in given path component, as described in
 * RFC 3986, section 5.2.4.
 *
 * @param {string} path A non-empty path component.
 * @return {string} Path component with removed dot segments.
 */
goog.Uri.removeDotSegments = function(path) {
  if (path == '..' || path == '.') {
    return '';

  } else if (!goog.string.contains(path, './') &&
             !goog.string.contains(path, '/.')) {
    // This optimization detects uris which do not contain dot-segments,
    // and as a consequence do not require any processing.
    return path;

  } else {
    var leadingSlash = goog.string.startsWith(path, '/');
    var segments = path.split('/');
    var out = [];

    for (var pos = 0; pos < segments.length; ) {
      var segment = segments[pos++];

      if (segment == '.') {
        if (leadingSlash && pos == segments.length) {
          out.push('');
        }
      } else if (segment == '..') {
        if (out.length > 1 || out.length == 1 && out[0] != '') {
          out.pop();
        }
        if (leadingSlash && pos == segments.length) {
          out.push('');
        }
      } else {
        out.push(segment);
        leadingSlash = true;
      }
    }

    return out.join('/');
  }
};


/**
 * Decodes a value or returns the empty string if it isn't defined or empty.
 * @throws URIError If decodeURIComponent fails to decode val.
 * @param {string|undefined} val Value to decode.
 * @param {boolean=} opt_preserveReserved If true, restricted characters will
 *     not be decoded.
 * @return {string} Decoded value.
 * @private
 */
goog.Uri.decodeOrEmpty_ = function(val, opt_preserveReserved) {
  // Don't use UrlDecode() here because val is not a query parameter.
  if (!val) {
    return '';
  }

  // decodeURI has the same output for '%2f' and '%252f'. We double encode %25
  // so that we can distinguish between the 2 inputs. This is later undone by
  // removeDoubleEncoding_.
  return opt_preserveReserved ?
      decodeURI(val.replace(/%25/g, '%2525')) : decodeURIComponent(val);
};


/**
 * If unescapedPart is non null, then escapes any characters in it that aren't
 * valid characters in a url and also escapes any special characters that
 * appear in extra.
 *
 * @param {*} unescapedPart The string to encode.
 * @param {RegExp} extra A character set of characters in [\01-\177].
 * @param {boolean=} opt_removeDoubleEncoding If true, remove double percent
 *     encoding.
 * @return {?string} null iff unescapedPart == null.
 * @private
 */
goog.Uri.encodeSpecialChars_ = function(unescapedPart, extra,
    opt_removeDoubleEncoding) {
  if (goog.isString(unescapedPart)) {
    var encoded = encodeURI(unescapedPart).
        replace(extra, goog.Uri.encodeChar_);
    if (opt_removeDoubleEncoding) {
      // encodeURI double-escapes %XX sequences used to represent restricted
      // characters in some URI components, remove the double escaping here.
      encoded = goog.Uri.removeDoubleEncoding_(encoded);
    }
    return encoded;
  }
  return null;
};


/**
 * Converts a character in [\01-\177] to its unicode character equivalent.
 * @param {string} ch One character string.
 * @return {string} Encoded string.
 * @private
 */
goog.Uri.encodeChar_ = function(ch) {
  var n = ch.charCodeAt(0);
  return '%' + ((n >> 4) & 0xf).toString(16) + (n & 0xf).toString(16);
};


/**
 * Removes double percent-encoding from a string.
 * @param  {string} doubleEncodedString String
 * @return {string} String with double encoding removed.
 * @private
 */
goog.Uri.removeDoubleEncoding_ = function(doubleEncodedString) {
  return doubleEncodedString.replace(/%25([0-9a-fA-F]{2})/g, '%$1');
};


/**
 * Regular expression for characters that are disallowed in the scheme or
 * userInfo part of the URI.
 * @type {RegExp}
 * @private
 */
goog.Uri.reDisallowedInSchemeOrUserInfo_ = /[#\/\?@]/g;


/**
 * Regular expression for characters that are disallowed in a relative path.
 * Colon is included due to RFC 3986 3.3.
 * @type {RegExp}
 * @private
 */
goog.Uri.reDisallowedInRelativePath_ = /[\#\?:]/g;


/**
 * Regular expression for characters that are disallowed in an absolute path.
 * @type {RegExp}
 * @private
 */
goog.Uri.reDisallowedInAbsolutePath_ = /[\#\?]/g;


/**
 * Regular expression for characters that are disallowed in the query.
 * @type {RegExp}
 * @private
 */
goog.Uri.reDisallowedInQuery_ = /[\#\?@]/g;


/**
 * Regular expression for characters that are disallowed in the fragment.
 * @type {RegExp}
 * @private
 */
goog.Uri.reDisallowedInFragment_ = /#/g;


/**
 * Checks whether two URIs have the same domain.
 * @param {string} uri1String First URI string.
 * @param {string} uri2String Second URI string.
 * @return {boolean} true if the two URIs have the same domain; false otherwise.
 */
goog.Uri.haveSameDomain = function(uri1String, uri2String) {
  // Differs from goog.uri.utils.haveSameDomain, since this ignores scheme.
  // TODO(gboyer): Have this just call goog.uri.util.haveSameDomain.
  var pieces1 = goog.uri.utils.split(uri1String);
  var pieces2 = goog.uri.utils.split(uri2String);
  return pieces1[goog.uri.utils.ComponentIndex.DOMAIN] ==
             pieces2[goog.uri.utils.ComponentIndex.DOMAIN] &&
         pieces1[goog.uri.utils.ComponentIndex.PORT] ==
             pieces2[goog.uri.utils.ComponentIndex.PORT];
};



/**
 * Class used to represent URI query parameters.  It is essentially a hash of
 * name-value pairs, though a name can be present more than once.
 *
 * Has the same interface as the collections in goog.structs.
 *
 * @param {?string=} opt_query Optional encoded query string to parse into
 *     the object.
 * @param {goog.Uri=} opt_uri Optional uri object that should have its
 *     cache invalidated when this object updates. Deprecated -- this
 *     is no longer required.
 * @param {boolean=} opt_ignoreCase If true, ignore the case of the parameter
 *     name in #get.
 * @constructor
 * @struct
 * @final
 */
goog.Uri.QueryData = function(opt_query, opt_uri, opt_ignoreCase) {
  /**
   * The map containing name/value or name/array-of-values pairs.
   * May be null if it requires parsing from the query string.
   *
   * We need to use a Map because we cannot guarantee that the key names will
   * not be problematic for IE.
   *
   * @private {goog.structs.Map<string, !Array<*>>}
   */
  this.keyMap_ = null;

  /**
   * The number of params, or null if it requires computing.
   * @private {?number}
   */
  this.count_ = null;

  /**
   * Encoded query string, or null if it requires computing from the key map.
   * @private {?string}
   */
  this.encodedQuery_ = opt_query || null;

  /**
   * If true, ignore the case of the parameter name in #get.
   * @private {boolean}
   */
  this.ignoreCase_ = !!opt_ignoreCase;
};


/**
 * If the underlying key map is not yet initialized, it parses the
 * query string and fills the map with parsed data.
 * @private
 */
goog.Uri.QueryData.prototype.ensureKeyMapInitialized_ = function() {
  if (!this.keyMap_) {
    this.keyMap_ = new goog.structs.Map();
    this.count_ = 0;
    if (this.encodedQuery_) {
      var self = this;
      goog.uri.utils.parseQueryData(this.encodedQuery_, function(name, value) {
        self.add(goog.string.urlDecode(name), value);
      });
    }
  }
};


/**
 * Creates a new query data instance from a map of names and values.
 *
 * @param {!goog.structs.Map<string, ?>|!Object} map Map of string parameter
 *     names to parameter value. If parameter value is an array, it is
 *     treated as if the key maps to each individual value in the
 *     array.
 * @param {goog.Uri=} opt_uri URI object that should have its cache
 *     invalidated when this object updates.
 * @param {boolean=} opt_ignoreCase If true, ignore the case of the parameter
 *     name in #get.
 * @return {!goog.Uri.QueryData} The populated query data instance.
 */
goog.Uri.QueryData.createFromMap = function(map, opt_uri, opt_ignoreCase) {
  var keys = goog.structs.getKeys(map);
  if (typeof keys == 'undefined') {
    throw Error('Keys are undefined');
  }

  var queryData = new goog.Uri.QueryData(null, null, opt_ignoreCase);
  var values = goog.structs.getValues(map);
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    var value = values[i];
    if (!goog.isArray(value)) {
      queryData.add(key, value);
    } else {
      queryData.setValues(key, value);
    }
  }
  return queryData;
};


/**
 * Creates a new query data instance from parallel arrays of parameter names
 * and values. Allows for duplicate parameter names. Throws an error if the
 * lengths of the arrays differ.
 *
 * @param {!Array<string>} keys Parameter names.
 * @param {!Array<?>} values Parameter values.
 * @param {goog.Uri=} opt_uri URI object that should have its cache
 *     invalidated when this object updates.
 * @param {boolean=} opt_ignoreCase If true, ignore the case of the parameter
 *     name in #get.
 * @return {!goog.Uri.QueryData} The populated query data instance.
 */
goog.Uri.QueryData.createFromKeysValues = function(
    keys, values, opt_uri, opt_ignoreCase) {
  if (keys.length != values.length) {
    throw Error('Mismatched lengths for keys/values');
  }
  var queryData = new goog.Uri.QueryData(null, null, opt_ignoreCase);
  for (var i = 0; i < keys.length; i++) {
    queryData.add(keys[i], values[i]);
  }
  return queryData;
};


/**
 * @return {?number} The number of parameters.
 */
goog.Uri.QueryData.prototype.getCount = function() {
  this.ensureKeyMapInitialized_();
  return this.count_;
};


/**
 * Adds a key value pair.
 * @param {string} key Name.
 * @param {*} value Value.
 * @return {!goog.Uri.QueryData} Instance of this object.
 */
goog.Uri.QueryData.prototype.add = function(key, value) {
  this.ensureKeyMapInitialized_();
  this.invalidateCache_();

  key = this.getKeyName_(key);
  var values = this.keyMap_.get(key);
  if (!values) {
    this.keyMap_.set(key, (values = []));
  }
  values.push(value);
  this.count_++;
  return this;
};


/**
 * Removes all the params with the given key.
 * @param {string} key Name.
 * @return {boolean} Whether any parameter was removed.
 */
goog.Uri.QueryData.prototype.remove = function(key) {
  this.ensureKeyMapInitialized_();

  key = this.getKeyName_(key);
  if (this.keyMap_.containsKey(key)) {
    this.invalidateCache_();

    // Decrement parameter count.
    this.count_ -= this.keyMap_.get(key).length;
    return this.keyMap_.remove(key);
  }
  return false;
};


/**
 * Clears the parameters.
 */
goog.Uri.QueryData.prototype.clear = function() {
  this.invalidateCache_();
  this.keyMap_ = null;
  this.count_ = 0;
};


/**
 * @return {boolean} Whether we have any parameters.
 */
goog.Uri.QueryData.prototype.isEmpty = function() {
  this.ensureKeyMapInitialized_();
  return this.count_ == 0;
};


/**
 * Whether there is a parameter with the given name
 * @param {string} key The parameter name to check for.
 * @return {boolean} Whether there is a parameter with the given name.
 */
goog.Uri.QueryData.prototype.containsKey = function(key) {
  this.ensureKeyMapInitialized_();
  key = this.getKeyName_(key);
  return this.keyMap_.containsKey(key);
};


/**
 * Whether there is a parameter with the given value.
 * @param {*} value The value to check for.
 * @return {boolean} Whether there is a parameter with the given value.
 */
goog.Uri.QueryData.prototype.containsValue = function(value) {
  // NOTE(arv): This solution goes through all the params even if it was the
  // first param. We can get around this by not reusing code or by switching to
  // iterators.
  var vals = this.getValues();
  return goog.array.contains(vals, value);
};


/**
 * Returns all the keys of the parameters. If a key is used multiple times
 * it will be included multiple times in the returned array
 * @return {!Array<string>} All the keys of the parameters.
 */
goog.Uri.QueryData.prototype.getKeys = function() {
  this.ensureKeyMapInitialized_();
  // We need to get the values to know how many keys to add.
  var vals = /** @type {!Array<*>} */ (this.keyMap_.getValues());
  var keys = this.keyMap_.getKeys();
  var rv = [];
  for (var i = 0; i < keys.length; i++) {
    var val = vals[i];
    for (var j = 0; j < val.length; j++) {
      rv.push(keys[i]);
    }
  }
  return rv;
};


/**
 * Returns all the values of the parameters with the given name. If the query
 * data has no such key this will return an empty array. If no key is given
 * all values wil be returned.
 * @param {string=} opt_key The name of the parameter to get the values for.
 * @return {!Array<?>} All the values of the parameters with the given name.
 */
goog.Uri.QueryData.prototype.getValues = function(opt_key) {
  this.ensureKeyMapInitialized_();
  var rv = [];
  if (goog.isString(opt_key)) {
    if (this.containsKey(opt_key)) {
      rv = goog.array.concat(rv, this.keyMap_.get(this.getKeyName_(opt_key)));
    }
  } else {
    // Return all values.
    var values = this.keyMap_.getValues();
    for (var i = 0; i < values.length; i++) {
      rv = goog.array.concat(rv, values[i]);
    }
  }
  return rv;
};


/**
 * Sets a key value pair and removes all other keys with the same value.
 *
 * @param {string} key Name.
 * @param {*} value Value.
 * @return {!goog.Uri.QueryData} Instance of this object.
 */
goog.Uri.QueryData.prototype.set = function(key, value) {
  this.ensureKeyMapInitialized_();
  this.invalidateCache_();

  // TODO(chrishenry): This could be better written as
  // this.remove(key), this.add(key, value), but that would reorder
  // the key (since the key is first removed and then added at the
  // end) and we would have to fix unit tests that depend on key
  // ordering.
  key = this.getKeyName_(key);
  if (this.containsKey(key)) {
    this.count_ -= this.keyMap_.get(key).length;
  }
  this.keyMap_.set(key, [value]);
  this.count_++;
  return this;
};


/**
 * Returns the first value associated with the key. If the query data has no
 * such key this will return undefined or the optional default.
 * @param {string} key The name of the parameter to get the value for.
 * @param {*=} opt_default The default value to return if the query data
 *     has no such key.
 * @return {*} The first string value associated with the key, or opt_default
 *     if there's no value.
 */
goog.Uri.QueryData.prototype.get = function(key, opt_default) {
  var values = key ? this.getValues(key) : [];
  if (goog.Uri.preserveParameterTypesCompatibilityFlag) {
    return values.length > 0 ? values[0] : opt_default;
  } else {
    return values.length > 0 ? String(values[0]) : opt_default;
  }
};


/**
 * Sets the values for a key. If the key already exists, this will
 * override all of the existing values that correspond to the key.
 * @param {string} key The key to set values for.
 * @param {!Array<?>} values The values to set.
 */
goog.Uri.QueryData.prototype.setValues = function(key, values) {
  this.remove(key);

  if (values.length > 0) {
    this.invalidateCache_();
    this.keyMap_.set(this.getKeyName_(key), goog.array.clone(values));
    this.count_ += values.length;
  }
};


/**
 * @return {string} Encoded query string.
 * @override
 */
goog.Uri.QueryData.prototype.toString = function() {
  if (this.encodedQuery_) {
    return this.encodedQuery_;
  }

  if (!this.keyMap_) {
    return '';
  }

  var sb = [];

  // In the past, we use this.getKeys() and this.getVals(), but that
  // generates a lot of allocations as compared to simply iterating
  // over the keys.
  var keys = this.keyMap_.getKeys();
  for (var i = 0; i < keys.length; i++) {
    var key = keys[i];
    var encodedKey = goog.string.urlEncode(key);
    var val = this.getValues(key);
    for (var j = 0; j < val.length; j++) {
      var param = encodedKey;
      // Ensure that null and undefined are encoded into the url as
      // literal strings.
      if (val[j] !== '') {
        param += '=' + goog.string.urlEncode(val[j]);
      }
      sb.push(param);
    }
  }

  return this.encodedQuery_ = sb.join('&');
};


/**
 * @throws URIError If URI is malformed (that is, if decodeURIComponent fails on
 *     any of the URI components).
 * @return {string} Decoded query string.
 */
goog.Uri.QueryData.prototype.toDecodedString = function() {
  return goog.Uri.decodeOrEmpty_(this.toString());
};


/**
 * Invalidate the cache.
 * @private
 */
goog.Uri.QueryData.prototype.invalidateCache_ = function() {
  this.encodedQuery_ = null;
};


/**
 * Removes all keys that are not in the provided list. (Modifies this object.)
 * @param {Array<string>} keys The desired keys.
 * @return {!goog.Uri.QueryData} a reference to this object.
 */
goog.Uri.QueryData.prototype.filterKeys = function(keys) {
  this.ensureKeyMapInitialized_();
  this.keyMap_.forEach(
      function(value, key) {
        if (!goog.array.contains(keys, key)) {
          this.remove(key);
        }
      }, this);
  return this;
};


/**
 * Clone the query data instance.
 * @return {!goog.Uri.QueryData} New instance of the QueryData object.
 */
goog.Uri.QueryData.prototype.clone = function() {
  var rv = new goog.Uri.QueryData();
  rv.encodedQuery_ = this.encodedQuery_;
  if (this.keyMap_) {
    rv.keyMap_ = this.keyMap_.clone();
    rv.count_ = this.count_;
  }
  return rv;
};


/**
 * Helper function to get the key name from a JavaScript object. Converts
 * the object to a string, and to lower case if necessary.
 * @private
 * @param {*} arg The object to get a key name from.
 * @return {string} valid key name which can be looked up in #keyMap_.
 */
goog.Uri.QueryData.prototype.getKeyName_ = function(arg) {
  var keyName = String(arg);
  if (this.ignoreCase_) {
    keyName = keyName.toLowerCase();
  }
  return keyName;
};


/**
 * Ignore case in parameter names.
 * NOTE: If there are already key/value pairs in the QueryData, and
 * ignoreCase_ is set to false, the keys will all be lower-cased.
 * @param {boolean} ignoreCase whether this goog.Uri should ignore case.
 */
goog.Uri.QueryData.prototype.setIgnoreCase = function(ignoreCase) {
  var resetKeys = ignoreCase && !this.ignoreCase_;
  if (resetKeys) {
    this.ensureKeyMapInitialized_();
    this.invalidateCache_();
    this.keyMap_.forEach(
        function(value, key) {
          var lowerCase = key.toLowerCase();
          if (key != lowerCase) {
            this.remove(key);
            this.setValues(lowerCase, value);
          }
        }, this);
  }
  this.ignoreCase_ = ignoreCase;
};


/**
 * Extends a query data object with another query data or map like object. This
 * operates 'in-place', it does not create a new QueryData object.
 *
 * @param {...(goog.Uri.QueryData|goog.structs.Map<?, ?>|Object)} var_args
 *     The object from which key value pairs will be copied.
 */
goog.Uri.QueryData.prototype.extend = function(var_args) {
  for (var i = 0; i < arguments.length; i++) {
    var data = arguments[i];
    goog.structs.forEach(data,
        /** @this {goog.Uri.QueryData} */
        function(value, key) {
          this.add(key, value);
        }, this);
  }
};

goog.provide('ol.style.Text');


goog.require('ol.style.Fill');


/**
 * @classdesc
 * Set text style for vector features.
 *
 * @constructor
 * @param {olx.style.TextOptions=} opt_options Options.
 * @api
 */
ol.style.Text = function(opt_options) {

  var options = opt_options || {};

  /**
   * @private
   * @type {string|undefined}
   */
  this.font_ = options.font;

  /**
   * @private
   * @type {number|undefined}
   */
  this.rotation_ = options.rotation;

  /**
   * @private
   * @type {number|undefined}
   */
  this.scale_ = options.scale;

  /**
   * @private
   * @type {string|undefined}
   */
  this.text_ = options.text;

  /**
   * @private
   * @type {string|undefined}
   */
  this.textAlign_ = options.textAlign;

  /**
   * @private
   * @type {string|undefined}
   */
  this.textBaseline_ = options.textBaseline;

  /**
   * @private
   * @type {ol.style.Fill}
   */
  this.fill_ = options.fill !== undefined ? options.fill :
      new ol.style.Fill({color: ol.style.Text.DEFAULT_FILL_COLOR_});

  /**
   * @private
   * @type {ol.style.Stroke}
   */
  this.stroke_ = options.stroke !== undefined ? options.stroke : null;

  /**
   * @private
   * @type {number}
   */
  this.offsetX_ = options.offsetX !== undefined ? options.offsetX : 0;

  /**
   * @private
   * @type {number}
   */
  this.offsetY_ = options.offsetY !== undefined ? options.offsetY : 0;
};


/**
 * The default fill color to use if no fill was set at construction time; a
 * blackish `#333`.
 *
 * @const {string}
 * @private
 */
ol.style.Text.DEFAULT_FILL_COLOR_ = '#333';


/**
 * Get the font name.
 * @return {string|undefined} Font.
 * @api
 */
ol.style.Text.prototype.getFont = function() {
  return this.font_;
};


/**
 * Get the x-offset for the text.
 * @return {number} Horizontal text offset.
 * @api
 */
ol.style.Text.prototype.getOffsetX = function() {
  return this.offsetX_;
};


/**
 * Get the y-offset for the text.
 * @return {number} Vertical text offset.
 * @api
 */
ol.style.Text.prototype.getOffsetY = function() {
  return this.offsetY_;
};


/**
 * Get the fill style for the text.
 * @return {ol.style.Fill} Fill style.
 * @api
 */
ol.style.Text.prototype.getFill = function() {
  return this.fill_;
};


/**
 * Get the text rotation.
 * @return {number|undefined} Rotation.
 * @api
 */
ol.style.Text.prototype.getRotation = function() {
  return this.rotation_;
};


/**
 * Get the text scale.
 * @return {number|undefined} Scale.
 * @api
 */
ol.style.Text.prototype.getScale = function() {
  return this.scale_;
};


/**
 * Get the stroke style for the text.
 * @return {ol.style.Stroke} Stroke style.
 * @api
 */
ol.style.Text.prototype.getStroke = function() {
  return this.stroke_;
};


/**
 * Get the text to be rendered.
 * @return {string|undefined} Text.
 * @api
 */
ol.style.Text.prototype.getText = function() {
  return this.text_;
};


/**
 * Get the text alignment.
 * @return {string|undefined} Text align.
 * @api
 */
ol.style.Text.prototype.getTextAlign = function() {
  return this.textAlign_;
};


/**
 * Get the text baseline.
 * @return {string|undefined} Text baseline.
 * @api
 */
ol.style.Text.prototype.getTextBaseline = function() {
  return this.textBaseline_;
};


/**
 * Set the font.
 *
 * @param {string|undefined} font Font.
 * @api
 */
ol.style.Text.prototype.setFont = function(font) {
  this.font_ = font;
};


/**
 * Set the x offset.
 *
 * @param {number} offsetX Horizontal text offset.
 * @api
 */
ol.style.Text.prototype.setOffsetX = function(offsetX) {
  this.offsetX_ = offsetX;
};


/**
 * Set the y offset.
 *
 * @param {number} offsetY Vertical text offset.
 * @api
 */
ol.style.Text.prototype.setOffsetY = function(offsetY) {
  this.offsetY_ = offsetY;
};


/**
 * Set the fill.
 *
 * @param {ol.style.Fill} fill Fill style.
 * @api
 */
ol.style.Text.prototype.setFill = function(fill) {
  this.fill_ = fill;
};


/**
 * Set the rotation.
 *
 * @param {number|undefined} rotation Rotation.
 * @api
 */
ol.style.Text.prototype.setRotation = function(rotation) {
  this.rotation_ = rotation;
};


/**
 * Set the scale.
 *
 * @param {number|undefined} scale Scale.
 * @api
 */
ol.style.Text.prototype.setScale = function(scale) {
  this.scale_ = scale;
};


/**
 * Set the stroke.
 *
 * @param {ol.style.Stroke} stroke Stroke style.
 * @api
 */
ol.style.Text.prototype.setStroke = function(stroke) {
  this.stroke_ = stroke;
};


/**
 * Set the text.
 *
 * @param {string|undefined} text Text.
 * @api
 */
ol.style.Text.prototype.setText = function(text) {
  this.text_ = text;
};


/**
 * Set the text alignment.
 *
 * @param {string|undefined} textAlign Text align.
 * @api
 */
ol.style.Text.prototype.setTextAlign = function(textAlign) {
  this.textAlign_ = textAlign;
};


/**
 * Set the text baseline.
 *
 * @param {string|undefined} textBaseline Text baseline.
 * @api
 */
ol.style.Text.prototype.setTextBaseline = function(textBaseline) {
  this.textBaseline_ = textBaseline;
};

// FIXME http://earth.google.com/kml/1.0 namespace?
// FIXME why does node.getAttribute return an unknown type?
// FIXME text
// FIXME serialize arbitrary feature properties
// FIXME don't parse style if extractStyles is false

goog.provide('ol.format.KML');

goog.require('goog.Uri');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom.NodeType');
goog.require('goog.object');
goog.require('ol');
goog.require('ol.Feature');
goog.require('ol.FeatureStyleFunction');
goog.require('ol.array');
goog.require('ol.color');
goog.require('ol.format.Feature');
goog.require('ol.format.XMLFeature');
goog.require('ol.format.XSD');
goog.require('ol.geom.Geometry');
goog.require('ol.geom.GeometryCollection');
goog.require('ol.geom.GeometryLayout');
goog.require('ol.geom.GeometryType');
goog.require('ol.geom.LineString');
goog.require('ol.geom.LinearRing');
goog.require('ol.geom.MultiLineString');
goog.require('ol.geom.MultiPoint');
goog.require('ol.geom.MultiPolygon');
goog.require('ol.geom.Point');
goog.require('ol.geom.Polygon');
goog.require('ol.math');
goog.require('ol.proj');
goog.require('ol.style.Fill');
goog.require('ol.style.Icon');
goog.require('ol.style.IconAnchorUnits');
goog.require('ol.style.IconOrigin');
goog.require('ol.style.Image');
goog.require('ol.style.Stroke');
goog.require('ol.style.Style');
goog.require('ol.style.Text');
goog.require('ol.xml');


/**
 * @typedef {{x: number, xunits: (ol.style.IconAnchorUnits|undefined),
 *            y: number, yunits: (ol.style.IconAnchorUnits|undefined)}}
 */
ol.format.KMLVec2_;


/**
 * @typedef {{flatCoordinates: Array.<number>,
 *            whens: Array.<number>}}
 */
ol.format.KMLGxTrackObject_;


/**
 * @classdesc
 * Feature format for reading and writing data in the KML format.
 *
 * @constructor
 * @extends {ol.format.XMLFeature}
 * @param {olx.format.KMLOptions=} opt_options Options.
 * @api stable
 */
ol.format.KML = function(opt_options) {

  var options = opt_options ? opt_options : {};

  goog.base(this);

  /**
   * @inheritDoc
   */
  this.defaultDataProjection = ol.proj.get('EPSG:4326');

  /**
   * @private
   * @type {Array.<ol.style.Style>}
   */
  this.defaultStyle_ = options.defaultStyle ?
      options.defaultStyle : ol.format.KML.DEFAULT_STYLE_ARRAY_;

  /**
   * @private
   * @type {boolean}
   */
  this.extractStyles_ = options.extractStyles !== undefined ?
      options.extractStyles : true;

  /**
   * @private
   * @type {boolean}
   */
  this.writeStyles_ = options.writeStyles !== undefined ?
      options.writeStyles : true;

  /**
   * @private
   * @type {Object.<string, (Array.<ol.style.Style>|string)>}
   */
  this.sharedStyles_ = {};

  /**
   * @private
   * @type {boolean}
   */
  this.showPointNames_ = options.showPointNames !== undefined ?
      options.showPointNames : true;

};
goog.inherits(ol.format.KML, ol.format.XMLFeature);


/**
 * @const
 * @type {Array.<string>}
 * @private
 */
ol.format.KML.EXTENSIONS_ = ['.kml'];


/**
 * @const
 * @type {Array.<string>}
 * @private
 */
ol.format.KML.GX_NAMESPACE_URIS_ = [
  'http://www.google.com/kml/ext/2.2'
];


/**
 * @const
 * @type {Array.<string>}
 * @private
 */
ol.format.KML.NAMESPACE_URIS_ = [
  null,
  'http://earth.google.com/kml/2.0',
  'http://earth.google.com/kml/2.1',
  'http://earth.google.com/kml/2.2',
  'http://www.opengis.net/kml/2.2'
];


/**
 * @const
 * @type {string}
 * @private
 */
ol.format.KML.SCHEMA_LOCATION_ = 'http://www.opengis.net/kml/2.2 ' +
    'https://developers.google.com/kml/schema/kml22gx.xsd';


/**
 * @const
 * @type {ol.Color}
 * @private
 */
ol.format.KML.DEFAULT_COLOR_ = [255, 255, 255, 1];


/**
 * @const
 * @type {ol.style.Fill}
 * @private
 */
ol.format.KML.DEFAULT_FILL_STYLE_ = new ol.style.Fill({
  color: ol.format.KML.DEFAULT_COLOR_
});


/**
 * @const
 * @type {ol.Size}
 * @private
 */
ol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_ = [20, 2]; // FIXME maybe [8, 32] ?


/**
 * @const
 * @type {ol.style.IconAnchorUnits}
 * @private
 */
ol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_X_UNITS_ =
    ol.style.IconAnchorUnits.PIXELS;


/**
 * @const
 * @type {ol.style.IconAnchorUnits}
 * @private
 */
ol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_Y_UNITS_ =
    ol.style.IconAnchorUnits.PIXELS;


/**
 * @const
 * @type {ol.Size}
 * @private
 */
ol.format.KML.DEFAULT_IMAGE_STYLE_SIZE_ = [64, 64];


/**
 * @const
 * @type {string}
 * @private
 */
ol.format.KML.DEFAULT_IMAGE_STYLE_SRC_ =
    'https://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png';


/**
 * @const
 * @type {number}
 * @private
 */
ol.format.KML.DEFAULT_IMAGE_SCALE_MULTIPLIER_ = 0.5;


/**
 * @const
 * @type {ol.style.Image}
 * @private
 */
ol.format.KML.DEFAULT_IMAGE_STYLE_ = new ol.style.Icon({
  anchor: ol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_,
  anchorOrigin: ol.style.IconOrigin.BOTTOM_LEFT,
  anchorXUnits: ol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_X_UNITS_,
  anchorYUnits: ol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_Y_UNITS_,
  crossOrigin: 'anonymous',
  rotation: 0,
  scale: ol.format.KML.DEFAULT_IMAGE_SCALE_MULTIPLIER_,
  size: ol.format.KML.DEFAULT_IMAGE_STYLE_SIZE_,
  src: ol.format.KML.DEFAULT_IMAGE_STYLE_SRC_
});


/**
 * @const
 * @type {ol.style.Stroke}
 * @private
 */
ol.format.KML.DEFAULT_STROKE_STYLE_ = new ol.style.Stroke({
  color: ol.format.KML.DEFAULT_COLOR_,
  width: 1
});


/**
 * @const
 * @type {ol.style.Stroke}
 * @private
 */
ol.format.KML.DEFAULT_TEXT_STROKE_STYLE_ = new ol.style.Stroke({
  color: [51, 51, 51, 1],
  width: 2
});


/**
 * @const
 * @type {ol.style.Text}
 * @private
 */
ol.format.KML.DEFAULT_TEXT_STYLE_ = new ol.style.Text({
  font: 'bold 16px Helvetica',
  fill: ol.format.KML.DEFAULT_FILL_STYLE_,
  stroke: ol.format.KML.DEFAULT_TEXT_STROKE_STYLE_,
  scale: 0.8
});


/**
 * @const
 * @type {ol.style.Style}
 * @private
 */
ol.format.KML.DEFAULT_STYLE_ = new ol.style.Style({
  fill: ol.format.KML.DEFAULT_FILL_STYLE_,
  image: ol.format.KML.DEFAULT_IMAGE_STYLE_,
  text: ol.format.KML.DEFAULT_TEXT_STYLE_,
  stroke: ol.format.KML.DEFAULT_STROKE_STYLE_,
  zIndex: 0
});


/**
 * @const
 * @type {Array.<ol.style.Style>}
 * @private
 */
ol.format.KML.DEFAULT_STYLE_ARRAY_ = [ol.format.KML.DEFAULT_STYLE_];


/**
 * @const
 * @type {Object.<string, ol.style.IconAnchorUnits>}
 * @private
 */
ol.format.KML.ICON_ANCHOR_UNITS_MAP_ = {
  'fraction': ol.style.IconAnchorUnits.FRACTION,
  'pixels': ol.style.IconAnchorUnits.PIXELS
};


/**
 * @param {ol.style.Style|undefined} foundStyle Style.
 * @param {string} name Name.
 * @return {ol.style.Style} style Style.
 * @private
 */
ol.format.KML.createNameStyleFunction_ = function(foundStyle, name) {
  /** @type {?ol.style.Text} */
  var textStyle = null;
  var textOffset = [0, 0];
  var textAlign = 'start';
  if (foundStyle.getImage()) {
    var imageSize = foundStyle.getImage().getImageSize();
    if (imageSize && imageSize.length == 2) {
      // Offset the label to be centered to the right of the icon, if there is
      // one.
      textOffset[0] = foundStyle.getImage().getScale() * imageSize[0] / 2;
      textOffset[1] = -foundStyle.getImage().getScale() * imageSize[1] / 2;
      textAlign = 'left';
    }
  }
  if (!goog.object.isEmpty(foundStyle.getText())) {
    textStyle = /** @type {ol.style.Text} */
        (goog.object.clone(foundStyle.getText()));
    textStyle.setText(name);
    textStyle.setTextAlign(textAlign);
    textStyle.setOffsetX(textOffset[0]);
    textStyle.setOffsetY(textOffset[1]);
  } else {
    textStyle = new ol.style.Text({
      text: name,
      offsetX: textOffset[0],
      offsetY: textOffset[1],
      textAlign: textAlign
    });
  }
  var nameStyle = new ol.style.Style({
    text: textStyle
  });
  return nameStyle;
};


/**
 * @param {Array.<ol.style.Style>|undefined} style Style.
 * @param {string} styleUrl Style URL.
 * @param {Array.<ol.style.Style>} defaultStyle Default style.
 * @param {Object.<string, (Array.<ol.style.Style>|string)>} sharedStyles Shared
 *          styles.
 * @param {boolean|undefined} showPointNames true to show names for point
 *          placemarks.
 * @return {ol.FeatureStyleFunction} Feature style function.
 * @private
 */
ol.format.KML.createFeatureStyleFunction_ = function(style, styleUrl,
    defaultStyle, sharedStyles, showPointNames) {

  return (
      /**
       * @param {number} resolution Resolution.
       * @return {Array.<ol.style.Style>} Style.
       * @this {ol.Feature}
       */
      function(resolution) {
        var drawName = showPointNames;
        /** @type {ol.style.Style|undefined} */
        var nameStyle;
        /** @type {string} */
        var name = '';
        if (drawName) {
          if (this.getGeometry()) {
            drawName = (this.getGeometry().getType() ===
                        ol.geom.GeometryType.POINT);
          }
        }

        if (drawName) {
          name = /** @type {string} */ (this.getProperties()['name']);
          drawName = drawName && name;
        }

        if (style) {
          if (drawName) {
            nameStyle = ol.format.KML.createNameStyleFunction_(style[0],
                name);
            return style.concat(nameStyle);
          }
          return style;
        }
        if (styleUrl) {
          var foundStyle = ol.format.KML.findStyle_(styleUrl, defaultStyle,
              sharedStyles);
          if (drawName) {
            nameStyle = ol.format.KML.createNameStyleFunction_(foundStyle[0],
                name);
            return foundStyle.concat(nameStyle);
          }
          return foundStyle;
        }
        if (drawName) {
          nameStyle = ol.format.KML.createNameStyleFunction_(defaultStyle[0],
              name);
          return defaultStyle.concat(nameStyle);
        }
        return defaultStyle;
      });
};


/**
 * @param {Array.<ol.style.Style>|string|undefined} styleValue Style value.
 * @param {Array.<ol.style.Style>} defaultStyle Default style.
 * @param {Object.<string, (Array.<ol.style.Style>|string)>} sharedStyles
 * Shared styles.
 * @return {Array.<ol.style.Style>} Style.
 * @private
 */
ol.format.KML.findStyle_ = function(styleValue, defaultStyle, sharedStyles) {
  if (goog.isArray(styleValue)) {
    return styleValue;
  } else if (goog.isString(styleValue)) {
    // KML files in the wild occasionally forget the leading `#` on styleUrls
    // defined in the same document.  Add a leading `#` if it enables to find
    // a style.
    if (!(styleValue in sharedStyles) && ('#' + styleValue in sharedStyles)) {
      styleValue = '#' + styleValue;
    }
    return ol.format.KML.findStyle_(
        sharedStyles[styleValue], defaultStyle, sharedStyles);
  } else {
    return defaultStyle;
  }
};


/**
 * @param {Node} node Node.
 * @private
 * @return {ol.Color|undefined} Color.
 */
ol.format.KML.readColor_ = function(node) {
  var s = ol.xml.getAllTextContent(node, false);
  // The KML specification states that colors should not include a leading `#`
  // but we tolerate them.
  var m = /^\s*#?\s*([0-9A-Fa-f]{8})\s*$/.exec(s);
  if (m) {
    var hexColor = m[1];
    return [
      parseInt(hexColor.substr(6, 2), 16),
      parseInt(hexColor.substr(4, 2), 16),
      parseInt(hexColor.substr(2, 2), 16),
      parseInt(hexColor.substr(0, 2), 16) / 255
    ];

  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @private
 * @return {Array.<number>|undefined} Flat coordinates.
 */
ol.format.KML.readFlatCoordinates_ = function(node) {
  var s = ol.xml.getAllTextContent(node, false);
  var flatCoordinates = [];
  // The KML specification states that coordinate tuples should not include
  // spaces, but we tolerate them.
  var re =
      /^\s*([+\-]?\d*\.?\d+(?:e[+\-]?\d+)?)\s*,\s*([+\-]?\d*\.?\d+(?:e[+\-]?\d+)?)(?:\s*,\s*([+\-]?\d*\.?\d+(?:e[+\-]?\d+)?))?\s*/i;
  var m;
  while ((m = re.exec(s))) {
    var x = parseFloat(m[1]);
    var y = parseFloat(m[2]);
    var z = m[3] ? parseFloat(m[3]) : 0;
    flatCoordinates.push(x, y, z);
    s = s.substr(m[0].length);
  }
  if (s !== '') {
    return undefined;
  }
  return flatCoordinates;
};


/**
 * @param {Node} node Node.
 * @private
 * @return {string|undefined} Style URL.
 */
ol.format.KML.readStyleUrl_ = function(node) {
  var s = ol.xml.getAllTextContent(node, false).trim();
  if (node.baseURI) {
    return goog.Uri.resolve(node.baseURI, s).toString();
  } else {
    return s;
  }

};


/**
 * @param {Node} node Node.
 * @private
 * @return {string} URI.
 */
ol.format.KML.readURI_ = function(node) {
  var s = ol.xml.getAllTextContent(node, false);
  if (node.baseURI) {
    return goog.Uri.resolve(node.baseURI, s.trim()).toString();
  } else {
    return s.trim();
  }
};


/**
 * @param {Node} node Node.
 * @private
 * @return {ol.format.KMLVec2_} Vec2.
 */
ol.format.KML.readVec2_ = function(node) {
  var xunits = node.getAttribute('xunits');
  var yunits = node.getAttribute('yunits');
  return {
    x: parseFloat(node.getAttribute('x')),
    xunits: ol.format.KML.ICON_ANCHOR_UNITS_MAP_[xunits],
    y: parseFloat(node.getAttribute('y')),
    yunits: ol.format.KML.ICON_ANCHOR_UNITS_MAP_[yunits]
  };
};


/**
 * @param {Node} node Node.
 * @private
 * @return {number|undefined} Scale.
 */
ol.format.KML.readScale_ = function(node) {
  var number = ol.format.XSD.readDecimal(node);
  if (number !== undefined) {
    return Math.sqrt(number);
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<ol.style.Style>|string|undefined} StyleMap.
 */
ol.format.KML.readStyleMapValue_ = function(node, objectStack) {
  return ol.xml.pushParseAndPop(
      /** @type {Array.<ol.style.Style>|string|undefined} */ (undefined),
      ol.format.KML.STYLE_MAP_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.IconStyleParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be an ELEMENT');
  goog.asserts.assert(node.localName == 'IconStyle',
      'localName should be IconStyle');
  // FIXME refreshMode
  // FIXME refreshInterval
  // FIXME viewRefreshTime
  // FIXME viewBoundScale
  // FIXME viewFormat
  // FIXME httpQuery
  var object = ol.xml.pushParseAndPop(
      {}, ol.format.KML.ICON_STYLE_PARSERS_, node, objectStack);
  if (!object) {
    return;
  }
  var styleObject = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  goog.asserts.assert(goog.isObject(styleObject),
      'styleObject should be an Object');
  var IconObject = 'Icon' in object ? object['Icon'] : {};
  var src;
  var href = /** @type {string|undefined} */
      (IconObject['href']);
  if (href) {
    src = href;
  } else {
    src = ol.format.KML.DEFAULT_IMAGE_STYLE_SRC_;
  }
  var anchor, anchorXUnits, anchorYUnits;
  var hotSpot = /** @type {ol.format.KMLVec2_|undefined} */
      (object['hotSpot']);
  if (hotSpot) {
    anchor = [hotSpot.x, hotSpot.y];
    anchorXUnits = hotSpot.xunits;
    anchorYUnits = hotSpot.yunits;
  } else if (src === ol.format.KML.DEFAULT_IMAGE_STYLE_SRC_) {
    anchor = ol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_;
    anchorXUnits = ol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_X_UNITS_;
    anchorYUnits = ol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_Y_UNITS_;
  } else if (/^http:\/\/maps\.(?:google|gstatic)\.com\//.test(src)) {
    anchor = [0.5, 0];
    anchorXUnits = ol.style.IconAnchorUnits.FRACTION;
    anchorYUnits = ol.style.IconAnchorUnits.FRACTION;
  }

  var offset;
  var x = /** @type {number|undefined} */
      (IconObject['x']);
  var y = /** @type {number|undefined} */
      (IconObject['y']);
  if (x !== undefined && y !== undefined) {
    offset = [x, y];
  }

  var size;
  var w = /** @type {number|undefined} */
      (IconObject['w']);
  var h = /** @type {number|undefined} */
      (IconObject['h']);
  if (w !== undefined && h !== undefined) {
    size = [w, h];
  }

  var rotation;
  var heading = /** @type {number} */
      (object['heading']);
  if (heading !== undefined) {
    rotation = ol.math.toRadians(heading);
  }

  var scale = /** @type {number|undefined} */
      (object['scale']);
  if (src == ol.format.KML.DEFAULT_IMAGE_STYLE_SRC_) {
    size = ol.format.KML.DEFAULT_IMAGE_STYLE_SIZE_;
    if (scale === undefined) {
      scale = ol.format.KML.DEFAULT_IMAGE_SCALE_MULTIPLIER_;
    }
  }

  var imageStyle = new ol.style.Icon({
    anchor: anchor,
    anchorOrigin: ol.style.IconOrigin.BOTTOM_LEFT,
    anchorXUnits: anchorXUnits,
    anchorYUnits: anchorYUnits,
    crossOrigin: 'anonymous', // FIXME should this be configurable?
    offset: offset,
    offsetOrigin: ol.style.IconOrigin.BOTTOM_LEFT,
    rotation: rotation,
    scale: scale,
    size: size,
    src: src
  });
  styleObject['imageStyle'] = imageStyle;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.LabelStyleParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'LabelStyle',
      'localName should be LabelStyle');
  // FIXME colorMode
  var object = ol.xml.pushParseAndPop(
      {}, ol.format.KML.LABEL_STYLE_PARSERS_, node, objectStack);
  if (!object) {
    return;
  }
  var styleObject = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(styleObject),
      'styleObject should be an Object');
  var textStyle = new ol.style.Text({
    fill: new ol.style.Fill({
      color: /** @type {ol.Color} */
          ('color' in object ? object['color'] : ol.format.KML.DEFAULT_COLOR_)
    }),
    scale: /** @type {number|undefined} */
        (object['scale'])
  });
  styleObject['textStyle'] = textStyle;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.LineStyleParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'LineStyle',
      'localName should be LineStyle');
  // FIXME colorMode
  // FIXME gx:outerColor
  // FIXME gx:outerWidth
  // FIXME gx:physicalWidth
  // FIXME gx:labelVisibility
  var object = ol.xml.pushParseAndPop(
      {}, ol.format.KML.LINE_STYLE_PARSERS_, node, objectStack);
  if (!object) {
    return;
  }
  var styleObject = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(styleObject),
      'styleObject should be an Object');
  var strokeStyle = new ol.style.Stroke({
    color: /** @type {ol.Color} */
        ('color' in object ? object['color'] : ol.format.KML.DEFAULT_COLOR_),
    width: /** @type {number} */ ('width' in object ? object['width'] : 1)
  });
  styleObject['strokeStyle'] = strokeStyle;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.PolyStyleParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'PolyStyle',
      'localName should be PolyStyle');
  // FIXME colorMode
  var object = ol.xml.pushParseAndPop(
      {}, ol.format.KML.POLY_STYLE_PARSERS_, node, objectStack);
  if (!object) {
    return;
  }
  var styleObject = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(styleObject),
      'styleObject should be an Object');
  var fillStyle = new ol.style.Fill({
    color: /** @type {ol.Color} */
        ('color' in object ? object['color'] : ol.format.KML.DEFAULT_COLOR_)
  });
  styleObject['fillStyle'] = fillStyle;
  var fill = /** @type {boolean|undefined} */ (object['fill']);
  if (fill !== undefined) {
    styleObject['fill'] = fill;
  }
  var outline =
      /** @type {boolean|undefined} */ (object['outline']);
  if (outline !== undefined) {
    styleObject['outline'] = outline;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<number>} LinearRing flat coordinates.
 */
ol.format.KML.readFlatLinearRing_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'LinearRing',
      'localName should be LinearRing');
  return /** @type {Array.<number>} */ (ol.xml.pushParseAndPop(
      null, ol.format.KML.FLAT_LINEAR_RING_PARSERS_, node, objectStack));
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.gxCoordParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(ol.array.includes(
      ol.format.KML.GX_NAMESPACE_URIS_, node.namespaceURI),
      'namespaceURI of the node should be known to the KML parser');
  goog.asserts.assert(node.localName == 'coord', 'localName should be coord');
  var gxTrackObject = /** @type {ol.format.KMLGxTrackObject_} */
      (objectStack[objectStack.length - 1]);
  goog.asserts.assert(goog.isObject(gxTrackObject),
      'gxTrackObject should be an Object');
  var flatCoordinates = gxTrackObject.flatCoordinates;
  var s = ol.xml.getAllTextContent(node, false);
  var re =
      /^\s*([+\-]?\d+(?:\.\d*)?(?:e[+\-]?\d*)?)\s+([+\-]?\d+(?:\.\d*)?(?:e[+\-]?\d*)?)\s+([+\-]?\d+(?:\.\d*)?(?:e[+\-]?\d*)?)\s*$/i;
  var m = re.exec(s);
  if (m) {
    var x = parseFloat(m[1]);
    var y = parseFloat(m[2]);
    var z = parseFloat(m[3]);
    flatCoordinates.push(x, y, z, 0);
  } else {
    flatCoordinates.push(0, 0, 0, 0);
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {ol.geom.MultiLineString|undefined} MultiLineString.
 */
ol.format.KML.readGxMultiTrack_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(ol.array.includes(
      ol.format.KML.GX_NAMESPACE_URIS_, node.namespaceURI),
      'namespaceURI of the node should be known to the KML parser');
  goog.asserts.assert(node.localName == 'MultiTrack',
      'localName should be MultiTrack');
  var lineStrings = ol.xml.pushParseAndPop(
      /** @type {Array.<ol.geom.LineString>} */ ([]),
      ol.format.KML.GX_MULTITRACK_GEOMETRY_PARSERS_, node, objectStack);
  if (!lineStrings) {
    return undefined;
  }
  var multiLineString = new ol.geom.MultiLineString(null);
  multiLineString.setLineStrings(lineStrings);
  return multiLineString;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {ol.geom.LineString|undefined} LineString.
 */
ol.format.KML.readGxTrack_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(ol.array.includes(
      ol.format.KML.GX_NAMESPACE_URIS_, node.namespaceURI),
      'namespaceURI of the node should be known to the KML parser');
  goog.asserts.assert(node.localName == 'Track', 'localName should be Track');
  var gxTrackObject = ol.xml.pushParseAndPop(
      /** @type {ol.format.KMLGxTrackObject_} */ ({
        flatCoordinates: [],
        whens: []
      }), ol.format.KML.GX_TRACK_PARSERS_, node, objectStack);
  if (!gxTrackObject) {
    return undefined;
  }
  var flatCoordinates = gxTrackObject.flatCoordinates;
  var whens = gxTrackObject.whens;
  goog.asserts.assert(flatCoordinates.length / 4 == whens.length,
      'the length of the flatCoordinates array divided by 4 should be the ' +
      'length of the whens array');
  var i, ii;
  for (i = 0, ii = Math.min(flatCoordinates.length, whens.length); i < ii;
       ++i) {
    flatCoordinates[4 * i + 3] = whens[i];
  }
  var lineString = new ol.geom.LineString(null);
  lineString.setFlatCoordinates(ol.geom.GeometryLayout.XYZM, flatCoordinates);
  return lineString;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object} Icon object.
 */
ol.format.KML.readIcon_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Icon', 'localName should be Icon');
  var iconObject = ol.xml.pushParseAndPop(
      {}, ol.format.KML.ICON_PARSERS_, node, objectStack);
  if (iconObject) {
    return iconObject;
  } else {
    return null;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<number>} Flat coordinates.
 */
ol.format.KML.readFlatCoordinatesFromNode_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  return /** @type {Array.<number>} */ (ol.xml.pushParseAndPop(null,
      ol.format.KML.GEOMETRY_FLAT_COORDINATES_PARSERS_, node, objectStack));
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {ol.geom.LineString|undefined} LineString.
 */
ol.format.KML.readLineString_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'LineString',
      'localName should be LineString');
  var properties = ol.xml.pushParseAndPop(/** @type {Object<string,*>} */ ({}),
      ol.format.KML.EXTRUDE_AND_ALTITUDE_MODE_PARSERS_, node,
      objectStack);
  var flatCoordinates =
      ol.format.KML.readFlatCoordinatesFromNode_(node, objectStack);
  if (flatCoordinates) {
    var lineString = new ol.geom.LineString(null);
    lineString.setFlatCoordinates(ol.geom.GeometryLayout.XYZ, flatCoordinates);
    lineString.setProperties(properties);
    return lineString;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {ol.geom.Polygon|undefined} Polygon.
 */
ol.format.KML.readLinearRing_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'LinearRing',
      'localName should be LinearRing');
  var properties = ol.xml.pushParseAndPop(/** @type {Object<string,*>} */ ({}),
      ol.format.KML.EXTRUDE_AND_ALTITUDE_MODE_PARSERS_, node,
      objectStack);
  var flatCoordinates =
      ol.format.KML.readFlatCoordinatesFromNode_(node, objectStack);
  if (flatCoordinates) {
    var polygon = new ol.geom.Polygon(null);
    polygon.setFlatCoordinates(ol.geom.GeometryLayout.XYZ, flatCoordinates,
        [flatCoordinates.length]);
    polygon.setProperties(properties);
    return polygon;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {ol.geom.Geometry} Geometry.
 */
ol.format.KML.readMultiGeometry_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'MultiGeometry',
      'localName should be MultiGeometry');
  var geometries = ol.xml.pushParseAndPop(
      /** @type {Array.<ol.geom.Geometry>} */ ([]),
      ol.format.KML.MULTI_GEOMETRY_PARSERS_, node, objectStack);
  if (!geometries) {
    return null;
  }
  if (geometries.length === 0) {
    return new ol.geom.GeometryCollection(geometries);
  }
  var homogeneous = true;
  var type = geometries[0].getType();
  var geometry, i, ii;
  for (i = 1, ii = geometries.length; i < ii; ++i) {
    geometry = geometries[i];
    if (geometry.getType() != type) {
      homogeneous = false;
      break;
    }
  }
  if (homogeneous) {
    /** @type {ol.geom.GeometryLayout} */
    var layout;
    /** @type {Array.<number>} */
    var flatCoordinates;
    if (type == ol.geom.GeometryType.POINT) {
      var point = geometries[0];
      goog.asserts.assertInstanceof(point, ol.geom.Point,
          'point should be an ol.geom.Point');
      layout = point.getLayout();
      flatCoordinates = point.getFlatCoordinates();
      for (i = 1, ii = geometries.length; i < ii; ++i) {
        geometry = geometries[i];
        goog.asserts.assertInstanceof(geometry, ol.geom.Point,
            'geometry should be an ol.geom.Point');
        goog.asserts.assert(geometry.getLayout() == layout,
            'geometry layout should be consistent');
        goog.array.extend(flatCoordinates, geometry.getFlatCoordinates());
      }
      var multiPoint = new ol.geom.MultiPoint(null);
      multiPoint.setFlatCoordinates(layout, flatCoordinates);
      ol.format.KML.setCommonGeometryProperties_(multiPoint, geometries);
      return multiPoint;
    } else if (type == ol.geom.GeometryType.LINE_STRING) {
      var multiLineString = new ol.geom.MultiLineString(null);
      multiLineString.setLineStrings(geometries);
      ol.format.KML.setCommonGeometryProperties_(multiLineString, geometries);
      return multiLineString;
    } else if (type == ol.geom.GeometryType.POLYGON) {
      var multiPolygon = new ol.geom.MultiPolygon(null);
      multiPolygon.setPolygons(geometries);
      ol.format.KML.setCommonGeometryProperties_(multiPolygon, geometries);
      return multiPolygon;
    } else if (type == ol.geom.GeometryType.GEOMETRY_COLLECTION) {
      return new ol.geom.GeometryCollection(geometries);
    } else {
      goog.asserts.fail('Unexpected type: ' + type);
      return null;
    }
  } else {
    return new ol.geom.GeometryCollection(geometries);
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {ol.geom.Point|undefined} Point.
 */
ol.format.KML.readPoint_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Point', 'localName should be Point');
  var properties = ol.xml.pushParseAndPop(/** @type {Object<string,*>} */ ({}),
      ol.format.KML.EXTRUDE_AND_ALTITUDE_MODE_PARSERS_, node,
      objectStack);
  var flatCoordinates =
      ol.format.KML.readFlatCoordinatesFromNode_(node, objectStack);
  if (flatCoordinates) {
    var point = new ol.geom.Point(null);
    goog.asserts.assert(flatCoordinates.length == 3,
        'flatCoordinates should have a length of 3');
    point.setFlatCoordinates(ol.geom.GeometryLayout.XYZ, flatCoordinates);
    point.setProperties(properties);
    return point;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {ol.geom.Polygon|undefined} Polygon.
 */
ol.format.KML.readPolygon_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Polygon',
      'localName should be Polygon');
  var properties = ol.xml.pushParseAndPop(/** @type {Object<string,*>} */ ({}),
      ol.format.KML.EXTRUDE_AND_ALTITUDE_MODE_PARSERS_, node,
      objectStack);
  var flatLinearRings = ol.xml.pushParseAndPop(
      /** @type {Array.<Array.<number>>} */ ([null]),
      ol.format.KML.FLAT_LINEAR_RINGS_PARSERS_, node, objectStack);
  if (flatLinearRings && flatLinearRings[0]) {
    var polygon = new ol.geom.Polygon(null);
    var flatCoordinates = flatLinearRings[0];
    var ends = [flatCoordinates.length];
    var i, ii;
    for (i = 1, ii = flatLinearRings.length; i < ii; ++i) {
      goog.array.extend(flatCoordinates, flatLinearRings[i]);
      ends.push(flatCoordinates.length);
    }
    polygon.setFlatCoordinates(
        ol.geom.GeometryLayout.XYZ, flatCoordinates, ends);
    polygon.setProperties(properties);
    return polygon;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<ol.style.Style>} Style.
 */
ol.format.KML.readStyle_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Style', 'localName should be Style');
  var styleObject = ol.xml.pushParseAndPop(
      {}, ol.format.KML.STYLE_PARSERS_, node, objectStack);
  if (!styleObject) {
    return null;
  }
  var fillStyle = /** @type {ol.style.Fill} */
      ('fillStyle' in styleObject ?
          styleObject['fillStyle'] : ol.format.KML.DEFAULT_FILL_STYLE_);
  var fill = /** @type {boolean|undefined} */ (styleObject['fill']);
  if (fill !== undefined && !fill) {
    fillStyle = null;
  }
  var imageStyle = /** @type {ol.style.Image} */
      ('imageStyle' in styleObject ?
          styleObject['imageStyle'] : ol.format.KML.DEFAULT_IMAGE_STYLE_);
  var textStyle = /** @type {ol.style.Text} */
      ('textStyle' in styleObject ?
          styleObject['textStyle'] : ol.format.KML.DEFAULT_TEXT_STYLE_);
  var strokeStyle = /** @type {ol.style.Stroke} */
      ('strokeStyle' in styleObject ?
          styleObject['strokeStyle'] : ol.format.KML.DEFAULT_STROKE_STYLE_);
  var outline = /** @type {boolean|undefined} */
      (styleObject['outline']);
  if (outline !== undefined && !outline) {
    strokeStyle = null;
  }
  return [new ol.style.Style({
    fill: fillStyle,
    image: imageStyle,
    stroke: strokeStyle,
    text: textStyle,
    zIndex: undefined // FIXME
  })];
};


/**
 * Reads an array of geometries and creates arrays for common geometry
 * properties. Then sets them to the multi geometry.
 * @param {ol.geom.MultiPoint|ol.geom.MultiLineString|ol.geom.MultiPolygon}
 *     multiGeometry A multi-geometry.
 * @param {Array.<ol.geom.Geometry>} geometries List of geometries.
 * @private
 */
ol.format.KML.setCommonGeometryProperties_ = function(multiGeometry,
    geometries) {
  var ii = geometries.length;
  var extrudes = new Array(geometries.length);
  var altitudeModes = new Array(geometries.length);
  var geometry, i, hasExtrude, hasAltitudeMode;
  hasExtrude = hasAltitudeMode = false;
  for (i = 0; i < ii; ++i) {
    geometry = geometries[i];
    extrudes[i] = geometry.get('extrude');
    altitudeModes[i] = geometry.get('altitudeMode');
    hasExtrude = hasExtrude || extrudes[i] !== undefined;
    hasAltitudeMode = hasAltitudeMode || altitudeModes[i];
  }
  if (hasExtrude) {
    multiGeometry.set('extrude', extrudes);
  }
  if (hasAltitudeMode) {
    multiGeometry.set('altitudeMode', altitudeModes);
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.DataParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Data', 'localName should be Data');
  var name = node.getAttribute('name');
  if (name !== null) {
    var data = ol.xml.pushParseAndPop(
        undefined, ol.format.KML.DATA_PARSERS_, node, objectStack);
    if (data) {
      var featureObject =
          /** @type {Object} */ (objectStack[objectStack.length - 1]);
      goog.asserts.assert(goog.isObject(featureObject),
          'featureObject should be an Object');
      featureObject[name] = data;
    }
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.ExtendedDataParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'ExtendedData',
      'localName should be ExtendedData');
  ol.xml.parseNode(ol.format.KML.EXTENDED_DATA_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.PairDataParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Pair', 'localName should be Pair');
  var pairObject = ol.xml.pushParseAndPop(
      {}, ol.format.KML.PAIR_PARSERS_, node, objectStack);
  if (!pairObject) {
    return;
  }
  var key = /** @type {string|undefined} */
      (pairObject['key']);
  if (key && key == 'normal') {
    var styleUrl = /** @type {string|undefined} */
        (pairObject['styleUrl']);
    if (styleUrl) {
      objectStack[objectStack.length - 1] = styleUrl;
    }
    var Style = /** @type {ol.style.Style} */
        (pairObject['Style']);
    if (Style) {
      objectStack[objectStack.length - 1] = Style;
    }
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.PlacemarkStyleMapParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'StyleMap',
      'localName should be StyleMap');
  var styleMapValue = ol.format.KML.readStyleMapValue_(node, objectStack);
  if (!styleMapValue) {
    return;
  }
  var placemarkObject = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(placemarkObject),
      'placemarkObject should be an Object');
  if (goog.isArray(styleMapValue)) {
    placemarkObject['Style'] = styleMapValue;
  } else if (goog.isString(styleMapValue)) {
    placemarkObject['styleUrl'] = styleMapValue;
  } else {
    goog.asserts.fail('styleMapValue has an unknown type');
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.SchemaDataParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'SchemaData',
      'localName should be SchemaData');
  ol.xml.parseNode(ol.format.KML.SCHEMA_DATA_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.SimpleDataParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'SimpleData',
      'localName should be SimpleData');
  var name = node.getAttribute('name');
  if (name !== null) {
    var data = ol.format.XSD.readString(node);
    var featureObject =
        /** @type {Object} */ (objectStack[objectStack.length - 1]);
    featureObject[name] = data;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.innerBoundaryIsParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'innerBoundaryIs',
      'localName should be innerBoundaryIs');
  var flatLinearRing = ol.xml.pushParseAndPop(
      /** @type {Array.<number>|undefined} */ (undefined),
      ol.format.KML.INNER_BOUNDARY_IS_PARSERS_, node, objectStack);
  if (flatLinearRing) {
    var flatLinearRings = /** @type {Array.<Array.<number>>} */
        (objectStack[objectStack.length - 1]);
    goog.asserts.assert(goog.isArray(flatLinearRings),
        'flatLinearRings should be an array');
    goog.asserts.assert(flatLinearRings.length > 0,
        'flatLinearRings array should not be empty');
    flatLinearRings.push(flatLinearRing);
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.outerBoundaryIsParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'outerBoundaryIs',
      'localName should be outerBoundaryIs');
  var flatLinearRing = ol.xml.pushParseAndPop(
      /** @type {Array.<number>|undefined} */ (undefined),
      ol.format.KML.OUTER_BOUNDARY_IS_PARSERS_, node, objectStack);
  if (flatLinearRing) {
    var flatLinearRings = /** @type {Array.<Array.<number>>} */
        (objectStack[objectStack.length - 1]);
    goog.asserts.assert(goog.isArray(flatLinearRings),
        'flatLinearRings should be an array');
    goog.asserts.assert(flatLinearRings.length > 0,
        'flatLinearRings array should not be empty');
    flatLinearRings[0] = flatLinearRing;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.LinkParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Link', 'localName should be Link');
  ol.xml.parseNode(ol.format.KML.LINK_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.whenParser_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'when', 'localName should be when');
  var gxTrackObject = /** @type {ol.format.KMLGxTrackObject_} */
      (objectStack[objectStack.length - 1]);
  goog.asserts.assert(goog.isObject(gxTrackObject),
      'gxTrackObject should be an Object');
  var whens = gxTrackObject.whens;
  var s = ol.xml.getAllTextContent(node, false);
  var re =
      /^\s*(\d{4})($|-(\d{2})($|-(\d{2})($|T(\d{2}):(\d{2}):(\d{2})(Z|(?:([+\-])(\d{2})(?::(\d{2}))?)))))\s*$/;
  var m = re.exec(s);
  if (m) {
    var year = parseInt(m[1], 10);
    var month = m[3] ? parseInt(m[3], 10) - 1 : 0;
    var day = m[5] ? parseInt(m[5], 10) : 1;
    var hour = m[7] ? parseInt(m[7], 10) : 0;
    var minute = m[8] ? parseInt(m[8], 10) : 0;
    var second = m[9] ? parseInt(m[9], 10) : 0;
    var when = Date.UTC(year, month, day, hour, minute, second);
    if (m[10] && m[10] != 'Z') {
      var sign = m[11] == '-' ? -1 : 1;
      when += sign * 60 * parseInt(m[12], 10);
      if (m[13]) {
        when += sign * 60 * 60 * parseInt(m[13], 10);
      }
    }
    whens.push(when);
  } else {
    whens.push(0);
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.DATA_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'value': ol.xml.makeReplacer(ol.format.XSD.readString)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.EXTENDED_DATA_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'Data': ol.format.KML.DataParser_,
      'SchemaData': ol.format.KML.SchemaDataParser_
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.EXTRUDE_AND_ALTITUDE_MODE_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'extrude': ol.xml.makeObjectPropertySetter(ol.format.XSD.readBoolean),
      'altitudeMode': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.FLAT_LINEAR_RING_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'coordinates': ol.xml.makeReplacer(ol.format.KML.readFlatCoordinates_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.FLAT_LINEAR_RINGS_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'innerBoundaryIs': ol.format.KML.innerBoundaryIsParser_,
      'outerBoundaryIs': ol.format.KML.outerBoundaryIsParser_
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.GX_TRACK_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'when': ol.format.KML.whenParser_
    }, ol.xml.makeStructureNS(
        ol.format.KML.GX_NAMESPACE_URIS_, {
          'coord': ol.format.KML.gxCoordParser_
        }));


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.GEOMETRY_FLAT_COORDINATES_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'coordinates': ol.xml.makeReplacer(ol.format.KML.readFlatCoordinates_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.ICON_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'href': ol.xml.makeObjectPropertySetter(ol.format.KML.readURI_)
    }, ol.xml.makeStructureNS(
        ol.format.KML.GX_NAMESPACE_URIS_, {
          'x': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
          'y': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
          'w': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
          'h': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal)
        }));


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.ICON_STYLE_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'Icon': ol.xml.makeObjectPropertySetter(ol.format.KML.readIcon_),
      'heading': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal),
      'hotSpot': ol.xml.makeObjectPropertySetter(ol.format.KML.readVec2_),
      'scale': ol.xml.makeObjectPropertySetter(ol.format.KML.readScale_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.INNER_BOUNDARY_IS_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'LinearRing': ol.xml.makeReplacer(ol.format.KML.readFlatLinearRing_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.LABEL_STYLE_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'color': ol.xml.makeObjectPropertySetter(ol.format.KML.readColor_),
      'scale': ol.xml.makeObjectPropertySetter(ol.format.KML.readScale_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.LINE_STYLE_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'color': ol.xml.makeObjectPropertySetter(ol.format.KML.readColor_),
      'width': ol.xml.makeObjectPropertySetter(ol.format.XSD.readDecimal)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.MULTI_GEOMETRY_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'LineString': ol.xml.makeArrayPusher(ol.format.KML.readLineString_),
      'LinearRing': ol.xml.makeArrayPusher(ol.format.KML.readLinearRing_),
      'MultiGeometry': ol.xml.makeArrayPusher(ol.format.KML.readMultiGeometry_),
      'Point': ol.xml.makeArrayPusher(ol.format.KML.readPoint_),
      'Polygon': ol.xml.makeArrayPusher(ol.format.KML.readPolygon_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.GX_MULTITRACK_GEOMETRY_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.GX_NAMESPACE_URIS_, {
      'Track': ol.xml.makeArrayPusher(ol.format.KML.readGxTrack_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.NETWORK_LINK_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'ExtendedData': ol.format.KML.ExtendedDataParser_,
      'Link': ol.format.KML.LinkParser_,
      'address': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'description': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'name': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'open': ol.xml.makeObjectPropertySetter(ol.format.XSD.readBoolean),
      'phoneNumber': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'visibility': ol.xml.makeObjectPropertySetter(ol.format.XSD.readBoolean)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.LINK_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'href': ol.xml.makeObjectPropertySetter(ol.format.KML.readURI_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.OUTER_BOUNDARY_IS_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'LinearRing': ol.xml.makeReplacer(ol.format.KML.readFlatLinearRing_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.PAIR_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'Style': ol.xml.makeObjectPropertySetter(ol.format.KML.readStyle_),
      'key': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'styleUrl': ol.xml.makeObjectPropertySetter(ol.format.KML.readStyleUrl_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.PLACEMARK_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'ExtendedData': ol.format.KML.ExtendedDataParser_,
      'MultiGeometry': ol.xml.makeObjectPropertySetter(
          ol.format.KML.readMultiGeometry_, 'geometry'),
      'LineString': ol.xml.makeObjectPropertySetter(
          ol.format.KML.readLineString_, 'geometry'),
      'LinearRing': ol.xml.makeObjectPropertySetter(
          ol.format.KML.readLinearRing_, 'geometry'),
      'Point': ol.xml.makeObjectPropertySetter(
          ol.format.KML.readPoint_, 'geometry'),
      'Polygon': ol.xml.makeObjectPropertySetter(
          ol.format.KML.readPolygon_, 'geometry'),
      'Style': ol.xml.makeObjectPropertySetter(ol.format.KML.readStyle_),
      'StyleMap': ol.format.KML.PlacemarkStyleMapParser_,
      'address': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'description': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'name': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'open': ol.xml.makeObjectPropertySetter(ol.format.XSD.readBoolean),
      'phoneNumber': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'styleUrl': ol.xml.makeObjectPropertySetter(ol.format.KML.readURI_),
      'visibility': ol.xml.makeObjectPropertySetter(ol.format.XSD.readBoolean)
    }, ol.xml.makeStructureNS(
        ol.format.KML.GX_NAMESPACE_URIS_, {
          'MultiTrack': ol.xml.makeObjectPropertySetter(
              ol.format.KML.readGxMultiTrack_, 'geometry'),
          'Track': ol.xml.makeObjectPropertySetter(
              ol.format.KML.readGxTrack_, 'geometry')
        }
    ));


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.POLY_STYLE_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'color': ol.xml.makeObjectPropertySetter(ol.format.KML.readColor_),
      'fill': ol.xml.makeObjectPropertySetter(ol.format.XSD.readBoolean),
      'outline': ol.xml.makeObjectPropertySetter(ol.format.XSD.readBoolean)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.SCHEMA_DATA_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'SimpleData': ol.format.KML.SimpleDataParser_
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.STYLE_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'IconStyle': ol.format.KML.IconStyleParser_,
      'LabelStyle': ol.format.KML.LabelStyleParser_,
      'LineStyle': ol.format.KML.LineStyleParser_,
      'PolyStyle': ol.format.KML.PolyStyleParser_
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.KML.STYLE_MAP_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'Pair': ol.format.KML.PairDataParser_
    });


/**
 * @inheritDoc
 */
ol.format.KML.prototype.getExtensions = function() {
  return ol.format.KML.EXTENSIONS_;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<ol.Feature>|undefined} Features.
 */
ol.format.KML.prototype.readDocumentOrFolder_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  var localName = ol.xml.getLocalName(node);
  goog.asserts.assert(localName == 'Document' || localName == 'Folder',
      'localName should be Document or Folder');
  // FIXME use scope somehow
  var parsersNS = ol.xml.makeStructureNS(
      ol.format.KML.NAMESPACE_URIS_, {
        'Document': ol.xml.makeArrayExtender(this.readDocumentOrFolder_, this),
        'Folder': ol.xml.makeArrayExtender(this.readDocumentOrFolder_, this),
        'Placemark': ol.xml.makeArrayPusher(this.readPlacemark_, this),
        'Style': this.readSharedStyle_.bind(this),
        'StyleMap': this.readSharedStyleMap_.bind(this)
      });
  var features = ol.xml.pushParseAndPop(/** @type {Array.<ol.Feature>} */ ([]),
      parsersNS, node, objectStack, this);
  if (features) {
    return features;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {ol.Feature|undefined} Feature.
 */
ol.format.KML.prototype.readPlacemark_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Placemark',
      'localName should be Placemark');
  var object = ol.xml.pushParseAndPop({'geometry': null},
      ol.format.KML.PLACEMARK_PARSERS_, node, objectStack);
  if (!object) {
    return undefined;
  }
  var feature = new ol.Feature();
  var id = node.getAttribute('id');
  if (id !== null) {
    feature.setId(id);
  }
  var options = /** @type {olx.format.ReadOptions} */ (objectStack[0]);

  var geometry = object['geometry'];
  if (geometry) {
    ol.format.Feature.transformWithOptions(geometry, false, options);
  }
  feature.setGeometry(geometry);
  delete object['geometry'];

  if (this.extractStyles_) {
    var style = object['Style'];
    var styleUrl = object['styleUrl'];
    var styleFunction = ol.format.KML.createFeatureStyleFunction_(
        style, styleUrl, this.defaultStyle_, this.sharedStyles_,
        this.showPointNames_);
    feature.setStyle(styleFunction);
  }
  delete object['Style'];
  // we do not remove the styleUrl property from the object, so it
  // gets stored on feature when setProperties is called

  feature.setProperties(object);

  return feature;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.prototype.readSharedStyle_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Style', 'localName should be Style');
  var id = node.getAttribute('id');
  if (id !== null) {
    var style = ol.format.KML.readStyle_(node, objectStack);
    if (style) {
      var styleUri;
      if (node.baseURI) {
        styleUri = goog.Uri.resolve(node.baseURI, '#' + id).toString();
      } else {
        styleUri = '#' + id;
      }
      this.sharedStyles_[styleUri] = style;
    }
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.prototype.readSharedStyleMap_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'StyleMap',
      'localName should be StyleMap');
  var id = node.getAttribute('id');
  if (id === null) {
    return;
  }
  var styleMapValue = ol.format.KML.readStyleMapValue_(node, objectStack);
  if (!styleMapValue) {
    return;
  }
  var styleUri;
  if (node.baseURI) {
    styleUri = goog.Uri.resolve(node.baseURI, '#' + id).toString();
  } else {
    styleUri = '#' + id;
  }
  this.sharedStyles_[styleUri] = styleMapValue;
};


/**
 * Read the first feature from a KML source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {ol.Feature} Feature.
 * @api stable
 */
ol.format.KML.prototype.readFeature;


/**
 * @inheritDoc
 */
ol.format.KML.prototype.readFeatureFromNode = function(node, opt_options) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  if (!ol.array.includes(ol.format.KML.NAMESPACE_URIS_, node.namespaceURI)) {
    return null;
  }
  goog.asserts.assert(node.localName == 'Placemark',
      'localName should be Placemark');
  var feature = this.readPlacemark_(
      node, [this.getReadOptions(node, opt_options)]);
  if (feature) {
    return feature;
  } else {
    return null;
  }
};


/**
 * Read all features from a KML source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<ol.Feature>} Features.
 * @api stable
 */
ol.format.KML.prototype.readFeatures;


/**
 * @inheritDoc
 */
ol.format.KML.prototype.readFeaturesFromNode = function(node, opt_options) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  if (!ol.array.includes(ol.format.KML.NAMESPACE_URIS_, node.namespaceURI)) {
    return [];
  }
  var features;
  var localName = ol.xml.getLocalName(node);
  if (localName == 'Document' || localName == 'Folder') {
    features = this.readDocumentOrFolder_(
        node, [this.getReadOptions(node, opt_options)]);
    if (features) {
      return features;
    } else {
      return [];
    }
  } else if (localName == 'Placemark') {
    var feature = this.readPlacemark_(
        node, [this.getReadOptions(node, opt_options)]);
    if (feature) {
      return [feature];
    } else {
      return [];
    }
  } else if (localName == 'kml') {
    features = [];
    var n;
    for (n = node.firstElementChild; n; n = n.nextElementSibling) {
      var fs = this.readFeaturesFromNode(n, opt_options);
      if (fs) {
        goog.array.extend(features, fs);
      }
    }
    return features;
  } else {
    return [];
  }
};


/**
 * Read the name of the KML.
 *
 * @param {Document|Node|string} source Souce.
 * @return {string|undefined} Name.
 * @api stable
 */
ol.format.KML.prototype.readName = function(source) {
  if (ol.xml.isDocument(source)) {
    return this.readNameFromDocument(/** @type {Document} */ (source));
  } else if (ol.xml.isNode(source)) {
    return this.readNameFromNode(/** @type {Node} */ (source));
  } else if (goog.isString(source)) {
    var doc = ol.xml.parse(source);
    return this.readNameFromDocument(doc);
  } else {
    goog.asserts.fail('Unknown type for source');
    return undefined;
  }
};


/**
 * @param {Document} doc Document.
 * @return {string|undefined} Name.
 */
ol.format.KML.prototype.readNameFromDocument = function(doc) {
  var n;
  for (n = doc.firstChild; n; n = n.nextSibling) {
    if (n.nodeType == goog.dom.NodeType.ELEMENT) {
      var name = this.readNameFromNode(n);
      if (name) {
        return name;
      }
    }
  }
  return undefined;
};


/**
 * @param {Node} node Node.
 * @return {string|undefined} Name.
 */
ol.format.KML.prototype.readNameFromNode = function(node) {
  var n;
  for (n = node.firstElementChild; n; n = n.nextElementSibling) {
    if (ol.array.includes(ol.format.KML.NAMESPACE_URIS_, n.namespaceURI) &&
        n.localName == 'name') {
      return ol.format.XSD.readString(n);
    }
  }
  for (n = node.firstElementChild; n; n = n.nextElementSibling) {
    var localName = ol.xml.getLocalName(n);
    if (ol.array.includes(ol.format.KML.NAMESPACE_URIS_, n.namespaceURI) &&
        (localName == 'Document' ||
         localName == 'Folder' ||
         localName == 'Placemark' ||
         localName == 'kml')) {
      var name = this.readNameFromNode(n);
      if (name) {
        return name;
      }
    }
  }
  return undefined;
};


/**
 * Read the network links of the KML.
 *
 * @param {Document|Node|string} source Source.
 * @return {Array.<Object>} Network links.
 * @api
 */
ol.format.KML.prototype.readNetworkLinks = function(source) {
  var networkLinks = [];
  if (ol.xml.isDocument(source)) {
    goog.array.extend(networkLinks, this.readNetworkLinksFromDocument(
        /** @type {Document} */ (source)));
  } else if (ol.xml.isNode(source)) {
    goog.array.extend(networkLinks, this.readNetworkLinksFromNode(
        /** @type {Node} */ (source)));
  } else if (goog.isString(source)) {
    var doc = ol.xml.parse(source);
    goog.array.extend(networkLinks, this.readNetworkLinksFromDocument(doc));
  } else {
    goog.asserts.fail('unknown type for source');
  }
  return networkLinks;
};


/**
 * @param {Document} doc Document.
 * @return {Array.<Object>} Network links.
 */
ol.format.KML.prototype.readNetworkLinksFromDocument = function(doc) {
  var n, networkLinks = [];
  for (n = doc.firstChild; n; n = n.nextSibling) {
    if (n.nodeType == goog.dom.NodeType.ELEMENT) {
      goog.array.extend(networkLinks, this.readNetworkLinksFromNode(n));
    }
  }
  return networkLinks;
};


/**
 * @param {Node} node Node.
 * @return {Array.<Object>} Network links.
 */
ol.format.KML.prototype.readNetworkLinksFromNode = function(node) {
  var n, networkLinks = [];
  for (n = node.firstElementChild; n; n = n.nextElementSibling) {
    if (ol.array.includes(ol.format.KML.NAMESPACE_URIS_, n.namespaceURI) &&
        n.localName == 'NetworkLink') {
      var obj = ol.xml.pushParseAndPop({}, ol.format.KML.NETWORK_LINK_PARSERS_,
          n, []);
      networkLinks.push(obj);
    }
  }
  for (n = node.firstElementChild; n; n = n.nextElementSibling) {
    var localName = ol.xml.getLocalName(n);
    if (ol.array.includes(ol.format.KML.NAMESPACE_URIS_, n.namespaceURI) &&
        (localName == 'Document' ||
         localName == 'Folder' ||
         localName == 'kml')) {
      goog.array.extend(networkLinks, this.readNetworkLinksFromNode(n));
    }
  }
  return networkLinks;
};


/**
 * Read the projection from a KML source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @return {ol.proj.Projection} Projection.
 * @api stable
 */
ol.format.KML.prototype.readProjection;


/**
 * @param {Node} node Node to append a TextNode with the color to.
 * @param {ol.Color|string} color Color.
 * @private
 */
ol.format.KML.writeColorTextNode_ = function(node, color) {
  var rgba = ol.color.asArray(color);
  var opacity = (rgba.length == 4) ? rgba[3] : 1;
  var abgr = [opacity * 255, rgba[2], rgba[1], rgba[0]];
  var i;
  for (i = 0; i < 4; ++i) {
    var hex = parseInt(abgr[i], 10).toString(16);
    abgr[i] = (hex.length == 1) ? '0' + hex : hex;
  }
  ol.format.XSD.writeStringTextNode(node, abgr.join(''));
};


/**
 * @param {Node} node Node to append a TextNode with the coordinates to.
 * @param {Array.<number>} coordinates Coordinates.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.writeCoordinatesTextNode_ = function(node, coordinates, objectStack) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');

  var layout = context['layout'];
  var stride = context['stride'];

  var dimension;
  if (layout == ol.geom.GeometryLayout.XY ||
      layout == ol.geom.GeometryLayout.XYM) {
    dimension = 2;
  } else if (layout == ol.geom.GeometryLayout.XYZ ||
      layout == ol.geom.GeometryLayout.XYZM) {
    dimension = 3;
  } else {
    goog.asserts.fail('Unknown geometry layout');
  }

  var d, i;
  var ii = coordinates.length;
  var text = '';
  if (ii > 0) {
    text += coordinates[0];
    for (d = 1; d < dimension; ++d) {
      text += ',' + coordinates[d];
    }
    for (i = stride; i < ii; i += stride) {
      text += ' ' + coordinates[i];
      for (d = 1; d < dimension; ++d) {
        text += ',' + coordinates[i + d];
      }
    }
  }
  ol.format.XSD.writeStringTextNode(node, text);
};


/**
 * @param {Node} node Node.
 * @param {Array.<ol.Feature>} features Features.
 * @param {Array.<*>} objectStack Object stack.
 * @this {ol.format.KML}
 * @private
 */
ol.format.KML.writeDocument_ = function(node, features, objectStack) {
  var /** @type {ol.xml.NodeStackItem} */ context = {node: node};
  ol.xml.pushSerializeAndPop(context, ol.format.KML.DOCUMENT_SERIALIZERS_,
      ol.format.KML.DOCUMENT_NODE_FACTORY_, features, objectStack, undefined,
      this);
};


/**
 * @param {Node} node Node.
 * @param {Object} icon Icon object.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.writeIcon_ = function(node, icon, objectStack) {
  var /** @type {ol.xml.NodeStackItem} */ context = {node: node};
  var parentNode = objectStack[objectStack.length - 1].node;
  var orderedKeys = ol.format.KML.ICON_SEQUENCE_[parentNode.namespaceURI];
  var values = ol.xml.makeSequence(icon, orderedKeys);
  ol.xml.pushSerializeAndPop(context,
      ol.format.KML.ICON_SERIALIZERS_, ol.xml.OBJECT_PROPERTY_NODE_FACTORY,
      values, objectStack, orderedKeys);
  orderedKeys =
      ol.format.KML.ICON_SEQUENCE_[ol.format.KML.GX_NAMESPACE_URIS_[0]];
  values = ol.xml.makeSequence(icon, orderedKeys);
  ol.xml.pushSerializeAndPop(context, ol.format.KML.ICON_SERIALIZERS_,
      ol.format.KML.GX_NODE_FACTORY_, values, objectStack, orderedKeys);
};


/**
 * @param {Node} node Node.
 * @param {ol.style.Icon} style Icon style.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.writeIconStyle_ = function(node, style, objectStack) {
  var /** @type {ol.xml.NodeStackItem} */ context = {node: node};
  var properties = {};
  var src = style.getSrc();
  var size = style.getSize();
  var iconImageSize = style.getImageSize();
  var iconProperties = {
    'href': src
  };

  if (size) {
    iconProperties['w'] = size[0];
    iconProperties['h'] = size[1];
    var anchor = style.getAnchor(); // top-left
    var origin = style.getOrigin(); // top-left

    if (origin && iconImageSize && origin[0] !== 0 && origin[1] !== size[1]) {
      iconProperties['x'] = origin[0];
      iconProperties['y'] = iconImageSize[1] - (origin[1] + size[1]);
    }

    if (anchor && anchor[0] !== 0 && anchor[1] !== size[1]) {
      var /** @type {ol.format.KMLVec2_} */ hotSpot = {
        x: anchor[0],
        xunits: ol.style.IconAnchorUnits.PIXELS,
        y: size[1] - anchor[1],
        yunits: ol.style.IconAnchorUnits.PIXELS
      };
      properties['hotSpot'] = hotSpot;
    }
  }

  properties['Icon'] = iconProperties;

  var scale = style.getScale();
  if (scale !== 1) {
    properties['scale'] = scale;
  }

  var rotation = style.getRotation();
  if (rotation !== 0) {
    properties['heading'] = rotation; // 0-360
  }

  var parentNode = objectStack[objectStack.length - 1].node;
  var orderedKeys = ol.format.KML.ICON_STYLE_SEQUENCE_[parentNode.namespaceURI];
  var values = ol.xml.makeSequence(properties, orderedKeys);
  ol.xml.pushSerializeAndPop(context, ol.format.KML.ICON_STYLE_SERIALIZERS_,
      ol.xml.OBJECT_PROPERTY_NODE_FACTORY, values, objectStack, orderedKeys);
};


/**
 * @param {Node} node Node.
 * @param {ol.style.Text} style style.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.writeLabelStyle_ = function(node, style, objectStack) {
  var /** @type {ol.xml.NodeStackItem} */ context = {node: node};
  var properties = {};
  var fill = style.getFill();
  if (fill) {
    properties['color'] = fill.getColor();
  }
  var scale = style.getScale();
  if (scale && scale !== 1) {
    properties['scale'] = scale;
  }
  var parentNode = objectStack[objectStack.length - 1].node;
  var orderedKeys =
      ol.format.KML.LABEL_STYLE_SEQUENCE_[parentNode.namespaceURI];
  var values = ol.xml.makeSequence(properties, orderedKeys);
  ol.xml.pushSerializeAndPop(context, ol.format.KML.LABEL_STYLE_SERIALIZERS_,
      ol.xml.OBJECT_PROPERTY_NODE_FACTORY, values, objectStack, orderedKeys);
};


/**
 * @param {Node} node Node.
 * @param {ol.style.Stroke} style style.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.writeLineStyle_ = function(node, style, objectStack) {
  var /** @type {ol.xml.NodeStackItem} */ context = {node: node};
  var properties = {
    'color': style.getColor(),
    'width': style.getWidth()
  };
  var parentNode = objectStack[objectStack.length - 1].node;
  var orderedKeys = ol.format.KML.LINE_STYLE_SEQUENCE_[parentNode.namespaceURI];
  var values = ol.xml.makeSequence(properties, orderedKeys);
  ol.xml.pushSerializeAndPop(context, ol.format.KML.LINE_STYLE_SERIALIZERS_,
      ol.xml.OBJECT_PROPERTY_NODE_FACTORY, values, objectStack, orderedKeys);
};


/**
 * @param {Node} node Node.
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.writeMultiGeometry_ = function(node, geometry, objectStack) {
  goog.asserts.assert(
      (geometry instanceof ol.geom.MultiPoint) ||
      (geometry instanceof ol.geom.MultiLineString) ||
      (geometry instanceof ol.geom.MultiPolygon),
      'geometry should be one of: ol.geom.MultiPoint, ' +
      'ol.geom.MultiLineString or ol.geom.MultiPolygon');
  /** @type {ol.xml.NodeStackItem} */
  var context = {node: node};
  var type = geometry.getType();
  /** @type {Array.<ol.geom.Geometry>} */
  var geometries;
  /** @type {function(*, Array.<*>, string=): (Node|undefined)} */
  var factory;
  if (type == ol.geom.GeometryType.MULTI_POINT) {
    geometries =
        (/** @type {ol.geom.MultiPoint} */ (geometry)).getPoints();
    factory = ol.format.KML.POINT_NODE_FACTORY_;
  } else if (type == ol.geom.GeometryType.MULTI_LINE_STRING) {
    geometries =
        (/** @type {ol.geom.MultiLineString} */ (geometry)).getLineStrings();
    factory = ol.format.KML.LINE_STRING_NODE_FACTORY_;
  } else if (type == ol.geom.GeometryType.MULTI_POLYGON) {
    geometries =
        (/** @type {ol.geom.MultiPolygon} */ (geometry)).getPolygons();
    factory = ol.format.KML.POLYGON_NODE_FACTORY_;
  } else {
    goog.asserts.fail('Unknown geometry type: ' + type);
  }
  ol.xml.pushSerializeAndPop(context,
      ol.format.KML.MULTI_GEOMETRY_SERIALIZERS_, factory,
      geometries, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {ol.geom.LinearRing} linearRing Linear ring.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.writeBoundaryIs_ = function(node, linearRing, objectStack) {
  var /** @type {ol.xml.NodeStackItem} */ context = {node: node};
  ol.xml.pushSerializeAndPop(context,
      ol.format.KML.BOUNDARY_IS_SERIALIZERS_,
      ol.format.KML.LINEAR_RING_NODE_FACTORY_, [linearRing], objectStack);
};


/**
 * FIXME currently we do serialize arbitrary/custom feature properties
 * (ExtendedData).
 * @param {Node} node Node.
 * @param {ol.Feature} feature Feature.
 * @param {Array.<*>} objectStack Object stack.
 * @this {ol.format.KML}
 * @private
 */
ol.format.KML.writePlacemark_ = function(node, feature, objectStack) {
  var /** @type {ol.xml.NodeStackItem} */ context = {node: node};

  // set id
  if (feature.getId()) {
    node.setAttribute('id', feature.getId());
  }

  // serialize properties (properties unknown to KML are not serialized)
  var properties = feature.getProperties();

  var styleFunction = feature.getStyleFunction();
  if (styleFunction) {
    // FIXME the styles returned by the style function are supposed to be
    // resolution-independent here
    var styles = styleFunction.call(feature, 0);
    if (styles && styles.length > 0) {
      var style = styles[0];
      if (this.writeStyles_) {
        properties['Style'] = styles[0];
      }
      var textStyle = style.getText();
      if (textStyle) {
        properties['name'] = textStyle.getText();
      }
    }
  }
  var parentNode = objectStack[objectStack.length - 1].node;
  var orderedKeys = ol.format.KML.PLACEMARK_SEQUENCE_[parentNode.namespaceURI];
  var values = ol.xml.makeSequence(properties, orderedKeys);
  ol.xml.pushSerializeAndPop(context, ol.format.KML.PLACEMARK_SERIALIZERS_,
      ol.xml.OBJECT_PROPERTY_NODE_FACTORY, values, objectStack, orderedKeys);

  // serialize geometry
  var options = /** @type {olx.format.WriteOptions} */ (objectStack[0]);
  var geometry = feature.getGeometry();
  if (geometry) {
    geometry =
        ol.format.Feature.transformWithOptions(geometry, true, options);
  }
  ol.xml.pushSerializeAndPop(context, ol.format.KML.PLACEMARK_SERIALIZERS_,
      ol.format.KML.GEOMETRY_NODE_FACTORY_, [geometry], objectStack);
};


/**
 * @param {Node} node Node.
 * @param {ol.geom.SimpleGeometry} geometry Geometry.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.writePrimitiveGeometry_ = function(node, geometry, objectStack) {
  goog.asserts.assert(
      (geometry instanceof ol.geom.Point) ||
      (geometry instanceof ol.geom.LineString) ||
      (geometry instanceof ol.geom.LinearRing),
      'geometry should be one of ol.geom.Point, ol.geom.LineString ' +
      'or ol.geom.LinearRing');
  var flatCoordinates = geometry.getFlatCoordinates();
  var /** @type {ol.xml.NodeStackItem} */ context = {node: node};
  context['layout'] = geometry.getLayout();
  context['stride'] = geometry.getStride();
  ol.xml.pushSerializeAndPop(context,
      ol.format.KML.PRIMITIVE_GEOMETRY_SERIALIZERS_,
      ol.format.KML.COORDINATES_NODE_FACTORY_,
      [flatCoordinates], objectStack);
};


/**
 * @param {Node} node Node.
 * @param {ol.geom.Polygon} polygon Polygon.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.writePolygon_ = function(node, polygon, objectStack) {
  goog.asserts.assertInstanceof(polygon, ol.geom.Polygon,
      'polygon should be an ol.geom.Polygon');
  var linearRings = polygon.getLinearRings();
  goog.asserts.assert(linearRings.length > 0,
      'linearRings should not be empty');
  var outerRing = linearRings.shift();
  var /** @type {ol.xml.NodeStackItem} */ context = {node: node};
  // inner rings
  ol.xml.pushSerializeAndPop(context,
      ol.format.KML.POLYGON_SERIALIZERS_,
      ol.format.KML.INNER_BOUNDARY_NODE_FACTORY_,
      linearRings, objectStack);
  // outer ring
  ol.xml.pushSerializeAndPop(context,
      ol.format.KML.POLYGON_SERIALIZERS_,
      ol.format.KML.OUTER_BOUNDARY_NODE_FACTORY_,
      [outerRing], objectStack);
};


/**
 * @param {Node} node Node.
 * @param {ol.style.Fill} style Style.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.writePolyStyle_ = function(node, style, objectStack) {
  var /** @type {ol.xml.NodeStackItem} */ context = {node: node};
  ol.xml.pushSerializeAndPop(context, ol.format.KML.POLY_STYLE_SERIALIZERS_,
      ol.format.KML.COLOR_NODE_FACTORY_, [style.getColor()], objectStack);
};


/**
 * @param {Node} node Node to append a TextNode with the scale to.
 * @param {number|undefined} scale Scale.
 * @private
 */
ol.format.KML.writeScaleTextNode_ = function(node, scale) {
  ol.format.XSD.writeDecimalTextNode(node, scale * scale);
};


/**
 * @param {Node} node Node.
 * @param {ol.style.Style} style Style.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.KML.writeStyle_ = function(node, style, objectStack) {
  var /** @type {ol.xml.NodeStackItem} */ context = {node: node};
  var properties = {};
  var fillStyle = style.getFill();
  var strokeStyle = style.getStroke();
  var imageStyle = style.getImage();
  var textStyle = style.getText();
  if (imageStyle instanceof ol.style.Icon) {
    properties['IconStyle'] = imageStyle;
  }
  if (textStyle) {
    properties['LabelStyle'] = textStyle;
  }
  if (strokeStyle) {
    properties['LineStyle'] = strokeStyle;
  }
  if (fillStyle) {
    properties['PolyStyle'] = fillStyle;
  }
  var parentNode = objectStack[objectStack.length - 1].node;
  var orderedKeys = ol.format.KML.STYLE_SEQUENCE_[parentNode.namespaceURI];
  var values = ol.xml.makeSequence(properties, orderedKeys);
  ol.xml.pushSerializeAndPop(context, ol.format.KML.STYLE_SERIALIZERS_,
      ol.xml.OBJECT_PROPERTY_NODE_FACTORY, values, objectStack, orderedKeys);
};


/**
 * @param {Node} node Node to append a TextNode with the Vec2 to.
 * @param {ol.format.KMLVec2_} vec2 Vec2.
 * @private
 */
ol.format.KML.writeVec2_ = function(node, vec2) {
  node.setAttribute('x', vec2.x);
  node.setAttribute('y', vec2.y);
  node.setAttribute('xunits', vec2.xunits);
  node.setAttribute('yunits', vec2.yunits);
};


/**
 * @const
 * @type {Object.<string, Array.<string>>}
 * @private
 */
ol.format.KML.KML_SEQUENCE_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, [
      'Document', 'Placemark'
    ]);


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.KML.KML_SERIALIZERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'Document': ol.xml.makeChildAppender(ol.format.KML.writeDocument_),
      'Placemark': ol.xml.makeChildAppender(ol.format.KML.writePlacemark_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.KML.DOCUMENT_SERIALIZERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'Placemark': ol.xml.makeChildAppender(ol.format.KML.writePlacemark_)
    });


/**
 * @const
 * @type {Object.<string, string>}
 * @private
 */
ol.format.KML.GEOMETRY_TYPE_TO_NODENAME_ = {
  'Point': 'Point',
  'LineString': 'LineString',
  'LinearRing': 'LinearRing',
  'Polygon': 'Polygon',
  'MultiPoint': 'MultiGeometry',
  'MultiLineString': 'MultiGeometry',
  'MultiPolygon': 'MultiGeometry'
};


/**
 * @const
 * @type {Object.<string, Array.<string>>}
 * @private
 */
ol.format.KML.ICON_SEQUENCE_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, [
      'href'
    ],
    ol.xml.makeStructureNS(ol.format.KML.GX_NAMESPACE_URIS_, [
      'x', 'y', 'w', 'h'
    ]));


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.KML.ICON_SERIALIZERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'href': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode)
    }, ol.xml.makeStructureNS(
        ol.format.KML.GX_NAMESPACE_URIS_, {
          'x': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode),
          'y': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode),
          'w': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode),
          'h': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode)
        }));


/**
 * @const
 * @type {Object.<string, Array.<string>>}
 * @private
 */
ol.format.KML.ICON_STYLE_SEQUENCE_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, [
      'scale', 'heading', 'Icon', 'hotSpot'
    ]);


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.KML.ICON_STYLE_SERIALIZERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'Icon': ol.xml.makeChildAppender(ol.format.KML.writeIcon_),
      'heading': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode),
      'hotSpot': ol.xml.makeChildAppender(ol.format.KML.writeVec2_),
      'scale': ol.xml.makeChildAppender(ol.format.KML.writeScaleTextNode_)
    });


/**
 * @const
 * @type {Object.<string, Array.<string>>}
 * @private
 */
ol.format.KML.LABEL_STYLE_SEQUENCE_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, [
      'color', 'scale'
    ]);


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.KML.LABEL_STYLE_SERIALIZERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'color': ol.xml.makeChildAppender(ol.format.KML.writeColorTextNode_),
      'scale': ol.xml.makeChildAppender(ol.format.KML.writeScaleTextNode_)
    });


/**
 * @const
 * @type {Object.<string, Array.<string>>}
 * @private
 */
ol.format.KML.LINE_STYLE_SEQUENCE_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, [
      'color', 'width'
    ]);


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.KML.LINE_STYLE_SERIALIZERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'color': ol.xml.makeChildAppender(ol.format.KML.writeColorTextNode_),
      'width': ol.xml.makeChildAppender(ol.format.XSD.writeDecimalTextNode)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.KML.BOUNDARY_IS_SERIALIZERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'LinearRing': ol.xml.makeChildAppender(
          ol.format.KML.writePrimitiveGeometry_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.KML.MULTI_GEOMETRY_SERIALIZERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'LineString': ol.xml.makeChildAppender(
          ol.format.KML.writePrimitiveGeometry_),
      'Point': ol.xml.makeChildAppender(
          ol.format.KML.writePrimitiveGeometry_),
      'Polygon': ol.xml.makeChildAppender(ol.format.KML.writePolygon_)
    });


/**
 * @const
 * @type {Object.<string, Array.<string>>}
 * @private
 */
ol.format.KML.PLACEMARK_SEQUENCE_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, [
      'name', 'open', 'visibility', 'address', 'phoneNumber', 'description',
      'styleUrl', 'Style'
    ]);


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.KML.PLACEMARK_SERIALIZERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'MultiGeometry': ol.xml.makeChildAppender(
          ol.format.KML.writeMultiGeometry_),
      'LineString': ol.xml.makeChildAppender(
          ol.format.KML.writePrimitiveGeometry_),
      'LinearRing': ol.xml.makeChildAppender(
          ol.format.KML.writePrimitiveGeometry_),
      'Point': ol.xml.makeChildAppender(
          ol.format.KML.writePrimitiveGeometry_),
      'Polygon': ol.xml.makeChildAppender(ol.format.KML.writePolygon_),
      'Style': ol.xml.makeChildAppender(ol.format.KML.writeStyle_),
      'address': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
      'description': ol.xml.makeChildAppender(
          ol.format.XSD.writeStringTextNode),
      'name': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
      'open': ol.xml.makeChildAppender(ol.format.XSD.writeBooleanTextNode),
      'phoneNumber': ol.xml.makeChildAppender(
          ol.format.XSD.writeStringTextNode),
      'styleUrl': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode),
      'visibility': ol.xml.makeChildAppender(
          ol.format.XSD.writeBooleanTextNode)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.KML.PRIMITIVE_GEOMETRY_SERIALIZERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'coordinates': ol.xml.makeChildAppender(
          ol.format.KML.writeCoordinatesTextNode_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.KML.POLYGON_SERIALIZERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'outerBoundaryIs': ol.xml.makeChildAppender(
          ol.format.KML.writeBoundaryIs_),
      'innerBoundaryIs': ol.xml.makeChildAppender(
          ol.format.KML.writeBoundaryIs_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.KML.POLY_STYLE_SERIALIZERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'color': ol.xml.makeChildAppender(ol.format.KML.writeColorTextNode_)
    });


/**
 * @const
 * @type {Object.<string, Array.<string>>}
 * @private
 */
ol.format.KML.STYLE_SEQUENCE_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, [
      'IconStyle', 'LabelStyle', 'LineStyle', 'PolyStyle'
    ]);


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.KML.STYLE_SERIALIZERS_ = ol.xml.makeStructureNS(
    ol.format.KML.NAMESPACE_URIS_, {
      'IconStyle': ol.xml.makeChildAppender(ol.format.KML.writeIconStyle_),
      'LabelStyle': ol.xml.makeChildAppender(ol.format.KML.writeLabelStyle_),
      'LineStyle': ol.xml.makeChildAppender(ol.format.KML.writeLineStyle_),
      'PolyStyle': ol.xml.makeChildAppender(ol.format.KML.writePolyStyle_)
    });


/**
 * @const
 * @param {*} value Value.
 * @param {Array.<*>} objectStack Object stack.
 * @param {string=} opt_nodeName Node name.
 * @return {Node|undefined} Node.
 * @private
 */
ol.format.KML.GX_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) {
  return ol.xml.createElementNS(ol.format.KML.GX_NAMESPACE_URIS_[0],
      'gx:' + opt_nodeName);
};


/**
 * @const
 * @param {*} value Value.
 * @param {Array.<*>} objectStack Object stack.
 * @param {string=} opt_nodeName Node name.
 * @return {Node|undefined} Node.
 * @private
 */
ol.format.KML.DOCUMENT_NODE_FACTORY_ = function(value, objectStack,
    opt_nodeName) {
  goog.asserts.assertInstanceof(value, ol.Feature,
      'value should be an ol.Feature');
  var parentNode = objectStack[objectStack.length - 1].node;
  goog.asserts.assert(ol.xml.isNode(parentNode),
      'parentNode should be an XML node');
  return ol.xml.createElementNS(parentNode.namespaceURI, 'Placemark');
};


/**
 * @const
 * @param {*} value Value.
 * @param {Array.<*>} objectStack Object stack.
 * @param {string=} opt_nodeName Node name.
 * @return {Node|undefined} Node.
 * @private
 */
ol.format.KML.GEOMETRY_NODE_FACTORY_ = function(value, objectStack,
    opt_nodeName) {
  if (value) {
    goog.asserts.assertInstanceof(value, ol.geom.Geometry,
        'value should be an ol.geom.Geometry');
    var parentNode = objectStack[objectStack.length - 1].node;
    goog.asserts.assert(ol.xml.isNode(parentNode),
        'parentNode should be an XML node');
    return ol.xml.createElementNS(parentNode.namespaceURI,
        ol.format.KML.GEOMETRY_TYPE_TO_NODENAME_[value.getType()]);
  }
};


/**
 * A factory for creating coordinates nodes.
 * @const
 * @type {function(*, Array.<*>, string=): (Node|undefined)}
 * @private
 */
ol.format.KML.COLOR_NODE_FACTORY_ = ol.xml.makeSimpleNodeFactory('color');


/**
 * A factory for creating coordinates nodes.
 * @const
 * @type {function(*, Array.<*>, string=): (Node|undefined)}
 * @private
 */
ol.format.KML.COORDINATES_NODE_FACTORY_ =
    ol.xml.makeSimpleNodeFactory('coordinates');


/**
 * A factory for creating innerBoundaryIs nodes.
 * @const
 * @type {function(*, Array.<*>, string=): (Node|undefined)}
 * @private
 */
ol.format.KML.INNER_BOUNDARY_NODE_FACTORY_ =
    ol.xml.makeSimpleNodeFactory('innerBoundaryIs');


/**
 * A factory for creating Point nodes.
 * @const
 * @type {function(*, Array.<*>, string=): (Node|undefined)}
 * @private
 */
ol.format.KML.POINT_NODE_FACTORY_ =
    ol.xml.makeSimpleNodeFactory('Point');


/**
 * A factory for creating LineString nodes.
 * @const
 * @type {function(*, Array.<*>, string=): (Node|undefined)}
 * @private
 */
ol.format.KML.LINE_STRING_NODE_FACTORY_ =
    ol.xml.makeSimpleNodeFactory('LineString');


/**
 * A factory for creating LinearRing nodes.
 * @const
 * @type {function(*, Array.<*>, string=): (Node|undefined)}
 * @private
 */
ol.format.KML.LINEAR_RING_NODE_FACTORY_ =
    ol.xml.makeSimpleNodeFactory('LinearRing');


/**
 * A factory for creating Polygon nodes.
 * @const
 * @type {function(*, Array.<*>, string=): (Node|undefined)}
 * @private
 */
ol.format.KML.POLYGON_NODE_FACTORY_ =
    ol.xml.makeSimpleNodeFactory('Polygon');


/**
 * A factory for creating outerBoundaryIs nodes.
 * @const
 * @type {function(*, Array.<*>, string=): (Node|undefined)}
 * @private
 */
ol.format.KML.OUTER_BOUNDARY_NODE_FACTORY_ =
    ol.xml.makeSimpleNodeFactory('outerBoundaryIs');


/**
 * Encode an array of features in the KML format.
 *
 * @function
 * @param {Array.<ol.Feature>} features Features.
 * @param {olx.format.WriteOptions=} opt_options Options.
 * @return {string} Result.
 * @api stable
 */
ol.format.KML.prototype.writeFeatures;


/**
 * Encode an array of features in the KML format as an XML node.
 *
 * @param {Array.<ol.Feature>} features Features.
 * @param {olx.format.WriteOptions=} opt_options Options.
 * @return {Node} Node.
 * @api
 */
ol.format.KML.prototype.writeFeaturesNode = function(features, opt_options) {
  opt_options = this.adaptOptions(opt_options);
  var kml = ol.xml.createElementNS(ol.format.KML.NAMESPACE_URIS_[4], 'kml');
  var xmlnsUri = 'http://www.w3.org/2000/xmlns/';
  var xmlSchemaInstanceUri = 'http://www.w3.org/2001/XMLSchema-instance';
  ol.xml.setAttributeNS(kml, xmlnsUri, 'xmlns:gx',
      ol.format.KML.GX_NAMESPACE_URIS_[0]);
  ol.xml.setAttributeNS(kml, xmlnsUri, 'xmlns:xsi', xmlSchemaInstanceUri);
  ol.xml.setAttributeNS(kml, xmlSchemaInstanceUri, 'xsi:schemaLocation',
      ol.format.KML.SCHEMA_LOCATION_);

  var /** @type {ol.xml.NodeStackItem} */ context = {node: kml};
  var properties = {};
  if (features.length > 1) {
    properties['Document'] = features;
  } else if (features.length == 1) {
    properties['Placemark'] = features[0];
  }
  var orderedKeys = ol.format.KML.KML_SEQUENCE_[kml.namespaceURI];
  var values = ol.xml.makeSequence(properties, orderedKeys);
  ol.xml.pushSerializeAndPop(context, ol.format.KML.KML_SERIALIZERS_,
      ol.xml.OBJECT_PROPERTY_NODE_FACTORY, values, [opt_options], orderedKeys,
      this);
  return kml;
};

goog.provide('ol.ext.pbf');
/** @typedef {function(*)} */
ol.ext.pbf;
(function() {
var exports = {};
var module = {exports: exports};
var define;
/**
 * @fileoverview
 * @suppress {accessControls, ambiguousFunctionDecl, checkDebuggerStatement, checkRegExp, checkTypes, checkVars, const, constantProperty, deprecated, duplicate, es5Strict, fileoverviewTags, missingProperties, nonStandardJsDocs, strictModuleDepCheck, suspiciousCode, undefinedNames, undefinedVars, unknownDefines, uselessCode, visibility}
 */
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.pbf = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
'use strict';

// lightweight Buffer shim for pbf browser build
// based on code from github.com/feross/buffer (MIT-licensed)

module.exports = Buffer;

var ieee754 = _dereq_('ieee754');

var BufferMethods;

function Buffer(length) {
    var arr;
    if (length && length.length) {
        arr = length;
        length = arr.length;
    }
    var buf = new Uint8Array(length || 0);
    if (arr) buf.set(arr);

    buf.readUInt32LE = BufferMethods.readUInt32LE;
    buf.writeUInt32LE = BufferMethods.writeUInt32LE;
    buf.readInt32LE = BufferMethods.readInt32LE;
    buf.writeInt32LE = BufferMethods.writeInt32LE;
    buf.readFloatLE = BufferMethods.readFloatLE;
    buf.writeFloatLE = BufferMethods.writeFloatLE;
    buf.readDoubleLE = BufferMethods.readDoubleLE;
    buf.writeDoubleLE = BufferMethods.writeDoubleLE;
    buf.toString = BufferMethods.toString;
    buf.write = BufferMethods.write;
    buf.slice = BufferMethods.slice;
    buf.copy = BufferMethods.copy;

    buf._isBuffer = true;
    return buf;
}

var lastStr, lastStrEncoded;

BufferMethods = {
    readUInt32LE: function(pos) {
        return ((this[pos]) |
            (this[pos + 1] << 8) |
            (this[pos + 2] << 16)) +
            (this[pos + 3] * 0x1000000);
    },

    writeUInt32LE: function(val, pos) {
        this[pos] = val;
        this[pos + 1] = (val >>> 8);
        this[pos + 2] = (val >>> 16);
        this[pos + 3] = (val >>> 24);
    },

    readInt32LE: function(pos) {
        return ((this[pos]) |
            (this[pos + 1] << 8) |
            (this[pos + 2] << 16)) +
            (this[pos + 3] << 24);
    },

    readFloatLE:  function(pos) { return ieee754.read(this, pos, true, 23, 4); },
    readDoubleLE: function(pos) { return ieee754.read(this, pos, true, 52, 8); },

    writeFloatLE:  function(val, pos) { return ieee754.write(this, val, pos, true, 23, 4); },
    writeDoubleLE: function(val, pos) { return ieee754.write(this, val, pos, true, 52, 8); },

    toString: function(encoding, start, end) {
        var str = '',
            tmp = '';

        start = start || 0;
        end = Math.min(this.length, end || this.length);

        for (var i = start; i < end; i++) {
            var ch = this[i];
            if (ch <= 0x7F) {
                str += decodeURIComponent(tmp) + String.fromCharCode(ch);
                tmp = '';
            } else {
                tmp += '%' + ch.toString(16);
            }
        }

        str += decodeURIComponent(tmp);

        return str;
    },

    write: function(str, pos) {
        var bytes = str === lastStr ? lastStrEncoded : encodeString(str);
        for (var i = 0; i < bytes.length; i++) {
            this[pos + i] = bytes[i];
        }
    },

    slice: function(start, end) {
        return this.subarray(start, end);
    },

    copy: function(buf, pos) {
        pos = pos || 0;
        for (var i = 0; i < this.length; i++) {
            buf[pos + i] = this[i];
        }
    }
};

BufferMethods.writeInt32LE = BufferMethods.writeUInt32LE;

Buffer.byteLength = function(str) {
    lastStr = str;
    lastStrEncoded = encodeString(str);
    return lastStrEncoded.length;
};

Buffer.isBuffer = function(buf) {
    return !!(buf && buf._isBuffer);
};

function encodeString(str) {
    var length = str.length,
        bytes = [];

    for (var i = 0, c, lead; i < length; i++) {
        c = str.charCodeAt(i); // code point

        if (c > 0xD7FF && c < 0xE000) {

            if (lead) {
                if (c < 0xDC00) {
                    bytes.push(0xEF, 0xBF, 0xBD);
                    lead = c;
                    continue;

                } else {
                    c = lead - 0xD800 << 10 | c - 0xDC00 | 0x10000;
                    lead = null;
                }

            } else {
                if (c > 0xDBFF || (i + 1 === length)) bytes.push(0xEF, 0xBF, 0xBD);
                else lead = c;

                continue;
            }

        } else if (lead) {
            bytes.push(0xEF, 0xBF, 0xBD);
            lead = null;
        }

        if (c < 0x80) bytes.push(c);
        else if (c < 0x800) bytes.push(c >> 0x6 | 0xC0, c & 0x3F | 0x80);
        else if (c < 0x10000) bytes.push(c >> 0xC | 0xE0, c >> 0x6 & 0x3F | 0x80, c & 0x3F | 0x80);
        else bytes.push(c >> 0x12 | 0xF0, c >> 0xC & 0x3F | 0x80, c >> 0x6 & 0x3F | 0x80, c & 0x3F | 0x80);
    }
    return bytes;
}

},{"ieee754":3}],2:[function(_dereq_,module,exports){
(function (global){
'use strict';

module.exports = Pbf;

var Buffer = global.Buffer || _dereq_('./buffer');

function Pbf(buf) {
    this.buf = !Buffer.isBuffer(buf) ? new Buffer(buf || 0) : buf;
    this.pos = 0;
    this.length = this.buf.length;
}

Pbf.Varint  = 0; // varint: int32, int64, uint32, uint64, sint32, sint64, bool, enum
Pbf.Fixed64 = 1; // 64-bit: double, fixed64, sfixed64
Pbf.Bytes   = 2; // length-delimited: string, bytes, embedded messages, packed repeated fields
Pbf.Fixed32 = 5; // 32-bit: float, fixed32, sfixed32

var SHIFT_LEFT_32 = (1 << 16) * (1 << 16),
    SHIFT_RIGHT_32 = 1 / SHIFT_LEFT_32,
    POW_2_63 = Math.pow(2, 63);

Pbf.prototype = {

    destroy: function() {
        this.buf = null;
    },

    // === READING =================================================================

    readFields: function(readField, result, end) {
        end = end || this.length;

        while (this.pos < end) {
            var val = this.readVarint(),
                tag = val >> 3,
                startPos = this.pos;

            readField(tag, result, this);

            if (this.pos === startPos) this.skip(val);
        }
        return result;
    },

    readMessage: function(readField, result) {
        return this.readFields(readField, result, this.readVarint() + this.pos);
    },

    readFixed32: function() {
        var val = this.buf.readUInt32LE(this.pos);
        this.pos += 4;
        return val;
    },

    readSFixed32: function() {
        var val = this.buf.readInt32LE(this.pos);
        this.pos += 4;
        return val;
    },

    // 64-bit int handling is based on github.com/dpw/node-buffer-more-ints (MIT-licensed)

    readFixed64: function() {
        var val = this.buf.readUInt32LE(this.pos) + this.buf.readUInt32LE(this.pos + 4) * SHIFT_LEFT_32;
        this.pos += 8;
        return val;
    },

    readSFixed64: function() {
        var val = this.buf.readUInt32LE(this.pos) + this.buf.readInt32LE(this.pos + 4) * SHIFT_LEFT_32;
        this.pos += 8;
        return val;
    },

    readFloat: function() {
        var val = this.buf.readFloatLE(this.pos);
        this.pos += 4;
        return val;
    },

    readDouble: function() {
        var val = this.buf.readDoubleLE(this.pos);
        this.pos += 8;
        return val;
    },

    readVarint: function() {
        var buf = this.buf,
            val, b, b0, b1, b2, b3;

        b0 = buf[this.pos++]; if (b0 < 0x80) return b0;                 b0 = b0 & 0x7f;
        b1 = buf[this.pos++]; if (b1 < 0x80) return b0 | b1 << 7;       b1 = (b1 & 0x7f) << 7;
        b2 = buf[this.pos++]; if (b2 < 0x80) return b0 | b1 | b2 << 14; b2 = (b2 & 0x7f) << 14;
        b3 = buf[this.pos++]; if (b3 < 0x80) return b0 | b1 | b2 | b3 << 21;

        val = b0 | b1 | b2 | (b3 & 0x7f) << 21;

        b = buf[this.pos++]; val += (b & 0x7f) * 0x10000000;         if (b < 0x80) return val;
        b = buf[this.pos++]; val += (b & 0x7f) * 0x800000000;        if (b < 0x80) return val;
        b = buf[this.pos++]; val += (b & 0x7f) * 0x40000000000;      if (b < 0x80) return val;
        b = buf[this.pos++]; val += (b & 0x7f) * 0x2000000000000;    if (b < 0x80) return val;
        b = buf[this.pos++]; val += (b & 0x7f) * 0x100000000000000;  if (b < 0x80) return val;
        b = buf[this.pos++]; val += (b & 0x7f) * 0x8000000000000000; if (b < 0x80) return val;

        throw new Error('Expected varint not more than 10 bytes');
    },

    readVarint64: function() {
        var startPos = this.pos,
            val = this.readVarint();

        if (val < POW_2_63) return val;

        var pos = this.pos - 2;
        while (this.buf[pos] === 0xff) pos--;
        if (pos < startPos) pos = startPos;

        val = 0;
        for (var i = 0; i < pos - startPos + 1; i++) {
            var b = ~this.buf[startPos + i] & 0x7f;
            val += i < 4 ? b << i * 7 : b * Math.pow(2, i * 7);
        }

        return -val - 1;
    },

    readSVarint: function() {
        var num = this.readVarint();
        return num % 2 === 1 ? (num + 1) / -2 : num / 2; // zigzag encoding
    },

    readBoolean: function() {
        return Boolean(this.readVarint());
    },

    readString: function() {
        var end = this.readVarint() + this.pos,
            str = this.buf.toString('utf8', this.pos, end);
        this.pos = end;
        return str;
    },

    readBytes: function() {
        var end = this.readVarint() + this.pos,
            buffer = this.buf.slice(this.pos, end);
        this.pos = end;
        return buffer;
    },

    // verbose for performance reasons; doesn't affect gzipped size

    readPackedVarint: function() {
        var end = this.readVarint() + this.pos, arr = [];
        while (this.pos < end) arr.push(this.readVarint());
        return arr;
    },
    readPackedSVarint: function() {
        var end = this.readVarint() + this.pos, arr = [];
        while (this.pos < end) arr.push(this.readSVarint());
        return arr;
    },
    readPackedBoolean: function() {
        var end = this.readVarint() + this.pos, arr = [];
        while (this.pos < end) arr.push(this.readBoolean());
        return arr;
    },
    readPackedFloat: function() {
        var end = this.readVarint() + this.pos, arr = [];
        while (this.pos < end) arr.push(this.readFloat());
        return arr;
    },
    readPackedDouble: function() {
        var end = this.readVarint() + this.pos, arr = [];
        while (this.pos < end) arr.push(this.readDouble());
        return arr;
    },
    readPackedFixed32: function() {
        var end = this.readVarint() + this.pos, arr = [];
        while (this.pos < end) arr.push(this.readFixed32());
        return arr;
    },
    readPackedSFixed32: function() {
        var end = this.readVarint() + this.pos, arr = [];
        while (this.pos < end) arr.push(this.readSFixed32());
        return arr;
    },
    readPackedFixed64: function() {
        var end = this.readVarint() + this.pos, arr = [];
        while (this.pos < end) arr.push(this.readFixed64());
        return arr;
    },
    readPackedSFixed64: function() {
        var end = this.readVarint() + this.pos, arr = [];
        while (this.pos < end) arr.push(this.readSFixed64());
        return arr;
    },

    skip: function(val) {
        var type = val & 0x7;
        if (type === Pbf.Varint) while (this.buf[this.pos++] > 0x7f) {}
        else if (type === Pbf.Bytes) this.pos = this.readVarint() + this.pos;
        else if (type === Pbf.Fixed32) this.pos += 4;
        else if (type === Pbf.Fixed64) this.pos += 8;
        else throw new Error('Unimplemented type: ' + type);
    },

    // === WRITING =================================================================

    writeTag: function(tag, type) {
        this.writeVarint((tag << 3) | type);
    },

    realloc: function(min) {
        var length = this.length || 16;

        while (length < this.pos + min) length *= 2;

        if (length !== this.length) {
            var buf = new Buffer(length);
            this.buf.copy(buf);
            this.buf = buf;
            this.length = length;
        }
    },

    finish: function() {
        this.length = this.pos;
        this.pos = 0;
        return this.buf.slice(0, this.length);
    },

    writeFixed32: function(val) {
        this.realloc(4);
        this.buf.writeUInt32LE(val, this.pos);
        this.pos += 4;
    },

    writeSFixed32: function(val) {
        this.realloc(4);
        this.buf.writeInt32LE(val, this.pos);
        this.pos += 4;
    },

    writeFixed64: function(val) {
        this.realloc(8);
        this.buf.writeInt32LE(val & -1, this.pos);
        this.buf.writeUInt32LE(Math.floor(val * SHIFT_RIGHT_32), this.pos + 4);
        this.pos += 8;
    },

    writeSFixed64: function(val) {
        this.realloc(8);
        this.buf.writeInt32LE(val & -1, this.pos);
        this.buf.writeInt32LE(Math.floor(val * SHIFT_RIGHT_32), this.pos + 4);
        this.pos += 8;
    },

    writeVarint: function(val) {
        val = +val;

        if (val <= 0x7f) {
            this.realloc(1);
            this.buf[this.pos++] = val;

        } else if (val <= 0x3fff) {
            this.realloc(2);
            this.buf[this.pos++] = ((val >>> 0) & 0x7f) | 0x80;
            this.buf[this.pos++] = ((val >>> 7) & 0x7f);

        } else if (val <= 0x1fffff) {
            this.realloc(3);
            this.buf[this.pos++] = ((val >>> 0) & 0x7f) | 0x80;
            this.buf[this.pos++] = ((val >>> 7) & 0x7f) | 0x80;
            this.buf[this.pos++] = ((val >>> 14) & 0x7f);

        } else if (val <= 0xfffffff) {
            this.realloc(4);
            this.buf[this.pos++] = ((val >>> 0) & 0x7f) | 0x80;
            this.buf[this.pos++] = ((val >>> 7) & 0x7f) | 0x80;
            this.buf[this.pos++] = ((val >>> 14) & 0x7f) | 0x80;
            this.buf[this.pos++] = ((val >>> 21) & 0x7f);

        } else {
            var pos = this.pos;
            while (val >= 0x80) {
                this.realloc(1);
                this.buf[this.pos++] = (val & 0xff) | 0x80;
                val /= 0x80;
            }
            this.realloc(1);
            this.buf[this.pos++] = val | 0;
            if (this.pos - pos > 10) throw new Error('Given varint doesn\'t fit into 10 bytes');
        }
    },

    writeSVarint: function(val) {
        this.writeVarint(val < 0 ? -val * 2 - 1 : val * 2);
    },

    writeBoolean: function(val) {
        this.writeVarint(Boolean(val));
    },

    writeString: function(str) {
        str = String(str);
        var bytes = Buffer.byteLength(str);
        this.writeVarint(bytes);
        this.realloc(bytes);
        this.buf.write(str, this.pos);
        this.pos += bytes;
    },

    writeFloat: function(val) {
        this.realloc(4);
        this.buf.writeFloatLE(val, this.pos);
        this.pos += 4;
    },

    writeDouble: function(val) {
        this.realloc(8);
        this.buf.writeDoubleLE(val, this.pos);
        this.pos += 8;
    },

    writeBytes: function(buffer) {
        var len = buffer.length;
        this.writeVarint(len);
        this.realloc(len);
        for (var i = 0; i < len; i++) this.buf[this.pos++] = buffer[i];
    },

    writeRawMessage: function(fn, obj) {
        this.pos++; // reserve 1 byte for short message length

        // write the message directly to the buffer and see how much was written
        var startPos = this.pos;
        fn(obj, this);
        var len = this.pos - startPos;

        var varintLen =
            len <= 0x7f ? 1 :
            len <= 0x3fff ? 2 :
            len <= 0x1fffff ? 3 :
            len <= 0xfffffff ? 4 : Math.ceil(Math.log(len) / (Math.LN2 * 7));

        // if 1 byte isn't enough for encoding message length, shift the data to the right
        if (varintLen > 1) {
            this.realloc(varintLen - 1);
            for (var i = this.pos - 1; i >= startPos; i--) this.buf[i + varintLen - 1] = this.buf[i];
        }

        // finally, write the message length in the reserved place and restore the position
        this.pos = startPos - 1;
        this.writeVarint(len);
        this.pos += len;
    },

    writeMessage: function(tag, fn, obj) {
        this.writeTag(tag, Pbf.Bytes);
        this.writeRawMessage(fn, obj);
    },

    writePackedVarint:   function(tag, arr) { this.writeMessage(tag, writePackedVarint, arr);   },
    writePackedSVarint:  function(tag, arr) { this.writeMessage(tag, writePackedSVarint, arr);  },
    writePackedBoolean:  function(tag, arr) { this.writeMessage(tag, writePackedBoolean, arr);  },
    writePackedFloat:    function(tag, arr) { this.writeMessage(tag, writePackedFloat, arr);    },
    writePackedDouble:   function(tag, arr) { this.writeMessage(tag, writePackedDouble, arr);   },
    writePackedFixed32:  function(tag, arr) { this.writeMessage(tag, writePackedFixed32, arr);  },
    writePackedSFixed32: function(tag, arr) { this.writeMessage(tag, writePackedSFixed32, arr); },
    writePackedFixed64:  function(tag, arr) { this.writeMessage(tag, writePackedFixed64, arr);  },
    writePackedSFixed64: function(tag, arr) { this.writeMessage(tag, writePackedSFixed64, arr); },

    writeBytesField: function(tag, buffer) {
        this.writeTag(tag, Pbf.Bytes);
        this.writeBytes(buffer);
    },
    writeFixed32Field: function(tag, val) {
        this.writeTag(tag, Pbf.Fixed32);
        this.writeFixed32(val);
    },
    writeSFixed32Field: function(tag, val) {
        this.writeTag(tag, Pbf.Fixed32);
        this.writeSFixed32(val);
    },
    writeFixed64Field: function(tag, val) {
        this.writeTag(tag, Pbf.Fixed64);
        this.writeFixed64(val);
    },
    writeSFixed64Field: function(tag, val) {
        this.writeTag(tag, Pbf.Fixed64);
        this.writeSFixed64(val);
    },
    writeVarintField: function(tag, val) {
        this.writeTag(tag, Pbf.Varint);
        this.writeVarint(val);
    },
    writeSVarintField: function(tag, val) {
        this.writeTag(tag, Pbf.Varint);
        this.writeSVarint(val);
    },
    writeStringField: function(tag, str) {
        this.writeTag(tag, Pbf.Bytes);
        this.writeString(str);
    },
    writeFloatField: function(tag, val) {
        this.writeTag(tag, Pbf.Fixed32);
        this.writeFloat(val);
    },
    writeDoubleField: function(tag, val) {
        this.writeTag(tag, Pbf.Fixed64);
        this.writeDouble(val);
    },
    writeBooleanField: function(tag, val) {
        this.writeVarintField(tag, Boolean(val));
    }
};

function writePackedVarint(arr, pbf)   { for (var i = 0; i < arr.length; i++) pbf.writeVarint(arr[i]);   }
function writePackedSVarint(arr, pbf)  { for (var i = 0; i < arr.length; i++) pbf.writeSVarint(arr[i]);  }
function writePackedFloat(arr, pbf)    { for (var i = 0; i < arr.length; i++) pbf.writeFloat(arr[i]);    }
function writePackedDouble(arr, pbf)   { for (var i = 0; i < arr.length; i++) pbf.writeDouble(arr[i]);   }
function writePackedBoolean(arr, pbf)  { for (var i = 0; i < arr.length; i++) pbf.writeBoolean(arr[i]);  }
function writePackedFixed32(arr, pbf)  { for (var i = 0; i < arr.length; i++) pbf.writeFixed32(arr[i]);  }
function writePackedSFixed32(arr, pbf) { for (var i = 0; i < arr.length; i++) pbf.writeSFixed32(arr[i]); }
function writePackedFixed64(arr, pbf)  { for (var i = 0; i < arr.length; i++) pbf.writeFixed64(arr[i]);  }
function writePackedSFixed64(arr, pbf) { for (var i = 0; i < arr.length; i++) pbf.writeSFixed64(arr[i]); }

}).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {})
},{"./buffer":1}],3:[function(_dereq_,module,exports){
exports.read = function (buffer, offset, isLE, mLen, nBytes) {
  var e, m
  var eLen = nBytes * 8 - mLen - 1
  var eMax = (1 << eLen) - 1
  var eBias = eMax >> 1
  var nBits = -7
  var i = isLE ? (nBytes - 1) : 0
  var d = isLE ? -1 : 1
  var s = buffer[offset + i]

  i += d

  e = s & ((1 << (-nBits)) - 1)
  s >>= (-nBits)
  nBits += eLen
  for (; nBits > 0; e = e * 256 + buffer[offset + i], i += d, nBits -= 8) {}

  m = e & ((1 << (-nBits)) - 1)
  e >>= (-nBits)
  nBits += mLen
  for (; nBits > 0; m = m * 256 + buffer[offset + i], i += d, nBits -= 8) {}

  if (e === 0) {
    e = 1 - eBias
  } else if (e === eMax) {
    return m ? NaN : ((s ? -1 : 1) * Infinity)
  } else {
    m = m + Math.pow(2, mLen)
    e = e - eBias
  }
  return (s ? -1 : 1) * m * Math.pow(2, e - mLen)
}

exports.write = function (buffer, value, offset, isLE, mLen, nBytes) {
  var e, m, c
  var eLen = nBytes * 8 - mLen - 1
  var eMax = (1 << eLen) - 1
  var eBias = eMax >> 1
  var rt = (mLen === 23 ? Math.pow(2, -24) - Math.pow(2, -77) : 0)
  var i = isLE ? 0 : (nBytes - 1)
  var d = isLE ? 1 : -1
  var s = value < 0 || (value === 0 && 1 / value < 0) ? 1 : 0

  value = Math.abs(value)

  if (isNaN(value) || value === Infinity) {
    m = isNaN(value) ? 1 : 0
    e = eMax
  } else {
    e = Math.floor(Math.log(value) / Math.LN2)
    if (value * (c = Math.pow(2, -e)) < 1) {
      e--
      c *= 2
    }
    if (e + eBias >= 1) {
      value += rt / c
    } else {
      value += rt * Math.pow(2, 1 - eBias)
    }
    if (value * c >= 2) {
      e++
      c /= 2
    }

    if (e + eBias >= eMax) {
      m = 0
      e = eMax
    } else if (e + eBias >= 1) {
      m = (value * c - 1) * Math.pow(2, mLen)
      e = e + eBias
    } else {
      m = value * Math.pow(2, eBias - 1) * Math.pow(2, mLen)
      e = 0
    }
  }

  for (; mLen >= 8; buffer[offset + i] = m & 0xff, i += d, m /= 256, mLen -= 8) {}

  e = (e << mLen) | m
  eLen += mLen
  for (; eLen > 0; buffer[offset + i] = e & 0xff, i += d, e /= 256, eLen -= 8) {}

  buffer[offset + i - d] |= s * 128
}

},{}]},{},[2])(2)
});
ol.ext.pbf = module.exports;
})();

goog.provide('ol.ext.vectortile');
/** @typedef {function(*)} */
ol.ext.vectortile;
(function() {
var exports = {};
var module = {exports: exports};
var define;
/**
 * @fileoverview
 * @suppress {accessControls, ambiguousFunctionDecl, checkDebuggerStatement, checkRegExp, checkTypes, checkVars, const, constantProperty, deprecated, duplicate, es5Strict, fileoverviewTags, missingProperties, nonStandardJsDocs, strictModuleDepCheck, suspiciousCode, undefinedNames, undefinedVars, unknownDefines, uselessCode, visibility}
 */
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.vectortile = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
module.exports.VectorTile = _dereq_('./lib/vectortile.js');
module.exports.VectorTileFeature = _dereq_('./lib/vectortilefeature.js');
module.exports.VectorTileLayer = _dereq_('./lib/vectortilelayer.js');

},{"./lib/vectortile.js":2,"./lib/vectortilefeature.js":3,"./lib/vectortilelayer.js":4}],2:[function(_dereq_,module,exports){
'use strict';

var VectorTileLayer = _dereq_('./vectortilelayer');

module.exports = VectorTile;

function VectorTile(pbf, end) {
    this.layers = pbf.readFields(readTile, {}, end);
}

function readTile(tag, layers, pbf) {
    if (tag === 3) {
        var layer = new VectorTileLayer(pbf, pbf.readVarint() + pbf.pos);
        if (layer.length) layers[layer.name] = layer;
    }
}


},{"./vectortilelayer":4}],3:[function(_dereq_,module,exports){
'use strict';

var Point = _dereq_('point-geometry');

module.exports = VectorTileFeature;

function VectorTileFeature(pbf, end, extent, keys, values) {
    // Public
    this.properties = {};
    this.extent = extent;
    this.type = 0;

    // Private
    this._pbf = pbf;
    this._geometry = -1;
    this._keys = keys;
    this._values = values;

    pbf.readFields(readFeature, this, end);
}

function readFeature(tag, feature, pbf) {
    if (tag == 1) feature._id = pbf.readVarint();
    else if (tag == 2) readTag(pbf, feature);
    else if (tag == 3) feature.type = pbf.readVarint();
    else if (tag == 4) feature._geometry = pbf.pos;
}

function readTag(pbf, feature) {
    var end = pbf.readVarint() + pbf.pos;

    while (pbf.pos < end) {
        var key = feature._keys[pbf.readVarint()],
            value = feature._values[pbf.readVarint()];
        feature.properties[key] = value;
    }
}

VectorTileFeature.types = ['Unknown', 'Point', 'LineString', 'Polygon'];

VectorTileFeature.prototype.loadGeometry = function() {
    var pbf = this._pbf;
    pbf.pos = this._geometry;

    var end = pbf.readVarint() + pbf.pos,
        cmd = 1,
        length = 0,
        x = 0,
        y = 0,
        lines = [],
        line;

    while (pbf.pos < end) {
        if (!length) {
            var cmdLen = pbf.readVarint();
            cmd = cmdLen & 0x7;
            length = cmdLen >> 3;
        }

        length--;

        if (cmd === 1 || cmd === 2) {
            x += pbf.readSVarint();
            y += pbf.readSVarint();

            if (cmd === 1) { // moveTo
                if (line) lines.push(line);
                line = [];
            }

            line.push(new Point(x, y));

        } else if (cmd === 7) {

            // Workaround for https://github.com/mapbox/mapnik-vector-tile/issues/90
            if (line) {
                line.push(line[0].clone()); // closePolygon
            }

        } else {
            throw new Error('unknown command ' + cmd);
        }
    }

    if (line) lines.push(line);

    return lines;
};

VectorTileFeature.prototype.bbox = function() {
    var pbf = this._pbf;
    pbf.pos = this._geometry;

    var end = pbf.readVarint() + pbf.pos,
        cmd = 1,
        length = 0,
        x = 0,
        y = 0,
        x1 = Infinity,
        x2 = -Infinity,
        y1 = Infinity,
        y2 = -Infinity;

    while (pbf.pos < end) {
        if (!length) {
            var cmdLen = pbf.readVarint();
            cmd = cmdLen & 0x7;
            length = cmdLen >> 3;
        }

        length--;

        if (cmd === 1 || cmd === 2) {
            x += pbf.readSVarint();
            y += pbf.readSVarint();
            if (x < x1) x1 = x;
            if (x > x2) x2 = x;
            if (y < y1) y1 = y;
            if (y > y2) y2 = y;

        } else if (cmd !== 7) {
            throw new Error('unknown command ' + cmd);
        }
    }

    return [x1, y1, x2, y2];
};

VectorTileFeature.prototype.toGeoJSON = function(x, y, z) {
    var size = this.extent * Math.pow(2, z),
        x0 = this.extent * x,
        y0 = this.extent * y,
        coords = this.loadGeometry(),
        type = VectorTileFeature.types[this.type];

    for (var i = 0; i < coords.length; i++) {
        var line = coords[i];
        for (var j = 0; j < line.length; j++) {
            var p = line[j], y2 = 180 - (p.y + y0) * 360 / size;
            line[j] = [
                (p.x + x0) * 360 / size - 180,
                360 / Math.PI * Math.atan(Math.exp(y2 * Math.PI / 180)) - 90
            ];
        }
    }

    if (type === 'Point' && coords.length === 1) {
        coords = coords[0][0];
    } else if (type === 'Point') {
        coords = coords[0];
        type = 'MultiPoint';
    } else if (type === 'LineString' && coords.length === 1) {
        coords = coords[0];
    } else if (type === 'LineString') {
        type = 'MultiLineString';
    }

    var result = {
        type: "Feature",
        geometry: {
            type: type,
            coordinates: coords
        },
        properties: this.properties
    };

    if ('_id' in this) {
        result.id = this._id;
    }

    return result;
};

},{"point-geometry":5}],4:[function(_dereq_,module,exports){
'use strict';

var VectorTileFeature = _dereq_('./vectortilefeature.js');

module.exports = VectorTileLayer;

function VectorTileLayer(pbf, end) {
    // Public
    this.version = 1;
    this.name = null;
    this.extent = 4096;
    this.length = 0;

    // Private
    this._pbf = pbf;
    this._keys = [];
    this._values = [];
    this._features = [];

    pbf.readFields(readLayer, this, end);

    this.length = this._features.length;
}

function readLayer(tag, layer, pbf) {
    if (tag === 15) layer.version = pbf.readVarint();
    else if (tag === 1) layer.name = pbf.readString();
    else if (tag === 5) layer.extent = pbf.readVarint();
    else if (tag === 2) layer._features.push(pbf.pos);
    else if (tag === 3) layer._keys.push(pbf.readString());
    else if (tag === 4) layer._values.push(readValueMessage(pbf));
}

function readValueMessage(pbf) {
    var value = null,
        end = pbf.readVarint() + pbf.pos;

    while (pbf.pos < end) {
        var tag = pbf.readVarint() >> 3;

        value = tag === 1 ? pbf.readString() :
            tag === 2 ? pbf.readFloat() :
            tag === 3 ? pbf.readDouble() :
            tag === 4 ? pbf.readVarint64() :
            tag === 5 ? pbf.readVarint() :
            tag === 6 ? pbf.readSVarint() :
            tag === 7 ? pbf.readBoolean() : null;
    }

    return value;
}

// return feature `i` from this layer as a `VectorTileFeature`
VectorTileLayer.prototype.feature = function(i) {
    if (i < 0 || i >= this._features.length) throw new Error('feature index out of bounds');

    this._pbf.pos = this._features[i];

    var end = this._pbf.readVarint() + this._pbf.pos;
    return new VectorTileFeature(this._pbf, end, this.extent, this._keys, this._values);
};

},{"./vectortilefeature.js":3}],5:[function(_dereq_,module,exports){
'use strict';

module.exports = Point;

function Point(x, y) {
    this.x = x;
    this.y = y;
}

Point.prototype = {
    clone: function() { return new Point(this.x, this.y); },

    add:     function(p) { return this.clone()._add(p);     },
    sub:     function(p) { return this.clone()._sub(p);     },
    mult:    function(k) { return this.clone()._mult(k);    },
    div:     function(k) { return this.clone()._div(k);     },
    rotate:  function(a) { return this.clone()._rotate(a);  },
    matMult: function(m) { return this.clone()._matMult(m); },
    unit:    function() { return this.clone()._unit(); },
    perp:    function() { return this.clone()._perp(); },
    round:   function() { return this.clone()._round(); },

    mag: function() {
        return Math.sqrt(this.x * this.x + this.y * this.y);
    },

    equals: function(p) {
        return this.x === p.x &&
               this.y === p.y;
    },

    dist: function(p) {
        return Math.sqrt(this.distSqr(p));
    },

    distSqr: function(p) {
        var dx = p.x - this.x,
            dy = p.y - this.y;
        return dx * dx + dy * dy;
    },

    angle: function() {
        return Math.atan2(this.y, this.x);
    },

    angleTo: function(b) {
        return Math.atan2(this.y - b.y, this.x - b.x);
    },

    angleWith: function(b) {
        return this.angleWithSep(b.x, b.y);
    },

    // Find the angle of the two vectors, solving the formula for the cross product a x b = |a||b|sin(θ) for θ.
    angleWithSep: function(x, y) {
        return Math.atan2(
            this.x * y - this.y * x,
            this.x * x + this.y * y);
    },

    _matMult: function(m) {
        var x = m[0] * this.x + m[1] * this.y,
            y = m[2] * this.x + m[3] * this.y;
        this.x = x;
        this.y = y;
        return this;
    },

    _add: function(p) {
        this.x += p.x;
        this.y += p.y;
        return this;
    },

    _sub: function(p) {
        this.x -= p.x;
        this.y -= p.y;
        return this;
    },

    _mult: function(k) {
        this.x *= k;
        this.y *= k;
        return this;
    },

    _div: function(k) {
        this.x /= k;
        this.y /= k;
        return this;
    },

    _unit: function() {
        this._div(this.mag());
        return this;
    },

    _perp: function() {
        var y = this.y;
        this.y = this.x;
        this.x = -y;
        return this;
    },

    _rotate: function(angle) {
        var cos = Math.cos(angle),
            sin = Math.sin(angle),
            x = cos * this.x - sin * this.y,
            y = sin * this.x + cos * this.y;
        this.x = x;
        this.y = y;
        return this;
    },

    _round: function() {
        this.x = Math.round(this.x);
        this.y = Math.round(this.y);
        return this;
    }
};

// constructs Point from an array if necessary
Point.convert = function (a) {
    if (a instanceof Point) {
        return a;
    }
    if (Array.isArray(a)) {
        return new Point(a[0], a[1]);
    }
    return a;
};

},{}]},{},[1])(1)
});
ol.ext.vectortile = module.exports;
})();

//FIXME Implement projection handling

goog.provide('ol.format.MVT');

goog.require('goog.asserts');
goog.require('ol.Feature');
goog.require('ol.ext.pbf');
goog.require('ol.ext.vectortile');
goog.require('ol.format.Feature');
goog.require('ol.format.FormatType');
goog.require('ol.geom.Geometry');
goog.require('ol.geom.GeometryLayout');
goog.require('ol.geom.GeometryType');
goog.require('ol.geom.LineString');
goog.require('ol.geom.MultiLineString');
goog.require('ol.geom.MultiPoint');
goog.require('ol.geom.Point');
goog.require('ol.geom.Polygon');
goog.require('ol.proj');
goog.require('ol.proj.Projection');
goog.require('ol.proj.Units');
goog.require('ol.render.Feature');


/**
 * @classdesc
 * Feature format for reading data in the Mapbox MVT format.
 *
 * @constructor
 * @extends {ol.format.Feature}
 * @param {olx.format.MVTOptions=} opt_options Options.
 * @api
 */
ol.format.MVT = function(opt_options) {

  goog.base(this);

  var options = opt_options ? opt_options : {};

  /**
   * @type {ol.proj.Projection}
   */
  this.defaultDataProjection = new ol.proj.Projection({
    code: 'EPSG:3857',
    units: ol.proj.Units.TILE_PIXELS
  });

  /**
   * @private
   * @type {function((ol.geom.Geometry|Object.<string, *>)=)|
   *     function(ol.geom.GeometryType,Array.<number>,
   *         (Array.<number>|Array.<Array.<number>>),Object.<string, *>)}
   */
  this.featureClass_ = options.featureClass ?
      options.featureClass : ol.render.Feature;

  /**
   * @private
   * @type {string}
   */
  this.geometryName_ = options.geometryName ?
      options.geometryName : 'geometry';

  /**
   * @private
   * @type {string}
   */
  this.layerName_ = options.layerName ? options.layerName : 'layer';

  /**
   * @private
   * @type {Array.<string>}
   */
  this.layers_ = options.layers ? options.layers : null;

};
goog.inherits(ol.format.MVT, ol.format.Feature);


/**
 * @inheritDoc
 */
ol.format.MVT.prototype.getType = function() {
  return ol.format.FormatType.ARRAY_BUFFER;
};


/**
 * @private
 * @param {Object} rawFeature Raw Mapbox feature.
 * @param {string} layer Layer.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {ol.Feature} Feature.
 */
ol.format.MVT.prototype.readFeature_ = function(
    rawFeature, layer, opt_options) {
  var feature = new this.featureClass_();
  var values = rawFeature.properties;
  values[this.layerName_] = layer;
  var geometry = ol.format.Feature.transformWithOptions(
      ol.format.MVT.readGeometry_(rawFeature), false,
      this.adaptOptions(opt_options));
  if (geometry) {
    goog.asserts.assertInstanceof(geometry, ol.geom.Geometry);
    values[this.geometryName_] = geometry;
  }
  feature.setProperties(values);
  feature.setGeometryName(this.geometryName_);
  return feature;
};


/**
 * @private
 * @param {Object} rawFeature Raw Mapbox feature.
 * @param {string} layer Layer.
 * @return {ol.render.Feature} Feature.
 */
ol.format.MVT.prototype.readRenderFeature_ = function(rawFeature, layer) {
  var coords = rawFeature.loadGeometry();
  var ends = [];
  var flatCoordinates = [];
  ol.format.MVT.calculateFlatCoordinates_(coords, flatCoordinates, ends);

  var type = rawFeature.type;
  /** @type {ol.geom.GeometryType} */
  var geometryType;
  if (type === 1) {
    geometryType = coords.length === 1 ?
        ol.geom.GeometryType.POINT : ol.geom.GeometryType.MULTI_POINT;
  } else if (type === 2) {
    if (coords.length === 1) {
      geometryType = ol.geom.GeometryType.LINE_STRING;
    } else {
      geometryType = ol.geom.GeometryType.MULTI_LINE_STRING;
    }
  } else if (type === 3) {
    geometryType = ol.geom.GeometryType.POLYGON;
  }

  var values = rawFeature.properties;
  values[this.layerName_] = layer;

  return new this.featureClass_(geometryType, flatCoordinates, ends, values);
};


/**
 * @inheritDoc
 */
ol.format.MVT.prototype.readFeatures = function(source, opt_options) {
  goog.asserts.assertInstanceof(source, ArrayBuffer);

  var layers = this.layers_;

  var pbf = new ol.ext.pbf(source);
  var tile = new ol.ext.vectortile.VectorTile(pbf);
  var features = [];
  var featureClass = this.featureClass_;
  var layer, feature;
  for (var name in tile.layers) {
    if (layers && layers.indexOf(name) == -1) {
      continue;
    }
    layer = tile.layers[name];

    for (var i = 0, ii = layer.length; i < ii; ++i) {
      if (featureClass === ol.render.Feature) {
        feature = this.readRenderFeature_(layer.feature(i), name);
      } else {
        feature = this.readFeature_(layer.feature(i), name, opt_options);
      }
      features.push(feature);
    }
  }

  return features;
};


/**
 * @inheritDoc
 */
ol.format.MVT.prototype.readProjection = function(source) {
  return this.defaultDataProjection;
};


/**
 * Sets the layers that features will be read from.
 * @param {Array.<string>} layers Layers.
 * @api
 */
ol.format.MVT.prototype.setLayers = function(layers) {
  this.layers_ = layers;
};


/**
 * @private
 * @param {Object} coords Raw feature coordinates.
 * @param {Array.<number>} flatCoordinates Flat coordinates to be populated by
 *     this function.
 * @param {Array.<number>} ends Ends to be populated by this function.
 */
ol.format.MVT.calculateFlatCoordinates_ = function(
    coords, flatCoordinates, ends) {
  var end = 0;
  for (var i = 0, ii = coords.length; i < ii; ++i) {
    var line = coords[i];
    var j, jj;
    for (j = 0, jj = line.length; j < jj; ++j) {
      var coord = line[j];
      // Non-tilespace coords can be calculated here when a TileGrid and
      // TileCoord are known.
      flatCoordinates.push(coord.x, coord.y);
    }
    end += 2 * j;
    ends.push(end);
  }
};


/**
 * @private
 * @param {Object} rawFeature Raw Mapbox feature.
 * @return {ol.geom.Geometry} Geometry.
 */
ol.format.MVT.readGeometry_ = function(rawFeature) {
  var type = rawFeature.type;
  if (type === 0) {
    return null;
  }

  var coords = rawFeature.loadGeometry();
  var ends = [];
  var flatCoordinates = [];
  ol.format.MVT.calculateFlatCoordinates_(coords, flatCoordinates, ends);

  var geom;
  if (type === 1) {
    geom = coords.length === 1 ?
        new ol.geom.Point(null) : new ol.geom.MultiPoint(null);
  } else if (type === 2) {
    if (coords.length === 1) {
      geom = new ol.geom.LineString(null);
    } else {
      geom = new ol.geom.MultiLineString(null);
    }
  } else if (type === 3) {
    geom = new ol.geom.Polygon(null);
  }

  geom.setFlatCoordinates(ol.geom.GeometryLayout.XY, flatCoordinates,
      ends);

  return geom;
};

// FIXME add typedef for stack state objects
goog.provide('ol.format.OSMXML');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom.NodeType');
goog.require('goog.object');
goog.require('ol.Feature');
goog.require('ol.format.Feature');
goog.require('ol.format.XMLFeature');
goog.require('ol.geom.GeometryLayout');
goog.require('ol.geom.LineString');
goog.require('ol.geom.Point');
goog.require('ol.geom.Polygon');
goog.require('ol.proj');
goog.require('ol.xml');


/**
 * @classdesc
 * Feature format for reading data in the
 * [OSMXML format](http://wiki.openstreetmap.org/wiki/OSM_XML).
 *
 * @constructor
 * @extends {ol.format.XMLFeature}
 * @api stable
 */
ol.format.OSMXML = function() {
  goog.base(this);

  /**
   * @inheritDoc
   */
  this.defaultDataProjection = ol.proj.get('EPSG:4326');
};
goog.inherits(ol.format.OSMXML, ol.format.XMLFeature);


/**
 * @const
 * @type {Array.<string>}
 * @private
 */
ol.format.OSMXML.EXTENSIONS_ = ['.osm'];


/**
 * @inheritDoc
 */
ol.format.OSMXML.prototype.getExtensions = function() {
  return ol.format.OSMXML.EXTENSIONS_;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.OSMXML.readNode_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'node', 'localName should be node');
  var options = /** @type {olx.format.ReadOptions} */ (objectStack[0]);
  var state = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  var id = node.getAttribute('id');
  var coordinates = /** @type {Array.<number>} */ ([
    parseFloat(node.getAttribute('lon')),
    parseFloat(node.getAttribute('lat'))
  ]);
  state.nodes[id] = coordinates;

  var values = ol.xml.pushParseAndPop({
    tags: {}
  }, ol.format.OSMXML.NODE_PARSERS_, node, objectStack);
  if (!goog.object.isEmpty(values.tags)) {
    var geometry = new ol.geom.Point(coordinates);
    ol.format.Feature.transformWithOptions(geometry, false, options);
    var feature = new ol.Feature(geometry);
    feature.setId(id);
    feature.setProperties(values.tags);
    state.features.push(feature);
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.OSMXML.readWay_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'way', 'localName should be way');
  var options = /** @type {olx.format.ReadOptions} */ (objectStack[0]);
  var id = node.getAttribute('id');
  var values = ol.xml.pushParseAndPop({
    ndrefs: [],
    tags: {}
  }, ol.format.OSMXML.WAY_PARSERS_, node, objectStack);
  var state = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  var flatCoordinates = /** @type {Array.<number>} */ ([]);
  for (var i = 0, ii = values.ndrefs.length; i < ii; i++) {
    var point = state.nodes[values.ndrefs[i]];
    goog.array.extend(flatCoordinates, point);
  }
  var geometry;
  if (values.ndrefs[0] == values.ndrefs[values.ndrefs.length - 1]) {
    // closed way
    geometry = new ol.geom.Polygon(null);
    geometry.setFlatCoordinates(ol.geom.GeometryLayout.XY, flatCoordinates,
        [flatCoordinates.length]);
  } else {
    geometry = new ol.geom.LineString(null);
    geometry.setFlatCoordinates(ol.geom.GeometryLayout.XY, flatCoordinates);
  }
  ol.format.Feature.transformWithOptions(geometry, false, options);
  var feature = new ol.Feature(geometry);
  feature.setId(id);
  feature.setProperties(values.tags);
  state.features.push(feature);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.OSMXML.readNd_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'nd', 'localName should be nd');
  var values = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  values.ndrefs.push(node.getAttribute('ref'));
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.OSMXML.readTag_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'tag', 'localName should be tag');
  var values = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  values.tags[node.getAttribute('k')] = node.getAttribute('v');
};


/**
 * @const
 * @private
 * @type {Array.<string>}
 */
ol.format.OSMXML.NAMESPACE_URIS_ = [
  null
];


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.OSMXML.WAY_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.OSMXML.NAMESPACE_URIS_, {
      'nd': ol.format.OSMXML.readNd_,
      'tag': ol.format.OSMXML.readTag_
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.OSMXML.PARSERS_ = ol.xml.makeStructureNS(
    ol.format.OSMXML.NAMESPACE_URIS_, {
      'node': ol.format.OSMXML.readNode_,
      'way': ol.format.OSMXML.readWay_
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.OSMXML.NODE_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.OSMXML.NAMESPACE_URIS_, {
      'tag': ol.format.OSMXML.readTag_
    });


/**
 * Read all features from an OSM source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<ol.Feature>} Features.
 * @api stable
 */
ol.format.OSMXML.prototype.readFeatures;


/**
 * @inheritDoc
 */
ol.format.OSMXML.prototype.readFeaturesFromNode = function(node, opt_options) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  var options = this.getReadOptions(node, opt_options);
  if (node.localName == 'osm') {
    var state = ol.xml.pushParseAndPop({
      nodes: {},
      features: []
    }, ol.format.OSMXML.PARSERS_, node, [options]);
    if (state.features) {
      return state.features;
    }
  }
  return [];
};


/**
 * Read the projection from an OSM source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @return {ol.proj.Projection} Projection.
 * @api stable
 */
ol.format.OSMXML.prototype.readProjection;

goog.provide('ol.format.XLink');


/**
 * @const
 * @type {string}
 */
ol.format.XLink.NAMESPACE_URI = 'http://www.w3.org/1999/xlink';


/**
 * @param {Node} node Node.
 * @return {boolean|undefined} Boolean.
 */
ol.format.XLink.readHref = function(node) {
  return node.getAttributeNS(ol.format.XLink.NAMESPACE_URI, 'href');
};

goog.provide('ol.format.XML');

goog.require('goog.asserts');
goog.require('ol.xml');


/**
 * @classdesc
 * Generic format for reading non-feature XML data
 *
 * @constructor
 */
ol.format.XML = function() {
};


/**
 * @param {Document|Node|string} source Source.
 * @return {Object} The parsed result.
 */
ol.format.XML.prototype.read = function(source) {
  if (ol.xml.isDocument(source)) {
    return this.readFromDocument(/** @type {Document} */ (source));
  } else if (ol.xml.isNode(source)) {
    return this.readFromNode(/** @type {Node} */ (source));
  } else if (goog.isString(source)) {
    var doc = ol.xml.parse(source);
    return this.readFromDocument(doc);
  } else {
    goog.asserts.fail();
    return null;
  }
};


/**
 * @param {Document} doc Document.
 * @return {Object}
 */
ol.format.XML.prototype.readFromDocument = goog.abstractMethod;


/**
 * @param {Node} node Node.
 * @return {Object}
 */
ol.format.XML.prototype.readFromNode = goog.abstractMethod;

goog.provide('ol.format.OWS');

goog.require('goog.asserts');
goog.require('goog.dom.NodeType');
goog.require('ol.format.XLink');
goog.require('ol.format.XML');
goog.require('ol.format.XSD');
goog.require('ol.xml');


/**
 * @constructor
 * @extends {ol.format.XML}
 */
ol.format.OWS = function() {
  goog.base(this);
};
goog.inherits(ol.format.OWS, ol.format.XML);


/**
 * @param {Document} doc Document.
 * @return {Object} OWS object.
 */
ol.format.OWS.prototype.readFromDocument = function(doc) {
  goog.asserts.assert(doc.nodeType == goog.dom.NodeType.DOCUMENT,
      'doc.nodeType should be DOCUMENT');
  for (var n = doc.firstChild; n; n = n.nextSibling) {
    if (n.nodeType == goog.dom.NodeType.ELEMENT) {
      return this.readFromNode(n);
    }
  }
  return null;
};


/**
 * @param {Node} node Node.
 * @return {Object} OWS object.
 */
ol.format.OWS.prototype.readFromNode = function(node) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  var owsObject = ol.xml.pushParseAndPop({},
      ol.format.OWS.PARSERS_, node, []);
  return owsObject ? owsObject : null;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The address.
 */
ol.format.OWS.readAddress_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Address',
      'localName should be Address');
  return ol.xml.pushParseAndPop({},
      ol.format.OWS.ADDRESS_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The values.
 */
ol.format.OWS.readAllowedValues_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'AllowedValues',
      'localName should be AllowedValues');
  return ol.xml.pushParseAndPop({},
      ol.format.OWS.ALLOWED_VALUES_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The constraint.
 */
ol.format.OWS.readConstraint_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Constraint',
      'localName should be Constraint');
  var name = node.getAttribute('name');
  if (!name) {
    return undefined;
  }
  return ol.xml.pushParseAndPop({'name': name},
      ol.format.OWS.CONSTRAINT_PARSERS_, node,
      objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The contact info.
 */
ol.format.OWS.readContactInfo_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'ContactInfo',
      'localName should be ContactInfo');
  return ol.xml.pushParseAndPop({},
      ol.format.OWS.CONTACT_INFO_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The DCP.
 */
ol.format.OWS.readDcp_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'DCP', 'localName should be DCP');
  return ol.xml.pushParseAndPop({},
      ol.format.OWS.DCP_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The GET object.
 */
ol.format.OWS.readGet_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Get', 'localName should be Get');
  var href = ol.format.XLink.readHref(node);
  if (!href) {
    return undefined;
  }
  return ol.xml.pushParseAndPop({'href': href},
      ol.format.OWS.REQUEST_METHOD_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The HTTP object.
 */
ol.format.OWS.readHttp_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'HTTP', 'localName should be HTTP');
  return ol.xml.pushParseAndPop({}, ol.format.OWS.HTTP_PARSERS_,
      node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The operation.
 */
ol.format.OWS.readOperation_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Operation',
      'localName should be Operation');
  var name = node.getAttribute('name');
  var value = ol.xml.pushParseAndPop({},
      ol.format.OWS.OPERATION_PARSERS_, node, objectStack);
  if (!value) {
    return undefined;
  }
  var object = /** @type {Object} */
      (objectStack[objectStack.length - 1]);
  goog.asserts.assert(goog.isObject(object), 'object should be an Object');
  object[name] = value;

};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The operations metadata.
 */
ol.format.OWS.readOperationsMetadata_ = function(node,
    objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'OperationsMetadata',
      'localName should be OperationsMetadata');
  return ol.xml.pushParseAndPop({},
      ol.format.OWS.OPERATIONS_METADATA_PARSERS_, node,
      objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The phone.
 */
ol.format.OWS.readPhone_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Phone', 'localName should be Phone');
  return ol.xml.pushParseAndPop({},
      ol.format.OWS.PHONE_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The service identification.
 */
ol.format.OWS.readServiceIdentification_ = function(node,
    objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'ServiceIdentification',
      'localName should be ServiceIdentification');
  return ol.xml.pushParseAndPop(
      {}, ol.format.OWS.SERVICE_IDENTIFICATION_PARSERS_, node,
      objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The service contact.
 */
ol.format.OWS.readServiceContact_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'ServiceContact',
      'localName should be ServiceContact');
  return ol.xml.pushParseAndPop(
      {}, ol.format.OWS.SERVICE_CONTACT_PARSERS_, node,
      objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The service provider.
 */
ol.format.OWS.readServiceProvider_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'ServiceProvider',
      'localName should be ServiceProvider');
  return ol.xml.pushParseAndPop(
      {}, ol.format.OWS.SERVICE_PROVIDER_PARSERS_, node,
      objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {string|undefined} The value.
 */
ol.format.OWS.readValue_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Value', 'localName should be Value');
  return ol.format.XSD.readString(node);
};


/**
 * @const
 * @type {Array.<string>}
 * @private
 */
ol.format.OWS.NAMESPACE_URIS_ = [
  null,
  'http://www.opengis.net/ows/1.1'
];


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.OWS.PARSERS_ = ol.xml.makeStructureNS(
    ol.format.OWS.NAMESPACE_URIS_, {
      'ServiceIdentification': ol.xml.makeObjectPropertySetter(
          ol.format.OWS.readServiceIdentification_),
      'ServiceProvider': ol.xml.makeObjectPropertySetter(
          ol.format.OWS.readServiceProvider_),
      'OperationsMetadata': ol.xml.makeObjectPropertySetter(
          ol.format.OWS.readOperationsMetadata_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.OWS.ADDRESS_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.OWS.NAMESPACE_URIS_, {
      'DeliveryPoint': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString),
      'City': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'AdministrativeArea': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString),
      'PostalCode': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'Country': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'ElectronicMailAddress': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.OWS.ALLOWED_VALUES_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.OWS.NAMESPACE_URIS_, {
      'Value': ol.xml.makeObjectPropertyPusher(ol.format.OWS.readValue_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.OWS.CONSTRAINT_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.OWS.NAMESPACE_URIS_, {
      'AllowedValues': ol.xml.makeObjectPropertySetter(
          ol.format.OWS.readAllowedValues_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.OWS.CONTACT_INFO_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.OWS.NAMESPACE_URIS_, {
      'Phone': ol.xml.makeObjectPropertySetter(ol.format.OWS.readPhone_),
      'Address': ol.xml.makeObjectPropertySetter(ol.format.OWS.readAddress_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.OWS.DCP_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.OWS.NAMESPACE_URIS_, {
      'HTTP': ol.xml.makeObjectPropertySetter(ol.format.OWS.readHttp_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.OWS.HTTP_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.OWS.NAMESPACE_URIS_, {
      'Get': ol.xml.makeObjectPropertyPusher(ol.format.OWS.readGet_),
      'Post': undefined // TODO
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.OWS.OPERATION_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.OWS.NAMESPACE_URIS_, {
      'DCP': ol.xml.makeObjectPropertySetter(ol.format.OWS.readDcp_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.OWS.OPERATIONS_METADATA_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.OWS.NAMESPACE_URIS_, {
      'Operation': ol.format.OWS.readOperation_
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.OWS.PHONE_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.OWS.NAMESPACE_URIS_, {
      'Voice': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'Facsimile': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.OWS.REQUEST_METHOD_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.OWS.NAMESPACE_URIS_, {
      'Constraint': ol.xml.makeObjectPropertyPusher(
          ol.format.OWS.readConstraint_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.OWS.SERVICE_CONTACT_PARSERS_ =
    ol.xml.makeStructureNS(
    ol.format.OWS.NAMESPACE_URIS_, {
      'IndividualName': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString),
      'PositionName': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'ContactInfo': ol.xml.makeObjectPropertySetter(
          ol.format.OWS.readContactInfo_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.OWS.SERVICE_IDENTIFICATION_PARSERS_ =
    ol.xml.makeStructureNS(
    ol.format.OWS.NAMESPACE_URIS_, {
      'Title': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'ServiceTypeVersion': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString),
      'ServiceType': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.OWS.SERVICE_PROVIDER_PARSERS_ =
    ol.xml.makeStructureNS(
    ol.format.OWS.NAMESPACE_URIS_, {
      'ProviderName': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'ProviderSite': ol.xml.makeObjectPropertySetter(ol.format.XLink.readHref),
      'ServiceContact': ol.xml.makeObjectPropertySetter(
          ol.format.OWS.readServiceContact_)
    });

goog.provide('ol.geom.flat.flip');

goog.require('goog.asserts');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {Array.<number>=} opt_dest Destination.
 * @param {number=} opt_destOffset Destination offset.
 * @return {Array.<number>} Flat coordinates.
 */
ol.geom.flat.flip.flipXY = function(flatCoordinates, offset, end, stride, opt_dest, opt_destOffset) {
  var dest, destOffset;
  if (opt_dest !== undefined) {
    dest = opt_dest;
    destOffset = opt_destOffset !== undefined ? opt_destOffset : 0;
  } else {
    goog.asserts.assert(opt_destOffset === undefined,
        'opt_destOffSet should be defined');
    dest = [];
    destOffset = 0;
  }
  var j = offset;
  while (j < end) {
    var x = flatCoordinates[j++];
    dest[destOffset++] = flatCoordinates[j++];
    dest[destOffset++] = x;
    for (var k = 2; k < stride; ++k) {
      dest[destOffset++] = flatCoordinates[j++];
    }
  }
  dest.length = destOffset;
  return dest;
};

goog.provide('ol.format.Polyline');

goog.require('goog.asserts');
goog.require('ol.Feature');
goog.require('ol.format.Feature');
goog.require('ol.format.TextFeature');
goog.require('ol.geom.GeometryLayout');
goog.require('ol.geom.LineString');
goog.require('ol.geom.SimpleGeometry');
goog.require('ol.geom.flat.flip');
goog.require('ol.geom.flat.inflate');
goog.require('ol.proj');


/**
 * @classdesc
 * Feature format for reading and writing data in the Encoded
 * Polyline Algorithm Format.
 *
 * @constructor
 * @extends {ol.format.TextFeature}
 * @param {olx.format.PolylineOptions=} opt_options
 *     Optional configuration object.
 * @api stable
 */
ol.format.Polyline = function(opt_options) {

  var options = opt_options ? opt_options : {};

  goog.base(this);

  /**
   * @inheritDoc
   */
  this.defaultDataProjection = ol.proj.get('EPSG:4326');

  /**
   * @private
   * @type {number}
   */
  this.factor_ = options.factor ? options.factor : 1e5;

  /**
   * @private
   * @type {ol.geom.GeometryLayout}
   */
  this.geometryLayout_ = options.geometryLayout ?
      options.geometryLayout : ol.geom.GeometryLayout.XY;
};
goog.inherits(ol.format.Polyline, ol.format.TextFeature);


/**
 * Encode a list of n-dimensional points and return an encoded string
 *
 * Attention: This function will modify the passed array!
 *
 * @param {Array.<number>} numbers A list of n-dimensional points.
 * @param {number} stride The number of dimension of the points in the list.
 * @param {number=} opt_factor The factor by which the numbers will be
 *     multiplied. The remaining decimal places will get rounded away.
 *     Default is `1e5`.
 * @return {string} The encoded string.
 * @api
 */
ol.format.Polyline.encodeDeltas = function(numbers, stride, opt_factor) {
  var factor = opt_factor ? opt_factor : 1e5;
  var d;

  var lastNumbers = new Array(stride);
  for (d = 0; d < stride; ++d) {
    lastNumbers[d] = 0;
  }

  var i, ii;
  for (i = 0, ii = numbers.length; i < ii;) {
    for (d = 0; d < stride; ++d, ++i) {
      var num = numbers[i];
      var delta = num - lastNumbers[d];
      lastNumbers[d] = num;

      numbers[i] = delta;
    }
  }

  return ol.format.Polyline.encodeFloats(numbers, factor);
};


/**
 * Decode a list of n-dimensional points from an encoded string
 *
 * @param {string} encoded An encoded string.
 * @param {number} stride The number of dimension of the points in the
 *     encoded string.
 * @param {number=} opt_factor The factor by which the resulting numbers will
 *     be divided. Default is `1e5`.
 * @return {Array.<number>} A list of n-dimensional points.
 * @api
 */
ol.format.Polyline.decodeDeltas = function(encoded, stride, opt_factor) {
  var factor = opt_factor ? opt_factor : 1e5;
  var d;

  /** @type {Array.<number>} */
  var lastNumbers = new Array(stride);
  for (d = 0; d < stride; ++d) {
    lastNumbers[d] = 0;
  }

  var numbers = ol.format.Polyline.decodeFloats(encoded, factor);

  var i, ii;
  for (i = 0, ii = numbers.length; i < ii;) {
    for (d = 0; d < stride; ++d, ++i) {
      lastNumbers[d] += numbers[i];

      numbers[i] = lastNumbers[d];
    }
  }

  return numbers;
};


/**
 * Encode a list of floating point numbers and return an encoded string
 *
 * Attention: This function will modify the passed array!
 *
 * @param {Array.<number>} numbers A list of floating point numbers.
 * @param {number=} opt_factor The factor by which the numbers will be
 *     multiplied. The remaining decimal places will get rounded away.
 *     Default is `1e5`.
 * @return {string} The encoded string.
 * @api
 */
ol.format.Polyline.encodeFloats = function(numbers, opt_factor) {
  var factor = opt_factor ? opt_factor : 1e5;
  var i, ii;
  for (i = 0, ii = numbers.length; i < ii; ++i) {
    numbers[i] = Math.round(numbers[i] * factor);
  }

  return ol.format.Polyline.encodeSignedIntegers(numbers);
};


/**
 * Decode a list of floating point numbers from an encoded string
 *
 * @param {string} encoded An encoded string.
 * @param {number=} opt_factor The factor by which the result will be divided.
 *     Default is `1e5`.
 * @return {Array.<number>} A list of floating point numbers.
 * @api
 */
ol.format.Polyline.decodeFloats = function(encoded, opt_factor) {
  var factor = opt_factor ? opt_factor : 1e5;
  var numbers = ol.format.Polyline.decodeSignedIntegers(encoded);
  var i, ii;
  for (i = 0, ii = numbers.length; i < ii; ++i) {
    numbers[i] /= factor;
  }
  return numbers;
};


/**
 * Encode a list of signed integers and return an encoded string
 *
 * Attention: This function will modify the passed array!
 *
 * @param {Array.<number>} numbers A list of signed integers.
 * @return {string} The encoded string.
 */
ol.format.Polyline.encodeSignedIntegers = function(numbers) {
  var i, ii;
  for (i = 0, ii = numbers.length; i < ii; ++i) {
    var num = numbers[i];
    numbers[i] = (num < 0) ? ~(num << 1) : (num << 1);
  }
  return ol.format.Polyline.encodeUnsignedIntegers(numbers);
};


/**
 * Decode a list of signed integers from an encoded string
 *
 * @param {string} encoded An encoded string.
 * @return {Array.<number>} A list of signed integers.
 */
ol.format.Polyline.decodeSignedIntegers = function(encoded) {
  var numbers = ol.format.Polyline.decodeUnsignedIntegers(encoded);
  var i, ii;
  for (i = 0, ii = numbers.length; i < ii; ++i) {
    var num = numbers[i];
    numbers[i] = (num & 1) ? ~(num >> 1) : (num >> 1);
  }
  return numbers;
};


/**
 * Encode a list of unsigned integers and return an encoded string
 *
 * @param {Array.<number>} numbers A list of unsigned integers.
 * @return {string} The encoded string.
 */
ol.format.Polyline.encodeUnsignedIntegers = function(numbers) {
  var encoded = '';
  var i, ii;
  for (i = 0, ii = numbers.length; i < ii; ++i) {
    encoded += ol.format.Polyline.encodeUnsignedInteger(numbers[i]);
  }
  return encoded;
};


/**
 * Decode a list of unsigned integers from an encoded string
 *
 * @param {string} encoded An encoded string.
 * @return {Array.<number>} A list of unsigned integers.
 */
ol.format.Polyline.decodeUnsignedIntegers = function(encoded) {
  var numbers = [];
  var current = 0;
  var shift = 0;
  var i, ii;
  for (i = 0, ii = encoded.length; i < ii; ++i) {
    var b = encoded.charCodeAt(i) - 63;
    current |= (b & 0x1f) << shift;
    if (b < 0x20) {
      numbers.push(current);
      current = 0;
      shift = 0;
    } else {
      shift += 5;
    }
  }
  return numbers;
};


/**
 * Encode one single unsigned integer and return an encoded string
 *
 * @param {number} num Unsigned integer that should be encoded.
 * @return {string} The encoded string.
 */
ol.format.Polyline.encodeUnsignedInteger = function(num) {
  var value, encoded = '';
  while (num >= 0x20) {
    value = (0x20 | (num & 0x1f)) + 63;
    encoded += String.fromCharCode(value);
    num >>= 5;
  }
  value = num + 63;
  encoded += String.fromCharCode(value);
  return encoded;
};


/**
 * Read the feature from the Polyline source. The coordinates are assumed to be
 * in two dimensions and in latitude, longitude order.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {ol.Feature} Feature.
 * @api stable
 */
ol.format.Polyline.prototype.readFeature;


/**
 * @inheritDoc
 */
ol.format.Polyline.prototype.readFeatureFromText = function(text, opt_options) {
  var geometry = this.readGeometryFromText(text, opt_options);
  return new ol.Feature(geometry);
};


/**
 * Read the feature from the source. As Polyline sources contain a single
 * feature, this will return the feature in an array.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<ol.Feature>} Features.
 * @api stable
 */
ol.format.Polyline.prototype.readFeatures;


/**
 * @inheritDoc
 */
ol.format.Polyline.prototype.readFeaturesFromText = function(text, opt_options) {
  var feature = this.readFeatureFromText(text, opt_options);
  return [feature];
};


/**
 * Read the geometry from the source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {ol.geom.Geometry} Geometry.
 * @api stable
 */
ol.format.Polyline.prototype.readGeometry;


/**
 * @inheritDoc
 */
ol.format.Polyline.prototype.readGeometryFromText = function(text, opt_options) {
  var stride = ol.geom.SimpleGeometry.getStrideForLayout(this.geometryLayout_);
  var flatCoordinates = ol.format.Polyline.decodeDeltas(
      text, stride, this.factor_);
  ol.geom.flat.flip.flipXY(
      flatCoordinates, 0, flatCoordinates.length, stride, flatCoordinates);
  var coordinates = ol.geom.flat.inflate.coordinates(
      flatCoordinates, 0, flatCoordinates.length, stride);

  return /** @type {ol.geom.Geometry} */ (
      ol.format.Feature.transformWithOptions(
          new ol.geom.LineString(coordinates, this.geometryLayout_), false,
          this.adaptOptions(opt_options)));
};


/**
 * Read the projection from a Polyline source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @return {ol.proj.Projection} Projection.
 * @api stable
 */
ol.format.Polyline.prototype.readProjection;


/**
 * @inheritDoc
 */
ol.format.Polyline.prototype.writeFeatureText = function(feature, opt_options) {
  var geometry = feature.getGeometry();
  if (geometry) {
    return this.writeGeometryText(geometry, opt_options);
  } else {
    goog.asserts.fail('geometry needs to be defined');
    return '';
  }
};


/**
 * @inheritDoc
 */
ol.format.Polyline.prototype.writeFeaturesText = function(features, opt_options) {
  goog.asserts.assert(features.length == 1,
      'features array should have 1 item');
  return this.writeFeatureText(features[0], opt_options);
};


/**
 * Write a single geometry in Polyline format.
 *
 * @function
 * @param {ol.geom.Geometry} geometry Geometry.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {string} Geometry.
 * @api stable
 */
ol.format.Polyline.prototype.writeGeometry;


/**
 * @inheritDoc
 */
ol.format.Polyline.prototype.writeGeometryText = function(geometry, opt_options) {
  goog.asserts.assertInstanceof(geometry, ol.geom.LineString,
      'geometry should be an ol.geom.LineString');
  geometry = /** @type {ol.geom.LineString} */
      (ol.format.Feature.transformWithOptions(
          geometry, true, this.adaptOptions(opt_options)));
  var flatCoordinates = geometry.getFlatCoordinates();
  var stride = geometry.getStride();
  ol.geom.flat.flip.flipXY(
      flatCoordinates, 0, flatCoordinates.length, stride, flatCoordinates);
  return ol.format.Polyline.encodeDeltas(flatCoordinates, stride, this.factor_);
};

goog.provide('ol.format.TopoJSON');

goog.require('goog.asserts');
goog.require('goog.object');
goog.require('ol.Feature');
goog.require('ol.format.Feature');
goog.require('ol.format.JSONFeature');
goog.require('ol.geom.LineString');
goog.require('ol.geom.MultiLineString');
goog.require('ol.geom.MultiPoint');
goog.require('ol.geom.MultiPolygon');
goog.require('ol.geom.Point');
goog.require('ol.geom.Polygon');
goog.require('ol.proj');


/**
 * @classdesc
 * Feature format for reading data in the TopoJSON format.
 *
 * @constructor
 * @extends {ol.format.JSONFeature}
 * @param {olx.format.TopoJSONOptions=} opt_options Options.
 * @api stable
 */
ol.format.TopoJSON = function(opt_options) {

  var options = opt_options ? opt_options : {};

  goog.base(this);

  /**
   * @inheritDoc
   */
  this.defaultDataProjection = ol.proj.get(
      options.defaultDataProjection ?
          options.defaultDataProjection : 'EPSG:4326');

};
goog.inherits(ol.format.TopoJSON, ol.format.JSONFeature);


/**
 * @const {Array.<string>}
 * @private
 */
ol.format.TopoJSON.EXTENSIONS_ = ['.topojson'];


/**
 * Concatenate arcs into a coordinate array.
 * @param {Array.<number>} indices Indices of arcs to concatenate.  Negative
 *     values indicate arcs need to be reversed.
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs (already
 *     transformed).
 * @return {Array.<ol.Coordinate>} Coordinates array.
 * @private
 */
ol.format.TopoJSON.concatenateArcs_ = function(indices, arcs) {
  /** @type {Array.<ol.Coordinate>} */
  var coordinates = [];
  var index, arc;
  var i, ii;
  var j, jj;
  for (i = 0, ii = indices.length; i < ii; ++i) {
    index = indices[i];
    if (i > 0) {
      // splicing together arcs, discard last point
      coordinates.pop();
    }
    if (index >= 0) {
      // forward arc
      arc = arcs[index];
    } else {
      // reverse arc
      arc = arcs[~index].slice().reverse();
    }
    coordinates.push.apply(coordinates, arc);
  }
  // provide fresh copies of coordinate arrays
  for (j = 0, jj = coordinates.length; j < jj; ++j) {
    coordinates[j] = coordinates[j].slice();
  }
  return coordinates;
};


/**
 * Create a point from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @return {ol.geom.Point} Geometry.
 * @private
 */
ol.format.TopoJSON.readPointGeometry_ = function(object, scale, translate) {
  var coordinates = object.coordinates;
  if (scale && translate) {
    ol.format.TopoJSON.transformVertex_(coordinates, scale, translate);
  }
  return new ol.geom.Point(coordinates);
};


/**
 * Create a multi-point from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @return {ol.geom.MultiPoint} Geometry.
 * @private
 */
ol.format.TopoJSON.readMultiPointGeometry_ = function(object, scale,
    translate) {
  var coordinates = object.coordinates;
  var i, ii;
  if (scale && translate) {
    for (i = 0, ii = coordinates.length; i < ii; ++i) {
      ol.format.TopoJSON.transformVertex_(coordinates[i], scale, translate);
    }
  }
  return new ol.geom.MultiPoint(coordinates);
};


/**
 * Create a linestring from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
 * @return {ol.geom.LineString} Geometry.
 * @private
 */
ol.format.TopoJSON.readLineStringGeometry_ = function(object, arcs) {
  var coordinates = ol.format.TopoJSON.concatenateArcs_(object.arcs, arcs);
  return new ol.geom.LineString(coordinates);
};


/**
 * Create a multi-linestring from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
 * @return {ol.geom.MultiLineString} Geometry.
 * @private
 */
ol.format.TopoJSON.readMultiLineStringGeometry_ = function(object, arcs) {
  var coordinates = [];
  var i, ii;
  for (i = 0, ii = object.arcs.length; i < ii; ++i) {
    coordinates[i] = ol.format.TopoJSON.concatenateArcs_(object.arcs[i], arcs);
  }
  return new ol.geom.MultiLineString(coordinates);
};


/**
 * Create a polygon from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
 * @return {ol.geom.Polygon} Geometry.
 * @private
 */
ol.format.TopoJSON.readPolygonGeometry_ = function(object, arcs) {
  var coordinates = [];
  var i, ii;
  for (i = 0, ii = object.arcs.length; i < ii; ++i) {
    coordinates[i] = ol.format.TopoJSON.concatenateArcs_(object.arcs[i], arcs);
  }
  return new ol.geom.Polygon(coordinates);
};


/**
 * Create a multi-polygon from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
 * @return {ol.geom.MultiPolygon} Geometry.
 * @private
 */
ol.format.TopoJSON.readMultiPolygonGeometry_ = function(object, arcs) {
  var coordinates = [];
  var polyArray, ringCoords, j, jj;
  var i, ii;
  for (i = 0, ii = object.arcs.length; i < ii; ++i) {
    // for each polygon
    polyArray = object.arcs[i];
    ringCoords = [];
    for (j = 0, jj = polyArray.length; j < jj; ++j) {
      // for each ring
      ringCoords[j] = ol.format.TopoJSON.concatenateArcs_(polyArray[j], arcs);
    }
    coordinates[i] = ringCoords;
  }
  return new ol.geom.MultiPolygon(coordinates);
};


/**
 * @inheritDoc
 */
ol.format.TopoJSON.prototype.getExtensions = function() {
  return ol.format.TopoJSON.EXTENSIONS_;
};


/**
 * Create features from a TopoJSON GeometryCollection object.
 *
 * @param {TopoJSONGeometryCollection} collection TopoJSON Geometry
 *     object.
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<ol.Feature>} Array of features.
 * @private
 */
ol.format.TopoJSON.readFeaturesFromGeometryCollection_ = function(
    collection, arcs, scale, translate, opt_options) {
  var geometries = collection.geometries;
  var features = [];
  var i, ii;
  for (i = 0, ii = geometries.length; i < ii; ++i) {
    features[i] = ol.format.TopoJSON.readFeatureFromGeometry_(
        geometries[i], arcs, scale, translate, opt_options);
  }
  return features;
};


/**
 * Create a feature from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON geometry object.
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {ol.Feature} Feature.
 * @private
 */
ol.format.TopoJSON.readFeatureFromGeometry_ = function(object, arcs,
    scale, translate, opt_options) {
  var geometry;
  var type = object.type;
  var geometryReader = ol.format.TopoJSON.GEOMETRY_READERS_[type];
  goog.asserts.assert(geometryReader, 'geometryReader should be defined');
  if ((type === 'Point') || (type === 'MultiPoint')) {
    geometry = geometryReader(object, scale, translate);
  } else {
    geometry = geometryReader(object, arcs);
  }
  var feature = new ol.Feature();
  feature.setGeometry(/** @type {ol.geom.Geometry} */ (
      ol.format.Feature.transformWithOptions(geometry, false, opt_options)));
  if (object.id !== undefined) {
    feature.setId(object.id);
  }
  if (object.properties) {
    feature.setProperties(object.properties);
  }
  return feature;
};


/**
 * Read all features from a TopoJSON source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @return {Array.<ol.Feature>} Features.
 * @api stable
 */
ol.format.TopoJSON.prototype.readFeatures;


/**
 * @inheritDoc
 */
ol.format.TopoJSON.prototype.readFeaturesFromObject = function(
    object, opt_options) {
  if (object.type == 'Topology') {
    var topoJSONTopology = /** @type {TopoJSONTopology} */ (object);
    var transform, scale = null, translate = null;
    if (topoJSONTopology.transform) {
      transform = topoJSONTopology.transform;
      scale = transform.scale;
      translate = transform.translate;
    }
    var arcs = topoJSONTopology.arcs;
    if (transform) {
      ol.format.TopoJSON.transformArcs_(arcs, scale, translate);
    }
    /** @type {Array.<ol.Feature>} */
    var features = [];
    var topoJSONFeatures = goog.object.getValues(topoJSONTopology.objects);
    var i, ii;
    var feature;
    for (i = 0, ii = topoJSONFeatures.length; i < ii; ++i) {
      if (topoJSONFeatures[i].type === 'GeometryCollection') {
        feature = /** @type {TopoJSONGeometryCollection} */
            (topoJSONFeatures[i]);
        features.push.apply(features,
            ol.format.TopoJSON.readFeaturesFromGeometryCollection_(
                feature, arcs, scale, translate, opt_options));
      } else {
        feature = /** @type {TopoJSONGeometry} */
            (topoJSONFeatures[i]);
        features.push(ol.format.TopoJSON.readFeatureFromGeometry_(
            feature, arcs, scale, translate, opt_options));
      }
    }
    return features;
  } else {
    return [];
  }
};


/**
 * Apply a linear transform to array of arcs.  The provided array of arcs is
 * modified in place.
 *
 * @param {Array.<Array.<ol.Coordinate>>} arcs Array of arcs.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @private
 */
ol.format.TopoJSON.transformArcs_ = function(arcs, scale, translate) {
  var i, ii;
  for (i = 0, ii = arcs.length; i < ii; ++i) {
    ol.format.TopoJSON.transformArc_(arcs[i], scale, translate);
  }
};


/**
 * Apply a linear transform to an arc.  The provided arc is modified in place.
 *
 * @param {Array.<ol.Coordinate>} arc Arc.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @private
 */
ol.format.TopoJSON.transformArc_ = function(arc, scale, translate) {
  var x = 0;
  var y = 0;
  var vertex;
  var i, ii;
  for (i = 0, ii = arc.length; i < ii; ++i) {
    vertex = arc[i];
    x += vertex[0];
    y += vertex[1];
    vertex[0] = x;
    vertex[1] = y;
    ol.format.TopoJSON.transformVertex_(vertex, scale, translate);
  }
};


/**
 * Apply a linear transform to a vertex.  The provided vertex is modified in
 * place.
 *
 * @param {ol.Coordinate} vertex Vertex.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @private
 */
ol.format.TopoJSON.transformVertex_ = function(vertex, scale, translate) {
  vertex[0] = vertex[0] * scale[0] + translate[0];
  vertex[1] = vertex[1] * scale[1] + translate[1];
};


/**
 * Read the projection from a TopoJSON source.
 *
 * @function
 * @param {Document|Node|Object|string} object Source.
 * @return {ol.proj.Projection} Projection.
 * @api stable
 */
ol.format.TopoJSON.prototype.readProjection = function(object) {
  return this.defaultDataProjection;
};


/**
 * @const
 * @private
 * @type {Object.<string, function(TopoJSONGeometry, Array, ...Array): ol.geom.Geometry>}
 */
ol.format.TopoJSON.GEOMETRY_READERS_ = {
  'Point': ol.format.TopoJSON.readPointGeometry_,
  'LineString': ol.format.TopoJSON.readLineStringGeometry_,
  'Polygon': ol.format.TopoJSON.readPolygonGeometry_,
  'MultiPoint': ol.format.TopoJSON.readMultiPointGeometry_,
  'MultiLineString': ol.format.TopoJSON.readMultiLineStringGeometry_,
  'MultiPolygon': ol.format.TopoJSON.readMultiPolygonGeometry_
};

goog.provide('ol.format.WFS');

goog.require('goog.asserts');
goog.require('goog.dom.NodeType');
goog.require('goog.object');
goog.require('ol');
goog.require('ol.format.GML3');
goog.require('ol.format.GMLBase');
goog.require('ol.format.XMLFeature');
goog.require('ol.format.XSD');
goog.require('ol.geom.Geometry');
goog.require('ol.proj');
goog.require('ol.xml');


/**
 * @classdesc
 * Feature format for reading and writing data in the WFS format.
 * By default, supports WFS version 1.1.0. You can pass a GML format
 * as option if you want to read a WFS that contains GML2 (WFS 1.0.0).
 * Also see {@link ol.format.GMLBase} which is used by this format.
 *
 * @constructor
 * @param {olx.format.WFSOptions=} opt_options
 *     Optional configuration object.
 * @extends {ol.format.XMLFeature}
 * @api stable
 */
ol.format.WFS = function(opt_options) {
  var options = opt_options ? opt_options : {};

  /**
   * @private
   * @type {Array.<string>|string|undefined}
   */
  this.featureType_ = options.featureType;

  /**
   * @private
   * @type {Object.<string, string>|string|undefined}
   */
  this.featureNS_ = options.featureNS;

  /**
   * @private
   * @type {ol.format.GMLBase}
   */
  this.gmlFormat_ = options.gmlFormat ?
      options.gmlFormat : new ol.format.GML3();

  /**
   * @private
   * @type {string}
   */
  this.schemaLocation_ = options.schemaLocation ?
      options.schemaLocation : ol.format.WFS.SCHEMA_LOCATION;

  goog.base(this);
};
goog.inherits(ol.format.WFS, ol.format.XMLFeature);


/**
 * @const
 * @type {string}
 */
ol.format.WFS.FEATURE_PREFIX = 'feature';


/**
 * @const
 * @type {string}
 */
ol.format.WFS.XMLNS = 'http://www.w3.org/2000/xmlns/';


/**
 * Number of features; bounds/extent.
 * @typedef {{numberOfFeatures: number,
 *            bounds: ol.Extent}}
 * @api stable
 */
ol.format.WFS.FeatureCollectionMetadata;


/**
 * Total deleted; total inserted; total updated; array of insert ids.
 * @typedef {{totalDeleted: number,
 *            totalInserted: number,
 *            totalUpdated: number,
 *            insertIds: Array.<string>}}
 * @api stable
 */
ol.format.WFS.TransactionResponse;


/**
 * @const
 * @type {string}
 */
ol.format.WFS.SCHEMA_LOCATION = 'http://www.opengis.net/wfs ' +
    'http://schemas.opengis.net/wfs/1.1.0/wfs.xsd';


/**
 * Read all features from a WFS FeatureCollection.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<ol.Feature>} Features.
 * @api stable
 */
ol.format.WFS.prototype.readFeatures;


/**
 * @inheritDoc
 */
ol.format.WFS.prototype.readFeaturesFromNode = function(node, opt_options) {
  var context = {
    'featureType': this.featureType_,
    'featureNS': this.featureNS_
  };
  goog.object.extend(context, this.getReadOptions(node,
      opt_options ? opt_options : {}));
  var objectStack = [context];
  this.gmlFormat_.FEATURE_COLLECTION_PARSERS[ol.format.GMLBase.GMLNS][
      'featureMember'] =
      ol.xml.makeArrayPusher(ol.format.GMLBase.prototype.readFeaturesInternal);
  var features = ol.xml.pushParseAndPop([],
      this.gmlFormat_.FEATURE_COLLECTION_PARSERS, node,
      objectStack, this.gmlFormat_);
  if (!features) {
    features = [];
  }
  return features;
};


/**
 * Read transaction response of the source.
 *
 * @param {Document|Node|Object|string} source Source.
 * @return {ol.format.WFS.TransactionResponse|undefined} Transaction response.
 * @api stable
 */
ol.format.WFS.prototype.readTransactionResponse = function(source) {
  if (ol.xml.isDocument(source)) {
    return this.readTransactionResponseFromDocument(
        /** @type {Document} */ (source));
  } else if (ol.xml.isNode(source)) {
    return this.readTransactionResponseFromNode(/** @type {Node} */ (source));
  } else if (goog.isString(source)) {
    var doc = ol.xml.parse(source);
    return this.readTransactionResponseFromDocument(doc);
  } else {
    goog.asserts.fail('Unknown source type');
    return undefined;
  }
};


/**
 * Read feature collection metadata of the source.
 *
 * @param {Document|Node|Object|string} source Source.
 * @return {ol.format.WFS.FeatureCollectionMetadata|undefined}
 *     FeatureCollection metadata.
 * @api stable
 */
ol.format.WFS.prototype.readFeatureCollectionMetadata = function(source) {
  if (ol.xml.isDocument(source)) {
    return this.readFeatureCollectionMetadataFromDocument(
        /** @type {Document} */ (source));
  } else if (ol.xml.isNode(source)) {
    return this.readFeatureCollectionMetadataFromNode(
        /** @type {Node} */ (source));
  } else if (goog.isString(source)) {
    var doc = ol.xml.parse(source);
    return this.readFeatureCollectionMetadataFromDocument(doc);
  } else {
    goog.asserts.fail('Unknown source type');
    return undefined;
  }
};


/**
 * @param {Document} doc Document.
 * @return {ol.format.WFS.FeatureCollectionMetadata|undefined}
 *     FeatureCollection metadata.
 */
ol.format.WFS.prototype.readFeatureCollectionMetadataFromDocument = function(doc) {
  goog.asserts.assert(doc.nodeType == goog.dom.NodeType.DOCUMENT,
      'doc.nodeType should be DOCUMENT');
  for (var n = doc.firstChild; n; n = n.nextSibling) {
    if (n.nodeType == goog.dom.NodeType.ELEMENT) {
      return this.readFeatureCollectionMetadataFromNode(n);
    }
  }
  return undefined;
};


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WFS.FEATURE_COLLECTION_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'boundedBy': ol.xml.makeObjectPropertySetter(
        ol.format.GMLBase.prototype.readGeometryElement, 'bounds')
  }
};


/**
 * @param {Node} node Node.
 * @return {ol.format.WFS.FeatureCollectionMetadata|undefined}
 *     FeatureCollection metadata.
 */
ol.format.WFS.prototype.readFeatureCollectionMetadataFromNode = function(node) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'FeatureCollection',
      'localName should be FeatureCollection');
  var result = {};
  var value = ol.format.XSD.readNonNegativeIntegerString(
      node.getAttribute('numberOfFeatures'));
  result['numberOfFeatures'] = value;
  return ol.xml.pushParseAndPop(
      /** @type {ol.format.WFS.FeatureCollectionMetadata} */ (result),
      ol.format.WFS.FEATURE_COLLECTION_PARSERS_, node, [], this.gmlFormat_);
};


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WFS.TRANSACTION_SUMMARY_PARSERS_ = {
  'http://www.opengis.net/wfs': {
    'totalInserted': ol.xml.makeObjectPropertySetter(
        ol.format.XSD.readNonNegativeInteger),
    'totalUpdated': ol.xml.makeObjectPropertySetter(
        ol.format.XSD.readNonNegativeInteger),
    'totalDeleted': ol.xml.makeObjectPropertySetter(
        ol.format.XSD.readNonNegativeInteger)
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Transaction Summary.
 * @private
 */
ol.format.WFS.readTransactionSummary_ = function(node, objectStack) {
  return ol.xml.pushParseAndPop(
      {}, ol.format.WFS.TRANSACTION_SUMMARY_PARSERS_, node, objectStack);
};


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WFS.OGC_FID_PARSERS_ = {
  'http://www.opengis.net/ogc': {
    'FeatureId': ol.xml.makeArrayPusher(function(node, objectStack) {
      return node.getAttribute('fid');
    })
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
ol.format.WFS.fidParser_ = function(node, objectStack) {
  ol.xml.parseNode(ol.format.WFS.OGC_FID_PARSERS_, node, objectStack);
};


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WFS.INSERT_RESULTS_PARSERS_ = {
  'http://www.opengis.net/wfs': {
    'Feature': ol.format.WFS.fidParser_
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Array.<string>|undefined} Insert results.
 * @private
 */
ol.format.WFS.readInsertResults_ = function(node, objectStack) {
  return ol.xml.pushParseAndPop(
      [], ol.format.WFS.INSERT_RESULTS_PARSERS_, node, objectStack);
};


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WFS.TRANSACTION_RESPONSE_PARSERS_ = {
  'http://www.opengis.net/wfs': {
    'TransactionSummary': ol.xml.makeObjectPropertySetter(
        ol.format.WFS.readTransactionSummary_, 'transactionSummary'),
    'InsertResults': ol.xml.makeObjectPropertySetter(
        ol.format.WFS.readInsertResults_, 'insertIds')
  }
};


/**
 * @param {Document} doc Document.
 * @return {ol.format.WFS.TransactionResponse|undefined} Transaction response.
 */
ol.format.WFS.prototype.readTransactionResponseFromDocument = function(doc) {
  goog.asserts.assert(doc.nodeType == goog.dom.NodeType.DOCUMENT,
      'doc.nodeType should be DOCUMENT');
  for (var n = doc.firstChild; n; n = n.nextSibling) {
    if (n.nodeType == goog.dom.NodeType.ELEMENT) {
      return this.readTransactionResponseFromNode(n);
    }
  }
  return undefined;
};


/**
 * @param {Node} node Node.
 * @return {ol.format.WFS.TransactionResponse|undefined} Transaction response.
 */
ol.format.WFS.prototype.readTransactionResponseFromNode = function(node) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should  be ELEMENT');
  goog.asserts.assert(node.localName == 'TransactionResponse',
      'localName should be TransactionResponse');
  return ol.xml.pushParseAndPop(
      /** @type {ol.format.WFS.TransactionResponse} */({}),
      ol.format.WFS.TRANSACTION_RESPONSE_PARSERS_, node, []);
};


/**
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.WFS.QUERY_SERIALIZERS_ = {
  'http://www.opengis.net/wfs': {
    'PropertyName': ol.xml.makeChildAppender(ol.format.XSD.writeStringTextNode)
  }
};


/**
 * @param {Node} node Node.
 * @param {ol.Feature} feature Feature.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.WFS.writeFeature_ = function(node, feature, objectStack) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var featureType = context['featureType'];
  var featureNS = context['featureNS'];
  var child = ol.xml.createElementNS(featureNS, featureType);
  node.appendChild(child);
  ol.format.GML3.prototype.writeFeatureElement(child, feature, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {number|string} fid Feature identifier.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.WFS.writeOgcFidFilter_ = function(node, fid, objectStack) {
  var filter = ol.xml.createElementNS('http://www.opengis.net/ogc', 'Filter');
  var child = ol.xml.createElementNS('http://www.opengis.net/ogc', 'FeatureId');
  filter.appendChild(child);
  child.setAttribute('fid', fid);
  node.appendChild(filter);
};


/**
 * @param {Node} node Node.
 * @param {ol.Feature} feature Feature.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.WFS.writeDelete_ = function(node, feature, objectStack) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var featureType = context['featureType'];
  var featurePrefix = context['featurePrefix'];
  featurePrefix = featurePrefix ? featurePrefix :
      ol.format.WFS.FEATURE_PREFIX;
  var featureNS = context['featureNS'];
  node.setAttribute('typeName', featurePrefix + ':' + featureType);
  ol.xml.setAttributeNS(node, ol.format.WFS.XMLNS, 'xmlns:' + featurePrefix,
      featureNS);
  var fid = feature.getId();
  if (fid) {
    ol.format.WFS.writeOgcFidFilter_(node, fid, objectStack);
  }
};


/**
 * @param {Node} node Node.
 * @param {ol.Feature} feature Feature.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.WFS.writeUpdate_ = function(node, feature, objectStack) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var featureType = context['featureType'];
  var featurePrefix = context['featurePrefix'];
  featurePrefix = featurePrefix ? featurePrefix :
      ol.format.WFS.FEATURE_PREFIX;
  var featureNS = context['featureNS'];
  node.setAttribute('typeName', featurePrefix + ':' + featureType);
  ol.xml.setAttributeNS(node, ol.format.WFS.XMLNS, 'xmlns:' + featurePrefix,
      featureNS);
  var fid = feature.getId();
  if (fid) {
    var keys = feature.getKeys();
    var values = [];
    for (var i = 0, ii = keys.length; i < ii; i++) {
      var value = feature.get(keys[i]);
      if (value !== undefined) {
        values.push({name: keys[i], value: value});
      }
    }
    ol.xml.pushSerializeAndPop({node: node, srsName:
          context['srsName']},
    ol.format.WFS.TRANSACTION_SERIALIZERS_,
    ol.xml.makeSimpleNodeFactory('Property'), values,
    objectStack);
    ol.format.WFS.writeOgcFidFilter_(node, fid, objectStack);
  }
};


/**
 * @param {Node} node Node.
 * @param {Object} pair Property name and value.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.WFS.writeProperty_ = function(node, pair, objectStack) {
  var name = ol.xml.createElementNS('http://www.opengis.net/wfs', 'Name');
  node.appendChild(name);
  ol.format.XSD.writeStringTextNode(name, pair.name);
  if (pair.value !== undefined && pair.value !== null) {
    var value = ol.xml.createElementNS('http://www.opengis.net/wfs', 'Value');
    node.appendChild(value);
    if (pair.value instanceof ol.geom.Geometry) {
      ol.format.GML3.prototype.writeGeometryElement(value,
          pair.value, objectStack);
    } else {
      ol.format.XSD.writeStringTextNode(value, pair.value);
    }
  }
};


/**
 * @param {Node} node Node.
 * @param {{vendorId: string, safeToIgnore: boolean, value: string}}
 *     nativeElement The native element.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.WFS.writeNative_ = function(node, nativeElement, objectStack) {
  if (nativeElement.vendorId) {
    node.setAttribute('vendorId', nativeElement.vendorId);
  }
  if (nativeElement.safeToIgnore !== undefined) {
    node.setAttribute('safeToIgnore', nativeElement.safeToIgnore);
  }
  if (nativeElement.value !== undefined) {
    ol.format.XSD.writeStringTextNode(node, nativeElement.value);
  }
};


/**
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.WFS.TRANSACTION_SERIALIZERS_ = {
  'http://www.opengis.net/wfs': {
    'Insert': ol.xml.makeChildAppender(ol.format.WFS.writeFeature_),
    'Update': ol.xml.makeChildAppender(ol.format.WFS.writeUpdate_),
    'Delete': ol.xml.makeChildAppender(ol.format.WFS.writeDelete_),
    'Property': ol.xml.makeChildAppender(ol.format.WFS.writeProperty_),
    'Native': ol.xml.makeChildAppender(ol.format.WFS.writeNative_)
  }
};


/**
 * @param {Node} node Node.
 * @param {string} featureType Feature type.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.WFS.writeQuery_ = function(node, featureType, objectStack) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var featurePrefix = context['featurePrefix'];
  var featureNS = context['featureNS'];
  var propertyNames = context['propertyNames'];
  var srsName = context['srsName'];
  var prefix = featurePrefix ? featurePrefix + ':' : '';
  node.setAttribute('typeName', prefix + featureType);
  if (srsName) {
    node.setAttribute('srsName', srsName);
  }
  if (featureNS) {
    ol.xml.setAttributeNS(node, ol.format.WFS.XMLNS, 'xmlns:' + featurePrefix,
        featureNS);
  }
  var item = goog.object.clone(context);
  item.node = node;
  ol.xml.pushSerializeAndPop(item,
      ol.format.WFS.QUERY_SERIALIZERS_,
      ol.xml.makeSimpleNodeFactory('PropertyName'), propertyNames,
      objectStack);
  var bbox = context['bbox'];
  if (bbox) {
    var child = ol.xml.createElementNS('http://www.opengis.net/ogc', 'Filter');
    ol.format.WFS.writeOgcBBOX_(child, bbox, objectStack);
    node.appendChild(child);
  }
};


/**
 * @param {Node} node Node.
 * @param {string} value PropertyName value.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.WFS.writeOgcPropertyName_ = function(node, value, objectStack) {
  var property = ol.xml.createElementNS('http://www.opengis.net/ogc',
      'PropertyName');
  ol.format.XSD.writeStringTextNode(property, value);
  node.appendChild(property);
};


/**
 * @param {Node} node Node.
 * @param {ol.Extent} bbox Bounding box.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.WFS.writeOgcBBOX_ = function(node, bbox, objectStack) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var geometryName = context['geometryName'];
  var bboxNode = ol.xml.createElementNS('http://www.opengis.net/ogc', 'BBOX');
  node.appendChild(bboxNode);
  ol.format.WFS.writeOgcPropertyName_(bboxNode, geometryName, objectStack);
  ol.format.GML3.prototype.writeGeometryElement(bboxNode, bbox, objectStack);
};


/**
 * @type {Object.<string, Object.<string, ol.xml.Serializer>>}
 * @private
 */
ol.format.WFS.GETFEATURE_SERIALIZERS_ = {
  'http://www.opengis.net/wfs': {
    'Query': ol.xml.makeChildAppender(
        ol.format.WFS.writeQuery_)
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<{string}>} featureTypes Feature types.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
ol.format.WFS.writeGetFeature_ = function(node, featureTypes, objectStack) {
  var context = objectStack[objectStack.length - 1];
  goog.asserts.assert(goog.isObject(context), 'context should be an Object');
  var item = goog.object.clone(context);
  item.node = node;
  ol.xml.pushSerializeAndPop(item,
      ol.format.WFS.GETFEATURE_SERIALIZERS_,
      ol.xml.makeSimpleNodeFactory('Query'), featureTypes,
      objectStack);
};


/**
 * Encode format as WFS `GetFeature` and return the Node.
 *
 * @param {olx.format.WFSWriteGetFeatureOptions} options Options.
 * @return {Node} Result.
 * @api stable
 */
ol.format.WFS.prototype.writeGetFeature = function(options) {
  var node = ol.xml.createElementNS('http://www.opengis.net/wfs',
      'GetFeature');
  node.setAttribute('service', 'WFS');
  node.setAttribute('version', '1.1.0');
  if (options) {
    if (options.handle) {
      node.setAttribute('handle', options.handle);
    }
    if (options.outputFormat) {
      node.setAttribute('outputFormat', options.outputFormat);
    }
    if (options.maxFeatures !== undefined) {
      node.setAttribute('maxFeatures', options.maxFeatures);
    }
    if (options.resultType) {
      node.setAttribute('resultType', options.resultType);
    }
    if (options.startIndex !== undefined) {
      node.setAttribute('startIndex', options.startIndex);
    }
    if (options.count !== undefined) {
      node.setAttribute('count', options.count);
    }
  }
  ol.xml.setAttributeNS(node, 'http://www.w3.org/2001/XMLSchema-instance',
      'xsi:schemaLocation', this.schemaLocation_);
  var context = {
    node: node,
    srsName: options.srsName,
    featureNS: options.featureNS ? options.featureNS : this.featureNS_,
    featurePrefix: options.featurePrefix,
    geometryName: options.geometryName,
    bbox: options.bbox,
    propertyNames: options.propertyNames ? options.propertyNames : []
  };
  goog.asserts.assert(goog.isArray(options.featureTypes),
      'options.featureTypes should be an array');
  ol.format.WFS.writeGetFeature_(node, options.featureTypes, [context]);
  return node;
};


/**
 * Encode format as WFS `Transaction` and return the Node.
 *
 * @param {Array.<ol.Feature>} inserts The features to insert.
 * @param {Array.<ol.Feature>} updates The features to update.
 * @param {Array.<ol.Feature>} deletes The features to delete.
 * @param {olx.format.WFSWriteTransactionOptions} options Write options.
 * @return {Node} Result.
 * @api stable
 */
ol.format.WFS.prototype.writeTransaction = function(inserts, updates, deletes,
    options) {
  var objectStack = [];
  var node = ol.xml.createElementNS('http://www.opengis.net/wfs',
      'Transaction');
  node.setAttribute('service', 'WFS');
  node.setAttribute('version', '1.1.0');
  var baseObj, obj;
  if (options) {
    baseObj = options.gmlOptions ? options.gmlOptions : {};
    if (options.handle) {
      node.setAttribute('handle', options.handle);
    }
  }
  ol.xml.setAttributeNS(node, 'http://www.w3.org/2001/XMLSchema-instance',
      'xsi:schemaLocation', this.schemaLocation_);
  if (inserts) {
    obj = {node: node, featureNS: options.featureNS,
      featureType: options.featureType, featurePrefix: options.featurePrefix};
    goog.object.extend(obj, baseObj);
    ol.xml.pushSerializeAndPop(obj,
        ol.format.WFS.TRANSACTION_SERIALIZERS_,
        ol.xml.makeSimpleNodeFactory('Insert'), inserts,
        objectStack);
  }
  if (updates) {
    obj = {node: node, featureNS: options.featureNS,
      featureType: options.featureType, featurePrefix: options.featurePrefix};
    goog.object.extend(obj, baseObj);
    ol.xml.pushSerializeAndPop(obj,
        ol.format.WFS.TRANSACTION_SERIALIZERS_,
        ol.xml.makeSimpleNodeFactory('Update'), updates,
        objectStack);
  }
  if (deletes) {
    ol.xml.pushSerializeAndPop({node: node, featureNS: options.featureNS,
      featureType: options.featureType, featurePrefix: options.featurePrefix},
    ol.format.WFS.TRANSACTION_SERIALIZERS_,
    ol.xml.makeSimpleNodeFactory('Delete'), deletes,
    objectStack);
  }
  if (options.nativeElements) {
    ol.xml.pushSerializeAndPop({node: node, featureNS: options.featureNS,
      featureType: options.featureType, featurePrefix: options.featurePrefix},
    ol.format.WFS.TRANSACTION_SERIALIZERS_,
    ol.xml.makeSimpleNodeFactory('Native'), options.nativeElements,
    objectStack);
  }
  return node;
};


/**
 * Read the projection from a WFS source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @return {?ol.proj.Projection} Projection.
 * @api stable
 */
ol.format.WFS.prototype.readProjection;


/**
 * @inheritDoc
 */
ol.format.WFS.prototype.readProjectionFromDocument = function(doc) {
  goog.asserts.assert(doc.nodeType == goog.dom.NodeType.DOCUMENT,
      'doc.nodeType should be a DOCUMENT');
  for (var n = doc.firstChild; n; n = n.nextSibling) {
    if (n.nodeType == goog.dom.NodeType.ELEMENT) {
      return this.readProjectionFromNode(n);
    }
  }
  return null;
};


/**
 * @inheritDoc
 */
ol.format.WFS.prototype.readProjectionFromNode = function(node) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'FeatureCollection',
      'localName should be FeatureCollection');

  if (node.firstElementChild &&
      node.firstElementChild.firstElementChild) {
    node = node.firstElementChild.firstElementChild;
    for (var n = node.firstElementChild; n; n = n.nextElementSibling) {
      if (!(n.childNodes.length === 0 ||
          (n.childNodes.length === 1 &&
          n.firstChild.nodeType === 3))) {
        var objectStack = [{}];
        this.gmlFormat_.readGeometryElement(n, objectStack);
        return ol.proj.get(objectStack.pop().srsName);
      }
    }
  }

  return null;
};

goog.provide('ol.format.WKT');

goog.require('goog.asserts');
goog.require('ol');
goog.require('ol.Feature');
goog.require('ol.format.Feature');
goog.require('ol.format.TextFeature');
goog.require('ol.geom.Geometry');
goog.require('ol.geom.GeometryCollection');
goog.require('ol.geom.GeometryType');
goog.require('ol.geom.LineString');
goog.require('ol.geom.MultiLineString');
goog.require('ol.geom.MultiPoint');
goog.require('ol.geom.MultiPolygon');
goog.require('ol.geom.Point');
goog.require('ol.geom.Polygon');


/**
 * @classdesc
 * Geometry format for reading and writing data in the `WellKnownText` (WKT)
 * format.
 *
 * @constructor
 * @extends {ol.format.TextFeature}
 * @param {olx.format.WKTOptions=} opt_options Options.
 * @api stable
 */
ol.format.WKT = function(opt_options) {

  var options = opt_options ? opt_options : {};

  goog.base(this);

  /**
   * Split GeometryCollection into multiple features.
   * @type {boolean}
   * @private
   */
  this.splitCollection_ = options.splitCollection !== undefined ?
      options.splitCollection : false;

};
goog.inherits(ol.format.WKT, ol.format.TextFeature);


/**
 * @const
 * @type {string}
 */
ol.format.WKT.EMPTY = 'EMPTY';


/**
 * @param {ol.geom.Point} geom Point geometry.
 * @return {string} Coordinates part of Point as WKT.
 * @private
 */
ol.format.WKT.encodePointGeometry_ = function(geom) {
  var coordinates = geom.getCoordinates();
  if (coordinates.length === 0) {
    return '';
  }
  return coordinates[0] + ' ' + coordinates[1];
};


/**
 * @param {ol.geom.MultiPoint} geom MultiPoint geometry.
 * @return {string} Coordinates part of MultiPoint as WKT.
 * @private
 */
ol.format.WKT.encodeMultiPointGeometry_ = function(geom) {
  var array = [];
  var components = geom.getPoints();
  for (var i = 0, ii = components.length; i < ii; ++i) {
    array.push('(' + ol.format.WKT.encodePointGeometry_(components[i]) + ')');
  }
  return array.join(',');
};


/**
 * @param {ol.geom.GeometryCollection} geom GeometryCollection geometry.
 * @return {string} Coordinates part of GeometryCollection as WKT.
 * @private
 */
ol.format.WKT.encodeGeometryCollectionGeometry_ = function(geom) {
  var array = [];
  var geoms = geom.getGeometries();
  for (var i = 0, ii = geoms.length; i < ii; ++i) {
    array.push(ol.format.WKT.encode_(geoms[i]));
  }
  return array.join(',');
};


/**
 * @param {ol.geom.LineString|ol.geom.LinearRing} geom LineString geometry.
 * @return {string} Coordinates part of LineString as WKT.
 * @private
 */
ol.format.WKT.encodeLineStringGeometry_ = function(geom) {
  var coordinates = geom.getCoordinates();
  var array = [];
  for (var i = 0, ii = coordinates.length; i < ii; ++i) {
    array.push(coordinates[i][0] + ' ' + coordinates[i][1]);
  }
  return array.join(',');
};


/**
 * @param {ol.geom.MultiLineString} geom MultiLineString geometry.
 * @return {string} Coordinates part of MultiLineString as WKT.
 * @private
 */
ol.format.WKT.encodeMultiLineStringGeometry_ = function(geom) {
  var array = [];
  var components = geom.getLineStrings();
  for (var i = 0, ii = components.length; i < ii; ++i) {
    array.push('(' + ol.format.WKT.encodeLineStringGeometry_(
        components[i]) + ')');
  }
  return array.join(',');
};


/**
 * @param {ol.geom.Polygon} geom Polygon geometry.
 * @return {string} Coordinates part of Polygon as WKT.
 * @private
 */
ol.format.WKT.encodePolygonGeometry_ = function(geom) {
  var array = [];
  var rings = geom.getLinearRings();
  for (var i = 0, ii = rings.length; i < ii; ++i) {
    array.push('(' + ol.format.WKT.encodeLineStringGeometry_(
        rings[i]) + ')');
  }
  return array.join(',');
};


/**
 * @param {ol.geom.MultiPolygon} geom MultiPolygon geometry.
 * @return {string} Coordinates part of MultiPolygon as WKT.
 * @private
 */
ol.format.WKT.encodeMultiPolygonGeometry_ = function(geom) {
  var array = [];
  var components = geom.getPolygons();
  for (var i = 0, ii = components.length; i < ii; ++i) {
    array.push('(' + ol.format.WKT.encodePolygonGeometry_(
        components[i]) + ')');
  }
  return array.join(',');
};


/**
 * Encode a geometry as WKT.
 * @param {ol.geom.Geometry} geom The geometry to encode.
 * @return {string} WKT string for the geometry.
 * @private
 */
ol.format.WKT.encode_ = function(geom) {
  var type = geom.getType();
  var geometryEncoder = ol.format.WKT.GeometryEncoder_[type];
  goog.asserts.assert(geometryEncoder, 'geometryEncoder should be defined');
  var enc = geometryEncoder(geom);
  type = type.toUpperCase();
  if (enc.length === 0) {
    return type + ' ' + ol.format.WKT.EMPTY;
  }
  return type + '(' + enc + ')';
};


/**
 * @const
 * @type {Object.<string, function(ol.geom.Geometry): string>}
 * @private
 */
ol.format.WKT.GeometryEncoder_ = {
  'Point': ol.format.WKT.encodePointGeometry_,
  'LineString': ol.format.WKT.encodeLineStringGeometry_,
  'Polygon': ol.format.WKT.encodePolygonGeometry_,
  'MultiPoint': ol.format.WKT.encodeMultiPointGeometry_,
  'MultiLineString': ol.format.WKT.encodeMultiLineStringGeometry_,
  'MultiPolygon': ol.format.WKT.encodeMultiPolygonGeometry_,
  'GeometryCollection': ol.format.WKT.encodeGeometryCollectionGeometry_
};


/**
 * Parse a WKT string.
 * @param {string} wkt WKT string.
 * @return {ol.geom.Geometry|ol.geom.GeometryCollection|undefined}
 *     The geometry created.
 * @private
 */
ol.format.WKT.prototype.parse_ = function(wkt) {
  var lexer = new ol.format.WKT.Lexer(wkt);
  var parser = new ol.format.WKT.Parser(lexer);
  return parser.parse();
};


/**
 * Read a feature from a WKT source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {ol.Feature} Feature.
 * @api stable
 */
ol.format.WKT.prototype.readFeature;


/**
 * @inheritDoc
 */
ol.format.WKT.prototype.readFeatureFromText = function(text, opt_options) {
  var geom = this.readGeometryFromText(text, opt_options);
  if (geom) {
    var feature = new ol.Feature();
    feature.setGeometry(geom);
    return feature;
  }
  return null;
};


/**
 * Read all features from a WKT source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<ol.Feature>} Features.
 * @api stable
 */
ol.format.WKT.prototype.readFeatures;


/**
 * @inheritDoc
 */
ol.format.WKT.prototype.readFeaturesFromText = function(text, opt_options) {
  var geometries = [];
  var geometry = this.readGeometryFromText(text, opt_options);
  if (this.splitCollection_ &&
      geometry.getType() == ol.geom.GeometryType.GEOMETRY_COLLECTION) {
    geometries = (/** @type {ol.geom.GeometryCollection} */ (geometry))
        .getGeometriesArray();
  } else {
    geometries = [geometry];
  }
  var feature, features = [];
  for (var i = 0, ii = geometries.length; i < ii; ++i) {
    feature = new ol.Feature();
    feature.setGeometry(geometries[i]);
    features.push(feature);
  }
  return features;
};


/**
 * Read a single geometry from a WKT source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Read options.
 * @return {ol.geom.Geometry} Geometry.
 * @api stable
 */
ol.format.WKT.prototype.readGeometry;


/**
 * @inheritDoc
 */
ol.format.WKT.prototype.readGeometryFromText = function(text, opt_options) {
  var geometry = this.parse_(text);
  if (geometry) {
    return /** @type {ol.geom.Geometry} */ (
        ol.format.Feature.transformWithOptions(geometry, false, opt_options));
  } else {
    return null;
  }
};


/**
 * Encode a feature as a WKT string.
 *
 * @function
 * @param {ol.Feature} feature Feature.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {string} WKT string.
 * @api stable
 */
ol.format.WKT.prototype.writeFeature;


/**
 * @inheritDoc
 */
ol.format.WKT.prototype.writeFeatureText = function(feature, opt_options) {
  var geometry = feature.getGeometry();
  if (geometry) {
    return this.writeGeometryText(geometry, opt_options);
  }
  return '';
};


/**
 * Encode an array of features as a WKT string.
 *
 * @function
 * @param {Array.<ol.Feature>} features Features.
 * @param {olx.format.WriteOptions=} opt_options Write options.
 * @return {string} WKT string.
 * @api stable
 */
ol.format.WKT.prototype.writeFeatures;


/**
 * @inheritDoc
 */
ol.format.WKT.prototype.writeFeaturesText = function(features, opt_options) {
  if (features.length == 1) {
    return this.writeFeatureText(features[0], opt_options);
  }
  var geometries = [];
  for (var i = 0, ii = features.length; i < ii; ++i) {
    geometries.push(features[i].getGeometry());
  }
  var collection = new ol.geom.GeometryCollection(geometries);
  return this.writeGeometryText(collection, opt_options);
};


/**
 * Write a single geometry as a WKT string.
 *
 * @function
 * @param {ol.geom.Geometry} geometry Geometry.
 * @return {string} WKT string.
 * @api stable
 */
ol.format.WKT.prototype.writeGeometry;


/**
 * @inheritDoc
 */
ol.format.WKT.prototype.writeGeometryText = function(geometry, opt_options) {
  return ol.format.WKT.encode_(/** @type {ol.geom.Geometry} */ (
      ol.format.Feature.transformWithOptions(geometry, true, opt_options)));
};


/**
 * @typedef {{type: number, value: (number|string|undefined), position: number}}
 */
ol.format.WKT.Token;


/**
 * @const
 * @enum {number}
 */
ol.format.WKT.TokenType = {
  TEXT: 1,
  LEFT_PAREN: 2,
  RIGHT_PAREN: 3,
  NUMBER: 4,
  COMMA: 5,
  EOF: 6
};


/**
 * Class to tokenize a WKT string.
 * @param {string} wkt WKT string.
 * @constructor
 * @protected
 */
ol.format.WKT.Lexer = function(wkt) {

  /**
   * @type {string}
   */
  this.wkt = wkt;

  /**
   * @type {number}
   * @private
   */
  this.index_ = -1;
};


/**
 * @param {string} c Character.
 * @return {boolean} Whether the character is alphabetic.
 * @private
 */
ol.format.WKT.Lexer.prototype.isAlpha_ = function(c) {
  return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z';
};


/**
 * @param {string} c Character.
 * @param {boolean=} opt_decimal Whether the string number
 *     contains a dot, i.e. is a decimal number.
 * @return {boolean} Whether the character is numeric.
 * @private
 */
ol.format.WKT.Lexer.prototype.isNumeric_ = function(c, opt_decimal) {
  var decimal = opt_decimal !== undefined ? opt_decimal : false;
  return c >= '0' && c <= '9' || c == '.' && !decimal;
};


/**
 * @param {string} c Character.
 * @return {boolean} Whether the character is whitespace.
 * @private
 */
ol.format.WKT.Lexer.prototype.isWhiteSpace_ = function(c) {
  return c == ' ' || c == '\t' || c == '\r' || c == '\n';
};


/**
 * @return {string} Next string character.
 * @private
 */
ol.format.WKT.Lexer.prototype.nextChar_ = function() {
  return this.wkt.charAt(++this.index_);
};


/**
 * Fetch and return the next token.
 * @return {!ol.format.WKT.Token} Next string token.
 */
ol.format.WKT.Lexer.prototype.nextToken = function() {
  var c = this.nextChar_();
  var token = {position: this.index_, value: c};

  if (c == '(') {
    token.type = ol.format.WKT.TokenType.LEFT_PAREN;
  } else if (c == ',') {
    token.type = ol.format.WKT.TokenType.COMMA;
  } else if (c == ')') {
    token.type = ol.format.WKT.TokenType.RIGHT_PAREN;
  } else if (this.isNumeric_(c) || c == '-') {
    token.type = ol.format.WKT.TokenType.NUMBER;
    token.value = this.readNumber_();
  } else if (this.isAlpha_(c)) {
    token.type = ol.format.WKT.TokenType.TEXT;
    token.value = this.readText_();
  } else if (this.isWhiteSpace_(c)) {
    return this.nextToken();
  } else if (c === '') {
    token.type = ol.format.WKT.TokenType.EOF;
  } else {
    throw new Error('Unexpected character: ' + c);
  }

  return token;
};


/**
 * @return {number} Numeric token value.
 * @private
 */
ol.format.WKT.Lexer.prototype.readNumber_ = function() {
  var c, index = this.index_;
  var decimal = false;
  var scientificNotation = false;
  do {
    if (c == '.') {
      decimal = true;
    } else if (c == 'e' || c == 'E') {
      scientificNotation = true;
    }
    c = this.nextChar_();
  } while (
      this.isNumeric_(c, decimal) ||
      // if we haven't detected a scientific number before, 'e' or 'E'
      // hint that we should continue to read
      !scientificNotation && (c == 'e' || c == 'E') ||
      // once we know that we have a scientific number, both '-' and '+'
      // are allowed
      scientificNotation && (c == '-' || c == '+')
  );
  return parseFloat(this.wkt.substring(index, this.index_--));
};


/**
 * @return {string} String token value.
 * @private
 */
ol.format.WKT.Lexer.prototype.readText_ = function() {
  var c, index = this.index_;
  do {
    c = this.nextChar_();
  } while (this.isAlpha_(c));
  return this.wkt.substring(index, this.index_--).toUpperCase();
};


/**
 * Class to parse the tokens from the WKT string.
 * @param {ol.format.WKT.Lexer} lexer The lexer.
 * @constructor
 * @protected
 */
ol.format.WKT.Parser = function(lexer) {

  /**
   * @type {ol.format.WKT.Lexer}
   * @private
   */
  this.lexer_ = lexer;

  /**
   * @type {ol.format.WKT.Token}
   * @private
   */
  this.token_;

  /**
   * @type {number}
   * @private
   */
  this.dimension_ = 2;
};


/**
 * Fetch the next token form the lexer and replace the active token.
 * @private
 */
ol.format.WKT.Parser.prototype.consume_ = function() {
  this.token_ = this.lexer_.nextToken();
};


/**
 * If the given type matches the current token, consume it.
 * @param {ol.format.WKT.TokenType.<number>} type Token type.
 * @return {boolean} Whether the token matches the given type.
 */
ol.format.WKT.Parser.prototype.match = function(type) {
  var isMatch = this.token_.type == type;
  if (isMatch) {
    this.consume_();
  }
  return isMatch;
};


/**
 * Try to parse the tokens provided by the lexer.
 * @return {ol.geom.Geometry|ol.geom.GeometryCollection} The geometry.
 */
ol.format.WKT.Parser.prototype.parse = function() {
  this.consume_();
  var geometry = this.parseGeometry_();
  goog.asserts.assert(this.token_.type == ol.format.WKT.TokenType.EOF,
      'token type should be end of file');
  return geometry;
};


/**
 * @return {!(ol.geom.Geometry|ol.geom.GeometryCollection)} The geometry.
 * @private
 */
ol.format.WKT.Parser.prototype.parseGeometry_ = function() {
  var token = this.token_;
  if (this.match(ol.format.WKT.TokenType.TEXT)) {
    var geomType = token.value;
    if (geomType == ol.geom.GeometryType.GEOMETRY_COLLECTION.toUpperCase()) {
      var geometries = this.parseGeometryCollectionText_();
      return new ol.geom.GeometryCollection(geometries);
    } else {
      var parser = ol.format.WKT.Parser.GeometryParser_[geomType];
      var ctor = ol.format.WKT.Parser.GeometryConstructor_[geomType];
      if (!parser || !ctor) {
        throw new Error('Invalid geometry type: ' + geomType);
      }
      var coordinates = parser.call(this);
      return new ctor(coordinates);
    }
  }
  throw new Error(this.formatErrorMessage_());
};


/**
 * @return {!Array.<ol.geom.Geometry>} A collection of geometries.
 * @private
 */
ol.format.WKT.Parser.prototype.parseGeometryCollectionText_ = function() {
  if (this.match(ol.format.WKT.TokenType.LEFT_PAREN)) {
    var geometries = [];
    do {
      geometries.push(this.parseGeometry_());
    } while (this.match(ol.format.WKT.TokenType.COMMA));
    if (this.match(ol.format.WKT.TokenType.RIGHT_PAREN)) {
      return geometries;
    }
  } else if (this.isEmptyGeometry_()) {
    return [];
  }
  throw new Error(this.formatErrorMessage_());
};


/**
 * @return {Array.<number>} All values in a point.
 * @private
 */
ol.format.WKT.Parser.prototype.parsePointText_ = function() {
  if (this.match(ol.format.WKT.TokenType.LEFT_PAREN)) {
    var coordinates = this.parsePoint_();
    if (this.match(ol.format.WKT.TokenType.RIGHT_PAREN)) {
      return coordinates;
    }
  } else if (this.isEmptyGeometry_()) {
    return null;
  }
  throw new Error(this.formatErrorMessage_());
};


/**
 * @return {!Array.<!Array.<number>>} All points in a linestring.
 * @private
 */
ol.format.WKT.Parser.prototype.parseLineStringText_ = function() {
  if (this.match(ol.format.WKT.TokenType.LEFT_PAREN)) {
    var coordinates = this.parsePointList_();
    if (this.match(ol.format.WKT.TokenType.RIGHT_PAREN)) {
      return coordinates;
    }
  } else if (this.isEmptyGeometry_()) {
    return [];
  }
  throw new Error(this.formatErrorMessage_());
};


/**
 * @return {!Array.<!Array.<number>>} All points in a polygon.
 * @private
 */
ol.format.WKT.Parser.prototype.parsePolygonText_ = function() {
  if (this.match(ol.format.WKT.TokenType.LEFT_PAREN)) {
    var coordinates = this.parseLineStringTextList_();
    if (this.match(ol.format.WKT.TokenType.RIGHT_PAREN)) {
      return coordinates;
    }
  } else if (this.isEmptyGeometry_()) {
    return [];
  }
  throw new Error(this.formatErrorMessage_());
};


/**
 * @return {!Array.<!Array.<number>>} All points in a multipoint.
 * @private
 */
ol.format.WKT.Parser.prototype.parseMultiPointText_ = function() {
  if (this.match(ol.format.WKT.TokenType.LEFT_PAREN)) {
    var coordinates;
    if (this.token_.type == ol.format.WKT.TokenType.LEFT_PAREN) {
      coordinates = this.parsePointTextList_();
    } else {
      coordinates = this.parsePointList_();
    }
    if (this.match(ol.format.WKT.TokenType.RIGHT_PAREN)) {
      return coordinates;
    }
  } else if (this.isEmptyGeometry_()) {
    return [];
  }
  throw new Error(this.formatErrorMessage_());
};


/**
 * @return {!Array.<!Array.<number>>} All linestring points
 *                                        in a multilinestring.
 * @private
 */
ol.format.WKT.Parser.prototype.parseMultiLineStringText_ = function() {
  if (this.match(ol.format.WKT.TokenType.LEFT_PAREN)) {
    var coordinates = this.parseLineStringTextList_();
    if (this.match(ol.format.WKT.TokenType.RIGHT_PAREN)) {
      return coordinates;
    }
  } else if (this.isEmptyGeometry_()) {
    return [];
  }
  throw new Error(this.formatErrorMessage_());
};


/**
 * @return {!Array.<!Array.<number>>} All polygon points in a multipolygon.
 * @private
 */
ol.format.WKT.Parser.prototype.parseMultiPolygonText_ = function() {
  if (this.match(ol.format.WKT.TokenType.LEFT_PAREN)) {
    var coordinates = this.parsePolygonTextList_();
    if (this.match(ol.format.WKT.TokenType.RIGHT_PAREN)) {
      return coordinates;
    }
  } else if (this.isEmptyGeometry_()) {
    return [];
  }
  throw new Error(this.formatErrorMessage_());
};


/**
 * @return {!Array.<number>} A point.
 * @private
 */
ol.format.WKT.Parser.prototype.parsePoint_ = function() {
  var coordinates = [];
  for (var i = 0; i < this.dimension_; ++i) {
    var token = this.token_;
    if (this.match(ol.format.WKT.TokenType.NUMBER)) {
      coordinates.push(token.value);
    } else {
      break;
    }
  }
  if (coordinates.length == this.dimension_) {
    return coordinates;
  }
  throw new Error(this.formatErrorMessage_());
};


/**
 * @return {!Array.<!Array.<number>>} An array of points.
 * @private
 */
ol.format.WKT.Parser.prototype.parsePointList_ = function() {
  var coordinates = [this.parsePoint_()];
  while (this.match(ol.format.WKT.TokenType.COMMA)) {
    coordinates.push(this.parsePoint_());
  }
  return coordinates;
};


/**
 * @return {!Array.<!Array.<number>>} An array of points.
 * @private
 */
ol.format.WKT.Parser.prototype.parsePointTextList_ = function() {
  var coordinates = [this.parsePointText_()];
  while (this.match(ol.format.WKT.TokenType.COMMA)) {
    coordinates.push(this.parsePointText_());
  }
  return coordinates;
};


/**
 * @return {!Array.<!Array.<number>>} An array of points.
 * @private
 */
ol.format.WKT.Parser.prototype.parseLineStringTextList_ = function() {
  var coordinates = [this.parseLineStringText_()];
  while (this.match(ol.format.WKT.TokenType.COMMA)) {
    coordinates.push(this.parseLineStringText_());
  }
  return coordinates;
};


/**
 * @return {!Array.<!Array.<number>>} An array of points.
 * @private
 */
ol.format.WKT.Parser.prototype.parsePolygonTextList_ = function() {
  var coordinates = [this.parsePolygonText_()];
  while (this.match(ol.format.WKT.TokenType.COMMA)) {
    coordinates.push(this.parsePolygonText_());
  }
  return coordinates;
};


/**
 * @return {boolean} Whether the token implies an empty geometry.
 * @private
 */
ol.format.WKT.Parser.prototype.isEmptyGeometry_ = function() {
  var isEmpty = this.token_.type == ol.format.WKT.TokenType.TEXT &&
      this.token_.value == ol.format.WKT.EMPTY;
  if (isEmpty) {
    this.consume_();
  }
  return isEmpty;
};


/**
 * Create an error message for an unexpected token error.
 * @return {string} Error message.
 * @private
 */
ol.format.WKT.Parser.prototype.formatErrorMessage_ = function() {
  return 'Unexpected `' + this.token_.value + '` at position ' +
      this.token_.position + ' in `' + this.lexer_.wkt + '`';
};


/**
 * @enum {function (new:ol.geom.Geometry, Array, ol.geom.GeometryLayout.<string>=)}
 * @private
 */
ol.format.WKT.Parser.GeometryConstructor_ = {
  'POINT': ol.geom.Point,
  'LINESTRING': ol.geom.LineString,
  'POLYGON': ol.geom.Polygon,
  'MULTIPOINT': ol.geom.MultiPoint,
  'MULTILINESTRING': ol.geom.MultiLineString,
  'MULTIPOLYGON': ol.geom.MultiPolygon
};


/**
 * @enum {(function(): Array)}
 * @private
 */
ol.format.WKT.Parser.GeometryParser_ = {
  'POINT': ol.format.WKT.Parser.prototype.parsePointText_,
  'LINESTRING': ol.format.WKT.Parser.prototype.parseLineStringText_,
  'POLYGON': ol.format.WKT.Parser.prototype.parsePolygonText_,
  'MULTIPOINT': ol.format.WKT.Parser.prototype.parseMultiPointText_,
  'MULTILINESTRING': ol.format.WKT.Parser.prototype.parseMultiLineStringText_,
  'MULTIPOLYGON': ol.format.WKT.Parser.prototype.parseMultiPolygonText_
};

goog.provide('ol.format.WMSCapabilities');

goog.require('goog.asserts');
goog.require('goog.dom.NodeType');
goog.require('goog.object');
goog.require('ol');
goog.require('ol.format.XLink');
goog.require('ol.format.XML');
goog.require('ol.format.XSD');
goog.require('ol.xml');


/**
 * @classdesc
 * Format for reading WMS capabilities data
 *
 * @constructor
 * @extends {ol.format.XML}
 * @api
 */
ol.format.WMSCapabilities = function() {

  goog.base(this);

  /**
   * @type {string|undefined}
   */
  this.version = undefined;
};
goog.inherits(ol.format.WMSCapabilities, ol.format.XML);


/**
 * Read a WMS capabilities document.
 *
 * @function
 * @param {Document|Node|string} source The XML source.
 * @return {Object} An object representing the WMS capabilities.
 * @api
 */
ol.format.WMSCapabilities.prototype.read;


/**
 * @param {Document} doc Document.
 * @return {Object} WMS Capability object.
 */
ol.format.WMSCapabilities.prototype.readFromDocument = function(doc) {
  goog.asserts.assert(doc.nodeType == goog.dom.NodeType.DOCUMENT,
      'doc.nodeType should be DOCUMENT');
  for (var n = doc.firstChild; n; n = n.nextSibling) {
    if (n.nodeType == goog.dom.NodeType.ELEMENT) {
      return this.readFromNode(n);
    }
  }
  return null;
};


/**
 * @param {Node} node Node.
 * @return {Object} WMS Capability object.
 */
ol.format.WMSCapabilities.prototype.readFromNode = function(node) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'WMS_Capabilities' ||
      node.localName == 'WMT_MS_Capabilities',
      'localName should be WMS_Capabilities or WMT_MS_Capabilities');
  this.version = node.getAttribute('version').trim();
  goog.asserts.assertString(this.version, 'this.version should be a string');
  var wmsCapabilityObject = ol.xml.pushParseAndPop({
    'version': this.version
  }, ol.format.WMSCapabilities.PARSERS_, node, []);
  return wmsCapabilityObject ? wmsCapabilityObject : null;
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Attribution object.
 */
ol.format.WMSCapabilities.readAttribution_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Attribution',
      'localName should be Attribution');
  return ol.xml.pushParseAndPop(
      {}, ol.format.WMSCapabilities.ATTRIBUTION_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object} Bounding box object.
 */
ol.format.WMSCapabilities.readBoundingBox_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'BoundingBox',
      'localName should be BoundingBox');

  var extent = [
    ol.format.XSD.readDecimalString(node.getAttribute('minx')),
    ol.format.XSD.readDecimalString(node.getAttribute('miny')),
    ol.format.XSD.readDecimalString(node.getAttribute('maxx')),
    ol.format.XSD.readDecimalString(node.getAttribute('maxy'))
  ];

  var resolutions = [
    ol.format.XSD.readDecimalString(node.getAttribute('resx')),
    ol.format.XSD.readDecimalString(node.getAttribute('resy'))
  ];

  return {
    'crs': node.getAttribute('CRS'),
    'extent': extent,
    'res': resolutions
  };
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {ol.Extent|undefined} Bounding box object.
 */
ol.format.WMSCapabilities.readEXGeographicBoundingBox_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'EX_GeographicBoundingBox',
      'localName should be EX_GeographicBoundingBox');
  var geographicBoundingBox = ol.xml.pushParseAndPop(
      {},
      ol.format.WMSCapabilities.EX_GEOGRAPHIC_BOUNDING_BOX_PARSERS_,
      node, objectStack);
  if (!geographicBoundingBox) {
    return undefined;
  }
  var westBoundLongitude = /** @type {number|undefined} */
      (geographicBoundingBox['westBoundLongitude']);
  var southBoundLatitude = /** @type {number|undefined} */
      (geographicBoundingBox['southBoundLatitude']);
  var eastBoundLongitude = /** @type {number|undefined} */
      (geographicBoundingBox['eastBoundLongitude']);
  var northBoundLatitude = /** @type {number|undefined} */
      (geographicBoundingBox['northBoundLatitude']);
  if (westBoundLongitude === undefined || southBoundLatitude === undefined ||
      eastBoundLongitude === undefined || northBoundLatitude === undefined) {
    return undefined;
  }
  return /** @type {ol.Extent} */ ([
    westBoundLongitude, southBoundLatitude,
    eastBoundLongitude, northBoundLatitude
  ]);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} Capability object.
 */
ol.format.WMSCapabilities.readCapability_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Capability',
      'localName should be Capability');
  return ol.xml.pushParseAndPop(
      {}, ol.format.WMSCapabilities.CAPABILITY_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} Service object.
 */
ol.format.WMSCapabilities.readService_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Service',
      'localName should be Service');
  return ol.xml.pushParseAndPop(
      {}, ol.format.WMSCapabilities.SERVICE_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} Contact information object.
 */
ol.format.WMSCapabilities.readContactInformation_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType shpuld be ELEMENT');
  goog.asserts.assert(node.localName == 'ContactInformation',
      'localName should be ContactInformation');
  return ol.xml.pushParseAndPop(
      {}, ol.format.WMSCapabilities.CONTACT_INFORMATION_PARSERS_,
      node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} Contact person object.
 */
ol.format.WMSCapabilities.readContactPersonPrimary_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'ContactPersonPrimary',
      'localName should be ContactPersonPrimary');
  return ol.xml.pushParseAndPop(
      {}, ol.format.WMSCapabilities.CONTACT_PERSON_PARSERS_,
      node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} Contact address object.
 */
ol.format.WMSCapabilities.readContactAddress_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'ContactAddress',
      'localName should be ContactAddress');
  return ol.xml.pushParseAndPop(
      {}, ol.format.WMSCapabilities.CONTACT_ADDRESS_PARSERS_,
      node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<string>|undefined} Format array.
 */
ol.format.WMSCapabilities.readException_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Exception',
      'localName should be Exception');
  return ol.xml.pushParseAndPop(
      [], ol.format.WMSCapabilities.EXCEPTION_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} Layer object.
 */
ol.format.WMSCapabilities.readCapabilityLayer_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Layer', 'localName should be Layer');
  return ol.xml.pushParseAndPop(
      {}, ol.format.WMSCapabilities.LAYER_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Layer object.
 */
ol.format.WMSCapabilities.readLayer_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Layer', 'localName should be Layer');
  var parentLayerObject = /**  @type {Object.<string,*>} */
      (objectStack[objectStack.length - 1]);

  var layerObject = /**  @type {Object.<string,*>} */ (ol.xml.pushParseAndPop(
      {}, ol.format.WMSCapabilities.LAYER_PARSERS_, node, objectStack));

  if (!layerObject) {
    return undefined;
  }
  var queryable =
      ol.format.XSD.readBooleanString(node.getAttribute('queryable'));
  if (queryable === undefined) {
    queryable = parentLayerObject['queryable'];
  }
  layerObject['queryable'] = queryable !== undefined ? queryable : false;

  var cascaded = ol.format.XSD.readNonNegativeIntegerString(
      node.getAttribute('cascaded'));
  if (cascaded === undefined) {
    cascaded = parentLayerObject['cascaded'];
  }
  layerObject['cascaded'] = cascaded;

  var opaque = ol.format.XSD.readBooleanString(node.getAttribute('opaque'));
  if (opaque === undefined) {
    opaque = parentLayerObject['opaque'];
  }
  layerObject['opaque'] = opaque !== undefined ? opaque : false;

  var noSubsets =
      ol.format.XSD.readBooleanString(node.getAttribute('noSubsets'));
  if (noSubsets === undefined) {
    noSubsets = parentLayerObject['noSubsets'];
  }
  layerObject['noSubsets'] = noSubsets !== undefined ? noSubsets : false;

  var fixedWidth =
      ol.format.XSD.readDecimalString(node.getAttribute('fixedWidth'));
  if (!fixedWidth) {
    fixedWidth = parentLayerObject['fixedWidth'];
  }
  layerObject['fixedWidth'] = fixedWidth;

  var fixedHeight =
      ol.format.XSD.readDecimalString(node.getAttribute('fixedHeight'));
  if (!fixedHeight) {
    fixedHeight = parentLayerObject['fixedHeight'];
  }
  layerObject['fixedHeight'] = fixedHeight;

  // See 7.2.4.8
  var addKeys = ['Style', 'CRS', 'AuthorityURL'];
  addKeys.forEach(function(key) {
    if (key in parentLayerObject) {
      var childValue = goog.object.setIfUndefined(layerObject, key, []);
      childValue = childValue.concat(parentLayerObject[key]);
      layerObject[key] = childValue;
    }
  });

  var replaceKeys = ['EX_GeographicBoundingBox', 'BoundingBox', 'Dimension',
    'Attribution', 'MinScaleDenominator', 'MaxScaleDenominator'];
  replaceKeys.forEach(function(key) {
    if (!(key in layerObject)) {
      var parentValue = parentLayerObject[key];
      layerObject[key] = parentValue;
    }
  });

  return layerObject;
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object} Dimension object.
 */
ol.format.WMSCapabilities.readDimension_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Dimension',
      'localName should be Dimension');
  var dimensionObject = {
    'name': node.getAttribute('name'),
    'units': node.getAttribute('units'),
    'unitSymbol': node.getAttribute('unitSymbol'),
    'default': node.getAttribute('default'),
    'multipleValues': ol.format.XSD.readBooleanString(
        node.getAttribute('multipleValues')),
    'nearestValue': ol.format.XSD.readBooleanString(
        node.getAttribute('nearestValue')),
    'current': ol.format.XSD.readBooleanString(node.getAttribute('current')),
    'values': ol.format.XSD.readString(node)
  };
  return dimensionObject;
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Online resource object.
 */
ol.format.WMSCapabilities.readFormatOnlineresource_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  return ol.xml.pushParseAndPop(
      {}, ol.format.WMSCapabilities.FORMAT_ONLINERESOURCE_PARSERS_,
      node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Request object.
 */
ol.format.WMSCapabilities.readRequest_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Request',
      'localName should be Request');
  return ol.xml.pushParseAndPop(
      {}, ol.format.WMSCapabilities.REQUEST_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} DCP type object.
 */
ol.format.WMSCapabilities.readDCPType_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'DCPType',
      'localName should be DCPType');
  return ol.xml.pushParseAndPop(
      {}, ol.format.WMSCapabilities.DCPTYPE_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} HTTP object.
 */
ol.format.WMSCapabilities.readHTTP_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'HTTP', 'localName should be HTTP');
  return ol.xml.pushParseAndPop(
      {}, ol.format.WMSCapabilities.HTTP_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Operation type object.
 */
ol.format.WMSCapabilities.readOperationType_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  return ol.xml.pushParseAndPop(
      {}, ol.format.WMSCapabilities.OPERATIONTYPE_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Online resource object.
 */
ol.format.WMSCapabilities.readSizedFormatOnlineresource_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  var formatOnlineresource =
      ol.format.WMSCapabilities.readFormatOnlineresource_(node, objectStack);
  if (formatOnlineresource) {
    var size = [
      ol.format.XSD.readNonNegativeIntegerString(node.getAttribute('width')),
      ol.format.XSD.readNonNegativeIntegerString(node.getAttribute('height'))
    ];
    formatOnlineresource['size'] = size;
    return formatOnlineresource;
  }
  return undefined;
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Authority URL object.
 */
ol.format.WMSCapabilities.readAuthorityURL_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'AuthorityURL',
      'localName should be AuthorityURL');
  var authorityObject =
      ol.format.WMSCapabilities.readFormatOnlineresource_(node, objectStack);
  if (authorityObject) {
    authorityObject['name'] = node.getAttribute('name');
    return authorityObject;
  }
  return undefined;
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Metadata URL object.
 */
ol.format.WMSCapabilities.readMetadataURL_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'MetadataURL',
      'localName should be MetadataURL');
  var metadataObject =
      ol.format.WMSCapabilities.readFormatOnlineresource_(node, objectStack);
  if (metadataObject) {
    metadataObject['type'] = node.getAttribute('type');
    return metadataObject;
  }
  return undefined;
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Style object.
 */
ol.format.WMSCapabilities.readStyle_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Style', 'localName should be Style');
  return ol.xml.pushParseAndPop(
      {}, ol.format.WMSCapabilities.STYLE_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Array.<string>|undefined} Keyword list.
 */
ol.format.WMSCapabilities.readKeywordList_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'KeywordList',
      'localName should be KeywordList');
  return ol.xml.pushParseAndPop(
      [], ol.format.WMSCapabilities.KEYWORDLIST_PARSERS_, node, objectStack);
};


/**
 * @const
 * @private
 * @type {Array.<string>}
 */
ol.format.WMSCapabilities.NAMESPACE_URIS_ = [
  null,
  'http://www.opengis.net/wms'
];


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMSCapabilities.PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Service': ol.xml.makeObjectPropertySetter(
          ol.format.WMSCapabilities.readService_),
      'Capability': ol.xml.makeObjectPropertySetter(
          ol.format.WMSCapabilities.readCapability_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMSCapabilities.CAPABILITY_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Request': ol.xml.makeObjectPropertySetter(
          ol.format.WMSCapabilities.readRequest_),
      'Exception': ol.xml.makeObjectPropertySetter(
          ol.format.WMSCapabilities.readException_),
      'Layer': ol.xml.makeObjectPropertySetter(
          ol.format.WMSCapabilities.readCapabilityLayer_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMSCapabilities.SERVICE_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Name': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'Title': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'Abstract': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'KeywordList': ol.xml.makeObjectPropertySetter(
          ol.format.WMSCapabilities.readKeywordList_),
      'OnlineResource': ol.xml.makeObjectPropertySetter(
          ol.format.XLink.readHref),
      'ContactInformation': ol.xml.makeObjectPropertySetter(
          ol.format.WMSCapabilities.readContactInformation_),
      'Fees': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'AccessConstraints': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString),
      'LayerLimit': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readNonNegativeInteger),
      'MaxWidth': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readNonNegativeInteger),
      'MaxHeight': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readNonNegativeInteger)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMSCapabilities.CONTACT_INFORMATION_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'ContactPersonPrimary': ol.xml.makeObjectPropertySetter(
          ol.format.WMSCapabilities.readContactPersonPrimary_),
      'ContactPosition': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString),
      'ContactAddress': ol.xml.makeObjectPropertySetter(
          ol.format.WMSCapabilities.readContactAddress_),
      'ContactVoiceTelephone': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString),
      'ContactFacsimileTelephone': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString),
      'ContactElectronicMailAddress': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMSCapabilities.CONTACT_PERSON_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'ContactPerson': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString),
      'ContactOrganization': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMSCapabilities.CONTACT_ADDRESS_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'AddressType': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'Address': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'City': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'StateOrProvince': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString),
      'PostCode': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'Country': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMSCapabilities.EXCEPTION_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Format': ol.xml.makeArrayPusher(ol.format.XSD.readString)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMSCapabilities.LAYER_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Name': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'Title': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'Abstract': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'KeywordList': ol.xml.makeObjectPropertySetter(
          ol.format.WMSCapabilities.readKeywordList_),
      'CRS': ol.xml.makeObjectPropertyPusher(ol.format.XSD.readString),
      'EX_GeographicBoundingBox': ol.xml.makeObjectPropertySetter(
          ol.format.WMSCapabilities.readEXGeographicBoundingBox_),
      'BoundingBox': ol.xml.makeObjectPropertyPusher(
          ol.format.WMSCapabilities.readBoundingBox_),
      'Dimension': ol.xml.makeObjectPropertyPusher(
          ol.format.WMSCapabilities.readDimension_),
      'Attribution': ol.xml.makeObjectPropertySetter(
          ol.format.WMSCapabilities.readAttribution_),
      'AuthorityURL': ol.xml.makeObjectPropertyPusher(
          ol.format.WMSCapabilities.readAuthorityURL_),
      'Identifier': ol.xml.makeObjectPropertyPusher(ol.format.XSD.readString),
      'MetadataURL': ol.xml.makeObjectPropertyPusher(
          ol.format.WMSCapabilities.readMetadataURL_),
      'DataURL': ol.xml.makeObjectPropertyPusher(
          ol.format.WMSCapabilities.readFormatOnlineresource_),
      'FeatureListURL': ol.xml.makeObjectPropertyPusher(
          ol.format.WMSCapabilities.readFormatOnlineresource_),
      'Style': ol.xml.makeObjectPropertyPusher(
          ol.format.WMSCapabilities.readStyle_),
      'MinScaleDenominator': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readDecimal),
      'MaxScaleDenominator': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readDecimal),
      'Layer': ol.xml.makeObjectPropertyPusher(
          ol.format.WMSCapabilities.readLayer_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMSCapabilities.ATTRIBUTION_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Title': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'OnlineResource': ol.xml.makeObjectPropertySetter(
          ol.format.XLink.readHref),
      'LogoURL': ol.xml.makeObjectPropertySetter(
          ol.format.WMSCapabilities.readSizedFormatOnlineresource_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMSCapabilities.EX_GEOGRAPHIC_BOUNDING_BOX_PARSERS_ =
    ol.xml.makeStructureNS(ol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'westBoundLongitude': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readDecimal),
      'eastBoundLongitude': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readDecimal),
      'southBoundLatitude': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readDecimal),
      'northBoundLatitude': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readDecimal)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMSCapabilities.REQUEST_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'GetCapabilities': ol.xml.makeObjectPropertySetter(
          ol.format.WMSCapabilities.readOperationType_),
      'GetMap': ol.xml.makeObjectPropertySetter(
          ol.format.WMSCapabilities.readOperationType_),
      'GetFeatureInfo': ol.xml.makeObjectPropertySetter(
          ol.format.WMSCapabilities.readOperationType_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMSCapabilities.OPERATIONTYPE_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Format': ol.xml.makeObjectPropertyPusher(ol.format.XSD.readString),
      'DCPType': ol.xml.makeObjectPropertyPusher(
          ol.format.WMSCapabilities.readDCPType_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMSCapabilities.DCPTYPE_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'HTTP': ol.xml.makeObjectPropertySetter(
          ol.format.WMSCapabilities.readHTTP_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMSCapabilities.HTTP_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Get': ol.xml.makeObjectPropertySetter(
          ol.format.WMSCapabilities.readFormatOnlineresource_),
      'Post': ol.xml.makeObjectPropertySetter(
          ol.format.WMSCapabilities.readFormatOnlineresource_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMSCapabilities.STYLE_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Name': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'Title': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'Abstract': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'LegendURL': ol.xml.makeObjectPropertyPusher(
          ol.format.WMSCapabilities.readSizedFormatOnlineresource_),
      'StyleSheetURL': ol.xml.makeObjectPropertySetter(
          ol.format.WMSCapabilities.readFormatOnlineresource_),
      'StyleURL': ol.xml.makeObjectPropertySetter(
          ol.format.WMSCapabilities.readFormatOnlineresource_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMSCapabilities.FORMAT_ONLINERESOURCE_PARSERS_ =
    ol.xml.makeStructureNS(ol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Format': ol.xml.makeObjectPropertySetter(ol.format.XSD.readString),
      'OnlineResource': ol.xml.makeObjectPropertySetter(
          ol.format.XLink.readHref)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMSCapabilities.KEYWORDLIST_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Keyword': ol.xml.makeArrayPusher(ol.format.XSD.readString)
    });

goog.provide('ol.format.WMSGetFeatureInfo');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.dom.NodeType');
goog.require('goog.object');
goog.require('ol.format.GML2');
goog.require('ol.format.XMLFeature');
goog.require('ol.xml');


/**
 * @classdesc
 * Format for reading WMSGetFeatureInfo format. It uses
 * {@link ol.format.GML2} to read features.
 *
 * @constructor
 * @extends {ol.format.XMLFeature}
 * @api
 */
ol.format.WMSGetFeatureInfo = function() {

  /**
   * @private
   * @type {string}
   */
  this.featureNS_ = 'http://mapserver.gis.umn.edu/mapserver';


  /**
   * @private
   * @type {ol.format.GML2}
   */
  this.gmlFormat_ = new ol.format.GML2();

  goog.base(this);
};
goog.inherits(ol.format.WMSGetFeatureInfo, ol.format.XMLFeature);


/**
 * @const
 * @type {string}
 * @private
 */
ol.format.WMSGetFeatureInfo.featureIdentifier_ = '_feature';


/**
 * @const
 * @type {string}
 * @private
 */
ol.format.WMSGetFeatureInfo.layerIdentifier_ = '_layer';


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Array.<ol.Feature>} Features.
 * @private
 */
ol.format.WMSGetFeatureInfo.prototype.readFeatures_ = function(node, objectStack) {

  node.namespaceURI = this.featureNS_;
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  var localName = ol.xml.getLocalName(node);
  /** @type {Array.<ol.Feature>} */
  var features = [];
  if (node.childNodes.length === 0) {
    return features;
  }
  if (localName == 'msGMLOutput') {
    for (var i = 0, ii = node.childNodes.length; i < ii; i++) {
      var layer = node.childNodes[i];
      if (layer.nodeType !== goog.dom.NodeType.ELEMENT) {
        continue;
      }
      var context = objectStack[0];
      goog.asserts.assert(goog.isObject(context),
          'context should be an Object');

      goog.asserts.assert(layer.localName.indexOf(
          ol.format.WMSGetFeatureInfo.layerIdentifier_) >= 0,
          'localName of layer node should match layerIdentifier');

      var toRemove = ol.format.WMSGetFeatureInfo.layerIdentifier_;
      var featureType = layer.localName.replace(toRemove, '') +
          ol.format.WMSGetFeatureInfo.featureIdentifier_;

      context['featureType'] = featureType;
      context['featureNS'] = this.featureNS_;

      var parsers = {};
      parsers[featureType] = ol.xml.makeArrayPusher(
          this.gmlFormat_.readFeatureElement, this.gmlFormat_);
      var parsersNS = ol.xml.makeStructureNS(
          [context['featureNS'], null], parsers);
      layer.namespaceURI = this.featureNS_;
      var layerFeatures = ol.xml.pushParseAndPop(
          [], parsersNS, layer, objectStack, this.gmlFormat_);
      if (layerFeatures) {
        goog.array.extend(features, layerFeatures);
      }
    }
  }
  if (localName == 'FeatureCollection') {
    var gmlFeatures = ol.xml.pushParseAndPop([],
        this.gmlFormat_.FEATURE_COLLECTION_PARSERS, node,
        [{}], this.gmlFormat_);
    if (gmlFeatures) {
      features = gmlFeatures;
    }
  }
  return features;
};


/**
 * Read all features from a WMSGetFeatureInfo response.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {olx.format.ReadOptions=} opt_options Options.
 * @return {Array.<ol.Feature>} Features.
 * @api stable
 */
ol.format.WMSGetFeatureInfo.prototype.readFeatures;


/**
 * @inheritDoc
 */
ol.format.WMSGetFeatureInfo.prototype.readFeaturesFromNode = function(node, opt_options) {
  var options = {
    'featureType': this.featureType,
    'featureNS': this.featureNS
  };
  if (opt_options) {
    goog.object.extend(options, this.getReadOptions(node, opt_options));
  }
  return this.readFeatures_(node, [options]);
};

goog.provide('ol.format.WMTSCapabilities');

goog.require('goog.asserts');
goog.require('goog.dom.NodeType');
goog.require('ol.extent');
goog.require('ol.format.OWS');
goog.require('ol.format.XLink');
goog.require('ol.format.XML');
goog.require('ol.format.XSD');
goog.require('ol.xml');


/**
 * @classdesc
 * Format for reading WMTS capabilities data.
 *
 * @constructor
 * @extends {ol.format.XML}
 * @api
 */
ol.format.WMTSCapabilities = function() {
  goog.base(this);

  /**
   * @type {ol.format.OWS}
   * @private
   */
  this.owsParser_ = new ol.format.OWS();
};
goog.inherits(ol.format.WMTSCapabilities, ol.format.XML);


/**
 * Read a WMTS capabilities document.
 *
 * @function
 * @param {Document|Node|string} source The XML source.
 * @return {Object} An object representing the WMTS capabilities.
 * @api
 */
ol.format.WMTSCapabilities.prototype.read;


/**
 * @param {Document} doc Document.
 * @return {Object} WMTS Capability object.
 */
ol.format.WMTSCapabilities.prototype.readFromDocument = function(doc) {
  goog.asserts.assert(doc.nodeType == goog.dom.NodeType.DOCUMENT,
      'doc.nodeType should be DOCUMENT');
  for (var n = doc.firstChild; n; n = n.nextSibling) {
    if (n.nodeType == goog.dom.NodeType.ELEMENT) {
      return this.readFromNode(n);
    }
  }
  return null;
};


/**
 * @param {Node} node Node.
 * @return {Object} WMTS Capability object.
 */
ol.format.WMTSCapabilities.prototype.readFromNode = function(node) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Capabilities',
      'localName should be Capabilities');
  this.version = node.getAttribute('version').trim();
  goog.asserts.assertString(this.version, 'this.version should be a string');
  var WMTSCapabilityObject = this.owsParser_.readFromNode(node);
  if (!WMTSCapabilityObject) {
    return null;
  }
  WMTSCapabilityObject['version'] = this.version;
  WMTSCapabilityObject = ol.xml.pushParseAndPop(WMTSCapabilityObject,
      ol.format.WMTSCapabilities.PARSERS_, node, []);
  return WMTSCapabilityObject ? WMTSCapabilityObject : null;
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Attribution object.
 */
ol.format.WMTSCapabilities.readContents_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Contents',
      'localName should be Contents');

  return ol.xml.pushParseAndPop({},
      ol.format.WMTSCapabilities.CONTENTS_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Layers object.
 */
ol.format.WMTSCapabilities.readLayer_ = function(node, objectStack) {
  goog.asserts.assert(node.nodeType == goog.dom.NodeType.ELEMENT,
      'node.nodeType should be ELEMENT');
  goog.asserts.assert(node.localName == 'Layer', 'localName should be Layer');
  return ol.xml.pushParseAndPop({},
      ol.format.WMTSCapabilities.LAYER_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Tile Matrix Set object.
 */
ol.format.WMTSCapabilities.readTileMatrixSet_ = function(node, objectStack) {
  return ol.xml.pushParseAndPop({},
      ol.format.WMTSCapabilities.TMS_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Style object.
 */
ol.format.WMTSCapabilities.readStyle_ = function(node, objectStack) {
  var style = ol.xml.pushParseAndPop({},
      ol.format.WMTSCapabilities.STYLE_PARSERS_, node, objectStack);
  if (!style) {
    return undefined;
  }
  var isDefault = node.getAttribute('isDefault') === 'true';
  style['isDefault'] = isDefault;
  return style;

};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Tile Matrix Set Link object.
 */
ol.format.WMTSCapabilities.readTileMatrixSetLink_ = function(node,
    objectStack) {
  return ol.xml.pushParseAndPop({},
      ol.format.WMTSCapabilities.TMS_LINKS_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Dimension object.
 */
ol.format.WMTSCapabilities.readDimensions_ = function(node, objectStack) {
  return ol.xml.pushParseAndPop({},
      ol.format.WMTSCapabilities.DIMENSION_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Resource URL object.
 */
ol.format.WMTSCapabilities.readResourceUrl_ = function(node, objectStack) {
  var format = node.getAttribute('format');
  var template = node.getAttribute('template');
  var resourceType = node.getAttribute('resourceType');
  var resource = {};
  if (format) {
    resource['format'] = format;
  }
  if (template) {
    resource['template'] = template;
  }
  if (resourceType) {
    resource['resourceType'] = resourceType;
  }
  return resource;
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} WGS84 BBox object.
 */
ol.format.WMTSCapabilities.readWgs84BoundingBox_ = function(node, objectStack) {
  var coordinates = ol.xml.pushParseAndPop([],
      ol.format.WMTSCapabilities.WGS84_BBOX_READERS_, node, objectStack);
  if (coordinates.length != 2) {
    return undefined;
  }
  return ol.extent.boundingExtent(coordinates);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Legend object.
 */
ol.format.WMTSCapabilities.readLegendUrl_ = function(node, objectStack) {
  var legend = {};
  legend['format'] = node.getAttribute('format');
  legend['href'] = ol.format.XLink.readHref(node);
  return legend;
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Coordinates object.
 */
ol.format.WMTSCapabilities.readCoordinates_ = function(node, objectStack) {
  var coordinates = ol.format.XSD.readString(node).split(' ');
  if (!coordinates || coordinates.length != 2) {
    return undefined;
  }
  var x = +coordinates[0];
  var y = +coordinates[1];
  if (isNaN(x) || isNaN(y)) {
    return undefined;
  }
  return [x, y];
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} TileMatrix object.
 */
ol.format.WMTSCapabilities.readTileMatrix_ = function(node, objectStack) {
  return ol.xml.pushParseAndPop({},
      ol.format.WMTSCapabilities.TM_PARSERS_, node, objectStack);
};


/**
 * @const
 * @private
 * @type {Array.<string>}
 */
ol.format.WMTSCapabilities.NAMESPACE_URIS_ = [
  null,
  'http://www.opengis.net/wmts/1.0'
];


/**
 * @const
 * @private
 * @type {Array.<string>}
 */
ol.format.WMTSCapabilities.OWS_NAMESPACE_URIS_ = [
  null,
  'http://www.opengis.net/ows/1.1'
];


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMTSCapabilities.PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMTSCapabilities.NAMESPACE_URIS_, {
      'Contents': ol.xml.makeObjectPropertySetter(
          ol.format.WMTSCapabilities.readContents_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMTSCapabilities.CONTENTS_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMTSCapabilities.NAMESPACE_URIS_, {
      'Layer': ol.xml.makeObjectPropertyPusher(
          ol.format.WMTSCapabilities.readLayer_),
      'TileMatrixSet': ol.xml.makeObjectPropertyPusher(
          ol.format.WMTSCapabilities.readTileMatrixSet_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMTSCapabilities.LAYER_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMTSCapabilities.NAMESPACE_URIS_, {
      'Style': ol.xml.makeObjectPropertyPusher(
          ol.format.WMTSCapabilities.readStyle_),
      'Format': ol.xml.makeObjectPropertyPusher(
          ol.format.XSD.readString),
      'TileMatrixSetLink': ol.xml.makeObjectPropertyPusher(
          ol.format.WMTSCapabilities.readTileMatrixSetLink_),
      'Dimension': ol.xml.makeObjectPropertyPusher(
          ol.format.WMTSCapabilities.readDimensions_),
      'ResourceURL': ol.xml.makeObjectPropertyPusher(
          ol.format.WMTSCapabilities.readResourceUrl_)
    }, ol.xml.makeStructureNS(ol.format.WMTSCapabilities.OWS_NAMESPACE_URIS_, {
      'Title': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString),
      'Abstract': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString),
      'WGS84BoundingBox': ol.xml.makeObjectPropertySetter(
          ol.format.WMTSCapabilities.readWgs84BoundingBox_),
      'Identifier': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString)
    }));


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMTSCapabilities.STYLE_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMTSCapabilities.NAMESPACE_URIS_, {
      'LegendURL': ol.xml.makeObjectPropertyPusher(
          ol.format.WMTSCapabilities.readLegendUrl_)
    }, ol.xml.makeStructureNS(ol.format.WMTSCapabilities.OWS_NAMESPACE_URIS_, {
      'Title': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString),
      'Identifier': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString)
    }));


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMTSCapabilities.TMS_LINKS_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMTSCapabilities.NAMESPACE_URIS_, {
      'TileMatrixSet': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMTSCapabilities.DIMENSION_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMTSCapabilities.NAMESPACE_URIS_, {
      'Default': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString),
      'Value': ol.xml.makeObjectPropertyPusher(
          ol.format.XSD.readString)
    }, ol.xml.makeStructureNS(ol.format.WMTSCapabilities.OWS_NAMESPACE_URIS_, {
      'Identifier': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString)
    }));


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMTSCapabilities.WGS84_BBOX_READERS_ = ol.xml.makeStructureNS(
    ol.format.WMTSCapabilities.OWS_NAMESPACE_URIS_, {
      'LowerCorner': ol.xml.makeArrayPusher(
          ol.format.WMTSCapabilities.readCoordinates_),
      'UpperCorner': ol.xml.makeArrayPusher(
          ol.format.WMTSCapabilities.readCoordinates_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMTSCapabilities.TMS_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMTSCapabilities.NAMESPACE_URIS_, {
      'WellKnownScaleSet': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString),
      'TileMatrix': ol.xml.makeObjectPropertyPusher(
          ol.format.WMTSCapabilities.readTileMatrix_)
    }, ol.xml.makeStructureNS(ol.format.WMTSCapabilities.OWS_NAMESPACE_URIS_, {
      'SupportedCRS': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString),
      'Identifier': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString)
    }));


/**
 * @const
 * @type {Object.<string, Object.<string, ol.xml.Parser>>}
 * @private
 */
ol.format.WMTSCapabilities.TM_PARSERS_ = ol.xml.makeStructureNS(
    ol.format.WMTSCapabilities.NAMESPACE_URIS_, {
      'TopLeftCorner': ol.xml.makeObjectPropertySetter(
          ol.format.WMTSCapabilities.readCoordinates_),
      'ScaleDenominator': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readDecimal),
      'TileWidth': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readNonNegativeInteger),
      'TileHeight': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readNonNegativeInteger),
      'MatrixWidth': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readNonNegativeInteger),
      'MatrixHeight': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readNonNegativeInteger)
    }, ol.xml.makeStructureNS(ol.format.WMTSCapabilities.OWS_NAMESPACE_URIS_, {
      'Identifier': ol.xml.makeObjectPropertySetter(
          ol.format.XSD.readString)
    }));

// FIXME handle geolocation not supported

goog.provide('ol.Geolocation');
goog.provide('ol.GeolocationProperty');

goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('ol.Coordinate');
goog.require('ol.Object');
goog.require('ol.geom.Geometry');
goog.require('ol.geom.Polygon');
goog.require('ol.has');
goog.require('ol.math');
goog.require('ol.proj');
goog.require('ol.sphere.WGS84');


/**
 * @enum {string}
 */
ol.GeolocationProperty = {
  ACCURACY: 'accuracy',
  ACCURACY_GEOMETRY: 'accuracyGeometry',
  ALTITUDE: 'altitude',
  ALTITUDE_ACCURACY: 'altitudeAccuracy',
  HEADING: 'heading',
  POSITION: 'position',
  PROJECTION: 'projection',
  SPEED: 'speed',
  TRACKING: 'tracking',
  TRACKING_OPTIONS: 'trackingOptions'
};


/**
 * @classdesc
 * Helper class for providing HTML5 Geolocation capabilities.
 * The [Geolocation API](http://www.w3.org/TR/geolocation-API/)
 * is used to locate a user's position.
 *
 * To get notified of position changes, register a listener for the generic
 * `change` event on your instance of `ol.Geolocation`.
 *
 * Example:
 *
 *     var geolocation = new ol.Geolocation({
 *       // take the projection to use from the map's view
 *       projection: view.getProjection()
 *     });
 *     // listen to changes in position
 *     geolocation.on('change', function(evt) {
 *       window.console.log(geolocation.getPosition());
 *     });
 *
 * @constructor
 * @extends {ol.Object}
 * @param {olx.GeolocationOptions=} opt_options Options.
 * @api stable
 */
ol.Geolocation = function(opt_options) {

  goog.base(this);

  var options = opt_options || {};

  /**
   * The unprojected (EPSG:4326) device position.
   * @private
   * @type {ol.Coordinate}
   */
  this.position_ = null;

  /**
   * @private
   * @type {ol.TransformFunction}
   */
  this.transform_ = ol.proj.identityTransform;

  /**
   * @private
   * @type {number|undefined}
   */
  this.watchId_ = undefined;

  goog.events.listen(
      this, ol.Object.getChangeEventType(ol.GeolocationProperty.PROJECTION),
      this.handleProjectionChanged_, false, this);
  goog.events.listen(
      this, ol.Object.getChangeEventType(ol.GeolocationProperty.TRACKING),
      this.handleTrackingChanged_, false, this);

  if (options.projection !== undefined) {
    this.setProjection(ol.proj.get(options.projection));
  }
  if (options.trackingOptions !== undefined) {
    this.setTrackingOptions(options.trackingOptions);
  }

  this.setTracking(options.tracking !== undefined ? options.tracking : false);

};
goog.inherits(ol.Geolocation, ol.Object);


/**
 * @inheritDoc
 */
ol.Geolocation.prototype.disposeInternal = function() {
  this.setTracking(false);
  goog.base(this, 'disposeInternal');
};


/**
 * @private
 */
ol.Geolocation.prototype.handleProjectionChanged_ = function() {
  var projection = this.getProjection();
  if (projection) {
    this.transform_ = ol.proj.getTransformFromProjections(
        ol.proj.get('EPSG:4326'), projection);
    if (this.position_) {
      this.set(
          ol.GeolocationProperty.POSITION, this.transform_(this.position_));
    }
  }
};


/**
 * @private
 */
ol.Geolocation.prototype.handleTrackingChanged_ = function() {
  if (ol.has.GEOLOCATION) {
    var tracking = this.getTracking();
    if (tracking && this.watchId_ === undefined) {
      this.watchId_ = goog.global.navigator.geolocation.watchPosition(
          this.positionChange_.bind(this),
          this.positionError_.bind(this),
          this.getTrackingOptions());
    } else if (!tracking && this.watchId_ !== undefined) {
      goog.global.navigator.geolocation.clearWatch(this.watchId_);
      this.watchId_ = undefined;
    }
  }
};


/**
 * @private
 * @param {GeolocationPosition} position position event.
 */
ol.Geolocation.prototype.positionChange_ = function(position) {
  var coords = position.coords;
  this.set(ol.GeolocationProperty.ACCURACY, coords.accuracy);
  this.set(ol.GeolocationProperty.ALTITUDE,
      coords.altitude === null ? undefined : coords.altitude);
  this.set(ol.GeolocationProperty.ALTITUDE_ACCURACY,
      coords.altitudeAccuracy === null ?
      undefined : coords.altitudeAccuracy);
  this.set(ol.GeolocationProperty.HEADING, coords.heading === null ?
      undefined : ol.math.toRadians(coords.heading));
  if (!this.position_) {
    this.position_ = [coords.longitude, coords.latitude];
  } else {
    this.position_[0] = coords.longitude;
    this.position_[1] = coords.latitude;
  }
  var projectedPosition = this.transform_(this.position_);
  this.set(ol.GeolocationProperty.POSITION, projectedPosition);
  this.set(ol.GeolocationProperty.SPEED,
      coords.speed === null ? undefined : coords.speed);
  var geometry = ol.geom.Polygon.circular(
      ol.sphere.WGS84, this.position_, coords.accuracy);
  geometry.applyTransform(this.transform_);
  this.set(ol.GeolocationProperty.ACCURACY_GEOMETRY, geometry);
  this.changed();
};


/**
 * @private
 * @param {GeolocationPositionError} error error object.
 */
ol.Geolocation.prototype.positionError_ = function(error) {
  error.type = goog.events.EventType.ERROR;
  this.setTracking(false);
  this.dispatchEvent(error);
};


/**
 * Get the accuracy of the position in meters.
 * @return {number|undefined} The accuracy of the position measurement in
 *     meters.
 * @observable
 * @api stable
 */
ol.Geolocation.prototype.getAccuracy = function() {
  return /** @type {number|undefined} */ (
      this.get(ol.GeolocationProperty.ACCURACY));
};


/**
 * Get a geometry of the position accuracy.
 * @return {?ol.geom.Geometry} A geometry of the position accuracy.
 * @observable
 * @api stable
 */
ol.Geolocation.prototype.getAccuracyGeometry = function() {
  return /** @type {?ol.geom.Geometry} */ (
      this.get(ol.GeolocationProperty.ACCURACY_GEOMETRY) || null);
};


/**
 * Get the altitude associated with the position.
 * @return {number|undefined} The altitude of the position in meters above mean
 *     sea level.
 * @observable
 * @api stable
 */
ol.Geolocation.prototype.getAltitude = function() {
  return /** @type {number|undefined} */ (
      this.get(ol.GeolocationProperty.ALTITUDE));
};


/**
 * Get the altitude accuracy of the position.
 * @return {number|undefined} The accuracy of the altitude measurement in
 *     meters.
 * @observable
 * @api stable
 */
ol.Geolocation.prototype.getAltitudeAccuracy = function() {
  return /** @type {number|undefined} */ (
      this.get(ol.GeolocationProperty.ALTITUDE_ACCURACY));
};


/**
 * Get the heading as radians clockwise from North.
 * @return {number|undefined} The heading of the device in radians from north.
 * @observable
 * @api stable
 */
ol.Geolocation.prototype.getHeading = function() {
  return /** @type {number|undefined} */ (
      this.get(ol.GeolocationProperty.HEADING));
};


/**
 * Get the position of the device.
 * @return {ol.Coordinate|undefined} The current position of the device reported
 *     in the current projection.
 * @observable
 * @api stable
 */
ol.Geolocation.prototype.getPosition = function() {
  return /** @type {ol.Coordinate|undefined} */ (
      this.get(ol.GeolocationProperty.POSITION));
};


/**
 * Get the projection associated with the position.
 * @return {ol.proj.Projection|undefined} The projection the position is
 *     reported in.
 * @observable
 * @api stable
 */
ol.Geolocation.prototype.getProjection = function() {
  return /** @type {ol.proj.Projection|undefined} */ (
      this.get(ol.GeolocationProperty.PROJECTION));
};


/**
 * Get the speed in meters per second.
 * @return {number|undefined} The instantaneous speed of the device in meters
 *     per second.
 * @observable
 * @api stable
 */
ol.Geolocation.prototype.getSpeed = function() {
  return /** @type {number|undefined} */ (
      this.get(ol.GeolocationProperty.SPEED));
};


/**
 * Determine if the device location is being tracked.
 * @return {boolean} The device location is being tracked.
 * @observable
 * @api stable
 */
ol.Geolocation.prototype.getTracking = function() {
  return /** @type {boolean} */ (
      this.get(ol.GeolocationProperty.TRACKING));
};


/**
 * Get the tracking options.
 * @see http://www.w3.org/TR/geolocation-API/#position-options
 * @return {GeolocationPositionOptions|undefined} PositionOptions as defined by
 *     the [HTML5 Geolocation spec
 *     ](http://www.w3.org/TR/geolocation-API/#position_options_interface).
 * @observable
 * @api stable
 */
ol.Geolocation.prototype.getTrackingOptions = function() {
  return /** @type {GeolocationPositionOptions|undefined} */ (
      this.get(ol.GeolocationProperty.TRACKING_OPTIONS));
};


/**
 * Set the projection to use for transforming the coordinates.
 * @param {ol.proj.Projection} projection The projection the position is
 *     reported in.
 * @observable
 * @api stable
 */
ol.Geolocation.prototype.setProjection = function(projection) {
  this.set(ol.GeolocationProperty.PROJECTION, projection);
};


/**
 * Enable or disable tracking.
 * @param {boolean} tracking Enable tracking.
 * @observable
 * @api stable
 */
ol.Geolocation.prototype.setTracking = function(tracking) {
  this.set(ol.GeolocationProperty.TRACKING, tracking);
};


/**
 * Set the tracking options.
 * @see http://www.w3.org/TR/geolocation-API/#position-options
 * @param {GeolocationPositionOptions} options PositionOptions as defined by the
 *     [HTML5 Geolocation spec
 *     ](http://www.w3.org/TR/geolocation-API/#position_options_interface).
 * @observable
 * @api stable
 */
ol.Geolocation.prototype.setTrackingOptions = function(options) {
  this.set(ol.GeolocationProperty.TRACKING_OPTIONS, options);
};

goog.provide('ol.geom.Circle');

goog.require('goog.asserts');
goog.require('ol.extent');
goog.require('ol.geom.GeometryLayout');
goog.require('ol.geom.GeometryType');
goog.require('ol.geom.SimpleGeometry');
goog.require('ol.geom.flat.deflate');
goog.require('ol.proj');


/**
 * @classdesc
 * Circle geometry.
 *
 * @constructor
 * @extends {ol.geom.SimpleGeometry}
 * @param {ol.Coordinate} center Center.
 * @param {number=} opt_radius Radius.
 * @param {ol.geom.GeometryLayout=} opt_layout Layout.
 * @api
 */
ol.geom.Circle = function(center, opt_radius, opt_layout) {
  goog.base(this);
  var radius = opt_radius ? opt_radius : 0;
  this.setCenterAndRadius(center, radius, opt_layout);
};
goog.inherits(ol.geom.Circle, ol.geom.SimpleGeometry);


/**
 * Make a complete copy of the geometry.
 * @return {!ol.geom.Circle} Clone.
 * @api
 */
ol.geom.Circle.prototype.clone = function() {
  var circle = new ol.geom.Circle(null);
  circle.setFlatCoordinates(this.layout, this.flatCoordinates.slice());
  return circle;
};


/**
 * @inheritDoc
 */
ol.geom.Circle.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
  var flatCoordinates = this.flatCoordinates;
  var dx = x - flatCoordinates[0];
  var dy = y - flatCoordinates[1];
  var squaredDistance = dx * dx + dy * dy;
  if (squaredDistance < minSquaredDistance) {
    var i;
    if (squaredDistance === 0) {
      for (i = 0; i < this.stride; ++i) {
        closestPoint[i] = flatCoordinates[i];
      }
    } else {
      var delta = this.getRadius() / Math.sqrt(squaredDistance);
      closestPoint[0] = flatCoordinates[0] + delta * dx;
      closestPoint[1] = flatCoordinates[1] + delta * dy;
      for (i = 2; i < this.stride; ++i) {
        closestPoint[i] = flatCoordinates[i];
      }
    }
    closestPoint.length = this.stride;
    return squaredDistance;
  } else {
    return minSquaredDistance;
  }
};


/**
 * @inheritDoc
 */
ol.geom.Circle.prototype.containsXY = function(x, y) {
  var flatCoordinates = this.flatCoordinates;
  var dx = x - flatCoordinates[0];
  var dy = y - flatCoordinates[1];
  return dx * dx + dy * dy <= this.getRadiusSquared_();
};


/**
 * Return the center of the circle as {@link ol.Coordinate coordinate}.
 * @return {ol.Coordinate} Center.
 * @api
 */
ol.geom.Circle.prototype.getCenter = function() {
  return this.flatCoordinates.slice(0, this.stride);
};


/**
 * @inheritDoc
 */
ol.geom.Circle.prototype.computeExtent = function(extent) {
  var flatCoordinates = this.flatCoordinates;
  var radius = flatCoordinates[this.stride] - flatCoordinates[0];
  return ol.extent.createOrUpdate(
      flatCoordinates[0] - radius, flatCoordinates[1] - radius,
      flatCoordinates[0] + radius, flatCoordinates[1] + radius,
      extent);
};


/**
 * Return the radius of the circle.
 * @return {number} Radius.
 * @api
 */
ol.geom.Circle.prototype.getRadius = function() {
  return Math.sqrt(this.getRadiusSquared_());
};


/**
 * @private
 * @return {number} Radius squared.
 */
ol.geom.Circle.prototype.getRadiusSquared_ = function() {
  var dx = this.flatCoordinates[this.stride] - this.flatCoordinates[0];
  var dy = this.flatCoordinates[this.stride + 1] - this.flatCoordinates[1];
  return dx * dx + dy * dy;
};


/**
 * @inheritDoc
 * @api
 */
ol.geom.Circle.prototype.getType = function() {
  return ol.geom.GeometryType.CIRCLE;
};


/**
 * @inheritDoc
 * @api stable
 */
ol.geom.Circle.prototype.intersectsExtent = function(extent) {
  var circleExtent = this.getExtent();
  if (ol.extent.intersects(extent, circleExtent)) {
    var center = this.getCenter();

    if (extent[0] <= center[0] && extent[2] >= center[0]) {
      return true;
    }
    if (extent[1] <= center[1] && extent[3] >= center[1]) {
      return true;
    }

    return ol.extent.forEachCorner(extent, this.containsCoordinate, this);
  }
  return false;

};


/**
 * Set the center of the circle as {@link ol.Coordinate coordinate}.
 * @param {ol.Coordinate} center Center.
 * @api
 */
ol.geom.Circle.prototype.setCenter = function(center) {
  var stride = this.stride;
  goog.asserts.assert(center.length == stride,
      'center array length should match stride');
  var radius = this.flatCoordinates[stride] - this.flatCoordinates[0];
  var flatCoordinates = center.slice();
  flatCoordinates[stride] = flatCoordinates[0] + radius;
  var i;
  for (i = 1; i < stride; ++i) {
    flatCoordinates[stride + i] = center[i];
  }
  this.setFlatCoordinates(this.layout, flatCoordinates);
};


/**
 * Set the center (as {@link ol.Coordinate coordinate}) and the radius (as
 * number) of the circle.
 * @param {ol.Coordinate} center Center.
 * @param {number} radius Radius.
 * @param {ol.geom.GeometryLayout=} opt_layout Layout.
 * @api
 */
ol.geom.Circle.prototype.setCenterAndRadius = function(center, radius, opt_layout) {
  if (!center) {
    this.setFlatCoordinates(ol.geom.GeometryLayout.XY, null);
  } else {
    this.setLayout(opt_layout, center, 0);
    if (!this.flatCoordinates) {
      this.flatCoordinates = [];
    }
    /** @type {Array.<number>} */
    var flatCoordinates = this.flatCoordinates;
    var offset = ol.geom.flat.deflate.coordinate(
        flatCoordinates, 0, center, this.stride);
    flatCoordinates[offset++] = flatCoordinates[0] + radius;
    var i, ii;
    for (i = 1, ii = this.stride; i < ii; ++i) {
      flatCoordinates[offset++] = flatCoordinates[i];
    }
    flatCoordinates.length = offset;
    this.changed();
  }
};


/**
 * @param {ol.geom.GeometryLayout} layout Layout.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 */
ol.geom.Circle.prototype.setFlatCoordinates = function(layout, flatCoordinates) {
  this.setFlatCoordinatesInternal(layout, flatCoordinates);
  this.changed();
};


/**
 * Set the radius of the circle. The radius is in the units of the projection.
 * @param {number} radius Radius.
 * @api
 */
ol.geom.Circle.prototype.setRadius = function(radius) {
  goog.asserts.assert(this.flatCoordinates,
      'truthy this.flatCoordinates expected');
  this.flatCoordinates[this.stride] = this.flatCoordinates[0] + radius;
  this.changed();
};


/**
 * Transform each coordinate of the circle from one coordinate reference system
 * to another. The geometry is modified in place.
 * If you do not want the geometry modified in place, first clone() it and
 * then use this function on the clone.
 *
 * Internally a circle is currently represented by two points: the center of
 * the circle `[cx, cy]`, and the point to the right of the circle
 * `[cx + r, cy]`. This `transform` function just transforms these two points.
 * So the resulting geometry is also a circle, and that circle does not
 * correspond to the shape that would be obtained by transforming every point
 * of the original circle.
 *
 * @param {ol.proj.ProjectionLike} source The current projection.  Can be a
 *     string identifier or a {@link ol.proj.Projection} object.
 * @param {ol.proj.ProjectionLike} destination The desired projection.  Can be a
 *     string identifier or a {@link ol.proj.Projection} object.
 * @return {ol.geom.Circle} This geometry.  Note that original geometry is
 *     modified in place.
 * @function
 * @api stable
 */
ol.geom.Circle.prototype.transform;

goog.provide('ol.geom.flat.geodesic');

goog.require('goog.asserts');
goog.require('ol.TransformFunction');
goog.require('ol.math');
goog.require('ol.proj');


/**
 * @private
 * @param {function(number): ol.Coordinate} interpolate Interpolate function.
 * @param {ol.TransformFunction} transform Transform from longitude/latitude to
 *     projected coordinates.
 * @param {number} squaredTolerance Squared tolerance.
 * @return {Array.<number>} Flat coordinates.
 */
ol.geom.flat.geodesic.line_ = function(interpolate, transform, squaredTolerance) {
  // FIXME reduce garbage generation
  // FIXME optimize stack operations

  /** @type {Array.<number>} */
  var flatCoordinates = [];

  var geoA = interpolate(0);
  var geoB = interpolate(1);

  var a = transform(geoA);
  var b = transform(geoB);

  /** @type {Array.<ol.Coordinate>} */
  var geoStack = [geoB, geoA];
  /** @type {Array.<ol.Coordinate>} */
  var stack = [b, a];
  /** @type {Array.<number>} */
  var fractionStack = [1, 0];

  /** @type {Object.<string, boolean>} */
  var fractions = {};

  var maxIterations = 1e5;
  var geoM, m, fracA, fracB, fracM, key;

  while (--maxIterations > 0 && fractionStack.length > 0) {
    // Pop the a coordinate off the stack
    fracA = fractionStack.pop();
    geoA = geoStack.pop();
    a = stack.pop();
    // Add the a coordinate if it has not been added yet
    key = fracA.toString();
    if (!(key in fractions)) {
      flatCoordinates.push(a[0], a[1]);
      fractions[key] = true;
    }
    // Pop the b coordinate off the stack
    fracB = fractionStack.pop();
    geoB = geoStack.pop();
    b = stack.pop();
    // Find the m point between the a and b coordinates
    fracM = (fracA + fracB) / 2;
    geoM = interpolate(fracM);
    m = transform(geoM);
    if (ol.math.squaredSegmentDistance(m[0], m[1], a[0], a[1],
        b[0], b[1]) < squaredTolerance) {
      // If the m point is sufficiently close to the straight line, then we
      // discard it.  Just use the b coordinate and move on to the next line
      // segment.
      flatCoordinates.push(b[0], b[1]);
      key = fracB.toString();
      goog.asserts.assert(!(key in fractions),
          'fractions object should contain key : ' + key);
      fractions[key] = true;
    } else {
      // Otherwise, we need to subdivide the current line segment.  Split it
      // into two and push the two line segments onto the stack.
      fractionStack.push(fracB, fracM, fracM, fracA);
      stack.push(b, m, m, a);
      geoStack.push(geoB, geoM, geoM, geoA);
    }
  }
  goog.asserts.assert(maxIterations > 0,
      'maxIterations should be more than 0');

  return flatCoordinates;
};


/**
* Generate a great-circle arcs between two lat/lon points.
* @param {number} lon1 Longitude 1 in degrees.
* @param {number} lat1 Latitude 1 in degrees.
* @param {number} lon2 Longitude 2 in degrees.
* @param {number} lat2 Latitude 2 in degrees.
 * @param {ol.proj.Projection} projection Projection.
* @param {number} squaredTolerance Squared tolerance.
* @return {Array.<number>} Flat coordinates.
*/
ol.geom.flat.geodesic.greatCircleArc = function(
    lon1, lat1, lon2, lat2, projection, squaredTolerance) {

  var geoProjection = ol.proj.get('EPSG:4326');

  var cosLat1 = Math.cos(ol.math.toRadians(lat1));
  var sinLat1 = Math.sin(ol.math.toRadians(lat1));
  var cosLat2 = Math.cos(ol.math.toRadians(lat2));
  var sinLat2 = Math.sin(ol.math.toRadians(lat2));
  var cosDeltaLon = Math.cos(ol.math.toRadians(lon2 - lon1));
  var sinDeltaLon = Math.sin(ol.math.toRadians(lon2 - lon1));
  var d = sinLat1 * sinLat2 + cosLat1 * cosLat2 * cosDeltaLon;

  return ol.geom.flat.geodesic.line_(
      /**
       * @param {number} frac Fraction.
       * @return {ol.Coordinate} Coordinate.
       */
      function(frac) {
        if (1 <= d) {
          return [lon2, lat2];
        }
        var D = frac * Math.acos(d);
        var cosD = Math.cos(D);
        var sinD = Math.sin(D);
        var y = sinDeltaLon * cosLat2;
        var x = cosLat1 * sinLat2 - sinLat1 * cosLat2 * cosDeltaLon;
        var theta = Math.atan2(y, x);
        var lat = Math.asin(sinLat1 * cosD + cosLat1 * sinD * Math.cos(theta));
        var lon = ol.math.toRadians(lon1) +
            Math.atan2(Math.sin(theta) * sinD * cosLat1,
                       cosD - sinLat1 * Math.sin(lat));
        return [ol.math.toDegrees(lon), ol.math.toDegrees(lat)];
      }, ol.proj.getTransform(geoProjection, projection), squaredTolerance);
};


/**
 * Generate a meridian (line at constant longitude).
 * @param {number} lon Longitude.
 * @param {number} lat1 Latitude 1.
 * @param {number} lat2 Latitude 2.
 * @param {ol.proj.Projection} projection Projection.
 * @param {number} squaredTolerance Squared tolerance.
 * @return {Array.<number>} Flat coordinates.
 */
ol.geom.flat.geodesic.meridian = function(lon, lat1, lat2, projection, squaredTolerance) {
  var epsg4326Projection = ol.proj.get('EPSG:4326');
  return ol.geom.flat.geodesic.line_(
      /**
       * @param {number} frac Fraction.
       * @return {ol.Coordinate} Coordinate.
       */
      function(frac) {
        return [lon, lat1 + ((lat2 - lat1) * frac)];
      },
      ol.proj.getTransform(epsg4326Projection, projection), squaredTolerance);
};


/**
 * Generate a parallel (line at constant latitude).
 * @param {number} lat Latitude.
 * @param {number} lon1 Longitude 1.
 * @param {number} lon2 Longitude 2.
 * @param {ol.proj.Projection} projection Projection.
 * @param {number} squaredTolerance Squared tolerance.
 * @return {Array.<number>} Flat coordinates.
 */
ol.geom.flat.geodesic.parallel = function(lat, lon1, lon2, projection, squaredTolerance) {
  var epsg4326Projection = ol.proj.get('EPSG:4326');
  return ol.geom.flat.geodesic.line_(
      /**
       * @param {number} frac Fraction.
       * @return {ol.Coordinate} Coordinate.
       */
      function(frac) {
        return [lon1 + ((lon2 - lon1) * frac), lat];
      },
      ol.proj.getTransform(epsg4326Projection, projection), squaredTolerance);
};

goog.provide('ol.Graticule');

goog.require('goog.asserts');
goog.require('ol.extent');
goog.require('ol.geom.GeometryLayout');
goog.require('ol.geom.LineString');
goog.require('ol.geom.flat.geodesic');
goog.require('ol.math');
goog.require('ol.proj');
goog.require('ol.render.EventType');
goog.require('ol.style.Stroke');


/**
 * Render a grid for a coordinate system on a map.
 * @constructor
 * @param {olx.GraticuleOptions=} opt_options Options.
 * @api
 */
ol.Graticule = function(opt_options) {

  var options = opt_options || {};

  /**
   * @type {ol.Map}
   * @private
   */
  this.map_ = null;

  /**
   * @type {ol.proj.Projection}
   * @private
   */
  this.projection_ = null;

  /**
   * @type {number}
   * @private
   */
  this.maxLat_ = Infinity;

  /**
   * @type {number}
   * @private
   */
  this.maxLon_ = Infinity;

  /**
   * @type {number}
   * @private
   */
  this.minLat_ = -Infinity;

  /**
   * @type {number}
   * @private
   */
  this.minLon_ = -Infinity;

  /**
   * @type {number}
   * @private
   */
  this.maxLatP_ = Infinity;

  /**
   * @type {number}
   * @private
   */
  this.maxLonP_ = Infinity;

  /**
   * @type {number}
   * @private
   */
  this.minLatP_ = -Infinity;

  /**
   * @type {number}
   * @private
   */
  this.minLonP_ = -Infinity;

  /**
   * @type {number}
   * @private
   */
  this.targetSize_ = options.targetSize !== undefined ?
      options.targetSize : 100;

  /**
   * @type {number}
   * @private
   */
  this.maxLines_ = options.maxLines !== undefined ? options.maxLines : 100;
  goog.asserts.assert(this.maxLines_ > 0,
      'this.maxLines_ should be more than 0');

  /**
   * @type {Array.<ol.geom.LineString>}
   * @private
   */
  this.meridians_ = [];

  /**
   * @type {Array.<ol.geom.LineString>}
   * @private
   */
  this.parallels_ = [];

  /**
   * @type {ol.style.Stroke}
   * @private
   */
  this.strokeStyle_ = options.strokeStyle !== undefined ?
      options.strokeStyle : ol.Graticule.DEFAULT_STROKE_STYLE_;

  /**
   * @type {ol.TransformFunction|undefined}
   * @private
   */
  this.fromLonLatTransform_ = undefined;

  /**
   * @type {ol.TransformFunction|undefined}
   * @private
   */
  this.toLonLatTransform_ = undefined;

  /**
   * @type {ol.Coordinate}
   * @private
   */
  this.projectionCenterLonLat_ = null;

  this.setMap(options.map !== undefined ? options.map : null);
};


/**
 * @type {ol.style.Stroke}
 * @private
 * @const
 */
ol.Graticule.DEFAULT_STROKE_STYLE_ = new ol.style.Stroke({
  color: 'rgba(0,0,0,0.2)'
});


/**
 * TODO can be configurable
 * @type {Array.<number>}
 * @private
 */
ol.Graticule.intervals_ = [90, 45, 30, 20, 10, 5, 2, 1, 0.5, 0.2, 0.1, 0.05,
  0.01, 0.005, 0.002, 0.001];


/**
 * @param {number} lon Longitude.
 * @param {number} minLat Minimal latitude.
 * @param {number} maxLat Maximal latitude.
 * @param {number} squaredTolerance Squared tolerance.
 * @param {ol.Extent} extent Extent.
 * @param {number} index Index.
 * @return {number} Index.
 * @private
 */
ol.Graticule.prototype.addMeridian_ = function(lon, minLat, maxLat, squaredTolerance, extent, index) {
  var lineString = this.getMeridian_(lon, minLat, maxLat,
      squaredTolerance, index);
  if (ol.extent.intersects(lineString.getExtent(), extent)) {
    this.meridians_[index++] = lineString;
  }
  return index;
};


/**
 * @param {number} lat Latitude.
 * @param {number} minLon Minimal longitude.
 * @param {number} maxLon Maximal longitude.
 * @param {number} squaredTolerance Squared tolerance.
 * @param {ol.Extent} extent Extent.
 * @param {number} index Index.
 * @return {number} Index.
 * @private
 */
ol.Graticule.prototype.addParallel_ = function(lat, minLon, maxLon, squaredTolerance, extent, index) {
  var lineString = this.getParallel_(lat, minLon, maxLon, squaredTolerance,
      index);
  if (ol.extent.intersects(lineString.getExtent(), extent)) {
    this.parallels_[index++] = lineString;
  }
  return index;
};


/**
 * @param {ol.Extent} extent Extent.
 * @param {ol.Coordinate} center Center.
 * @param {number} resolution Resolution.
 * @param {number} squaredTolerance Squared tolerance.
 * @private
 */
ol.Graticule.prototype.createGraticule_ = function(extent, center, resolution, squaredTolerance) {

  var interval = this.getInterval_(resolution);
  if (interval == -1) {
    this.meridians_.length = this.parallels_.length = 0;
    return;
  }

  var centerLonLat = this.toLonLatTransform_(center);
  var centerLon = centerLonLat[0];
  var centerLat = centerLonLat[1];
  var maxLines = this.maxLines_;
  var cnt, idx, lat, lon;

  var validExtent = [
    Math.max(extent[0], this.minLonP_),
    Math.max(extent[1], this.minLatP_),
    Math.min(extent[2], this.maxLonP_),
    Math.min(extent[3], this.maxLatP_)
  ];

  validExtent = ol.proj.transformExtent(validExtent, this.projection_,
      'EPSG:4326');
  var maxLat = validExtent[3];
  var maxLon = validExtent[2];
  var minLat = validExtent[1];
  var minLon = validExtent[0];

  // Create meridians

  centerLon = Math.floor(centerLon / interval) * interval;
  lon = ol.math.clamp(centerLon, this.minLon_, this.maxLon_);

  idx = this.addMeridian_(lon, minLat, maxLat, squaredTolerance, extent, 0);

  cnt = 0;
  while (lon != this.minLon_ && cnt++ < maxLines) {
    lon = Math.max(lon - interval, this.minLon_);
    idx = this.addMeridian_(lon, minLat, maxLat, squaredTolerance, extent, idx);
  }

  lon = ol.math.clamp(centerLon, this.minLon_, this.maxLon_);

  cnt = 0;
  while (lon != this.maxLon_ && cnt++ < maxLines) {
    lon = Math.min(lon + interval, this.maxLon_);
    idx = this.addMeridian_(lon, minLat, maxLat, squaredTolerance, extent, idx);
  }

  this.meridians_.length = idx;

  // Create parallels

  centerLat = Math.floor(centerLat / interval) * interval;
  lat = ol.math.clamp(centerLat, this.minLat_, this.maxLat_);

  idx = this.addParallel_(lat, minLon, maxLon, squaredTolerance, extent, 0);

  cnt = 0;
  while (lat != this.minLat_ && cnt++ < maxLines) {
    lat = Math.max(lat - interval, this.minLat_);
    idx = this.addParallel_(lat, minLon, maxLon, squaredTolerance, extent, idx);
  }

  lat = ol.math.clamp(centerLat, this.minLat_, this.maxLat_);

  cnt = 0;
  while (lat != this.maxLat_ && cnt++ < maxLines) {
    lat = Math.min(lat + interval, this.maxLat_);
    idx = this.addParallel_(lat, minLon, maxLon, squaredTolerance, extent, idx);
  }

  this.parallels_.length = idx;

};


/**
 * @param {number} resolution Resolution.
 * @return {number} The interval in degrees.
 * @private
 */
ol.Graticule.prototype.getInterval_ = function(resolution) {
  var centerLon = this.projectionCenterLonLat_[0];
  var centerLat = this.projectionCenterLonLat_[1];
  var interval = -1;
  var i, ii, delta, dist;
  var target = Math.pow(this.targetSize_ * resolution, 2);
  /** @type {Array.<number>} **/
  var p1 = [];
  /** @type {Array.<number>} **/
  var p2 = [];
  for (i = 0, ii = ol.Graticule.intervals_.length; i < ii; ++i) {
    delta = ol.Graticule.intervals_[i] / 2;
    p1[0] = centerLon - delta;
    p1[1] = centerLat - delta;
    p2[0] = centerLon + delta;
    p2[1] = centerLat + delta;
    this.fromLonLatTransform_(p1, p1);
    this.fromLonLatTransform_(p2, p2);
    dist = Math.pow(p2[0] - p1[0], 2) + Math.pow(p2[1] - p1[1], 2);
    if (dist <= target) {
      break;
    }
    interval = ol.Graticule.intervals_[i];
  }
  return interval;
};


/**
 * Get the map associated with this graticule.
 * @return {ol.Map} The map.
 * @api
 */
ol.Graticule.prototype.getMap = function() {
  return this.map_;
};


/**
 * @param {number} lon Longitude.
 * @param {number} minLat Minimal latitude.
 * @param {number} maxLat Maximal latitude.
 * @param {number} squaredTolerance Squared tolerance.
 * @return {ol.geom.LineString} The meridian line string.
 * @param {number} index Index.
 * @private
 */
ol.Graticule.prototype.getMeridian_ = function(lon, minLat, maxLat,
                                               squaredTolerance, index) {
  goog.asserts.assert(lon >= this.minLon_,
      'lon should be larger than or equal to this.minLon_');
  goog.asserts.assert(lon <= this.maxLon_,
      'lon should be smaller than or equal to this.maxLon_');
  var flatCoordinates = ol.geom.flat.geodesic.meridian(lon,
      minLat, maxLat, this.projection_, squaredTolerance);
  goog.asserts.assert(flatCoordinates.length > 0,
      'flatCoordinates cannot be empty');
  var lineString = this.meridians_[index] !== undefined ?
      this.meridians_[index] : new ol.geom.LineString(null);
  lineString.setFlatCoordinates(ol.geom.GeometryLayout.XY, flatCoordinates);
  return lineString;
};


/**
 * Get the list of meridians.  Meridians are lines of equal longitude.
 * @return {Array.<ol.geom.LineString>} The meridians.
 * @api
 */
ol.Graticule.prototype.getMeridians = function() {
  return this.meridians_;
};


/**
 * @param {number} lat Latitude.
 * @param {number} minLon Minimal longitude.
 * @param {number} maxLon Maximal longitude.
 * @param {number} squaredTolerance Squared tolerance.
 * @return {ol.geom.LineString} The parallel line string.
 * @param {number} index Index.
 * @private
 */
ol.Graticule.prototype.getParallel_ = function(lat, minLon, maxLon,
                                               squaredTolerance, index) {
  goog.asserts.assert(lat >= this.minLat_,
      'lat should be larger than or equal to this.minLat_');
  goog.asserts.assert(lat <= this.maxLat_,
      'lat should be smaller than or equal to this.maxLat_');
  var flatCoordinates = ol.geom.flat.geodesic.parallel(lat,
      this.minLon_, this.maxLon_, this.projection_, squaredTolerance);
  goog.asserts.assert(flatCoordinates.length > 0,
      'flatCoordinates cannot be empty');
  var lineString = this.parallels_[index] !== undefined ?
      this.parallels_[index] : new ol.geom.LineString(null);
  lineString.setFlatCoordinates(ol.geom.GeometryLayout.XY, flatCoordinates);
  return lineString;
};


/**
 * Get the list of parallels.  Pallels are lines of equal latitude.
 * @return {Array.<ol.geom.LineString>} The parallels.
 * @api
 */
ol.Graticule.prototype.getParallels = function() {
  return this.parallels_;
};


/**
 * @param {ol.render.Event} e Event.
 * @private
 */
ol.Graticule.prototype.handlePostCompose_ = function(e) {
  var vectorContext = e.vectorContext;
  var frameState = e.frameState;
  var extent = frameState.extent;
  var viewState = frameState.viewState;
  var center = viewState.center;
  var projection = viewState.projection;
  var resolution = viewState.resolution;
  var pixelRatio = frameState.pixelRatio;
  var squaredTolerance =
      resolution * resolution / (4 * pixelRatio * pixelRatio);

  var updateProjectionInfo = !this.projection_ ||
      !ol.proj.equivalent(this.projection_, projection);

  if (updateProjectionInfo) {
    this.updateProjectionInfo_(projection);
  }

  //Fix the extent if wrapped.
  //(note: this is the same extent as vectorContext.extent_)
  var offsetX = 0;
  if (projection.canWrapX()) {
    var projectionExtent = projection.getExtent();
    var worldWidth = ol.extent.getWidth(projectionExtent);
    var x = frameState.focus[0];
    if (x < projectionExtent[0] || x > projectionExtent[2]) {
      var worldsAway = Math.ceil((projectionExtent[0] - x) / worldWidth);
      offsetX = worldWidth * worldsAway;
      extent = [
        extent[0] + offsetX, extent[1],
        extent[2] + offsetX, extent[3]
      ];
    }
  }

  this.createGraticule_(extent, center, resolution, squaredTolerance);

  // Draw the lines
  vectorContext.setFillStrokeStyle(null, this.strokeStyle_);
  var i, l, line;
  for (i = 0, l = this.meridians_.length; i < l; ++i) {
    line = this.meridians_[i];
    vectorContext.drawLineStringGeometry(line, null);
  }
  for (i = 0, l = this.parallels_.length; i < l; ++i) {
    line = this.parallels_[i];
    vectorContext.drawLineStringGeometry(line, null);
  }
};


/**
 * @param {ol.proj.Projection} projection Projection.
 * @private
 */
ol.Graticule.prototype.updateProjectionInfo_ = function(projection) {
  goog.asserts.assert(projection, 'projection cannot be null');

  var epsg4326Projection = ol.proj.get('EPSG:4326');

  var extent = projection.getExtent();
  var worldExtent = projection.getWorldExtent();
  var worldExtentP = ol.proj.transformExtent(worldExtent,
      epsg4326Projection, projection);

  var maxLat = worldExtent[3];
  var maxLon = worldExtent[2];
  var minLat = worldExtent[1];
  var minLon = worldExtent[0];

  var maxLatP = worldExtentP[3];
  var maxLonP = worldExtentP[2];
  var minLatP = worldExtentP[1];
  var minLonP = worldExtentP[0];

  goog.asserts.assert(extent, 'extent cannot be null');
  goog.asserts.assert(maxLat !== undefined, 'maxLat should be defined');
  goog.asserts.assert(maxLon !== undefined, 'maxLon should be defined');
  goog.asserts.assert(minLat !== undefined, 'minLat should be defined');
  goog.asserts.assert(minLon !== undefined, 'minLon should be defined');

  goog.asserts.assert(maxLatP !== undefined,
      'projected maxLat should be defined');
  goog.asserts.assert(maxLonP !== undefined,
      'projected maxLon should be defined');
  goog.asserts.assert(minLatP !== undefined,
      'projected minLat should be defined');
  goog.asserts.assert(minLonP !== undefined,
      'projected minLon should be defined');

  this.maxLat_ = maxLat;
  this.maxLon_ = maxLon;
  this.minLat_ = minLat;
  this.minLon_ = minLon;

  this.maxLatP_ = maxLatP;
  this.maxLonP_ = maxLonP;
  this.minLatP_ = minLatP;
  this.minLonP_ = minLonP;


  this.fromLonLatTransform_ = ol.proj.getTransform(
      epsg4326Projection, projection);

  this.toLonLatTransform_ = ol.proj.getTransform(
      projection, epsg4326Projection);

  this.projectionCenterLonLat_ = this.toLonLatTransform_(
      ol.extent.getCenter(extent));

  this.projection_ = projection;
};


/**
 * Set the map for this graticule.  The graticule will be rendered on the
 * provided map.
 * @param {ol.Map} map Map.
 * @api
 */
ol.Graticule.prototype.setMap = function(map) {
  if (this.map_) {
    this.map_.un(ol.render.EventType.POSTCOMPOSE,
        this.handlePostCompose_, this);
    this.map_.render();
  }
  if (map) {
    map.on(ol.render.EventType.POSTCOMPOSE,
        this.handlePostCompose_, this);
    map.render();
  }
  this.map_ = map;
};

goog.provide('ol.Image');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.object');
goog.require('ol.ImageBase');
goog.require('ol.ImageState');
goog.require('ol.extent');


/**
 * @constructor
 * @extends {ol.ImageBase}
 * @param {ol.Extent} extent Extent.
 * @param {number|undefined} resolution Resolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {Array.<ol.Attribution>} attributions Attributions.
 * @param {string} src Image source URI.
 * @param {?string} crossOrigin Cross origin.
 * @param {ol.ImageLoadFunctionType} imageLoadFunction Image load function.
 */
ol.Image = function(extent, resolution, pixelRatio, attributions, src,
    crossOrigin, imageLoadFunction) {

  goog.base(this, extent, resolution, pixelRatio, ol.ImageState.IDLE,
      attributions);

  /**
   * @private
   * @type {string}
   */
  this.src_ = src;

  /**
   * @private
   * @type {HTMLCanvasElement|Image|HTMLVideoElement}
   */
  this.image_ = new Image();
  if (crossOrigin !== null) {
    this.image_.crossOrigin = crossOrigin;
  }

  /**
   * @private
   * @type {Object.<number, (HTMLCanvasElement|Image|HTMLVideoElement)>}
   */
  this.imageByContext_ = {};

  /**
   * @private
   * @type {Array.<goog.events.Key>}
   */
  this.imageListenerKeys_ = null;

  /**
   * @protected
   * @type {ol.ImageState}
   */
  this.state = ol.ImageState.IDLE;

  /**
   * @private
   * @type {ol.ImageLoadFunctionType}
   */
  this.imageLoadFunction_ = imageLoadFunction;

};
goog.inherits(ol.Image, ol.ImageBase);


/**
 * Get the HTML image element (may be a Canvas, Image, or Video).
 * @param {Object=} opt_context Object.
 * @return {HTMLCanvasElement|Image|HTMLVideoElement} Image.
 * @api
 */
ol.Image.prototype.getImage = function(opt_context) {
  if (opt_context !== undefined) {
    var image;
    var key = goog.getUid(opt_context);
    if (key in this.imageByContext_) {
      return this.imageByContext_[key];
    } else if (goog.object.isEmpty(this.imageByContext_)) {
      image = this.image_;
    } else {
      image = /** @type {Image} */ (this.image_.cloneNode(false));
    }
    this.imageByContext_[key] = image;
    return image;
  } else {
    return this.image_;
  }
};


/**
 * Tracks loading or read errors.
 *
 * @private
 */
ol.Image.prototype.handleImageError_ = function() {
  this.state = ol.ImageState.ERROR;
  this.unlistenImage_();
  this.changed();
};


/**
 * Tracks successful image load.
 *
 * @private
 */
ol.Image.prototype.handleImageLoad_ = function() {
  if (this.resolution === undefined) {
    this.resolution = ol.extent.getHeight(this.extent) / this.image_.height;
  }
  this.state = ol.ImageState.LOADED;
  this.unlistenImage_();
  this.changed();
};


/**
 * Load not yet loaded URI.
 */
ol.Image.prototype.load = function() {
  if (this.state == ol.ImageState.IDLE) {
    this.state = ol.ImageState.LOADING;
    this.changed();
    goog.asserts.assert(!this.imageListenerKeys_,
        'this.imageListenerKeys_ should be null');
    this.imageListenerKeys_ = [
      goog.events.listenOnce(this.image_, goog.events.EventType.ERROR,
          this.handleImageError_, false, this),
      goog.events.listenOnce(this.image_, goog.events.EventType.LOAD,
          this.handleImageLoad_, false, this)
    ];
    this.imageLoadFunction_(this, this.src_);
  }
};


/**
 * @param {HTMLCanvasElement|Image|HTMLVideoElement} image Image.
 */
ol.Image.prototype.setImage = function(image) {
  this.image_ = image;
};


/**
 * Discards event handlers which listen for load completion or errors.
 *
 * @private
 */
ol.Image.prototype.unlistenImage_ = function() {
  goog.asserts.assert(this.imageListenerKeys_,
      'this.imageListenerKeys_ should not be null');
  this.imageListenerKeys_.forEach(goog.events.unlistenByKey);
  this.imageListenerKeys_ = null;
};

goog.provide('ol.ImageLoadFunctionType');


/**
 * A function that takes an {@link ol.Image} for the image and a `{string}` for
 * the src as arguments. It is supposed to make it so the underlying image
 * {@link ol.Image#getImage} is assigned the content specified by the src. If
 * not specified, the default is
 *
 *     function(image, src) {
 *       image.getImage().src = src;
 *     }
 *
 * Providing a custom `imageLoadFunction` can be useful to load images with
 * post requests or - in general - through XHR requests, where the src of the
 * image element would be set to a data URI when the content is loaded.
 *
 * @typedef {function(ol.Image, string)}
 * @api
 */
ol.ImageLoadFunctionType;

goog.provide('ol.ImageTile');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.object');
goog.require('ol.Tile');
goog.require('ol.TileCoord');
goog.require('ol.TileLoadFunctionType');
goog.require('ol.TileState');


/**
 * @constructor
 * @extends {ol.Tile}
 * @param {ol.TileCoord} tileCoord Tile coordinate.
 * @param {ol.TileState} state State.
 * @param {string} src Image source URI.
 * @param {?string} crossOrigin Cross origin.
 * @param {ol.TileLoadFunctionType} tileLoadFunction Tile load function.
 */
ol.ImageTile = function(tileCoord, state, src, crossOrigin, tileLoadFunction) {

  goog.base(this, tileCoord, state);

  /**
   * Image URI
   *
   * @private
   * @type {string}
   */
  this.src_ = src;

  /**
   * @private
   * @type {Image}
   */
  this.image_ = new Image();
  if (crossOrigin !== null) {
    this.image_.crossOrigin = crossOrigin;
  }

  /**
   * @private
   * @type {Object.<number, Image>}
   */
  this.imageByContext_ = {};

  /**
   * @private
   * @type {Array.<goog.events.Key>}
   */
  this.imageListenerKeys_ = null;

  /**
   * @private
   * @type {ol.TileLoadFunctionType}
   */
  this.tileLoadFunction_ = tileLoadFunction;

};
goog.inherits(ol.ImageTile, ol.Tile);


/**
 * @inheritDoc
 */
ol.ImageTile.prototype.disposeInternal = function() {
  if (this.state == ol.TileState.LOADING) {
    this.unlistenImage_();
  }
  if (this.interimTile) {
    goog.dispose(this.interimTile);
  }
  goog.base(this, 'disposeInternal');
};


/**
 * Get the image element for this tile.
 * @inheritDoc
 * @api
 */
ol.ImageTile.prototype.getImage = function(opt_context) {
  if (opt_context !== undefined) {
    var image;
    var key = goog.getUid(opt_context);
    if (key in this.imageByContext_) {
      return this.imageByContext_[key];
    } else if (goog.object.isEmpty(this.imageByContext_)) {
      image = this.image_;
    } else {
      image = /** @type {Image} */ (this.image_.cloneNode(false));
    }
    this.imageByContext_[key] = image;
    return image;
  } else {
    return this.image_;
  }
};


/**
 * @inheritDoc
 */
ol.ImageTile.prototype.getKey = function() {
  return this.src_;
};


/**
 * Tracks loading or read errors.
 *
 * @private
 */
ol.ImageTile.prototype.handleImageError_ = function() {
  this.state = ol.TileState.ERROR;
  this.unlistenImage_();
  this.changed();
};


/**
 * Tracks successful image load.
 *
 * @private
 */
ol.ImageTile.prototype.handleImageLoad_ = function() {
  if (this.image_.naturalWidth && this.image_.naturalHeight) {
    this.state = ol.TileState.LOADED;
  } else {
    this.state = ol.TileState.EMPTY;
  }
  this.unlistenImage_();
  this.changed();
};


/**
 * Load not yet loaded URI.
 */
ol.ImageTile.prototype.load = function() {
  if (this.state == ol.TileState.IDLE) {
    this.state = ol.TileState.LOADING;
    this.changed();
    goog.asserts.assert(!this.imageListenerKeys_,
        'this.imageListenerKeys_ should be null');
    this.imageListenerKeys_ = [
      goog.events.listenOnce(this.image_, goog.events.EventType.ERROR,
          this.handleImageError_, false, this),
      goog.events.listenOnce(this.image_, goog.events.EventType.LOAD,
          this.handleImageLoad_, false, this)
    ];
    this.tileLoadFunction_(this, this.src_);
  }
};


/**
 * Discards event handlers which listen for load completion or errors.
 *
 * @private
 */
ol.ImageTile.prototype.unlistenImage_ = function() {
  goog.asserts.assert(this.imageListenerKeys_,
      'this.imageListenerKeys_ should not be null');
  this.imageListenerKeys_.forEach(goog.events.unlistenByKey);
  this.imageListenerKeys_ = null;
};

// Copyright 2010 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Provides a files drag and drop event detector. It works on
 * HTML5 browsers.
 *
 * @see ../demos/filedrophandler.html
 */

goog.provide('goog.events.FileDropHandler');
goog.provide('goog.events.FileDropHandler.EventType');

goog.require('goog.array');
goog.require('goog.dom');
goog.require('goog.events.BrowserEvent');
goog.require('goog.events.EventHandler');
goog.require('goog.events.EventTarget');
goog.require('goog.events.EventType');
goog.require('goog.log');
goog.require('goog.log.Level');



/**
 * A files drag and drop event detector. Gets an {@code element} as parameter
 * and fires {@code goog.events.FileDropHandler.EventType.DROP} event when files
 * are dropped in the {@code element}.
 *
 * @param {Element|Document} element The element or document to listen on.
 * @param {boolean=} opt_preventDropOutside Whether to prevent a drop on the
 *     area outside the {@code element}. Default false.
 * @constructor
 * @extends {goog.events.EventTarget}
 * @final
 */
goog.events.FileDropHandler = function(element, opt_preventDropOutside) {
  goog.events.EventTarget.call(this);

  /**
   * Handler for drag/drop events.
   * @type {!goog.events.EventHandler<!goog.events.FileDropHandler>}
   * @private
   */
  this.eventHandler_ = new goog.events.EventHandler(this);

  var doc = element;
  if (opt_preventDropOutside) {
    doc = goog.dom.getOwnerDocument(element);
  }

  // Add dragenter listener to the owner document of the element.
  this.eventHandler_.listen(doc,
                            goog.events.EventType.DRAGENTER,
                            this.onDocDragEnter_);

  // Add dragover listener to the owner document of the element only if the
  // document is not the element itself.
  if (doc != element) {
    this.eventHandler_.listen(doc,
                              goog.events.EventType.DRAGOVER,
                              this.onDocDragOver_);
  }

  // Add dragover and drop listeners to the element.
  this.eventHandler_.listen(element,
                            goog.events.EventType.DRAGOVER,
                            this.onElemDragOver_);
  this.eventHandler_.listen(element,
                            goog.events.EventType.DROP,
                            this.onElemDrop_);
};
goog.inherits(goog.events.FileDropHandler, goog.events.EventTarget);


/**
 * Whether the drag event contains files. It is initialized only in the
 * dragenter event. It is used in all the drag events to prevent default actions
 * only if the drag contains files. Preventing default actions is necessary to
 * go from dragenter to dragover and from dragover to drop. However we do not
 * always want to prevent default actions, e.g. when the user drags text or
 * links on a text area we should not prevent the browser default action that
 * inserts the text in the text area. It is also necessary to stop propagation
 * when handling drag events on the element to prevent them from propagating
 * to the document.
 * @private
 * @type {boolean}
 */
goog.events.FileDropHandler.prototype.dndContainsFiles_ = false;


/**
 * A logger, used to help us debug the algorithm.
 * @type {goog.log.Logger}
 * @private
 */
goog.events.FileDropHandler.prototype.logger_ =
    goog.log.getLogger('goog.events.FileDropHandler');


/**
 * The types of events fired by this class.
 * @enum {string}
 */
goog.events.FileDropHandler.EventType = {
  DROP: goog.events.EventType.DROP
};


/** @override */
goog.events.FileDropHandler.prototype.disposeInternal = function() {
  goog.events.FileDropHandler.superClass_.disposeInternal.call(this);
  this.eventHandler_.dispose();
};


/**
 * Dispatches the DROP event.
 * @param {goog.events.BrowserEvent} e The underlying browser event.
 * @private
 */
goog.events.FileDropHandler.prototype.dispatch_ = function(e) {
  goog.log.fine(this.logger_, 'Firing DROP event...');
  var event = new goog.events.BrowserEvent(e.getBrowserEvent());
  event.type = goog.events.FileDropHandler.EventType.DROP;
  this.dispatchEvent(event);
};


/**
 * Handles dragenter on the document.
 * @param {goog.events.BrowserEvent} e The dragenter event.
 * @private
 */
goog.events.FileDropHandler.prototype.onDocDragEnter_ = function(e) {
  goog.log.log(this.logger_, goog.log.Level.FINER,
      '"' + e.target.id + '" (' + e.target + ') dispatched: ' + e.type);
  var dt = e.getBrowserEvent().dataTransfer;
  // Check whether the drag event contains files.
  this.dndContainsFiles_ = !!(dt &&
      ((dt.types &&
          (goog.array.contains(dt.types, 'Files') ||
          goog.array.contains(dt.types, 'public.file-url'))) ||
      (dt.files && dt.files.length > 0)));
  // If it does
  if (this.dndContainsFiles_) {
    // Prevent default actions.
    e.preventDefault();
  }
  goog.log.log(this.logger_, goog.log.Level.FINER,
      'dndContainsFiles_: ' + this.dndContainsFiles_);
};


/**
 * Handles dragging something over the document.
 * @param {goog.events.BrowserEvent} e The dragover event.
 * @private
 */
goog.events.FileDropHandler.prototype.onDocDragOver_ = function(e) {
  goog.log.log(this.logger_, goog.log.Level.FINEST,
      '"' + e.target.id + '" (' + e.target + ') dispatched: ' + e.type);
  if (this.dndContainsFiles_) {
    // Prevent default actions.
    e.preventDefault();
    // Disable the drop on the document outside the drop zone.
    var dt = e.getBrowserEvent().dataTransfer;
    dt.dropEffect = 'none';
  }
};


/**
 * Handles dragging something over the element (drop zone).
 * @param {goog.events.BrowserEvent} e The dragover event.
 * @private
 */
goog.events.FileDropHandler.prototype.onElemDragOver_ = function(e) {
  goog.log.log(this.logger_, goog.log.Level.FINEST,
      '"' + e.target.id + '" (' + e.target + ') dispatched: ' + e.type);
  if (this.dndContainsFiles_) {
    // Prevent default actions and stop the event from propagating further to
    // the document. Both lines are needed! (See comment above).
    e.preventDefault();
    e.stopPropagation();
    // Allow the drop on the drop zone.
    var dt = e.getBrowserEvent().dataTransfer;

    // IE bug #811625 (https://goo.gl/UWuxX0) will throw error SCRIPT65535
    // when attempting to set property effectAllowed on IE10+.
    // See more: https://github.com/google/closure-library/issues/485.
    try {
      dt.effectAllowed = 'all';
    } catch (err) {
    }
    dt.dropEffect = 'copy';
  }
};


/**
 * Handles dropping something onto the element (drop zone).
 * @param {goog.events.BrowserEvent} e The drop event.
 * @private
 */
goog.events.FileDropHandler.prototype.onElemDrop_ = function(e) {
  goog.log.log(this.logger_, goog.log.Level.FINER,
      '"' + e.target.id + '" (' + e.target + ') dispatched: ' + e.type);
  // If the drag and drop event contains files.
  if (this.dndContainsFiles_) {
    // Prevent default actions and stop the event from propagating further to
    // the document. Both lines are needed! (See comment above).
    e.preventDefault();
    e.stopPropagation();
    // Dispatch DROP event.
    this.dispatch_(e);
  }
};

// Copyright 2007 Bob Ippolito. All Rights Reserved.
// Modifications Copyright 2009 The Closure Library Authors. All Rights
// Reserved.

/**
 * @license Portions of this code are from MochiKit, received by
 * The Closure Authors under the MIT license. All other code is Copyright
 * 2005-2009 The Closure Authors. All Rights Reserved.
 */

/**
 * @fileoverview Classes for tracking asynchronous operations and handling the
 * results. The Deferred object here is patterned after the Deferred object in
 * the Twisted python networking framework.
 *
 * See: http://twistedmatrix.com/projects/core/documentation/howto/defer.html
 *
 * Based on the Dojo code which in turn is based on the MochiKit code.
 *
 * @author arv@google.com (Erik Arvidsson)
 * @author brenneman@google.com (Shawn Brenneman)
 */

goog.provide('goog.async.Deferred');
goog.provide('goog.async.Deferred.AlreadyCalledError');
goog.provide('goog.async.Deferred.CanceledError');

goog.require('goog.Promise');
goog.require('goog.Thenable');
goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.debug.Error');



/**
 * A Deferred represents the result of an asynchronous operation. A Deferred
 * instance has no result when it is created, and is "fired" (given an initial
 * result) by calling {@code callback} or {@code errback}.
 *
 * Once fired, the result is passed through a sequence of callback functions
 * registered with {@code addCallback} or {@code addErrback}. The functions may
 * mutate the result before it is passed to the next function in the sequence.
 *
 * Callbacks and errbacks may be added at any time, including after the Deferred
 * has been "fired". If there are no pending actions in the execution sequence
 * of a fired Deferred, any new callback functions will be called with the last
 * computed result. Adding a callback function is the only way to access the
 * result of the Deferred.
 *
 * If a Deferred operation is canceled, an optional user-provided cancellation
 * function is invoked which may perform any special cleanup, followed by firing
 * the Deferred's errback sequence with a {@code CanceledError}. If the
 * Deferred has already fired, cancellation is ignored.
 *
 * Deferreds may be templated to a specific type they produce using generics
 * with syntax such as:
 * <code>
 *   /** @type {goog.async.Deferred<string>} *&#47;
 *   var d = new goog.async.Deferred();
 *   // Compiler can infer that foo is a string.
 *   d.addCallback(function(foo) {...});
 *   d.callback('string');  // Checked to be passed a string
 * </code>
 * Since deferreds are often used to produce different values across a chain,
 * the type information is not propagated across chains, but rather only
 * associated with specifically cast objects.
 *
 * @param {Function=} opt_onCancelFunction A function that will be called if the
 *     Deferred is canceled. If provided, this function runs before the
 *     Deferred is fired with a {@code CanceledError}.
 * @param {Object=} opt_defaultScope The default object context to call
 *     callbacks and errbacks in.
 * @constructor
 * @implements {goog.Thenable<VALUE>}
 * @template VALUE
 */
goog.async.Deferred = function(opt_onCancelFunction, opt_defaultScope) {
  /**
   * Entries in the sequence are arrays containing a callback, an errback, and
   * an optional scope. The callback or errback in an entry may be null.
   * @type {!Array<!Array>}
   * @private
   */
  this.sequence_ = [];

  /**
   * Optional function that will be called if the Deferred is canceled.
   * @type {Function|undefined}
   * @private
   */
  this.onCancelFunction_ = opt_onCancelFunction;

  /**
   * The default scope to execute callbacks and errbacks in.
   * @type {Object}
   * @private
   */
  this.defaultScope_ = opt_defaultScope || null;

  /**
   * Whether the Deferred has been fired.
   * @type {boolean}
   * @private
   */
  this.fired_ = false;

  /**
   * Whether the last result in the execution sequence was an error.
   * @type {boolean}
   * @private
   */
  this.hadError_ = false;

  /**
   * The current Deferred result, updated as callbacks and errbacks are
   * executed.
   * @type {*}
   * @private
   */
  this.result_ = undefined;

  /**
   * Whether the Deferred is blocked waiting on another Deferred to fire. If a
   * callback or errback returns a Deferred as a result, the execution sequence
   * is blocked until that Deferred result becomes available.
   * @type {boolean}
   * @private
   */
  this.blocked_ = false;

  /**
   * Whether this Deferred is blocking execution of another Deferred. If this
   * instance was returned as a result in another Deferred's execution
   * sequence,that other Deferred becomes blocked until this instance's
   * execution sequence completes. No additional callbacks may be added to a
   * Deferred once it is blocking another instance.
   * @type {boolean}
   * @private
   */
  this.blocking_ = false;

  /**
   * Whether the Deferred has been canceled without having a custom cancel
   * function.
   * @type {boolean}
   * @private
   */
  this.silentlyCanceled_ = false;

  /**
   * If an error is thrown during Deferred execution with no errback to catch
   * it, the error is rethrown after a timeout. Reporting the error after a
   * timeout allows execution to continue in the calling context (empty when
   * no error is scheduled).
   * @type {number}
   * @private
   */
  this.unhandledErrorId_ = 0;

  /**
   * If this Deferred was created by branch(), this will be the "parent"
   * Deferred.
   * @type {goog.async.Deferred}
   * @private
   */
  this.parent_ = null;

  /**
   * The number of Deferred objects that have been branched off this one. This
   * will be decremented whenever a branch is fired or canceled.
   * @type {number}
   * @private
   */
  this.branches_ = 0;

  if (goog.async.Deferred.LONG_STACK_TRACES) {
    /**
     * Holds the stack trace at time of deferred creation if the JS engine
     * provides the Error.captureStackTrace API.
     * @private {?string}
     */
    this.constructorStack_ = null;
    if (Error.captureStackTrace) {
      var target = { stack: '' };
      Error.captureStackTrace(target, goog.async.Deferred);
      // Check if Error.captureStackTrace worked. It fails in gjstest.
      if (typeof target.stack == 'string') {
        // Remove first line and force stringify to prevent memory leak due to
        // holding on to actual stack frames.
        this.constructorStack_ = target.stack.replace(/^[^\n]*\n/, '');
      }
    }
  }
};


/**
 * @define {boolean} Whether unhandled errors should always get rethrown to the
 * global scope. Defaults to the value of goog.DEBUG.
 */
goog.define('goog.async.Deferred.STRICT_ERRORS', false);


/**
 * @define {boolean} Whether to attempt to make stack traces long.  Defaults to
 * the value of goog.DEBUG.
 */
goog.define('goog.async.Deferred.LONG_STACK_TRACES', false);


/**
 * Cancels a Deferred that has not yet been fired, or is blocked on another
 * deferred operation. If this Deferred is waiting for a blocking Deferred to
 * fire, the blocking Deferred will also be canceled.
 *
 * If this Deferred was created by calling branch() on a parent Deferred with
 * opt_propagateCancel set to true, the parent may also be canceled. If
 * opt_deepCancel is set, cancel() will be called on the parent (as well as any
 * other ancestors if the parent is also a branch). If one or more branches were
 * created with opt_propagateCancel set to true, the parent will be canceled if
 * cancel() is called on all of those branches.
 *
 * @param {boolean=} opt_deepCancel If true, cancels this Deferred's parent even
 *     if cancel() hasn't been called on some of the parent's branches. Has no
 *     effect on a branch without opt_propagateCancel set to true.
 */
goog.async.Deferred.prototype.cancel = function(opt_deepCancel) {
  if (!this.hasFired()) {
    if (this.parent_) {
      // Get rid of the parent reference before potentially running the parent's
      // canceler function to ensure that this cancellation isn't
      // double-counted.
      var parent = this.parent_;
      delete this.parent_;
      if (opt_deepCancel) {
        parent.cancel(opt_deepCancel);
      } else {
        parent.branchCancel_();
      }
    }

    if (this.onCancelFunction_) {
      // Call in user-specified scope.
      this.onCancelFunction_.call(this.defaultScope_, this);
    } else {
      this.silentlyCanceled_ = true;
    }
    if (!this.hasFired()) {
      this.errback(new goog.async.Deferred.CanceledError(this));
    }
  } else if (this.result_ instanceof goog.async.Deferred) {
    this.result_.cancel();
  }
};


/**
 * Handle a single branch being canceled. Once all branches are canceled, this
 * Deferred will be canceled as well.
 *
 * @private
 */
goog.async.Deferred.prototype.branchCancel_ = function() {
  this.branches_--;
  if (this.branches_ <= 0) {
    this.cancel();
  }
};


/**
 * Called after a blocking Deferred fires. Unblocks this Deferred and resumes
 * its execution sequence.
 *
 * @param {boolean} isSuccess Whether the result is a success or an error.
 * @param {*} res The result of the blocking Deferred.
 * @private
 */
goog.async.Deferred.prototype.continue_ = function(isSuccess, res) {
  this.blocked_ = false;
  this.updateResult_(isSuccess, res);
};


/**
 * Updates the current result based on the success or failure of the last action
 * in the execution sequence.
 *
 * @param {boolean} isSuccess Whether the new result is a success or an error.
 * @param {*} res The result.
 * @private
 */
goog.async.Deferred.prototype.updateResult_ = function(isSuccess, res) {
  this.fired_ = true;
  this.result_ = res;
  this.hadError_ = !isSuccess;
  this.fire_();
};


/**
 * Verifies that the Deferred has not yet been fired.
 *
 * @private
 * @throws {Error} If this has already been fired.
 */
goog.async.Deferred.prototype.check_ = function() {
  if (this.hasFired()) {
    if (!this.silentlyCanceled_) {
      throw new goog.async.Deferred.AlreadyCalledError(this);
    }
    this.silentlyCanceled_ = false;
  }
};


/**
 * Fire the execution sequence for this Deferred by passing the starting result
 * to the first registered callback.
 * @param {VALUE=} opt_result The starting result.
 */
goog.async.Deferred.prototype.callback = function(opt_result) {
  this.check_();
  this.assertNotDeferred_(opt_result);
  this.updateResult_(true /* isSuccess */, opt_result);
};


/**
 * Fire the execution sequence for this Deferred by passing the starting error
 * result to the first registered errback.
 * @param {*=} opt_result The starting error.
 */
goog.async.Deferred.prototype.errback = function(opt_result) {
  this.check_();
  this.assertNotDeferred_(opt_result);
  this.makeStackTraceLong_(opt_result);
  this.updateResult_(false /* isSuccess */, opt_result);
};


/**
 * Attempt to make the error's stack trace be long in that it contains the
 * stack trace from the point where the deferred was created on top of the
 * current stack trace to give additional context.
 * @param {*} error
 * @private
 */
goog.async.Deferred.prototype.makeStackTraceLong_ = function(error) {
  if (!goog.async.Deferred.LONG_STACK_TRACES) {
    return;
  }
  if (this.constructorStack_ && goog.isObject(error) && error.stack &&
      // Stack looks like it was system generated. See
      // https://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
      (/^[^\n]+(\n   [^\n]+)+/).test(error.stack)) {
    error.stack = error.stack + '\nDEFERRED OPERATION:\n' +
        this.constructorStack_;
  }
};


/**
 * Asserts that an object is not a Deferred.
 * @param {*} obj The object to test.
 * @throws {Error} Throws an exception if the object is a Deferred.
 * @private
 */
goog.async.Deferred.prototype.assertNotDeferred_ = function(obj) {
  goog.asserts.assert(
      !(obj instanceof goog.async.Deferred),
      'An execution sequence may not be initiated with a blocking Deferred.');
};


/**
 * Register a callback function to be called with a successful result. If no
 * value is returned by the callback function, the result value is unchanged. If
 * a new value is returned, it becomes the Deferred result and will be passed to
 * the next callback in the execution sequence.
 *
 * If the function throws an error, the error becomes the new result and will be
 * passed to the next errback in the execution chain.
 *
 * If the function returns a Deferred, the execution sequence will be blocked
 * until that Deferred fires. Its result will be passed to the next callback (or
 * errback if it is an error result) in this Deferred's execution sequence.
 *
 * @param {!function(this:T,VALUE):?} cb The function to be called with a
 *     successful result.
 * @param {T=} opt_scope An optional scope to call the callback in.
 * @return {!goog.async.Deferred} This Deferred.
 * @template T
 */
goog.async.Deferred.prototype.addCallback = function(cb, opt_scope) {
  return this.addCallbacks(cb, null, opt_scope);
};


/**
 * Register a callback function to be called with an error result. If no value
 * is returned by the function, the error result is unchanged. If a new error
 * value is returned or thrown, that error becomes the Deferred result and will
 * be passed to the next errback in the execution sequence.
 *
 * If the errback function handles the error by returning a non-error value,
 * that result will be passed to the next normal callback in the sequence.
 *
 * If the function returns a Deferred, the execution sequence will be blocked
 * until that Deferred fires. Its result will be passed to the next callback (or
 * errback if it is an error result) in this Deferred's execution sequence.
 *
 * @param {!function(this:T,?):?} eb The function to be called on an
 *     unsuccessful result.
 * @param {T=} opt_scope An optional scope to call the errback in.
 * @return {!goog.async.Deferred<VALUE>} This Deferred.
 * @template T
 */
goog.async.Deferred.prototype.addErrback = function(eb, opt_scope) {
  return this.addCallbacks(null, eb, opt_scope);
};


/**
 * Registers one function as both a callback and errback.
 *
 * @param {!function(this:T,?):?} f The function to be called on any result.
 * @param {T=} opt_scope An optional scope to call the function in.
 * @return {!goog.async.Deferred} This Deferred.
 * @template T
 */
goog.async.Deferred.prototype.addBoth = function(f, opt_scope) {
  return this.addCallbacks(f, f, opt_scope);
};


/**
 * Like addBoth, but propagates uncaught exceptions in the errback.
 *
 * @param {function(this:T,?):?} f The function to be called on any result.
 * @param {T=} opt_scope An optional scope to call the function in.
 * @return {!goog.async.Deferred<VALUE>} This Deferred.
 * @template T
 */
goog.async.Deferred.prototype.addFinally = function(f, opt_scope) {
  var self = this;
  return this.addCallbacks(f, function(err) {
    var result = f.call(self, err);
    if (!goog.isDef(result)) {
      throw err;
    }
    return result;
  }, opt_scope);
};


/**
 * Registers a callback function and an errback function at the same position
 * in the execution sequence. Only one of these functions will execute,
 * depending on the error state during the execution sequence.
 *
 * NOTE: This is not equivalent to {@code def.addCallback().addErrback()}! If
 * the callback is invoked, the errback will be skipped, and vice versa.
 *
 * @param {?(function(this:T,VALUE):?)} cb The function to be called on a
 *     successful result.
 * @param {?(function(this:T,?):?)} eb The function to be called on an
 *     unsuccessful result.
 * @param {T=} opt_scope An optional scope to call the functions in.
 * @return {!goog.async.Deferred} This Deferred.
 * @template T
 */
goog.async.Deferred.prototype.addCallbacks = function(cb, eb, opt_scope) {
  goog.asserts.assert(!this.blocking_, 'Blocking Deferreds can not be re-used');
  this.sequence_.push([cb, eb, opt_scope]);
  if (this.hasFired()) {
    this.fire_();
  }
  return this;
};


/**
 * Implements {@see goog.Thenable} for seamless integration with
 * {@see goog.Promise}.
 * Deferred results are mutable and may represent multiple values over
 * their lifetime. Calling {@code then} on a Deferred returns a Promise
 * with the result of the Deferred at that point in its callback chain.
 * Note that if the Deferred result is never mutated, and only
 * {@code then} calls are made, the Deferred will behave like a Promise.
 *
 * @override
 */
goog.async.Deferred.prototype.then = function(opt_onFulfilled, opt_onRejected,
    opt_context) {
  var resolve, reject;
  var promise = new goog.Promise(function(res, rej) {
    // Copying resolvers to outer scope, so that they are available when the
    // deferred callback fires (which may be synchronous).
    resolve = res;
    reject = rej;
  });
  this.addCallbacks(resolve, function(reason) {
    if (reason instanceof goog.async.Deferred.CanceledError) {
      promise.cancel();
    } else {
      reject(reason);
    }
  });
  return promise.then(opt_onFulfilled, opt_onRejected, opt_context);
};
goog.Thenable.addImplementation(goog.async.Deferred);


/**
 * Links another Deferred to the end of this Deferred's execution sequence. The
 * result of this execution sequence will be passed as the starting result for
 * the chained Deferred, invoking either its first callback or errback.
 *
 * @param {!goog.async.Deferred} otherDeferred The Deferred to chain.
 * @return {!goog.async.Deferred} This Deferred.
 */
goog.async.Deferred.prototype.chainDeferred = function(otherDeferred) {
  this.addCallbacks(
      otherDeferred.callback, otherDeferred.errback, otherDeferred);
  return this;
};


/**
 * Makes this Deferred wait for another Deferred's execution sequence to
 * complete before continuing.
 *
 * This is equivalent to adding a callback that returns {@code otherDeferred},
 * but doesn't prevent additional callbacks from being added to
 * {@code otherDeferred}.
 *
 * @param {!goog.async.Deferred|!goog.Thenable} otherDeferred The Deferred
 *     to wait for.
 * @return {!goog.async.Deferred} This Deferred.
 */
goog.async.Deferred.prototype.awaitDeferred = function(otherDeferred) {
  if (!(otherDeferred instanceof goog.async.Deferred)) {
    // The Thenable case.
    return this.addCallback(function() {
      return otherDeferred;
    });
  }
  return this.addCallback(goog.bind(otherDeferred.branch, otherDeferred));
};


/**
 * Creates a branch off this Deferred's execution sequence, and returns it as a
 * new Deferred. The branched Deferred's starting result will be shared with the
 * parent at the point of the branch, even if further callbacks are added to the
 * parent.
 *
 * All branches at the same stage in the execution sequence will receive the
 * same starting value.
 *
 * @param {boolean=} opt_propagateCancel If cancel() is called on every child
 *     branch created with opt_propagateCancel, the parent will be canceled as
 *     well.
 * @return {!goog.async.Deferred<VALUE>} A Deferred that will be started with
 *     the computed result from this stage in the execution sequence.
 */
goog.async.Deferred.prototype.branch = function(opt_propagateCancel) {
  var d = new goog.async.Deferred();
  this.chainDeferred(d);
  if (opt_propagateCancel) {
    d.parent_ = this;
    this.branches_++;
  }
  return d;
};


/**
 * @return {boolean} Whether the execution sequence has been started on this
 *     Deferred by invoking {@code callback} or {@code errback}.
 */
goog.async.Deferred.prototype.hasFired = function() {
  return this.fired_;
};


/**
 * @param {*} res The latest result in the execution sequence.
 * @return {boolean} Whether the current result is an error that should cause
 *     the next errback to fire. May be overridden by subclasses to handle
 *     special error types.
 * @protected
 */
goog.async.Deferred.prototype.isError = function(res) {
  return res instanceof Error;
};


/**
 * @return {boolean} Whether an errback exists in the remaining sequence.
 * @private
 */
goog.async.Deferred.prototype.hasErrback_ = function() {
  return goog.array.some(this.sequence_, function(sequenceRow) {
    // The errback is the second element in the array.
    return goog.isFunction(sequenceRow[1]);
  });
};


/**
 * Exhausts the execution sequence while a result is available. The result may
 * be modified by callbacks or errbacks, and execution will block if the
 * returned result is an incomplete Deferred.
 *
 * @private
 */
goog.async.Deferred.prototype.fire_ = function() {
  if (this.unhandledErrorId_ && this.hasFired() && this.hasErrback_()) {
    // It is possible to add errbacks after the Deferred has fired. If a new
    // errback is added immediately after the Deferred encountered an unhandled
    // error, but before that error is rethrown, the error is unscheduled.
    goog.async.Deferred.unscheduleError_(this.unhandledErrorId_);
    this.unhandledErrorId_ = 0;
  }

  if (this.parent_) {
    this.parent_.branches_--;
    delete this.parent_;
  }

  var res = this.result_;
  var unhandledException = false;
  var isNewlyBlocked = false;

  while (this.sequence_.length && !this.blocked_) {
    var sequenceEntry = this.sequence_.shift();

    var callback = sequenceEntry[0];
    var errback = sequenceEntry[1];
    var scope = sequenceEntry[2];

    var f = this.hadError_ ? errback : callback;
    if (f) {
      /** @preserveTry */
      try {
        var ret = f.call(scope || this.defaultScope_, res);

        // If no result, then use previous result.
        if (goog.isDef(ret)) {
          // Bubble up the error as long as the return value hasn't changed.
          this.hadError_ = this.hadError_ && (ret == res || this.isError(ret));
          this.result_ = res = ret;
        }

        if (goog.Thenable.isImplementedBy(res) ||
            (typeof goog.global['Promise'] === 'function' &&
            res instanceof goog.global['Promise'])) {
          isNewlyBlocked = true;
          this.blocked_ = true;
        }

      } catch (ex) {
        res = ex;
        this.hadError_ = true;
        this.makeStackTraceLong_(res);

        if (!this.hasErrback_()) {
          // If an error is thrown with no additional errbacks in the queue,
          // prepare to rethrow the error.
          unhandledException = true;
        }
      }
    }
  }

  this.result_ = res;

  if (isNewlyBlocked) {
    var onCallback = goog.bind(this.continue_, this, true /* isSuccess */);
    var onErrback = goog.bind(this.continue_, this, false /* isSuccess */);

    if (res instanceof goog.async.Deferred) {
      res.addCallbacks(onCallback, onErrback);
      res.blocking_ = true;
    } else {
      res.then(onCallback, onErrback);
    }
  } else if (goog.async.Deferred.STRICT_ERRORS && this.isError(res) &&
      !(res instanceof goog.async.Deferred.CanceledError)) {
    this.hadError_ = true;
    unhandledException = true;
  }

  if (unhandledException) {
    // Rethrow the unhandled error after a timeout. Execution will continue, but
    // the error will be seen by global handlers and the user. The throw will
    // be canceled if another errback is appended before the timeout executes.
    // The error's original stack trace is preserved where available.
    this.unhandledErrorId_ = goog.async.Deferred.scheduleError_(res);
  }
};


/**
 * Creates a Deferred that has an initial result.
 *
 * @param {*=} opt_result The result.
 * @return {!goog.async.Deferred} The new Deferred.
 */
goog.async.Deferred.succeed = function(opt_result) {
  var d = new goog.async.Deferred();
  d.callback(opt_result);
  return d;
};


/**
 * Creates a Deferred that fires when the given promise resolves.
 * Use only during migration to Promises.
 *
 * @param {!goog.Promise<T>} promise
 * @return {!goog.async.Deferred<T>} The new Deferred.
 * @template T
 */
goog.async.Deferred.fromPromise = function(promise) {
  var d = new goog.async.Deferred();
  d.callback();
  d.addCallback(function() {
    return promise;
  });
  return d;
};


/**
 * Creates a Deferred that has an initial error result.
 *
 * @param {*} res The error result.
 * @return {!goog.async.Deferred} The new Deferred.
 */
goog.async.Deferred.fail = function(res) {
  var d = new goog.async.Deferred();
  d.errback(res);
  return d;
};


/**
 * Creates a Deferred that has already been canceled.
 *
 * @return {!goog.async.Deferred} The new Deferred.
 */
goog.async.Deferred.canceled = function() {
  var d = new goog.async.Deferred();
  d.cancel();
  return d;
};


/**
 * Normalizes values that may or may not be Deferreds.
 *
 * If the input value is a Deferred, the Deferred is branched (so the original
 * execution sequence is not modified) and the input callback added to the new
 * branch. The branch is returned to the caller.
 *
 * If the input value is not a Deferred, the callback will be executed
 * immediately and an already firing Deferred will be returned to the caller.
 *
 * In the following (contrived) example, if <code>isImmediate</code> is true
 * then 3 is alerted immediately, otherwise 6 is alerted after a 2-second delay.
 *
 * <pre>
 * var value;
 * if (isImmediate) {
 *   value = 3;
 * } else {
 *   value = new goog.async.Deferred();
 *   setTimeout(function() { value.callback(6); }, 2000);
 * }
 *
 * var d = goog.async.Deferred.when(value, alert);
 * </pre>
 *
 * @param {*} value Deferred or normal value to pass to the callback.
 * @param {!function(this:T, ?):?} callback The callback to execute.
 * @param {T=} opt_scope An optional scope to call the callback in.
 * @return {!goog.async.Deferred} A new Deferred that will call the input
 *     callback with the input value.
 * @template T
 */
goog.async.Deferred.when = function(value, callback, opt_scope) {
  if (value instanceof goog.async.Deferred) {
    return value.branch(true).addCallback(callback, opt_scope);
  } else {
    return goog.async.Deferred.succeed(value).addCallback(callback, opt_scope);
  }
};



/**
 * An error sub class that is used when a Deferred has already been called.
 * @param {!goog.async.Deferred} deferred The Deferred.
 *
 * @constructor
 * @extends {goog.debug.Error}
 */
goog.async.Deferred.AlreadyCalledError = function(deferred) {
  goog.debug.Error.call(this);

  /**
   * The Deferred that raised this error.
   * @type {goog.async.Deferred}
   */
  this.deferred = deferred;
};
goog.inherits(goog.async.Deferred.AlreadyCalledError, goog.debug.Error);


/** @override */
goog.async.Deferred.AlreadyCalledError.prototype.message =
    'Deferred has already fired';


/** @override */
goog.async.Deferred.AlreadyCalledError.prototype.name = 'AlreadyCalledError';



/**
 * An error sub class that is used when a Deferred is canceled.
 *
 * @param {!goog.async.Deferred} deferred The Deferred object.
 * @constructor
 * @extends {goog.debug.Error}
 */
goog.async.Deferred.CanceledError = function(deferred) {
  goog.debug.Error.call(this);

  /**
   * The Deferred that raised this error.
   * @type {goog.async.Deferred}
   */
  this.deferred = deferred;
};
goog.inherits(goog.async.Deferred.CanceledError, goog.debug.Error);


/** @override */
goog.async.Deferred.CanceledError.prototype.message = 'Deferred was canceled';


/** @override */
goog.async.Deferred.CanceledError.prototype.name = 'CanceledError';



/**
 * Wrapper around errors that are scheduled to be thrown by failing deferreds
 * after a timeout.
 *
 * @param {*} error Error from a failing deferred.
 * @constructor
 * @final
 * @private
 * @struct
 */
goog.async.Deferred.Error_ = function(error) {
  /** @const @private {number} */
  this.id_ = goog.global.setTimeout(goog.bind(this.throwError, this), 0);

  /** @const @private {*} */
  this.error_ = error;
};


/**
 * Actually throws the error and removes it from the list of pending
 * deferred errors.
 */
goog.async.Deferred.Error_.prototype.throwError = function() {
  goog.asserts.assert(goog.async.Deferred.errorMap_[this.id_],
      'Cannot throw an error that is not scheduled.');
  delete goog.async.Deferred.errorMap_[this.id_];
  throw this.error_;
};


/**
 * Resets the error throw timer.
 */
goog.async.Deferred.Error_.prototype.resetTimer = function() {
  goog.global.clearTimeout(this.id_);
};


/**
 * Map of unhandled errors scheduled to be rethrown in a future timestep.
 * @private {!Object<number|string, goog.async.Deferred.Error_>}
 */
goog.async.Deferred.errorMap_ = {};


/**
 * Schedules an error to be thrown after a delay.
 * @param {*} error Error from a failing deferred.
 * @return {number} Id of the error.
 * @private
 */
goog.async.Deferred.scheduleError_ = function(error) {
  var deferredError = new goog.async.Deferred.Error_(error);
  goog.async.Deferred.errorMap_[deferredError.id_] = deferredError;
  return deferredError.id_;
};


/**
 * Unschedules an error from being thrown.
 * @param {number} id Id of the deferred error to unschedule.
 * @private
 */
goog.async.Deferred.unscheduleError_ = function(id) {
  var error = goog.async.Deferred.errorMap_[id];
  if (error) {
    error.resetTimer();
    delete goog.async.Deferred.errorMap_[id];
  }
};


/**
 * Asserts that there are no pending deferred errors. If there are any
 * scheduled errors, one will be thrown immediately to make this function fail.
 */
goog.async.Deferred.assertNoErrors = function() {
  var map = goog.async.Deferred.errorMap_;
  for (var key in map) {
    var error = map[key];
    error.resetTimer();
    error.throwError();
  }
};

// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview A wrapper for the HTML5 FileError object.
 *
 */

goog.provide('goog.fs.Error');
goog.provide('goog.fs.Error.ErrorCode');

goog.require('goog.debug.Error');
goog.require('goog.object');
goog.require('goog.string');



/**
 * A filesystem error. Since the filesystem API is asynchronous, stack traces
 * are less useful for identifying where errors come from, so this includes a
 * large amount of metadata in the message.
 *
 * @param {!DOMError} error
 * @param {string} action The action being undertaken when the error was raised.
 * @constructor
 * @extends {goog.debug.Error}
 * @final
 */
goog.fs.Error = function(error, action) {
  /** @type {string} */
  this.name;

  /**
   * @type {goog.fs.Error.ErrorCode}
   * @deprecated Use the 'name' or 'message' field instead.
   */
  this.code;

  if (goog.isDef(error.name)) {
    this.name = error.name;
    // TODO(user): Remove warning suppression after JSCompiler stops
    // firing a spurious warning here.
    /** @suppress {deprecated} */
    this.code = goog.fs.Error.getCodeFromName_(error.name);
  } else {
    this.code = error.code;
    this.name = goog.fs.Error.getNameFromCode_(error.code);
  }
  goog.fs.Error.base(this, 'constructor',
      goog.string.subs('%s %s', this.name, action));
};
goog.inherits(goog.fs.Error, goog.debug.Error);


/**
 * Names of errors that may be thrown by the File API, the File System API, or
 * the File Writer API.
 *
 * @see http://dev.w3.org/2006/webapi/FileAPI/#ErrorAndException
 * @see http://www.w3.org/TR/file-system-api/#definitions
 * @see http://dev.w3.org/2009/dap/file-system/file-writer.html#definitions
 * @enum {string}
 */
goog.fs.Error.ErrorName = {
  ABORT: 'AbortError',
  ENCODING: 'EncodingError',
  INVALID_MODIFICATION: 'InvalidModificationError',
  INVALID_STATE: 'InvalidStateError',
  NOT_FOUND: 'NotFoundError',
  NOT_READABLE: 'NotReadableError',
  NO_MODIFICATION_ALLOWED: 'NoModificationAllowedError',
  PATH_EXISTS: 'PathExistsError',
  QUOTA_EXCEEDED: 'QuotaExceededError',
  SECURITY: 'SecurityError',
  SYNTAX: 'SyntaxError',
  TYPE_MISMATCH: 'TypeMismatchError'
};


/**
 * Error codes for file errors.
 * @see http://www.w3.org/TR/file-system-api/#idl-def-FileException
 *
 * @enum {number}
 * @deprecated Use the 'name' or 'message' attribute instead.
 */
goog.fs.Error.ErrorCode = {
  NOT_FOUND: 1,
  SECURITY: 2,
  ABORT: 3,
  NOT_READABLE: 4,
  ENCODING: 5,
  NO_MODIFICATION_ALLOWED: 6,
  INVALID_STATE: 7,
  SYNTAX: 8,
  INVALID_MODIFICATION: 9,
  QUOTA_EXCEEDED: 10,
  TYPE_MISMATCH: 11,
  PATH_EXISTS: 12
};


/**
 * @param {goog.fs.Error.ErrorCode} code
 * @return {string} name
 * @private
 */
goog.fs.Error.getNameFromCode_ = function(code) {
  var name = goog.object.findKey(goog.fs.Error.NameToCodeMap_, function(c) {
    return code == c;
  });
  if (!goog.isDef(name)) {
    throw new Error('Invalid code: ' + code);
  }
  return name;
};


/**
 * Returns the code that corresponds to the given name.
 * @param {string} name
 * @return {goog.fs.Error.ErrorCode} code
 * @private
 */
goog.fs.Error.getCodeFromName_ = function(name) {
  return goog.fs.Error.NameToCodeMap_[name];
};


/**
 * Mapping from error names to values from the ErrorCode enum.
 * @see http://www.w3.org/TR/file-system-api/#definitions.
 * @private {!Object<string, goog.fs.Error.ErrorCode>}
 */
goog.fs.Error.NameToCodeMap_ = goog.object.create(
    goog.fs.Error.ErrorName.ABORT,
    goog.fs.Error.ErrorCode.ABORT,

    goog.fs.Error.ErrorName.ENCODING,
    goog.fs.Error.ErrorCode.ENCODING,

    goog.fs.Error.ErrorName.INVALID_MODIFICATION,
    goog.fs.Error.ErrorCode.INVALID_MODIFICATION,

    goog.fs.Error.ErrorName.INVALID_STATE,
    goog.fs.Error.ErrorCode.INVALID_STATE,

    goog.fs.Error.ErrorName.NOT_FOUND,
    goog.fs.Error.ErrorCode.NOT_FOUND,

    goog.fs.Error.ErrorName.NOT_READABLE,
    goog.fs.Error.ErrorCode.NOT_READABLE,

    goog.fs.Error.ErrorName.NO_MODIFICATION_ALLOWED,
    goog.fs.Error.ErrorCode.NO_MODIFICATION_ALLOWED,

    goog.fs.Error.ErrorName.PATH_EXISTS,
    goog.fs.Error.ErrorCode.PATH_EXISTS,

    goog.fs.Error.ErrorName.QUOTA_EXCEEDED,
    goog.fs.Error.ErrorCode.QUOTA_EXCEEDED,

    goog.fs.Error.ErrorName.SECURITY,
    goog.fs.Error.ErrorCode.SECURITY,

    goog.fs.Error.ErrorName.SYNTAX,
    goog.fs.Error.ErrorCode.SYNTAX,

    goog.fs.Error.ErrorName.TYPE_MISMATCH,
    goog.fs.Error.ErrorCode.TYPE_MISMATCH);

// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview A wrapper for the HTML5 File ProgressEvent objects.
 *
 */
goog.provide('goog.fs.ProgressEvent');

goog.require('goog.events.Event');



/**
 * A wrapper for the progress events emitted by the File APIs.
 *
 * @param {!ProgressEvent} event The underlying event object.
 * @param {!Object} target The file access object emitting the event.
 * @extends {goog.events.Event}
 * @constructor
 * @final
 */
goog.fs.ProgressEvent = function(event, target) {
  goog.fs.ProgressEvent.base(this, 'constructor', event.type, target);

  /**
   * The underlying event object.
   * @type {!ProgressEvent}
   * @private
   */
  this.event_ = event;
};
goog.inherits(goog.fs.ProgressEvent, goog.events.Event);


/**
 * @return {boolean} Whether or not the total size of the of the file being
 *     saved is known.
 */
goog.fs.ProgressEvent.prototype.isLengthComputable = function() {
  return this.event_.lengthComputable;
};


/**
 * @return {number} The number of bytes saved so far.
 */
goog.fs.ProgressEvent.prototype.getLoaded = function() {
  return this.event_.loaded;
};


/**
 * @return {number} The total number of bytes in the file being saved.
 */
goog.fs.ProgressEvent.prototype.getTotal = function() {
  return this.event_.total;
};

// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview A wrapper for the HTML5 FileReader object.
 *
 */

goog.provide('goog.fs.FileReader');
goog.provide('goog.fs.FileReader.EventType');
goog.provide('goog.fs.FileReader.ReadyState');

goog.require('goog.async.Deferred');
goog.require('goog.events.EventTarget');
goog.require('goog.fs.Error');
goog.require('goog.fs.ProgressEvent');



/**
 * An object for monitoring the reading of files. This emits ProgressEvents of
 * the types listed in {@link goog.fs.FileReader.EventType}.
 *
 * @constructor
 * @extends {goog.events.EventTarget}
 * @final
 */
goog.fs.FileReader = function() {
  goog.fs.FileReader.base(this, 'constructor');

  /**
   * The underlying FileReader object.
   *
   * @type {!FileReader}
   * @private
   */
  this.reader_ = new FileReader();

  this.reader_.onloadstart = goog.bind(this.dispatchProgressEvent_, this);
  this.reader_.onprogress = goog.bind(this.dispatchProgressEvent_, this);
  this.reader_.onload = goog.bind(this.dispatchProgressEvent_, this);
  this.reader_.onabort = goog.bind(this.dispatchProgressEvent_, this);
  this.reader_.onerror = goog.bind(this.dispatchProgressEvent_, this);
  this.reader_.onloadend = goog.bind(this.dispatchProgressEvent_, this);
};
goog.inherits(goog.fs.FileReader, goog.events.EventTarget);


/**
 * Possible states for a FileReader.
 *
 * @enum {number}
 */
goog.fs.FileReader.ReadyState = {
  /**
   * The object has been constructed, but there is no pending read.
   */
  INIT: 0,
  /**
   * Data is being read.
   */
  LOADING: 1,
  /**
   * The data has been read from the file, the read was aborted, or an error
   * occurred.
   */
  DONE: 2
};


/**
 * Events emitted by a FileReader.
 *
 * @enum {string}
 */
goog.fs.FileReader.EventType = {
  /**
   * Emitted when the reading begins. readyState will be LOADING.
   */
  LOAD_START: 'loadstart',
  /**
   * Emitted when progress has been made in reading the file. readyState will be
   * LOADING.
   */
  PROGRESS: 'progress',
  /**
   * Emitted when the data has been successfully read. readyState will be
   * LOADING.
   */
  LOAD: 'load',
  /**
   * Emitted when the reading has been aborted. readyState will be LOADING.
   */
  ABORT: 'abort',
  /**
   * Emitted when an error is encountered or the reading has been aborted.
   * readyState will be LOADING.
   */
  ERROR: 'error',
  /**
   * Emitted when the reading is finished, whether successfully or not.
   * readyState will be DONE.
   */
  LOAD_END: 'loadend'
};


/**
 * Abort the reading of the file.
 */
goog.fs.FileReader.prototype.abort = function() {
  try {
    this.reader_.abort();
  } catch (e) {
    throw new goog.fs.Error(e, 'aborting read');
  }
};


/**
 * @return {goog.fs.FileReader.ReadyState} The current state of the FileReader.
 */
goog.fs.FileReader.prototype.getReadyState = function() {
  return /** @type {goog.fs.FileReader.ReadyState} */ (this.reader_.readyState);
};


/**
 * @return {*} The result of the file read.
 */
goog.fs.FileReader.prototype.getResult = function() {
  return this.reader_.result;
};


/**
 * @return {goog.fs.Error} The error encountered while reading, if any.
 */
goog.fs.FileReader.prototype.getError = function() {
  return this.reader_.error &&
      new goog.fs.Error(this.reader_.error, 'reading file');
};


/**
 * Wrap a progress event emitted by the underlying file reader and re-emit it.
 *
 * @param {!ProgressEvent} event The underlying event.
 * @private
 */
goog.fs.FileReader.prototype.dispatchProgressEvent_ = function(event) {
  this.dispatchEvent(new goog.fs.ProgressEvent(event, this));
};


/** @override */
goog.fs.FileReader.prototype.disposeInternal = function() {
  goog.fs.FileReader.base(this, 'disposeInternal');
  delete this.reader_;
};


/**
 * Starts reading a blob as a binary string.
 * @param {!Blob} blob The blob to read.
 */
goog.fs.FileReader.prototype.readAsBinaryString = function(blob) {
  this.reader_.readAsBinaryString(blob);
};


/**
 * Reads a blob as a binary string.
 * @param {!Blob} blob The blob to read.
 * @return {!goog.async.Deferred} The deferred Blob contents as a binary string.
 *     If an error occurs, the errback is called with a {@link goog.fs.Error}.
 */
goog.fs.FileReader.readAsBinaryString = function(blob) {
  var reader = new goog.fs.FileReader();
  var d = goog.fs.FileReader.createDeferred_(reader);
  reader.readAsBinaryString(blob);
  return d;
};


/**
 * Starts reading a blob as an array buffer.
 * @param {!Blob} blob The blob to read.
 */
goog.fs.FileReader.prototype.readAsArrayBuffer = function(blob) {
  this.reader_.readAsArrayBuffer(blob);
};


/**
 * Reads a blob as an array buffer.
 * @param {!Blob} blob The blob to read.
 * @return {!goog.async.Deferred} The deferred Blob contents as an array buffer.
 *     If an error occurs, the errback is called with a {@link goog.fs.Error}.
 */
goog.fs.FileReader.readAsArrayBuffer = function(blob) {
  var reader = new goog.fs.FileReader();
  var d = goog.fs.FileReader.createDeferred_(reader);
  reader.readAsArrayBuffer(blob);
  return d;
};


/**
 * Starts reading a blob as text.
 * @param {!Blob} blob The blob to read.
 * @param {string=} opt_encoding The name of the encoding to use.
 */
goog.fs.FileReader.prototype.readAsText = function(blob, opt_encoding) {
  this.reader_.readAsText(blob, opt_encoding);
};


/**
 * Reads a blob as text.
 * @param {!Blob} blob The blob to read.
 * @param {string=} opt_encoding The name of the encoding to use.
 * @return {!goog.async.Deferred} The deferred Blob contents as text.
 *     If an error occurs, the errback is called with a {@link goog.fs.Error}.
 */
goog.fs.FileReader.readAsText = function(blob, opt_encoding) {
  var reader = new goog.fs.FileReader();
  var d = goog.fs.FileReader.createDeferred_(reader);
  reader.readAsText(blob, opt_encoding);
  return d;
};


/**
 * Starts reading a blob as a data URL.
 * @param {!Blob} blob The blob to read.
 */
goog.fs.FileReader.prototype.readAsDataUrl = function(blob) {
  this.reader_.readAsDataURL(blob);
};


/**
 * Reads a blob as a data URL.
 * @param {!Blob} blob The blob to read.
 * @return {!goog.async.Deferred} The deferred Blob contents as a data URL.
 *     If an error occurs, the errback is called with a {@link goog.fs.Error}.
 */
goog.fs.FileReader.readAsDataUrl = function(blob) {
  var reader = new goog.fs.FileReader();
  var d = goog.fs.FileReader.createDeferred_(reader);
  reader.readAsDataUrl(blob);
  return d;
};


/**
 * Creates a new deferred object for the results of a read method.
 * @param {goog.fs.FileReader} reader The reader to create a deferred for.
 * @return {!goog.async.Deferred} The deferred results.
 * @private
 */
goog.fs.FileReader.createDeferred_ = function(reader) {
  var deferred = new goog.async.Deferred();
  reader.listen(goog.fs.FileReader.EventType.LOAD_END,
      goog.partial(function(d, r, e) {
        var result = r.getResult();
        var error = r.getError();
        if (result != null && !error) {
          d.callback(result);
        } else {
          d.errback(error);
        }
        r.dispose();
      }, deferred, reader));
  return deferred;
};

// FIXME should handle all geo-referenced data, not just vector data

goog.provide('ol.interaction.DragAndDrop');
goog.provide('ol.interaction.DragAndDropEvent');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.FileDropHandler');
goog.require('goog.events.FileDropHandler.EventType');
goog.require('goog.fs.FileReader');
goog.require('goog.functions');
goog.require('ol.interaction.Interaction');
goog.require('ol.proj');


/**
 * @classdesc
 * Handles input of vector data by drag and drop.
 *
 * @constructor
 * @extends {ol.interaction.Interaction}
 * @fires ol.interaction.DragAndDropEvent
 * @param {olx.interaction.DragAndDropOptions=} opt_options Options.
 * @api stable
 */
ol.interaction.DragAndDrop = function(opt_options) {

  var options = opt_options ? opt_options : {};

  goog.base(this, {
    handleEvent: ol.interaction.DragAndDrop.handleEvent
  });

  /**
   * @private
   * @type {Array.<function(new: ol.format.Feature)>}
   */
  this.formatConstructors_ = options.formatConstructors ?
      options.formatConstructors : [];

  /**
   * @private
   * @type {ol.proj.Projection}
   */
  this.projection_ = options.projection ?
      ol.proj.get(options.projection) : null;

  /**
   * @private
   * @type {goog.events.FileDropHandler}
   */
  this.fileDropHandler_ = null;

  /**
   * @private
   * @type {goog.events.Key|undefined}
   */
  this.dropListenKey_ = undefined;

};
goog.inherits(ol.interaction.DragAndDrop, ol.interaction.Interaction);


/**
 * @inheritDoc
 */
ol.interaction.DragAndDrop.prototype.disposeInternal = function() {
  if (this.dropListenKey_) {
    goog.events.unlistenByKey(this.dropListenKey_);
  }
  goog.base(this, 'disposeInternal');
};


/**
 * @param {goog.events.BrowserEvent} event Event.
 * @private
 */
ol.interaction.DragAndDrop.prototype.handleDrop_ = function(event) {
  var files = event.getBrowserEvent().dataTransfer.files;
  var i, ii, file;
  for (i = 0, ii = files.length; i < ii; ++i) {
    file = files[i];
    // The empty string param is a workaround for
    // https://code.google.com/p/closure-library/issues/detail?id=524
    var reader = goog.fs.FileReader.readAsText(file, '');
    reader.addCallback(goog.partial(this.handleResult_, file), this);
  }
};


/**
 * @param {File} file File.
 * @param {string} result Result.
 * @private
 */
ol.interaction.DragAndDrop.prototype.handleResult_ = function(file, result) {
  var map = this.getMap();
  goog.asserts.assert(map, 'map must be set');
  var projection = this.projection_;
  if (!projection) {
    var view = map.getView();
    goog.asserts.assert(view, 'map must have view');
    projection = view.getProjection();
    goog.asserts.assert(projection !== undefined,
        'projection should be defined');
  }
  var formatConstructors = this.formatConstructors_;
  var features = [];
  var i, ii;
  for (i = 0, ii = formatConstructors.length; i < ii; ++i) {
    var formatConstructor = formatConstructors[i];
    var format = new formatConstructor();
    var readFeatures = this.tryReadFeatures_(format, result);
    if (readFeatures) {
      var featureProjection = format.readProjection(result);
      var transform = ol.proj.getTransform(featureProjection, projection);
      var j, jj;
      for (j = 0, jj = readFeatures.length; j < jj; ++j) {
        var feature = readFeatures[j];
        var geometry = feature.getGeometry();
        if (geometry) {
          geometry.applyTransform(transform);
        }
        features.push(feature);
      }
    }
  }
  this.dispatchEvent(
      new ol.interaction.DragAndDropEvent(
          ol.interaction.DragAndDropEventType.ADD_FEATURES, this, file,
          features, projection));
};


/**
 * Handles the {@link ol.MapBrowserEvent map browser event} unconditionally and
 * neither prevents the browser default nor stops event propagation.
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} `false` to stop event propagation.
 * @this {ol.interaction.DragAndDrop}
 * @api
 */
ol.interaction.DragAndDrop.handleEvent = goog.functions.TRUE;


/**
 * @inheritDoc
 */
ol.interaction.DragAndDrop.prototype.setMap = function(map) {
  if (this.dropListenKey_) {
    goog.events.unlistenByKey(this.dropListenKey_);
    this.dropListenKey_ = undefined;
  }
  if (this.fileDropHandler_) {
    goog.dispose(this.fileDropHandler_);
    this.fileDropHandler_ = null;
  }
  goog.asserts.assert(this.dropListenKey_ === undefined,
      'this.dropListenKey_ should be undefined');
  goog.base(this, 'setMap', map);
  if (map) {
    this.fileDropHandler_ = new goog.events.FileDropHandler(map.getViewport());
    this.dropListenKey_ = goog.events.listen(
        this.fileDropHandler_, goog.events.FileDropHandler.EventType.DROP,
        this.handleDrop_, false, this);
  }
};


/**
 * @param {ol.format.Feature} format Format.
 * @param {string} text Text.
 * @private
 * @return {Array.<ol.Feature>} Features.
 */
ol.interaction.DragAndDrop.prototype.tryReadFeatures_ = function(format, text) {
  try {
    return format.readFeatures(text);
  } catch (e) {
    return null;
  }
};


/**
 * @enum {string}
 */
ol.interaction.DragAndDropEventType = {
  /**
   * Triggered when features are added
   * @event ol.interaction.DragAndDropEvent#addfeatures
   * @api stable
   */
  ADD_FEATURES: 'addfeatures'
};


/**
 * @classdesc
 * Events emitted by {@link ol.interaction.DragAndDrop} instances are instances
 * of this type.
 *
 * @constructor
 * @extends {goog.events.Event}
 * @implements {oli.interaction.DragAndDropEvent}
 * @param {ol.interaction.DragAndDropEventType} type Type.
 * @param {Object} target Target.
 * @param {File} file File.
 * @param {Array.<ol.Feature>=} opt_features Features.
 * @param {ol.proj.Projection=} opt_projection Projection.
 */
ol.interaction.DragAndDropEvent = function(type, target, file, opt_features, opt_projection) {

  goog.base(this, type, target);

  /**
   * The features parsed from dropped data.
   * @type {Array.<ol.Feature>|undefined}
   * @api stable
   */
  this.features = opt_features;

  /**
   * The dropped file.
   * @type {File}
   * @api stable
   */
  this.file = file;

  /**
   * The feature projection.
   * @type {ol.proj.Projection|undefined}
   * @api
   */
  this.projection = opt_projection;

};
goog.inherits(ol.interaction.DragAndDropEvent, goog.events.Event);

// Copyright 2007 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview Defines a 2-element vector class that can be used for
 * coordinate math, useful for animation systems and point manipulation.
 *
 * Vec2 objects inherit from goog.math.Coordinate and may be used wherever a
 * Coordinate is required. Where appropriate, Vec2 functions accept both Vec2
 * and Coordinate objects as input.
 *
 * @author brenneman@google.com (Shawn Brenneman)
 */

goog.provide('goog.math.Vec2');

goog.require('goog.math');
goog.require('goog.math.Coordinate');



/**
 * Class for a two-dimensional vector object and assorted functions useful for
 * manipulating points.
 *
 * @param {number} x The x coordinate for the vector.
 * @param {number} y The y coordinate for the vector.
 * @struct
 * @constructor
 * @extends {goog.math.Coordinate}
 */
goog.math.Vec2 = function(x, y) {
  /**
   * X-value
   * @type {number}
   */
  this.x = x;

  /**
   * Y-value
   * @type {number}
   */
  this.y = y;
};
goog.inherits(goog.math.Vec2, goog.math.Coordinate);


/**
 * @return {!goog.math.Vec2} A random unit-length vector.
 */
goog.math.Vec2.randomUnit = function() {
  var angle = Math.random() * Math.PI * 2;
  return new goog.math.Vec2(Math.cos(angle), Math.sin(angle));
};


/**
 * @return {!goog.math.Vec2} A random vector inside the unit-disc.
 */
goog.math.Vec2.random = function() {
  var mag = Math.sqrt(Math.random());
  var angle = Math.random() * Math.PI * 2;

  return new goog.math.Vec2(Math.cos(angle) * mag, Math.sin(angle) * mag);
};


/**
 * Returns a new Vec2 object from a given coordinate.
 * @param {!goog.math.Coordinate} a The coordinate.
 * @return {!goog.math.Vec2} A new vector object.
 */
goog.math.Vec2.fromCoordinate = function(a) {
  return new goog.math.Vec2(a.x, a.y);
};


/**
 * @return {!goog.math.Vec2} A new vector with the same coordinates as this one.
 * @override
 */
goog.math.Vec2.prototype.clone = function() {
  return new goog.math.Vec2(this.x, this.y);
};


/**
 * Returns the magnitude of the vector measured from the origin.
 * @return {number} The length of the vector.
 */
goog.math.Vec2.prototype.magnitude = function() {
  return Math.sqrt(this.x * this.x + this.y * this.y);
};


/**
 * Returns the squared magnitude of the vector measured from the origin.
 * NOTE(brenneman): Leaving out the square root is not a significant
 * optimization in JavaScript.
 * @return {number} The length of the vector, squared.
 */
goog.math.Vec2.prototype.squaredMagnitude = function() {
  return this.x * this.x + this.y * this.y;
};


/**
 * @return {!goog.math.Vec2} This coordinate after scaling.
 * @override
 */
goog.math.Vec2.prototype.scale =
    /** @type {function(number, number=):!goog.math.Vec2} */
    (goog.math.Coordinate.prototype.scale);


/**
 * Reverses the sign of the vector. Equivalent to scaling the vector by -1.
 * @return {!goog.math.Vec2} The inverted vector.
 */
goog.math.Vec2.prototype.invert = function() {
  this.x = -this.x;
  this.y = -this.y;
  return this;
};


/**
 * Normalizes the current vector to have a magnitude of 1.
 * @return {!goog.math.Vec2} The normalized vector.
 */
goog.math.Vec2.prototype.normalize = function() {
  return this.scale(1 / this.magnitude());
};


/**
 * Adds another vector to this vector in-place.
 * @param {!goog.math.Coordinate} b The vector to add.
 * @return {!goog.math.Vec2}  This vector with {@code b} added.
 */
goog.math.Vec2.prototype.add = function(b) {
  this.x += b.x;
  this.y += b.y;
  return this;
};


/**
 * Subtracts another vector from this vector in-place.
 * @param {!goog.math.Coordinate} b The vector to subtract.
 * @return {!goog.math.Vec2} This vector with {@code b} subtracted.
 */
goog.math.Vec2.prototype.subtract = function(b) {
  this.x -= b.x;
  this.y -= b.y;
  return this;
};


/**
 * Rotates this vector in-place by a given angle, specified in radians.
 * @param {number} angle The angle, in radians.
 * @return {!goog.math.Vec2} This vector rotated {@code angle} radians.
 */
goog.math.Vec2.prototype.rotate = function(angle) {
  var cos = Math.cos(angle);
  var sin = Math.sin(angle);
  var newX = this.x * cos - this.y * sin;
  var newY = this.y * cos + this.x * sin;
  this.x = newX;
  this.y = newY;
  return this;
};


/**
 * Rotates a vector by a given angle, specified in radians, relative to a given
 * axis rotation point. The returned vector is a newly created instance - no
 * in-place changes are done.
 * @param {!goog.math.Vec2} v A vector.
 * @param {!goog.math.Vec2} axisPoint The rotation axis point.
 * @param {number} angle The angle, in radians.
 * @return {!goog.math.Vec2} The rotated vector in a newly created instance.
 */
goog.math.Vec2.rotateAroundPoint = function(v, axisPoint, angle) {
  var res = v.clone();
  return res.subtract(axisPoint).rotate(angle).add(axisPoint);
};


/**
 * Compares this vector with another for equality.
 * @param {!goog.math.Vec2} b The other vector.
 * @return {boolean} Whether this vector has the same x and y as the given
 *     vector.
 */
goog.math.Vec2.prototype.equals = function(b) {
  return this == b || !!b && this.x == b.x && this.y == b.y;
};


/**
 * Returns the distance between two vectors.
 * @param {!goog.math.Coordinate} a The first vector.
 * @param {!goog.math.Coordinate} b The second vector.
 * @return {number} The distance.
 */
goog.math.Vec2.distance = goog.math.Coordinate.distance;


/**
 * Returns the squared distance between two vectors.
 * @param {!goog.math.Coordinate} a The first vector.
 * @param {!goog.math.Coordinate} b The second vector.
 * @return {number} The squared distance.
 */
goog.math.Vec2.squaredDistance = goog.math.Coordinate.squaredDistance;


/**
 * Compares vectors for equality.
 * @param {!goog.math.Coordinate} a The first vector.
 * @param {!goog.math.Coordinate} b The second vector.
 * @return {boolean} Whether the vectors have the same x and y coordinates.
 */
goog.math.Vec2.equals = goog.math.Coordinate.equals;


/**
 * Returns the sum of two vectors as a new Vec2.
 * @param {!goog.math.Coordinate} a The first vector.
 * @param {!goog.math.Coordinate} b The second vector.
 * @return {!goog.math.Vec2} The sum vector.
 */
goog.math.Vec2.sum = function(a, b) {
  return new goog.math.Vec2(a.x + b.x, a.y + b.y);
};


/**
 * Returns the difference between two vectors as a new Vec2.
 * @param {!goog.math.Coordinate} a The first vector.
 * @param {!goog.math.Coordinate} b The second vector.
 * @return {!goog.math.Vec2} The difference vector.
 */
goog.math.Vec2.difference = function(a, b) {
  return new goog.math.Vec2(a.x - b.x, a.y - b.y);
};


/**
 * Returns the dot-product of two vectors.
 * @param {!goog.math.Coordinate} a The first vector.
 * @param {!goog.math.Coordinate} b The second vector.
 * @return {number} The dot-product of the two vectors.
 */
goog.math.Vec2.dot = function(a, b) {
  return a.x * b.x + a.y * b.y;
};


/**
 * Returns the determinant of two vectors.
 * @param {!goog.math.Vec2} a The first vector.
 * @param {!goog.math.Vec2} b The second vector.
 * @return {number} The determinant of the two vectors.
 */
goog.math.Vec2.determinant = function(a, b) {
  return a.x * b.y - a.y * b.x;
};


/**
 * Returns a new Vec2 that is the linear interpolant between vectors a and b at
 * scale-value x.
 * @param {!goog.math.Coordinate} a Vector a.
 * @param {!goog.math.Coordinate} b Vector b.
 * @param {number} x The proportion between a and b.
 * @return {!goog.math.Vec2} The interpolated vector.
 */
goog.math.Vec2.lerp = function(a, b, x) {
  return new goog.math.Vec2(goog.math.lerp(a.x, b.x, x),
                            goog.math.lerp(a.y, b.y, x));
};

goog.provide('ol.interaction.DragRotateAndZoom');

goog.require('goog.math.Vec2');
goog.require('ol');
goog.require('ol.ViewHint');
goog.require('ol.events.ConditionType');
goog.require('ol.events.condition');
goog.require('ol.interaction.Interaction');
goog.require('ol.interaction.Pointer');


/**
 * @classdesc
 * Allows the user to zoom and rotate the map by clicking and dragging
 * on the map.  By default, this interaction is limited to when the shift
 * key is held down.
 *
 * This interaction is only supported for mouse devices.
 *
 * And this interaction is not included in the default interactions.
 *
 * @constructor
 * @extends {ol.interaction.Pointer}
 * @param {olx.interaction.DragRotateAndZoomOptions=} opt_options Options.
 * @api stable
 */
ol.interaction.DragRotateAndZoom = function(opt_options) {

  var options = opt_options ? opt_options : {};

  goog.base(this, {
    handleDownEvent: ol.interaction.DragRotateAndZoom.handleDownEvent_,
    handleDragEvent: ol.interaction.DragRotateAndZoom.handleDragEvent_,
    handleUpEvent: ol.interaction.DragRotateAndZoom.handleUpEvent_
  });

  /**
   * @private
   * @type {ol.events.ConditionType}
   */
  this.condition_ = options.condition ?
      options.condition : ol.events.condition.shiftKeyOnly;

  /**
   * @private
   * @type {number|undefined}
   */
  this.lastAngle_ = undefined;

  /**
   * @private
   * @type {number|undefined}
   */
  this.lastMagnitude_ = undefined;

  /**
   * @private
   * @type {number}
   */
  this.lastScaleDelta_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.duration_ = options.duration !== undefined ? options.duration : 400;

};
goog.inherits(ol.interaction.DragRotateAndZoom, ol.interaction.Pointer);


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @this {ol.interaction.DragRotateAndZoom}
 * @private
 */
ol.interaction.DragRotateAndZoom.handleDragEvent_ = function(mapBrowserEvent) {
  if (!ol.events.condition.mouseOnly(mapBrowserEvent)) {
    return;
  }

  var map = mapBrowserEvent.map;
  var size = map.getSize();
  var offset = mapBrowserEvent.pixel;
  var delta = new goog.math.Vec2(
      offset[0] - size[0] / 2,
      size[1] / 2 - offset[1]);
  var theta = Math.atan2(delta.y, delta.x);
  var magnitude = delta.magnitude();
  var view = map.getView();
  map.render();
  if (this.lastAngle_ !== undefined) {
    var angleDelta = theta - this.lastAngle_;
    ol.interaction.Interaction.rotateWithoutConstraints(
        map, view, view.getRotation() - angleDelta);
  }
  this.lastAngle_ = theta;
  if (this.lastMagnitude_ !== undefined) {
    var resolution = this.lastMagnitude_ * (view.getResolution() / magnitude);
    ol.interaction.Interaction.zoomWithoutConstraints(map, view, resolution);
  }
  if (this.lastMagnitude_ !== undefined) {
    this.lastScaleDelta_ = this.lastMagnitude_ / magnitude;
  }
  this.lastMagnitude_ = magnitude;
};


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boolean} Stop drag sequence?
 * @this {ol.interaction.DragRotateAndZoom}
 * @private
 */
ol.interaction.DragRotateAndZoom.handleUpEvent_ = function(mapBrowserEvent) {
  if (!ol.events.condition.mouseOnly(mapBrowserEvent)) {
    return true;
  }

  var map = mapBrowserEvent.map;
  var view = map.getView();
  view.setHint(ol.ViewHint.INTERACTING, -1);
  var direction = this.lastScaleDelta_ - 1;
  ol.interaction.Interaction.rotate(map, view, view.getRotation());
  ol.interaction.Interaction.zoom(map, view, view.getResolution(),
      undefined, this.duration_, direction);
  this.lastScaleDelta_ = 0;
  return false;
};


/**
 * @param {ol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boolean} Start drag sequence?
 * @this {ol.interaction.DragRotateAndZoom}
 * @private
 */
ol.interaction.DragRotateAndZoom.handleDownEvent_ = function(mapBrowserEvent) {
  if (!ol.events.condition.mouseOnly(mapBrowserEvent)) {
    return false;
  }

  if (this.condition_(mapBrowserEvent)) {
    mapBrowserEvent.map.getView().setHint(ol.ViewHint.INTERACTING, 1);
    this.lastAngle_ = undefined;
    this.lastMagnitude_ = undefined;
    return true;
  } else {
    return false;
  }
};

goog.provide('ol.interaction.Draw');
goog.provide('ol.interaction.DrawEvent');
goog.provide('ol.interaction.DrawEventType');
goog.provide('ol.interaction.DrawGeometryFunctionType');
goog.provide('ol.interaction.DrawMode');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('ol.Collection');
goog.require('ol.Coordinate');
goog.require('ol.Feature');
goog.require('ol.MapBrowserEvent');
goog.require('ol.MapBrowserEvent.EventType');
goog.require('ol.Object');
goog.require('ol.coordinate');
goog.require('ol.events.condition');
goog.require('ol.geom.Circle');
goog.require('ol.geom.GeometryType');
goog.require('ol.geom.LineString');
goog.require('ol.geom.MultiLineString');
goog.require('ol.geom.MultiPoint');
goog.require('ol.geom.MultiPolygon');
goog.require('ol.geom.Point');
goog.require('ol.geom.Polygon');
goog.require('ol.geom.SimpleGeometry');
goog.require('ol.interaction.InteractionProperty');
goog.require('ol.interaction.Pointer');
goog.require('ol.layer.Vector');
goog.require('ol.source.Vector');


/**
 * @enum {string}
 */
ol.interaction.DrawEventType = {
  /**
   * Triggered upon feature draw start
   * @event ol.interaction.DrawEvent#drawstart
   * @api stable
   */
  DRAWSTART: 'drawstart',
  /**
   * Triggered upon feature draw end
   * @event ol.interaction.DrawEvent#drawend
   * @api stable
   */
  DRAWEND: 'drawend'
};


/**
 * @classdesc
 * Events emitted by {@link ol.interaction.Draw} instances are instances of
 * this type.
 *
 * @constructor
 * @extends {goog.events.Event}
 * @implements {oli.DrawEvent}
 * @param {ol.interaction.DrawEventType} type Type.
 * @param {ol.Feature} feature The feature drawn.
 */
ol.interaction.DrawEvent = function(type, feature) {

  goog.base(this, type);

  /**
   * The feature being drawn.
   * @type {ol.Feature}
   * @api stable
   */
  this.feature = feature;

};
goog.inherits(ol.interaction.DrawEvent, goog.events.Event);


/**
 * @classdesc
 * Interaction for drawing feature geometries.
 *
 * @constructor
 * @extends {ol.interaction.Pointer}
 * @fires ol.interaction.DrawEvent
 * @param {olx.interaction.DrawOptions} options Options.
 * @api stable
 */
ol.interaction.Draw = function(options) {

  goog.base(this, {
    handleDownEvent: ol.interaction.Draw.handleDownEvent_,
    handleEvent: ol.interaction.Draw.handleEvent,
    handleUpEvent: ol.interaction.Draw.handleUpEvent_
  });

  /**
   * @type {ol.Pixel}
   * @private
   */
  this.downPx_ = null;

  /**
   * @type {boolean}
   * @private
   */
  this.freehand_ = false;

  /**
   * Target source for drawn features.
   * @type {ol.source.Vector}
   * @private
   */
  this.source_ = options.source ? options.source : null;

  /**
   * Target collection for drawn features.
   * @type {ol.Collection.<ol.Feature>}
   * @private
   */
  this.features_ = options.features ? options.features : null;

  /**
   * Pixel distance for snapping.
   * @type {number}
   * @private
   */
  this.snapTolerance_ = options.snapTolerance ? options.snapTolerance : 12;

  /**
   * Geometry type.
   * @type {ol.geom.GeometryType}
   * @private
   */
  this.type_ = options.type;

  /**
   * Drawing mode (derived from geometry type.
   * @type {ol.interaction.DrawMode}
   * @private
   */
  this.mode_ = ol.interaction.Draw.getMode_(this.type_);

  /**
   * The number of points that must be drawn before a polygon ring or line
   * string can be finished.  The default is 3 for polygon rings and 2 for
   * line strings.
   * @type {number}
   * @private
   */
  this.minPoints_ = options.minPoints ?
      options.minPoints :
      (this.mode_ === ol.interaction.DrawMode.POLYGON ? 3 : 2);

  /**
   * The number of points that can be drawn before a polygon ring or line string
   * is finished. The default is no restriction.
   * @type {number}
   * @private
   */
  this.maxPoints_ = options.maxPoints ? options.maxPoints : Infinity;

  var geometryFunction = options.geometryFunction;
  if (!geometryFunction) {
    if (this.type_ === ol.geom.GeometryType.CIRCLE) {
      /**
       * @param {ol.Coordinate|Array.<ol.Coordinate>|Array.<Array.<ol.Coordinate>>} coordinates
       *     The coordinates.
       * @param {ol.geom.SimpleGeometry=} opt_geometry Optional geometry.
       * @return {ol.geom.SimpleGeometry} A geometry.
       */
      geometryFunction = function(coordinates, opt_geometry) {
        var circle = opt_geometry ? opt_geometry :
            new ol.geom.Circle([NaN, NaN]);
        goog.asserts.assertInstanceof(circle, ol.geom.Circle,
            'geometry must be an ol.geom.Circle');
        var squaredLength = ol.coordinate.squaredDistance(
            coordinates[0], coordinates[1]);
        circle.setCenterAndRadius(coordinates[0], Math.sqrt(squaredLength));
        return circle;
      };
    } else {
      var Constructor;
      var mode = this.mode_;
      if (mode === ol.interaction.DrawMode.POINT) {
        Constructor = ol.geom.Point;
      } else if (mode === ol.interaction.DrawMode.LINE_STRING) {
        Constructor = ol.geom.LineString;
      } else if (mode === ol.interaction.DrawMode.POLYGON) {
        Constructor = ol.geom.Polygon;
      }
      /**
       * @param {ol.Coordinate|Array.<ol.Coordinate>|Array.<Array.<ol.Coordinate>>} coordinates
       *     The coordinates.
       * @param {ol.geom.SimpleGeometry=} opt_geometry Optional geometry.
       * @return {ol.geom.SimpleGeometry} A geometry.
       */
      geometryFunction = function(coordinates, opt_geometry) {
        var geometry = opt_geometry;
        if (geometry) {
          geometry.setCoordinates(coordinates);
        } else {
          geometry = new Constructor(coordinates);
        }
        return geometry;
      };
    }
  }

  /**
   * @type {ol.interaction.DrawGeometryFunctionType}
   * @private
   */
  this.geometryFunction_ = geometryFunction;

  /**
   * Finish coordinate for the feature (first point for polygons, last point for
   * linestrings).
   * @type {ol.Coordinate}
   * @private
   */
  this.finishCoordinate_ = null;

  /**
   * Sketch feature.
   * @type {ol.Feature}
   * @private
   */
  this.sketchFeature_ = null;

  /**
   * Sketch point.
   * @type {ol.Feature}
   * @private
   */
  this.sketchPoint_ = null;

  /**
   * Sketch coordinates. Used when drawing a line or polygon.
   * @type {ol.Coordinate|Array.<ol.Coordinate>|Array.<Array.<ol.Coordinate>>}
   * @private
   */
  this.sketchCoords_ = null;

  /**
   * Sketch line. Used when drawing polygon.
   * @type {ol.Feature}
   * @private
   */
  this.sketchLine_ = null;

  /**
   * Sketch line coordinates. Used when drawing a polygon or circle.
   * @type {Array.<ol.Coordinate>}
   * @private
   */
  this.sketchLineCoords_ = null;

  /**
   * Squared tolerance for handling up events.  If the squared distance
   * between a down and up event is greater than this tolerance, up events
   * will not be handled.
   * @type {number}
   * @private
   */
  this.squaredClickTolerance_ = options.clickTolerance ?
      options.clickTolerance * options.clickTolerance : 36;

  /**
   * Draw overlay where our sketch features are drawn.
   * @type {ol.layer.Vector}
   * @private
   */
  this.overlay_ = new ol.layer.Vector({
    source: new ol.source.Vector({
      useSpatialIndex: false,
      wrapX: options.wrapX ? options.wrapX : false
    }),
    style: options.style ? options.style :
        ol.interaction.Draw.getDefaultStyleFunction()
  });

  /**
   * Name of the geometry attribute for newly created features.
   * @type {string|undefined}
   * @private
   */
  this.geometryName_ = options.geometryName;

  /**
   * @private
   * @type {ol.events.ConditionType}
   */
  this.condition_ = options.condition ?
      options.condition : ol.events.condition.noModifierKeys;

  /**
   * @private
   * @type {ol.events.ConditionType}
   */
  this.freehandCondition_ = options.freehandCondition ?
      options.freehandCondition : ol.events.condition.shiftKeyOnly;

  goog.events.listen(this,
      ol.Object.getChangeEventType(ol.interaction.InteractionProperty.ACTIVE),
      this.updateState_, false, this);

};
goog.inherits(ol.interaction.Draw, ol.interaction.Pointer);


/**
 * @return {ol.style.StyleFunction} Styles.
 */
ol.interaction.Draw.getDefaultStyleFunction = function() {
  var styles = ol.style.createDefaultEditingStyles();
  return function(feature, resolution) {
    return styles[feature.getGeometry().getType()];
  };
};


/**
 * @inheritDoc
 */
ol.interaction.Draw.prototype.setMap = function(map) {
  goog.base(this, 'setMap', map);
  this.updateState_();
};


/**
 * Handles the {@link ol.MapBrowserEvent map browser event} and may actually
 * draw or finish the drawing.
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} `false` to stop event propagation.
 * @this {ol.interaction.Draw}
 * @api
 */
ol.interaction.Draw.handleEvent = function(mapBrowserEvent) {
  var pass = !this.freehand_;
  if (this.freehand_ &&
      mapBrowserEvent.type === ol.MapBrowserEvent.EventType.POINTERDRAG) {
    this.addToDrawing_(mapBrowserEvent);
    pass = false;
  } else if (mapBrowserEvent.type ===
      ol.MapBrowserEvent.EventType.POINTERMOVE) {
    pass = this.handlePointerMove_(mapBrowserEvent);
  } else if (mapBrowserEvent.type === ol.MapBrowserEvent.EventType.DBLCLICK) {
    pass = false;
  }
  return ol.interaction.Pointer.handleEvent.call(this, mapBrowserEvent) && pass;
};


/**
 * @param {ol.MapBrowserPointerEvent} event Event.
 * @return {boolean} Start drag sequence?
 * @this {ol.interaction.Draw}
 * @private
 */
ol.interaction.Draw.handleDownEvent_ = function(event) {
  if (this.condition_(event)) {
    this.downPx_ = event.pixel;
    return true;
  } else if ((this.mode_ === ol.interaction.DrawMode.LINE_STRING ||
      this.mode_ === ol.interaction.DrawMode.POLYGON) &&
      this.freehandCondition_(event)) {
    this.downPx_ = event.pixel;
    this.freehand_ = true;
    if (!this.finishCoordinate_) {
      this.startDrawing_(event);
    }
    return true;
  } else {
    return false;
  }
};


/**
 * @param {ol.MapBrowserPointerEvent} event Event.
 * @return {boolean} Stop drag sequence?
 * @this {ol.interaction.Draw}
 * @private
 */
ol.interaction.Draw.handleUpEvent_ = function(event) {
  this.freehand_ = false;
  var downPx = this.downPx_;
  var clickPx = event.pixel;
  var dx = downPx[0] - clickPx[0];
  var dy = downPx[1] - clickPx[1];
  var squaredDistance = dx * dx + dy * dy;
  var pass = true;
  if (squaredDistance <= this.squaredClickTolerance_) {
    this.handlePointerMove_(event);
    if (!this.finishCoordinate_) {
      this.startDrawing_(event);
      if (this.mode_ === ol.interaction.DrawMode.POINT) {
        this.finishDrawing();
      }
    } else if (this.mode_ === ol.interaction.DrawMode.CIRCLE) {
      this.finishDrawing();
    } else if (this.atFinish_(event)) {
      this.finishDrawing();
    } else {
      this.addToDrawing_(event);
    }
    pass = false;
  }
  return pass;
};


/**
 * Handle move events.
 * @param {ol.MapBrowserEvent} event A move event.
 * @return {boolean} Pass the event to other interactions.
 * @private
 */
ol.interaction.Draw.prototype.handlePointerMove_ = function(event) {
  if (this.finishCoordinate_) {
    this.modifyDrawing_(event);
  } else {
    this.createOrUpdateSketchPoint_(event);
  }
  return true;
};


/**
 * Determine if an event is within the snapping tolerance of the start coord.
 * @param {ol.MapBrowserEvent} event Event.
 * @return {boolean} The event is within the snapping tolerance of the start.
 * @private
 */
ol.interaction.Draw.prototype.atFinish_ = function(event) {
  var at = false;
  if (this.sketchFeature_) {
    var potentiallyDone = false;
    var potentiallyFinishCoordinates = [this.finishCoordinate_];
    if (this.mode_ === ol.interaction.DrawMode.LINE_STRING) {
      potentiallyDone = this.sketchCoords_.length > this.minPoints_;
    } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) {
      potentiallyDone = this.sketchCoords_[0].length >
          this.minPoints_;
      potentiallyFinishCoordinates = [this.sketchCoords_[0][0],
        this.sketchCoords_[0][this.sketchCoords_[0].length - 2]];
    }
    if (potentiallyDone) {
      var map = event.map;
      for (var i = 0, ii = potentiallyFinishCoordinates.length; i < ii; i++) {
        var finishCoordinate = potentiallyFinishCoordinates[i];
        var finishPixel = map.getPixelFromCoordinate(finishCoordinate);
        var pixel = event.pixel;
        var dx = pixel[0] - finishPixel[0];
        var dy = pixel[1] - finishPixel[1];
        var freehand = this.freehand_ && this.freehandCondition_(event);
        var snapTolerance = freehand ? 1 : this.snapTolerance_;
        at = Math.sqrt(dx * dx + dy * dy) <= snapTolerance;
        if (at) {
          this.finishCoordinate_ = finishCoordinate;
          break;
        }
      }
    }
  }
  return at;
};


/**
 * @param {ol.MapBrowserEvent} event Event.
 * @private
 */
ol.interaction.Draw.prototype.createOrUpdateSketchPoint_ = function(event) {
  var coordinates = event.coordinate.slice();
  if (!this.sketchPoint_) {
    this.sketchPoint_ = new ol.Feature(new ol.geom.Point(coordinates));
    this.updateSketchFeatures_();
  } else {
    var sketchPointGeom = this.sketchPoint_.getGeometry();
    goog.asserts.assertInstanceof(sketchPointGeom, ol.geom.Point,
        'sketchPointGeom should be an ol.geom.Point');
    sketchPointGeom.setCoordinates(coordinates);
  }
};


/**
 * Start the drawing.
 * @param {ol.MapBrowserEvent} event Event.
 * @private
 */
ol.interaction.Draw.prototype.startDrawing_ = function(event) {
  var start = event.coordinate;
  this.finishCoordinate_ = start;
  if (this.mode_ === ol.interaction.DrawMode.POINT) {
    this.sketchCoords_ = start.slice();
  } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) {
    this.sketchCoords_ = [[start.slice(), start.slice()]];
    this.sketchLineCoords_ = this.sketchCoords_[0];
  } else {
    this.sketchCoords_ = [start.slice(), start.slice()];
    if (this.mode_ === ol.interaction.DrawMode.CIRCLE) {
      this.sketchLineCoords_ = this.sketchCoords_;
    }
  }
  if (this.sketchLineCoords_) {
    this.sketchLine_ = new ol.Feature(
        new ol.geom.LineString(this.sketchLineCoords_));
  }
  var geometry = this.geometryFunction_(this.sketchCoords_);
  goog.asserts.assert(geometry !== undefined, 'geometry should be defined');
  this.sketchFeature_ = new ol.Feature();
  if (this.geometryName_) {
    this.sketchFeature_.setGeometryName(this.geometryName_);
  }
  this.sketchFeature_.setGeometry(geometry);
  this.updateSketchFeatures_();
  this.dispatchEvent(new ol.interaction.DrawEvent(
      ol.interaction.DrawEventType.DRAWSTART, this.sketchFeature_));
};


/**
 * Modify the drawing.
 * @param {ol.MapBrowserEvent} event Event.
 * @private
 */
ol.interaction.Draw.prototype.modifyDrawing_ = function(event) {
  var coordinate = event.coordinate;
  var geometry = this.sketchFeature_.getGeometry();
  goog.asserts.assertInstanceof(geometry, ol.geom.SimpleGeometry,
      'geometry should be ol.geom.SimpleGeometry or subclass');
  var coordinates, last;
  if (this.mode_ === ol.interaction.DrawMode.POINT) {
    last = this.sketchCoords_;
  } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) {
    coordinates = this.sketchCoords_[0];
    last = coordinates[coordinates.length - 1];
    if (this.atFinish_(event)) {
      // snap to finish
      coordinate = this.finishCoordinate_.slice();
    }
  } else {
    coordinates = this.sketchCoords_;
    last = coordinates[coordinates.length - 1];
  }
  last[0] = coordinate[0];
  last[1] = coordinate[1];
  goog.asserts.assert(this.sketchCoords_, 'sketchCoords_ expected');
  this.geometryFunction_(this.sketchCoords_, geometry);
  if (this.sketchPoint_) {
    var sketchPointGeom = this.sketchPoint_.getGeometry();
    goog.asserts.assertInstanceof(sketchPointGeom, ol.geom.Point,
        'sketchPointGeom should be an ol.geom.Point');
    sketchPointGeom.setCoordinates(coordinate);
  }
  var sketchLineGeom;
  if (geometry instanceof ol.geom.Polygon &&
      this.mode_ !== ol.interaction.DrawMode.POLYGON) {
    if (!this.sketchLine_) {
      this.sketchLine_ = new ol.Feature(new ol.geom.LineString(null));
    }
    var ring = geometry.getLinearRing(0);
    sketchLineGeom = this.sketchLine_.getGeometry();
    goog.asserts.assertInstanceof(sketchLineGeom, ol.geom.LineString,
        'sketchLineGeom must be an ol.geom.LineString');
    sketchLineGeom.setFlatCoordinates(
        ring.getLayout(), ring.getFlatCoordinates());
  } else if (this.sketchLineCoords_) {
    sketchLineGeom = this.sketchLine_.getGeometry();
    goog.asserts.assertInstanceof(sketchLineGeom, ol.geom.LineString,
        'sketchLineGeom must be an ol.geom.LineString');
    sketchLineGeom.setCoordinates(this.sketchLineCoords_);
  }
  this.updateSketchFeatures_();
};


/**
 * Add a new coordinate to the drawing.
 * @param {ol.MapBrowserEvent} event Event.
 * @private
 */
ol.interaction.Draw.prototype.addToDrawing_ = function(event) {
  var coordinate = event.coordinate;
  var geometry = this.sketchFeature_.getGeometry();
  goog.asserts.assertInstanceof(geometry, ol.geom.SimpleGeometry,
      'geometry must be an ol.geom.SimpleGeometry');
  var done;
  var coordinates;
  if (this.mode_ === ol.interaction.DrawMode.LINE_STRING) {
    this.finishCoordinate_ = coordinate.slice();
    coordinates = this.sketchCoords_;
    coordinates.push(coordinate.slice());
    done = coordinates.length > this.maxPoints_;
    this.geometryFunction_(coordinates, geometry);
  } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) {
    coordinates = this.sketchCoords_[0];
    coordinates.push(coordinate.slice());
    done = coordinates.length > this.maxPoints_;
    if (done) {
      this.finishCoordinate_ = coordinates[0];
    }
    this.geometryFunction_(this.sketchCoords_, geometry);
  }
  this.updateSketchFeatures_();
  if (done) {
    this.finishDrawing();
  }
};


/**
 * Remove last point of the feature currently being drawn.
 * @api
 */
ol.interaction.Draw.prototype.removeLastPoint = function() {
  var geometry = this.sketchFeature_.getGeometry();
  goog.asserts.assertInstanceof(geometry, ol.geom.SimpleGeometry,
      'geometry must be an ol.geom.SimpleGeometry');
  var coordinates, sketchLineGeom;
  if (this.mode_ === ol.interaction.DrawMode.LINE_STRING) {
    coordinates = this.sketchCoords_;
    coordinates.splice(-2, 1);
    this.geometryFunction_(coordinates, geometry);
  } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) {
    coordinates = this.sketchCoords_[0];
    coordinates.splice(-2, 1);
    sketchLineGeom = this.sketchLine_.getGeometry();
    goog.asserts.assertInstanceof(sketchLineGeom, ol.geom.LineString,
        'sketchLineGeom must be an ol.geom.LineString');
    sketchLineGeom.setCoordinates(coordinates);
    this.geometryFunction_(this.sketchCoords_, geometry);
  }

  if (coordinates.length === 0) {
    this.finishCoordinate_ = null;
  }

  this.updateSketchFeatures_();
};


/**
 * Stop drawing and add the sketch feature to the target layer.
 * The {@link ol.interaction.DrawEventType.DRAWEND} event is dispatched before
 * inserting the feature.
 * @api
 */
ol.interaction.Draw.prototype.finishDrawing = function() {
  var sketchFeature = this.abortDrawing_();
  goog.asserts.assert(sketchFeature, 'sketchFeature expected to be truthy');
  var coordinates = this.sketchCoords_;
  var geometry = sketchFeature.getGeometry();
  goog.asserts.assertInstanceof(geometry, ol.geom.SimpleGeometry,
      'geometry must be an ol.geom.SimpleGeometry');
  if (this.mode_ === ol.interaction.DrawMode.LINE_STRING) {
    // remove the redundant last point
    coordinates.pop();
    this.geometryFunction_(coordinates, geometry);
  } else if (this.mode_ === ol.interaction.DrawMode.POLYGON) {
    // When we finish drawing a polygon on the last point,
    // the last coordinate is duplicated as for LineString
    // we force the replacement by the first point
    coordinates[0].pop();
    coordinates[0].push(coordinates[0][0]);
    this.geometryFunction_(coordinates, geometry);
  }

  // cast multi-part geometries
  if (this.type_ === ol.geom.GeometryType.MULTI_POINT) {
    sketchFeature.setGeometry(new ol.geom.MultiPoint([coordinates]));
  } else if (this.type_ === ol.geom.GeometryType.MULTI_LINE_STRING) {
    sketchFeature.setGeometry(new ol.geom.MultiLineString([coordinates]));
  } else if (this.type_ === ol.geom.GeometryType.MULTI_POLYGON) {
    sketchFeature.setGeometry(new ol.geom.MultiPolygon([coordinates]));
  }

  // First dispatch event to allow full set up of feature
  this.dispatchEvent(new ol.interaction.DrawEvent(
      ol.interaction.DrawEventType.DRAWEND, sketchFeature));

  // Then insert feature
  if (this.features_) {
    this.features_.push(sketchFeature);
  }
  if (this.source_) {
    this.source_.addFeature(sketchFeature);
  }
};


/**
 * Stop drawing without adding the sketch feature to the target layer.
 * @return {ol.Feature} The sketch feature (or null if none).
 * @private
 */
ol.interaction.Draw.prototype.abortDrawing_ = function() {
  this.finishCoordinate_ = null;
  var sketchFeature = this.sketchFeature_;
  if (sketchFeature) {
    this.sketchFeature_ = null;
    this.sketchPoint_ = null;
    this.sketchLine_ = null;
    this.overlay_.getSource().clear(true);
  }
  return sketchFeature;
};


/**
 * Extend an existing geometry by adding additional points. This only works
 * on features with `LineString` geometries, where the interaction will
 * extend lines by adding points to the end of the coordinates array.
 * @param {!ol.Feature} feature Feature to be extended.
 * @api
 */
ol.interaction.Draw.prototype.extend = function(feature) {
  var geometry = feature.getGeometry();
  goog.asserts.assert(this.mode_ == ol.interaction.DrawMode.LINE_STRING,
      'interaction mode must be "line"');
  goog.asserts.assert(geometry, 'feature must have a geometry');
  goog.asserts.assert(geometry.getType() == ol.geom.GeometryType.LINE_STRING,
      'feature geometry must be a line string');
  var lineString = /** @type {ol.geom.LineString} */ (geometry);
  this.sketchFeature_ = feature;
  this.sketchCoords_ = lineString.getCoordinates();
  var last = this.sketchCoords_[this.sketchCoords_.length - 1];
  this.finishCoordinate_ = last.slice();
  this.sketchCoords_.push(last.slice());
  this.updateSketchFeatures_();
  this.dispatchEvent(new ol.interaction.DrawEvent(
      ol.interaction.DrawEventType.DRAWSTART, this.sketchFeature_));
};


/**
 * @inheritDoc
 */
ol.interaction.Draw.prototype.shouldStopEvent = goog.functions.FALSE;


/**
 * Redraw the sketch features.
 * @private
 */
ol.interaction.Draw.prototype.updateSketchFeatures_ = function() {
  var sketchFeatures = [];
  if (this.sketchFeature_) {
    sketchFeatures.push(this.sketchFeature_);
  }
  if (this.sketchLine_) {
    sketchFeatures.push(this.sketchLine_);
  }
  if (this.sketchPoint_) {
    sketchFeatures.push(this.sketchPoint_);
  }
  var overlaySource = this.overlay_.getSource();
  overlaySource.clear(true);
  overlaySource.addFeatures(sketchFeatures);
};


/**
 * @private
 */
ol.interaction.Draw.prototype.updateState_ = function() {
  var map = this.getMap();
  var active = this.getActive();
  if (!map || !active) {
    this.abortDrawing_();
  }
  this.overlay_.setMap(active ? map : null);
};


/**
 * Create a `geometryFunction` for `mode: 'Circle'` that will create a regular
 * polygon with a user specified number of sides and start angle instead of an
 * `ol.geom.Circle` geometry.
 * @param {number=} opt_sides Number of sides of the regular polygon. Default is
 *     32.
 * @param {number=} opt_angle Angle of the first point in radians. 0 means East.
 *     Default is the angle defined by the heading from the center of the
 *     regular polygon to the current pointer position.
 * @return {ol.interaction.DrawGeometryFunctionType} Function that draws a
 *     polygon.
 * @api
 */
ol.interaction.Draw.createRegularPolygon = function(opt_sides, opt_angle) {
  return (
      /**
       * @param {ol.Coordinate|Array.<ol.Coordinate>|Array.<Array.<ol.Coordinate>>} coordinates
       * @param {ol.geom.SimpleGeometry=} opt_geometry
       * @return {ol.geom.SimpleGeometry}
       */
      function(coordinates, opt_geometry) {
        var center = coordinates[0];
        var end = coordinates[1];
        var radius = Math.sqrt(
            ol.coordinate.squaredDistance(center, end));
        var geometry = opt_geometry ? opt_geometry :
            ol.geom.Polygon.fromCircle(new ol.geom.Circle(center), opt_sides);
        goog.asserts.assertInstanceof(geometry, ol.geom.Polygon,
            'geometry must be a polygon');
        var angle = opt_angle ? opt_angle :
            Math.atan((end[1] - center[1]) / (end[0] - center[0]));
        ol.geom.Polygon.makeRegular(geometry, center, radius, angle);
        return geometry;
      }
  );
};


/**
 * Get the drawing mode.  The mode for mult-part geometries is the same as for
 * their single-part cousins.
 * @param {ol.geom.GeometryType} type Geometry type.
 * @return {ol.interaction.DrawMode} Drawing mode.
 * @private
 */
ol.interaction.Draw.getMode_ = function(type) {
  var mode;
  if (type === ol.geom.GeometryType.POINT ||
      type === ol.geom.GeometryType.MULTI_POINT) {
    mode = ol.interaction.DrawMode.POINT;
  } else if (type === ol.geom.GeometryType.LINE_STRING ||
      type === ol.geom.GeometryType.MULTI_LINE_STRING) {
    mode = ol.interaction.DrawMode.LINE_STRING;
  } else if (type === ol.geom.GeometryType.POLYGON ||
      type === ol.geom.GeometryType.MULTI_POLYGON) {
    mode = ol.interaction.DrawMode.POLYGON;
  } else if (type === ol.geom.GeometryType.CIRCLE) {
    mode = ol.interaction.DrawMode.CIRCLE;
  }
  goog.asserts.assert(mode !== undefined, 'mode should be defined');
  return mode;
};


/**
 * Function that takes coordinates and an optional existing geometry as
 * arguments, and returns a geometry. The optional existing geometry is the
 * geometry that is returned when the function is called without a second
 * argument.
 * @typedef {function(!(ol.Coordinate|Array.<ol.Coordinate>|
 *     Array.<Array.<ol.Coordinate>>), ol.geom.SimpleGeometry=):
 *     ol.geom.SimpleGeometry}
 * @api
 */
ol.interaction.DrawGeometryFunctionType;


/**
 * Draw mode.  This collapses multi-part geometry types with their single-part
 * cousins.
 * @enum {string}
 */
ol.interaction.DrawMode = {
  POINT: 'Point',
  LINE_STRING: 'LineString',
  POLYGON: 'Polygon',
  CIRCLE: 'Circle'
};

goog.provide('ol.interaction.Modify');
goog.provide('ol.interaction.ModifyEvent');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.EventType');
goog.require('goog.functions');
goog.require('ol');
goog.require('ol.Collection');
goog.require('ol.CollectionEventType');
goog.require('ol.Feature');
goog.require('ol.MapBrowserEvent.EventType');
goog.require('ol.MapBrowserPointerEvent');
goog.require('ol.ViewHint');
goog.require('ol.coordinate');
goog.require('ol.events.condition');
goog.require('ol.extent');
goog.require('ol.geom.GeometryType');
goog.require('ol.geom.LineString');
goog.require('ol.geom.MultiLineString');
goog.require('ol.geom.MultiPoint');
goog.require('ol.geom.MultiPolygon');
goog.require('ol.geom.Point');
goog.require('ol.geom.Polygon');
goog.require('ol.interaction.Pointer');
goog.require('ol.layer.Vector');
goog.require('ol.source.Vector');
goog.require('ol.structs.RBush');


/**
 * @enum {string}
 */
ol.ModifyEventType = {
  /**
   * Triggered upon feature modification start
   * @event ol.interaction.ModifyEvent#modifystart
   * @api
   */
  MODIFYSTART: 'modifystart',
  /**
   * Triggered upon feature modification end
   * @event ol.interaction.ModifyEvent#modifyend
   * @api
   */
  MODIFYEND: 'modifyend'
};


/**
 * @classdesc
 * Events emitted by {@link ol.interaction.Modify} instances are instances of
 * this type.
 *
 * @constructor
 * @extends {goog.events.Event}
 * @implements {oli.ModifyEvent}
 * @param {ol.ModifyEventType} type Type.
 * @param {ol.Collection.<ol.Feature>} features The features modified.
 * @param {ol.MapBrowserPointerEvent} mapBrowserPointerEvent Associated
 *     {@link ol.MapBrowserPointerEvent}.
 */
ol.interaction.ModifyEvent = function(type, features, mapBrowserPointerEvent) {

  goog.base(this, type);

  /**
   * The features being modified.
   * @type {ol.Collection.<ol.Feature>}
   * @api
   */
  this.features = features;

  /**
   * Associated {@link ol.MapBrowserPointerEvent}.
   * @type {ol.MapBrowserPointerEvent}
   * @api
   */
  this.mapBrowserPointerEvent = mapBrowserPointerEvent;
};
goog.inherits(ol.interaction.ModifyEvent, goog.events.Event);


/**
 * @typedef {{depth: (Array.<number>|undefined),
 *            feature: ol.Feature,
 *            geometry: ol.geom.SimpleGeometry,
 *            index: (number|undefined),
 *            segment: Array.<ol.Extent>}}
 */
ol.interaction.SegmentDataType;


/**
 * @classdesc
 * Interaction for modifying feature geometries.
 *
 * @constructor
 * @extends {ol.interaction.Pointer}
 * @param {olx.interaction.ModifyOptions} options Options.
 * @fires ol.interaction.ModifyEvent
 * @api
 */
ol.interaction.Modify = function(options) {

  goog.base(this, {
    handleDownEvent: ol.interaction.Modify.handleDownEvent_,
    handleDragEvent: ol.interaction.Modify.handleDragEvent_,
    handleEvent: ol.interaction.Modify.handleEvent,
    handleUpEvent: ol.interaction.Modify.handleUpEvent_
  });

  /**
   * @type {ol.events.ConditionType}
   * @private
   */
  this.deleteCondition_ = options.deleteCondition ?
      options.deleteCondition :
      /** @type {ol.events.ConditionType} */ (goog.functions.and(
          ol.events.condition.noModifierKeys,
          ol.events.condition.singleClick));

  /**
   * Editing vertex.
   * @type {ol.Feature}
   * @private
   */
  this.vertexFeature_ = null;

  /**
   * Segments intersecting {@link this.vertexFeature_} by segment uid.
   * @type {Object.<string, boolean>}
   * @private
   */
  this.vertexSegments_ = null;

  /**
   * @type {ol.Pixel}
   * @private
   */
  this.lastPixel_ = [0, 0];

  /**
   * Tracks if the next `singleclick` event should be ignored to prevent
   * accidental deletion right after vertex creation.
   * @type {boolean}
   * @private
   */
  this.ignoreNextSingleClick_ = false;

  /**
   * @type {boolean}
   * @private
   */
  this.modified_ = false;

  /**
   * Segment RTree for each layer
   * @type {ol.structs.RBush.<ol.interaction.SegmentDataType>}
   * @private
   */
  this.rBush_ = new ol.structs.RBush();

  /**
   * @type {number}
   * @private
   */
  this.pixelTolerance_ = options.pixelTolerance !== undefined ?
      options.pixelTolerance : 10;

  /**
   * @type {boolean}
   * @private
   */
  this.snappedToVertex_ = false;

  /**
   * Indicate whether the interaction is currently changing a feature's
   * coordinates.
   * @type {boolean}
   * @private
   */
  this.changingFeature_ = false;

  /**
   * @type {Array}
   * @private
   */
  this.dragSegments_ = null;

  /**
   * Draw overlay where sketch features are drawn.
   * @type {ol.layer.Vector}
   * @private
   */
  this.overlay_ = new ol.layer.Vector({
    source: new ol.source.Vector({
      useSpatialIndex: false,
      wrapX: !!options.wrapX
    }),
    style: options.style ? options.style :
        ol.interaction.Modify.getDefaultStyleFunction(),
    updateWhileAnimating: true,
    updateWhileInteracting: true
  });

  /**
  * @const
  * @private
  * @type {Object.<string, function(ol.Feature, ol.geom.Geometry)>}
  */
  this.SEGMENT_WRITERS_ = {
    'Point': this.writePointGeometry_,
    'LineString': this.writeLineStringGeometry_,
    'LinearRing': this.writeLineStringGeometry_,
    'Polygon': this.writePolygonGeometry_,
    'MultiPoint': this.writeMultiPointGeometry_,
    'MultiLineString': this.writeMultiLineStringGeometry_,
    'MultiPolygon': this.writeMultiPolygonGeometry_,
    'GeometryCollection': this.writeGeometryCollectionGeometry_
  };

  /**
   * @type {ol.Collection.<ol.Feature>}
   * @private
   */
  this.features_ = options.features;

  this.features_.forEach(this.addFeature_, this);
  goog.events.listen(this.features_, ol.CollectionEventType.ADD,
      this.handleFeatureAdd_, false, this);
  goog.events.listen(this.features_, ol.CollectionEventType.REMOVE,
      this.handleFeatureRemove_, false, this);

};
goog.inherits(ol.interaction.Modify, ol.interaction.Pointer);


/**
 * @param {ol.Feature} feature Feature.
 * @private
 */
ol.interaction.Modify.prototype.addFeature_ = function(feature) {
  var geometry = feature.getGeometry();
  if (geometry.getType() in this.SEGMENT_WRITERS_) {
    this.SEGMENT_WRITERS_[geometry.getType()].call(this, feature, geometry);
  }
  var map = this.getMap();
  if (map) {
    this.handlePointerAtPixel_(this.lastPixel_, map);
  }
  goog.events.listen(feature, goog.events.EventType.CHANGE,
      this.handleFeatureChange_, false, this);
};


/**
 * @param {ol.MapBrowserPointerEvent} evt Map browser event
 * @private
 */
ol.interaction.Modify.prototype.willModifyFeatures_ = function(evt) {
  if (!this.modified_) {
    this.modified_ = true;
    this.dispatchEvent(new ol.interaction.ModifyEvent(
        ol.ModifyEventType.MODIFYSTART, this.features_, evt));
  }
};


/**
 * @param {ol.Feature} feature Feature.
 * @private
 */
ol.interaction.Modify.prototype.removeFeature_ = function(feature) {
  this.removeFeatureSegmentData_(feature);
  // Remove the vertex feature if the collection of canditate features
  // is empty.
  if (this.vertexFeature_ && this.features_.getLength() === 0) {
    this.overlay_.getSource().removeFeature(this.vertexFeature_);
    this.vertexFeature_ = null;
  }
  goog.events.unlisten(feature, goog.events.EventType.CHANGE,
      this.handleFeatureChange_, false, this);
};


/**
 * @param {ol.Feature} feature Feature.
 * @private
 */
ol.interaction.Modify.prototype.removeFeatureSegmentData_ = function(feature) {
  var rBush = this.rBush_;
  var /** @type {Array.<ol.interaction.SegmentDataType>} */ nodesToRemove = [];
  rBush.forEach(
      /**
       * @param {ol.interaction.SegmentDataType} node RTree node.
       */
      function(node) {
        if (feature === node.feature) {
          nodesToRemove.push(node);
        }
      });
  for (var i = nodesToRemove.length - 1; i >= 0; --i) {
    rBush.remove(nodesToRemove[i]);
  }
};


/**
 * @inheritDoc
 */
ol.interaction.Modify.prototype.setMap = function(map) {
  this.overlay_.setMap(map);
  goog.base(this, 'setMap', map);
};


/**
 * @param {ol.CollectionEvent} evt Event.
 * @private
 */
ol.interaction.Modify.prototype.handleFeatureAdd_ = function(evt) {
  var feature = evt.element;
  goog.asserts.assertInstanceof(feature, ol.Feature,
      'feature should be an ol.Feature');
  this.addFeature_(feature);
};


/**
 * @param {goog.events.Event} evt Event.
 * @private
 */
ol.interaction.Modify.prototype.handleFeatureChange_ = function(evt) {
  if (!this.changingFeature_) {
    var feature = /** @type {ol.Feature} */ (evt.target);
    this.removeFeature_(feature);
    this.addFeature_(feature);
  }
};


/**
 * @param {ol.CollectionEvent} evt Event.
 * @private
 */
ol.interaction.Modify.prototype.handleFeatureRemove_ = function(evt) {
  var feature = /** @type {ol.Feature} */ (evt.element);
  this.removeFeature_(feature);
};


/**
 * @param {ol.Feature} feature Feature
 * @param {ol.geom.Point} geometry Geometry.
 * @private
 */
ol.interaction.Modify.prototype.writePointGeometry_ = function(feature, geometry) {
  var coordinates = geometry.getCoordinates();
  var segmentData = /** @type {ol.interaction.SegmentDataType} */ ({
    feature: feature,
    geometry: geometry,
    segment: [coordinates, coordinates]
  });
  this.rBush_.insert(geometry.getExtent(), segmentData);
};


/**
 * @param {ol.Feature} feature Feature
 * @param {ol.geom.MultiPoint} geometry Geometry.
 * @private
 */
ol.interaction.Modify.prototype.writeMultiPointGeometry_ = function(feature, geometry) {
  var points = geometry.getCoordinates();
  var coordinates, i, ii, segmentData;
  for (i = 0, ii = points.length; i < ii; ++i) {
    coordinates = points[i];
    segmentData = /** @type {ol.interaction.SegmentDataType} */ ({
      feature: feature,
      geometry: geometry,
      depth: [i],
      index: i,
      segment: [coordinates, coordinates]
    });
    this.rBush_.insert(geometry.getExtent(), segmentData);
  }
};


/**
 * @param {ol.Feature} feature Feature
 * @param {ol.geom.LineString} geometry Geometry.
 * @private
 */
ol.interaction.Modify.prototype.writeLineStringGeometry_ = function(feature, geometry) {
  var coordinates = geometry.getCoordinates();
  var i, ii, segment, segmentData;
  for (i = 0, ii = coordinates.length - 1; i < ii; ++i) {
    segment = coordinates.slice(i, i + 2);
    segmentData = /** @type {ol.interaction.SegmentDataType} */ ({
      feature: feature,
      geometry: geometry,
      index: i,
      segment: segment
    });
    this.rBush_.insert(ol.extent.boundingExtent(segment), segmentData);
  }
};


/**
 * @param {ol.Feature} feature Feature
 * @param {ol.geom.MultiLineString} geometry Geometry.
 * @private
 */
ol.interaction.Modify.prototype.writeMultiLineStringGeometry_ = function(feature, geometry) {
  var lines = geometry.getCoordinates();
  var coordinates, i, ii, j, jj, segment, segmentData;
  for (j = 0, jj = lines.length; j < jj; ++j) {
    coordinates = lines[j];
    for (i = 0, ii = coordinates.length - 1; i < ii; ++i) {
      segment = coordinates.slice(i, i + 2);
      segmentData = /** @type {ol.interaction.SegmentDataType} */ ({
        feature: feature,
        geometry: geometry,
        depth: [j],
        index: i,
        segment: segment
      });
      this.rBush_.insert(ol.extent.boundingExtent(segment), segmentData);
    }
  }
};


/**
 * @param {ol.Feature} feature Feature
 * @param {ol.geom.Polygon} geometry Geometry.
 * @private
 */
ol.interaction.Modify.prototype.writePolygonGeometry_ = function(feature, geometry) {
  var rings = geometry.getCoordinates();
  var coordinates, i, ii, j, jj, segment, segmentData;
  for (j = 0, jj = rings.length; j < jj; ++j) {
    coordinates = rings[j];
    for (i = 0, ii = coordinates.length - 1; i < ii; ++i) {
      segment = coordinates.slice(i, i + 2);
      segmentData = /** @type {ol.interaction.SegmentDataType} */ ({
        feature: feature,
        geometry: geometry,
        depth: [j],
        index: i,
        segment: segment
      });
      this.rBush_.insert(ol.extent.boundingExtent(segment), segmentData);
    }
  }
};


/**
 * @param {ol.Feature} feature Feature
 * @param {ol.geom.MultiPolygon} geometry Geometry.
 * @private
 */
ol.interaction.Modify.prototype.writeMultiPolygonGeometry_ = function(feature, geometry) {
  var polygons = geometry.getCoordinates();
  var coordinates, i, ii, j, jj, k, kk, rings, segment, segmentData;
  for (k = 0, kk = polygons.length; k < kk; ++k) {
    rings = polygons[k];
    for (j = 0, jj = rings.length; j < jj; ++j) {
      coordinates = rings[j];
      for (i = 0, ii = coordinates.length - 1; i < ii; ++i) {
        segment = coordinates.slice(i, i + 2);
        segmentData = /** @type {ol.interaction.SegmentDataType} */ ({
          feature: feature,
          geometry: geometry,
          depth: [j, k],
          index: i,
          segment: segment
        });
        this.rBush_.insert(ol.extent.boundingExtent(segment), segmentData);
      }
    }
  }
};


/**
 * @param {ol.Feature} feature Feature
 * @param {ol.geom.GeometryCollection} geometry Geometry.
 * @private
 */
ol.interaction.Modify.prototype.writeGeometryCollectionGeometry_ = function(feature, geometry) {
  var i, geometries = geometry.getGeometriesArray();
  for (i = 0; i < geometries.length; ++i) {
    this.SEGMENT_WRITERS_[geometries[i].getType()].call(
        this, feature, geometries[i]);
  }
};


/**
 * @param {ol.Coordinate} coordinates Coordinates.
 * @return {ol.Feature} Vertex feature.
 * @private
 */
ol.interaction.Modify.prototype.createOrUpdateVertexFeature_ = function(coordinates) {
  var vertexFeature = this.vertexFeature_;
  if (!vertexFeature) {
    vertexFeature = new ol.Feature(new ol.geom.Point(coordinates));
    this.vertexFeature_ = vertexFeature;
    this.overlay_.getSource().addFeature(vertexFeature);
  } else {
    var geometry = /** @type {ol.geom.Point} */ (vertexFeature.getGeometry());
    geometry.setCoordinates(coordinates);
  }
  return vertexFeature;
};


/**
 * @param {ol.interaction.SegmentDataType} a The first segment data.
 * @param {ol.interaction.SegmentDataType} b The second segment data.
 * @return {number} The difference in indexes.
 * @private
 */
ol.interaction.Modify.compareIndexes_ = function(a, b) {
  return a.index - b.index;
};


/**
 * @param {ol.MapBrowserPointerEvent} evt Event.
 * @return {boolean} Start drag sequence?
 * @this {ol.interaction.Modify}
 * @private
 */
ol.interaction.Modify.handleDownEvent_ = function(evt) {
  this.handlePointerAtPixel_(evt.pixel, evt.map);
  this.dragSegments_ = [];
  this.modified_ = false;
  var vertexFeature = this.vertexFeature_;
  if (vertexFeature) {
    var insertVertices = [];
    var geometry = /** @type {ol.geom.Point} */ (vertexFeature.getGeometry());
    var vertex = geometry.getCoordinates();
    var vertexExtent = ol.extent.boundingExtent([vertex]);
    var segmentDataMatches = this.rBush_.getInExtent(vertexExtent);
    var componentSegments = {};
    segmentDataMatches.sort(ol.interaction.Modify.compareIndexes_);
    for (var i = 0, ii = segmentDataMatches.length; i < ii; ++i) {
      var segmentDataMatch = segmentDataMatches[i];
      var segment = segmentDataMatch.segment;
      var uid = goog.getUid(segmentDataMatch.feature);
      var depth = segmentDataMatch.depth;
      if (depth) {
        uid += '-' + depth.join('-'); // separate feature components
      }
      if (!componentSegments[uid]) {
        componentSegments[uid] = new Array(2);
      }
      if (ol.coordinate.equals(segment[0], vertex) &&
          !componentSegments[uid][0]) {
        this.dragSegments_.push([segmentDataMatch, 0]);
        componentSegments[uid][0] = segmentDataMatch;
      } else if (ol.coordinate.equals(segment[1], vertex) &&
          !componentSegments[uid][1]) {

        // prevent dragging closed linestrings by the connecting node
        if ((segmentDataMatch.geometry.getType() ===
            ol.geom.GeometryType.LINE_STRING ||
            segmentDataMatch.geometry.getType() ===
            ol.geom.GeometryType.MULTI_LINE_STRING) &&
            componentSegments[uid][0] &&
            componentSegments[uid][0].index === 0) {
          continue;
        }

        this.dragSegments_.push([segmentDataMatch, 1]);
        componentSegments[uid][1] = segmentDataMatch;
      } else if (goog.getUid(segment) in this.vertexSegments_ &&
          (!componentSegments[uid][0] && !componentSegments[uid][1])) {
        insertVertices.push([segmentDataMatch, vertex]);
      }
    }
    if (insertVertices.length) {
      this.willModifyFeatures_(evt);
    }
    for (var j = insertVertices.length - 1; j >= 0; --j) {
      this.insertVertex_.apply(this, insertVertices[j]);
    }
  }
  return !!this.vertexFeature_;
};


/**
 * @param {ol.MapBrowserPointerEvent} evt Event.
 * @this {ol.interaction.Modify}
 * @private
 */
ol.interaction.Modify.handleDragEvent_ = function(evt) {
  this.ignoreNextSingleClick_ = false;
  this.willModifyFeatures_(evt);

  var vertex = evt.coordinate;
  for (var i = 0, ii = this.dragSegments_.length; i < ii; ++i) {
    var dragSegment = this.dragSegments_[i];
    var segmentData = dragSegment[0];
    var depth = segmentData.depth;
    var geometry = segmentData.geometry;
    var coordinates = geometry.getCoordinates();
    var segment = segmentData.segment;
    var index = dragSegment[1];

    while (vertex.length < geometry.getStride()) {
      vertex.push(0);
    }

    switch (geometry.getType()) {
      case ol.geom.GeometryType.POINT:
        coordinates = vertex;
        segment[0] = segment[1] = vertex;
        break;
      case ol.geom.GeometryType.MULTI_POINT:
        coordinates[segmentData.index] = vertex;
        segment[0] = segment[1] = vertex;
        break;
      case ol.geom.GeometryType.LINE_STRING:
        coordinates[segmentData.index + index] = vertex;
        segment[index] = vertex;
        break;
      case ol.geom.GeometryType.MULTI_LINE_STRING:
        coordinates[depth[0]][segmentData.index + index] = vertex;
        segment[index] = vertex;
        break;
      case ol.geom.GeometryType.POLYGON:
        coordinates[depth[0]][segmentData.index + index] = vertex;
        segment[index] = vertex;
        break;
      case ol.geom.GeometryType.MULTI_POLYGON:
        coordinates[depth[1]][depth[0]][segmentData.index + index] = vertex;
        segment[index] = vertex;
        break;
      default:
        // pass
    }

    this.setGeometryCoordinates_(geometry, coordinates);
  }
  this.createOrUpdateVertexFeature_(vertex);
};


/**
 * @param {ol.MapBrowserPointerEvent} evt Event.
 * @return {boolean} Stop drag sequence?
 * @this {ol.interaction.Modify}
 * @private
 */
ol.interaction.Modify.handleUpEvent_ = function(evt) {
  var segmentData;
  for (var i = this.dragSegments_.length - 1; i >= 0; --i) {
    segmentData = this.dragSegments_[i][0];
    this.rBush_.update(ol.extent.boundingExtent(segmentData.segment),
        segmentData);
  }
  if (this.modified_) {
    this.dispatchEvent(new ol.interaction.ModifyEvent(
        ol.ModifyEventType.MODIFYEND, this.features_, evt));
    this.modified_ = false;
  }
  return false;
};


/**
 * Handles the {@link ol.MapBrowserEvent map browser event} and may modify the
 * geometry.
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} `false` to stop event propagation.
 * @this {ol.interaction.Modify}
 * @api
 */
ol.interaction.Modify.handleEvent = function(mapBrowserEvent) {
  if (!(mapBrowserEvent instanceof ol.MapBrowserPointerEvent)) {
    return true;
  }

  var handled;
  if (!mapBrowserEvent.map.getView().getHints()[ol.ViewHint.INTERACTING] &&
      mapBrowserEvent.type == ol.MapBrowserEvent.EventType.POINTERMOVE &&
      !this.handlingDownUpSequence) {
    this.handlePointerMove_(mapBrowserEvent);
  }
  if (this.vertexFeature_ && this.deleteCondition_(mapBrowserEvent)) {
    if (mapBrowserEvent.type != ol.MapBrowserEvent.EventType.SINGLECLICK ||
        !this.ignoreNextSingleClick_) {
      var geometry = this.vertexFeature_.getGeometry();
      goog.asserts.assertInstanceof(geometry, ol.geom.Point,
          'geometry should be an ol.geom.Point');
      this.willModifyFeatures_(mapBrowserEvent);
      handled = this.removeVertex_();
      this.dispatchEvent(new ol.interaction.ModifyEvent(
          ol.ModifyEventType.MODIFYEND, this.features_, mapBrowserEvent));
      this.modified_ = false;
    } else {
      handled = true;
    }
  }

  if (mapBrowserEvent.type == ol.MapBrowserEvent.EventType.SINGLECLICK) {
    this.ignoreNextSingleClick_ = false;
  }

  return ol.interaction.Pointer.handleEvent.call(this, mapBrowserEvent) &&
      !handled;
};


/**
 * @param {ol.MapBrowserEvent} evt Event.
 * @private
 */
ol.interaction.Modify.prototype.handlePointerMove_ = function(evt) {
  this.lastPixel_ = evt.pixel;
  this.handlePointerAtPixel_(evt.pixel, evt.map);
};


/**
 * @param {ol.Pixel} pixel Pixel
 * @param {ol.Map} map Map.
 * @private
 */
ol.interaction.Modify.prototype.handlePointerAtPixel_ = function(pixel, map) {
  var pixelCoordinate = map.getCoordinateFromPixel(pixel);
  var sortByDistance = function(a, b) {
    return ol.coordinate.squaredDistanceToSegment(pixelCoordinate, a.segment) -
        ol.coordinate.squaredDistanceToSegment(pixelCoordinate, b.segment);
  };

  var lowerLeft = map.getCoordinateFromPixel(
      [pixel[0] - this.pixelTolerance_, pixel[1] + this.pixelTolerance_]);
  var upperRight = map.getCoordinateFromPixel(
      [pixel[0] + this.pixelTolerance_, pixel[1] - this.pixelTolerance_]);
  var box = ol.extent.boundingExtent([lowerLeft, upperRight]);

  var rBush = this.rBush_;
  var nodes = rBush.getInExtent(box);
  if (nodes.length > 0) {
    nodes.sort(sortByDistance);
    var node = nodes[0];
    var closestSegment = node.segment;
    var vertex = (ol.coordinate.closestOnSegment(pixelCoordinate,
        closestSegment));
    var vertexPixel = map.getPixelFromCoordinate(vertex);
    if (Math.sqrt(ol.coordinate.squaredDistance(pixel, vertexPixel)) <=
        this.pixelTolerance_) {
      var pixel1 = map.getPixelFromCoordinate(closestSegment[0]);
      var pixel2 = map.getPixelFromCoordinate(closestSegment[1]);
      var squaredDist1 = ol.coordinate.squaredDistance(vertexPixel, pixel1);
      var squaredDist2 = ol.coordinate.squaredDistance(vertexPixel, pixel2);
      var dist = Math.sqrt(Math.min(squaredDist1, squaredDist2));
      this.snappedToVertex_ = dist <= this.pixelTolerance_;
      if (this.snappedToVertex_) {
        vertex = squaredDist1 > squaredDist2 ?
            closestSegment[1] : closestSegment[0];
      }
      this.createOrUpdateVertexFeature_(vertex);
      var vertexSegments = {};
      vertexSegments[goog.getUid(closestSegment)] = true;
      var segment;
      for (var i = 1, ii = nodes.length; i < ii; ++i) {
        segment = nodes[i].segment;
        if ((ol.coordinate.equals(closestSegment[0], segment[0]) &&
            ol.coordinate.equals(closestSegment[1], segment[1]) ||
            (ol.coordinate.equals(closestSegment[0], segment[1]) &&
            ol.coordinate.equals(closestSegment[1], segment[0])))) {
          vertexSegments[goog.getUid(segment)] = true;
        } else {
          break;
        }
      }
      this.vertexSegments_ = vertexSegments;
      return;
    }
  }
  if (this.vertexFeature_) {
    this.overlay_.getSource().removeFeature(this.vertexFeature_);
    this.vertexFeature_ = null;
  }
};


/**
 * @param {ol.interaction.SegmentDataType} segmentData Segment data.
 * @param {ol.Coordinate} vertex Vertex.
 * @private
 */
ol.interaction.Modify.prototype.insertVertex_ = function(segmentData, vertex) {
  var segment = segmentData.segment;
  var feature = segmentData.feature;
  var geometry = segmentData.geometry;
  var depth = segmentData.depth;
  var index = segmentData.index;
  var coordinates;

  while (vertex.length < geometry.getStride()) {
    vertex.push(0);
  }

  switch (geometry.getType()) {
    case ol.geom.GeometryType.MULTI_LINE_STRING:
      goog.asserts.assertInstanceof(geometry, ol.geom.MultiLineString,
          'geometry should be an ol.geom.MultiLineString');
      coordinates = geometry.getCoordinates();
      coordinates[depth[0]].splice(index + 1, 0, vertex);
      break;
    case ol.geom.GeometryType.POLYGON:
      goog.asserts.assertInstanceof(geometry, ol.geom.Polygon,
          'geometry should be an ol.geom.Polygon');
      coordinates = geometry.getCoordinates();
      coordinates[depth[0]].splice(index + 1, 0, vertex);
      break;
    case ol.geom.GeometryType.MULTI_POLYGON:
      goog.asserts.assertInstanceof(geometry, ol.geom.MultiPolygon,
          'geometry should be an ol.geom.MultiPolygon');
      coordinates = geometry.getCoordinates();
      coordinates[depth[1]][depth[0]].splice(index + 1, 0, vertex);
      break;
    case ol.geom.GeometryType.LINE_STRING:
      goog.asserts.assertInstanceof(geometry, ol.geom.LineString,
          'geometry should be an ol.geom.LineString');
      coordinates = geometry.getCoordinates();
      coordinates.splice(index + 1, 0, vertex);
      break;
    default:
      return;
  }

  this.setGeometryCoordinates_(geometry, coordinates);
  var rTree = this.rBush_;
  goog.asserts.assert(segment !== undefined, 'segment should be defined');
  rTree.remove(segmentData);
  goog.asserts.assert(index !== undefined, 'index should be defined');
  this.updateSegmentIndices_(geometry, index, depth, 1);
  var newSegmentData = /** @type {ol.interaction.SegmentDataType} */ ({
    segment: [segment[0], vertex],
    feature: feature,
    geometry: geometry,
    depth: depth,
    index: index
  });
  rTree.insert(ol.extent.boundingExtent(newSegmentData.segment),
      newSegmentData);
  this.dragSegments_.push([newSegmentData, 1]);

  var newSegmentData2 = /** @type {ol.interaction.SegmentDataType} */ ({
    segment: [vertex, segment[1]],
    feature: feature,
    geometry: geometry,
    depth: depth,
    index: index + 1
  });
  rTree.insert(ol.extent.boundingExtent(newSegmentData2.segment),
      newSegmentData2);
  this.dragSegments_.push([newSegmentData2, 0]);
  this.ignoreNextSingleClick_ = true;
};


/**
 * Removes a vertex from all matching features.
 * @return {boolean} True when a vertex was removed.
 * @private
 */
ol.interaction.Modify.prototype.removeVertex_ = function() {
  var dragSegments = this.dragSegments_;
  var segmentsByFeature = {};
  var component, coordinates, dragSegment, geometry, i, index, left;
  var newIndex, newSegment, right, segmentData, uid, deleted;
  for (i = dragSegments.length - 1; i >= 0; --i) {
    dragSegment = dragSegments[i];
    segmentData = dragSegment[0];
    geometry = segmentData.geometry;
    coordinates = geometry.getCoordinates();
    uid = goog.getUid(segmentData.feature);
    if (segmentData.depth) {
      // separate feature components
      uid += '-' + segmentData.depth.join('-');
    }
    left = right = index = undefined;
    if (dragSegment[1] === 0) {
      right = segmentData;
      index = segmentData.index;
    } else if (dragSegment[1] == 1) {
      left = segmentData;
      index = segmentData.index + 1;
    }
    if (!(uid in segmentsByFeature)) {
      segmentsByFeature[uid] = [left, right, index];
    }
    newSegment = segmentsByFeature[uid];
    if (left !== undefined) {
      newSegment[0] = left;
    }
    if (right !== undefined) {
      newSegment[1] = right;
    }
    if (newSegment[0] !== undefined && newSegment[1] !== undefined) {
      component = coordinates;
      deleted = false;
      newIndex = index - 1;
      switch (geometry.getType()) {
        case ol.geom.GeometryType.MULTI_LINE_STRING:
          coordinates[segmentData.depth[0]].splice(index, 1);
          deleted = true;
          break;
        case ol.geom.GeometryType.LINE_STRING:
          coordinates.splice(index, 1);
          deleted = true;
          break;
        case ol.geom.GeometryType.MULTI_POLYGON:
          component = component[segmentData.depth[1]];
          /* falls through */
        case ol.geom.GeometryType.POLYGON:
          component = component[segmentData.depth[0]];
          if (component.length > 4) {
            if (index == component.length - 1) {
              index = 0;
            }
            component.splice(index, 1);
            deleted = true;
            if (index === 0) {
              // close the ring again
              component.pop();
              component.push(component[0]);
              newIndex = component.length - 1;
            }
          }
          break;
        default:
          // pass
      }

      if (deleted) {
        this.rBush_.remove(newSegment[0]);
        this.rBush_.remove(newSegment[1]);
        this.setGeometryCoordinates_(geometry, coordinates);
        goog.asserts.assert(newIndex >= 0, 'newIndex should be larger than 0');
        var newSegmentData = /** @type {ol.interaction.SegmentDataType} */ ({
          depth: segmentData.depth,
          feature: segmentData.feature,
          geometry: segmentData.geometry,
          index: newIndex,
          segment: [newSegment[0].segment[0], newSegment[1].segment[1]]
        });
        this.rBush_.insert(ol.extent.boundingExtent(newSegmentData.segment),
            newSegmentData);
        this.updateSegmentIndices_(geometry, index, segmentData.depth, -1);

        if (this.vertexFeature_) {
          this.overlay_.getSource().removeFeature(this.vertexFeature_);
          this.vertexFeature_ = null;
        }
      }
    }
  }
  return true;
};


/**
 * @param {ol.geom.SimpleGeometry} geometry Geometry.
 * @param {Array} coordinates Coordinates.
 * @private
 */
ol.interaction.Modify.prototype.setGeometryCoordinates_ = function(geometry, coordinates) {
  this.changingFeature_ = true;
  geometry.setCoordinates(coordinates);
  this.changingFeature_ = false;
};


/**
 * @param {ol.geom.SimpleGeometry} geometry Geometry.
 * @param {number} index Index.
 * @param {Array.<number>|undefined} depth Depth.
 * @param {number} delta Delta (1 or -1).
 * @private
 */
ol.interaction.Modify.prototype.updateSegmentIndices_ = function(
    geometry, index, depth, delta) {
  this.rBush_.forEachInExtent(geometry.getExtent(), function(segmentDataMatch) {
    if (segmentDataMatch.geometry === geometry &&
        (depth === undefined || segmentDataMatch.depth === undefined ||
        goog.array.equals(
            /** @type {null|{length: number}} */ (segmentDataMatch.depth),
            depth)) &&
        segmentDataMatch.index > index) {
      segmentDataMatch.index += delta;
    }
  });
};


/**
 * @return {ol.style.StyleFunction} Styles.
 */
ol.interaction.Modify.getDefaultStyleFunction = function() {
  var style = ol.style.createDefaultEditingStyles();
  return function(feature, resolution) {
    return style[ol.geom.GeometryType.POINT];
  };
};

goog.provide('ol.interaction.Select');
goog.provide('ol.interaction.SelectEvent');
goog.provide('ol.interaction.SelectEventType');
goog.provide('ol.interaction.SelectFilterFunction');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.functions');
goog.require('goog.object');
goog.require('ol.CollectionEventType');
goog.require('ol.Feature');
goog.require('ol.array');
goog.require('ol.events.condition');
goog.require('ol.geom.GeometryType');
goog.require('ol.interaction.Interaction');
goog.require('ol.layer.Vector');
goog.require('ol.source.Vector');


/**
 * @enum {string}
 */
ol.interaction.SelectEventType = {
  /**
   * Triggered when feature(s) has been (de)selected.
   * @event ol.interaction.SelectEvent#select
   * @api
   */
  SELECT: 'select'
};


/**
 * A function that takes an {@link ol.Feature} or {@link ol.render.Feature} and
 * an {@link ol.layer.Layer} and returns `true` if the feature may be selected
 * or `false` otherwise.
 * @typedef {function((ol.Feature|ol.render.Feature), ol.layer.Layer):
 *     boolean}
 * @api
 */
ol.interaction.SelectFilterFunction;


/**
 * @classdesc
 * Events emitted by {@link ol.interaction.Select} instances are instances of
 * this type.
 *
 * @param {string} type The event type.
 * @param {Array.<ol.Feature>} selected Selected features.
 * @param {Array.<ol.Feature>} deselected Deselected features.
 * @param {ol.MapBrowserEvent} mapBrowserEvent Associated
 *     {@link ol.MapBrowserEvent}.
 * @implements {oli.SelectEvent}
 * @extends {goog.events.Event}
 * @constructor
 */
ol.interaction.SelectEvent = function(type, selected, deselected, mapBrowserEvent) {
  goog.base(this, type);

  /**
   * Selected features array.
   * @type {Array.<ol.Feature>}
   * @api
   */
  this.selected = selected;

  /**
   * Deselected features array.
   * @type {Array.<ol.Feature>}
   * @api
   */
  this.deselected = deselected;

  /**
   * Associated {@link ol.MapBrowserEvent}.
   * @type {ol.MapBrowserEvent}
   * @api
   */
  this.mapBrowserEvent = mapBrowserEvent;
};
goog.inherits(ol.interaction.SelectEvent, goog.events.Event);


/**
 * @classdesc
 * Interaction for selecting vector features. By default, selected features are
 * styled differently, so this interaction can be used for visual highlighting,
 * as well as selecting features for other actions, such as modification or
 * output. There are three ways of controlling which features are selected:
 * using the browser event as defined by the `condition` and optionally the
 * `toggle`, `add`/`remove`, and `multi` options; a `layers` filter; and a
 * further feature filter using the `filter` option.
 *
 * Selected features are added to an internal unmanaged layer.
 *
 * @constructor
 * @extends {ol.interaction.Interaction}
 * @param {olx.interaction.SelectOptions=} opt_options Options.
 * @fires ol.interaction.SelectEvent
 * @api stable
 */
ol.interaction.Select = function(opt_options) {

  goog.base(this, {
    handleEvent: ol.interaction.Select.handleEvent
  });

  var options = opt_options ? opt_options : {};

  /**
   * @private
   * @type {ol.events.ConditionType}
   */
  this.condition_ = options.condition ?
      options.condition : ol.events.condition.singleClick;

  /**
   * @private
   * @type {ol.events.ConditionType}
   */
  this.addCondition_ = options.addCondition ?
      options.addCondition : ol.events.condition.never;

  /**
   * @private
   * @type {ol.events.ConditionType}
   */
  this.removeCondition_ = options.removeCondition ?
      options.removeCondition : ol.events.condition.never;

  /**
   * @private
   * @type {ol.events.ConditionType}
   */
  this.toggleCondition_ = options.toggleCondition ?
      options.toggleCondition : ol.events.condition.shiftKeyOnly;

  /**
   * @private
   * @type {boolean}
   */
  this.multi_ = options.multi ? options.multi : false;

  /**
   * @private
   * @type {ol.interaction.SelectFilterFunction}
   */
  this.filter_ = options.filter ? options.filter :
      goog.functions.TRUE;

  var featureOverlay = new ol.layer.Vector({
    source: new ol.source.Vector({
      useSpatialIndex: false,
      features: options.features,
      wrapX: options.wrapX
    }),
    style: options.style ? options.style :
        ol.interaction.Select.getDefaultStyleFunction(),
    updateWhileAnimating: true,
    updateWhileInteracting: true
  });

  /**
   * @private
   * @type {ol.layer.Vector}
   */
  this.featureOverlay_ = featureOverlay;

  var layerFilter;
  if (options.layers) {
    if (goog.isFunction(options.layers)) {
      /**
       * @param {ol.layer.Layer} layer Layer.
       * @return {boolean} Include.
       */
      layerFilter = function(layer) {
        goog.asserts.assertFunction(options.layers);
        return layer === featureOverlay || options.layers(layer);
      };
    } else {
      var layers = options.layers;
      /**
       * @param {ol.layer.Layer} layer Layer.
       * @return {boolean} Include.
       */
      layerFilter = function(layer) {
        return layer === featureOverlay || ol.array.includes(layers, layer);
      };
    }
  } else {
    layerFilter = goog.functions.TRUE;
  }

  /**
   * @private
   * @type {function(ol.layer.Layer): boolean}
   */
  this.layerFilter_ = layerFilter;

  /**
   * An association between selected feature (key)
   * and layer (value)
   * @private
   * @type {Object.<number, ol.layer.Layer>}
   */
  this.featureLayerAssociation_ = {};

  var features = this.featureOverlay_.getSource().getFeaturesCollection();
  goog.events.listen(features, ol.CollectionEventType.ADD,
      this.addFeature_, false, this);
  goog.events.listen(features, ol.CollectionEventType.REMOVE,
      this.removeFeature_, false, this);

};
goog.inherits(ol.interaction.Select, ol.interaction.Interaction);


/**
 * @param {ol.Feature|ol.render.Feature} feature Feature.
 * @param {ol.layer.Layer} layer Layer.
 * @private
 */
ol.interaction.Select.prototype.addFeatureLayerAssociation_ = function(feature, layer) {
  var key = goog.getUid(feature);
  this.featureLayerAssociation_[key] = layer;
};


/**
 * Get the selected features.
 * @return {ol.Collection.<ol.Feature>} Features collection.
 * @api stable
 */
ol.interaction.Select.prototype.getFeatures = function() {
  return this.featureOverlay_.getSource().getFeaturesCollection();
};


/**
 * Returns the associated {@link ol.layer.Vector vectorlayer} of
 * the (last) selected feature. Note that this will not work with any
 * programmatic method like pushing features to
 * {@link ol.interaction.Select#getFeatures collection}.
 * @param {ol.Feature|ol.render.Feature} feature Feature
 * @return {ol.layer.Vector} Layer.
 * @api
 */
ol.interaction.Select.prototype.getLayer = function(feature) {
  goog.asserts.assertInstanceof(feature, ol.Feature,
      'feature should be an ol.Feature');
  var key = goog.getUid(feature);
  return /** @type {ol.layer.Vector} */ (this.featureLayerAssociation_[key]);
};


/**
 * Handles the {@link ol.MapBrowserEvent map browser event} and may change the
 * selected state of features.
 * @param {ol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boolean} `false` to stop event propagation.
 * @this {ol.interaction.Select}
 * @api
 */
ol.interaction.Select.handleEvent = function(mapBrowserEvent) {
  if (!this.condition_(mapBrowserEvent)) {
    return true;
  }
  var add = this.addCondition_(mapBrowserEvent);
  var remove = this.removeCondition_(mapBrowserEvent);
  var toggle = this.toggleCondition_(mapBrowserEvent);
  var set = !add && !remove && !toggle;
  var map = mapBrowserEvent.map;
  var features = this.featureOverlay_.getSource().getFeaturesCollection();
  var /** @type {!Array.<ol.Feature>} */ deselected = [];
  var /** @type {!Array.<ol.Feature>} */ selected = [];
  var change = false;
  if (set) {
    // Replace the currently selected feature(s) with the feature(s) at the
    // pixel, or clear the selected feature(s) if there is no feature at
    // the pixel.
    map.forEachFeatureAtPixel(mapBrowserEvent.pixel,
        /**
         * @param {ol.Feature|ol.render.Feature} feature Feature.
         * @param {ol.layer.Layer} layer Layer.
         */
        function(feature, layer) {
          if (this.filter_(feature, layer)) {
            selected.push(feature);
            this.addFeatureLayerAssociation_(feature, layer);
            return !this.multi_;
          }
        }, this, this.layerFilter_);
    if (selected.length > 0 && features.getLength() == 1 &&
        features.item(0) == selected[0]) {
      // No change
    } else {
      change = true;
      if (features.getLength() !== 0) {
        deselected = Array.prototype.concat(features.getArray());
        features.clear();
      }
      features.extend(selected);
      // Modify object this.featureLayerAssociation_
      if (selected.length === 0) {
        goog.object.clear(this.featureLayerAssociation_);
      } else {
        if (deselected.length > 0) {
          deselected.forEach(function(feature) {
            this.removeFeatureLayerAssociation_(feature);
          }, this);
        }
      }
    }
  } else {
    // Modify the currently selected feature(s).
    map.forEachFeatureAtPixel(mapBrowserEvent.pixel,
        /**
         * @param {ol.Feature|ol.render.Feature} feature Feature.
         * @param {ol.layer.Layer} layer Layer.
         */
        function(feature, layer) {
          goog.asserts.assertInstanceof(feature, ol.Feature);
          if (layer !== null) {
            if (add || toggle) {
              if (this.filter_(feature, layer) &&
                  !ol.array.includes(features.getArray(), feature) &&
                  !ol.array.includes(selected, feature)) {
                selected.push(feature);
                this.addFeatureLayerAssociation_(feature, layer);
              }
            }
          } else if (this.featureOverlay_.getSource().hasFeature(feature)) {
            if (remove || toggle) {
              deselected.push(feature);
              this.removeFeatureLayerAssociation_(feature);
            }
          }
        }, this, this.layerFilter_);
    var i;
    for (i = deselected.length - 1; i >= 0; --i) {
      features.remove(deselected[i]);
    }
    features.extend(selected);
    if (selected.length > 0 || deselected.length > 0) {
      change = true;
    }
  }
  if (change) {
    this.dispatchEvent(
        new ol.interaction.SelectEvent(ol.interaction.SelectEventType.SELECT,
            selected, deselected, mapBrowserEvent));
  }
  return ol.events.condition.pointerMove(mapBrowserEvent);
};


/**
 * Remove the interaction from its current map, if any,  and attach it to a new
 * map, if any. Pass `null` to just remove the interaction from the current map.
 * @param {ol.Map} map Map.
 * @api stable
 */
ol.interaction.Select.prototype.setMap = function(map) {
  var currentMap = this.getMap();
  var selectedFeatures =
      this.featureOverlay_.getSource().getFeaturesCollection();
  if (currentMap) {
    selectedFeatures.forEach(currentMap.unskipFeature, currentMap);
  }
  goog.base(this, 'setMap', map);
  this.featureOverlay_.setMap(map);
  if (map) {
    selectedFeatures.forEach(map.skipFeature, map);
  }
};


/**
 * @return {ol.style.StyleFunction} Styles.
 */
ol.interaction.Select.getDefaultStyleFunction = function() {
  var styles = ol.style.createDefaultEditingStyles();
  goog.array.extend(styles[ol.geom.GeometryType.POLYGON],
      styles[ol.geom.GeometryType.LINE_STRING]);
  goog.array.extend(styles[ol.geom.GeometryType.GEOMETRY_COLLECTION],
      styles[ol.geom.GeometryType.LINE_STRING]);

  return function(feature, resolution) {
    return styles[feature.getGeometry().getType()];
  };
};


/**
 * @param {ol.CollectionEvent} evt Event.
 * @private
 */
ol.interaction.Select.prototype.addFeature_ = function(evt) {
  var feature = evt.element;
  var map = this.getMap();
  goog.asserts.assertInstanceof(feature, ol.Feature,
      'feature should be an ol.Feature');
  if (map) {
    map.skipFeature(feature);
  }
};


/**
 * @param {ol.CollectionEvent} evt Event.
 * @private
 */
ol.interaction.Select.prototype.removeFeature_ = function(evt) {
  var feature = evt.element;
  var map = this.getMap();
  goog.asserts.assertInstanceof(feature, ol.Feature,
      'feature should be an ol.Feature');
  if (map) {
    map.unskipFeature(feature);
  }
};


/**
 * @param {ol.Feature|ol.render.Feature} feature Feature.
 * @private
 */
ol.interaction.Select.prototype.removeFeatureLayerAssociation_ = function(feature) {
  var key = goog.getUid(feature);
  delete this.featureLayerAssociation_[key];
};

goog.provide('ol.interaction.Snap');
goog.provide('ol.interaction.SnapProperty');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.object');
goog.require('ol');
goog.require('ol.Collection');
goog.require('ol.CollectionEvent');
goog.require('ol.CollectionEventType');
goog.require('ol.Extent');
goog.require('ol.Feature');
goog.require('ol.Object');
goog.require('ol.Observable');
goog.require('ol.coordinate');
goog.require('ol.extent');
goog.require('ol.geom.Geometry');
goog.require('ol.interaction.Pointer');
goog.require('ol.source.Vector');
goog.require('ol.source.VectorEvent');
goog.require('ol.source.VectorEventType');
goog.require('ol.structs.RBush');


/**
 * @classdesc
 * Handles snapping of vector features while modifying or drawing them.  The
 * features can come from a {@link ol.source.Vector} or {@link ol.Collection}
 * Any interaction object that allows the user to interact
 * with the features using the mouse can benefit from the snapping, as long
 * as it is added before.
 *
 * The snap interaction modifies map browser event `coordinate` and `pixel`
 * properties to force the snap to occur to any interaction that them.
 *
 * Example:
 *
 *     var snap = new ol.interaction.Snap({
 *       source: source
 *     });
 *
 * @constructor
 * @extends {ol.interaction.Pointer}
 * @param {olx.interaction.SnapOptions=} opt_options Options.
 * @api
 */
ol.interaction.Snap = function(opt_options) {

  goog.base(this, {
    handleEvent: ol.interaction.Snap.handleEvent_,
    handleDownEvent: goog.functions.TRUE,
    handleUpEvent: ol.interaction.Snap.handleUpEvent_
  });

  var options = opt_options ? opt_options : {};

  /**
   * @type {ol.source.Vector}
   * @private
   */
  this.source_ = options.source ? options.source : null;

  /**
   * @type {ol.Collection.<ol.Feature>}
   * @private
   */
  this.features_ = options.features ? options.features : null;

  /**
   * @type {Array.<goog.events.Key>}
   * @private
   */
  this.featuresListenerKeys_ = [];

  /**
   * @type {Object.<number, goog.events.Key>}
   * @private
   */
  this.geometryChangeListenerKeys_ = {};

  /**
   * @type {Object.<number, goog.events.Key>}
   * @private
   */
  this.geometryModifyListenerKeys_ = {};

  /**
   * Extents are preserved so indexed segment can be quickly removed
   * when its feature geometry changes
   * @type {Object.<number, ol.Extent>}
   * @private
   */
  this.indexedFeaturesExtents_ = {};

  /**
   * If a feature geometry changes while a pointer drag|move event occurs, the
   * feature doesn't get updated right away.  It will be at the next 'pointerup'
   * event fired.
   * @type {Object.<number, ol.Feature>}
   * @private
   */
  this.pendingFeatures_ = {};

  /**
   * Used for distance sorting in sortByDistance_
   * @type {ol.Coordinate}
   * @private
   */
  this.pixelCoordinate_ = null;

  /**
   * @type {number}
   * @private
   */
  this.pixelTolerance_ = options.pixelTolerance !== undefined ?
      options.pixelTolerance : 10;

  /**
   * @type {function(ol.interaction.Snap.SegmentDataType, ol.interaction.Snap.SegmentDataType): number}
   * @private
   */
  this.sortByDistance_ = ol.interaction.Snap.sortByDistance.bind(this);


  /**
  * Segment RTree for each layer
  * @type {ol.structs.RBush.<ol.interaction.Snap.SegmentDataType>}
  * @private
  */
  this.rBush_ = new ol.structs.RBush();


  /**
  * @const
  * @private
  * @type {Object.<string, function(ol.Feature, ol.geom.Geometry)>}
  */
  this.SEGMENT_WRITERS_ = {
    'Point': this.writePointGeometry_,
    'LineString': this.writeLineStringGeometry_,
    'LinearRing': this.writeLineStringGeometry_,
    'Polygon': this.writePolygonGeometry_,
    'MultiPoint': this.writeMultiPointGeometry_,
    'MultiLineString': this.writeMultiLineStringGeometry_,
    'MultiPolygon': this.writeMultiPolygonGeometry_,
    'GeometryCollection': this.writeGeometryCollectionGeometry_
  };
};
goog.inherits(ol.interaction.Snap, ol.interaction.Pointer);


/**
 * Add a feature to the collection of features that we may snap to.
 * @param {ol.Feature} feature Feature.
 * @param {boolean=} opt_listen Whether to listen to the geometry change or not
 *     Defaults to `true`.
 * @api
 */
ol.interaction.Snap.prototype.addFeature = function(feature, opt_listen) {
  var listen = opt_listen !== undefined ? opt_listen : true;
  var geometry = feature.getGeometry();
  var segmentWriter = this.SEGMENT_WRITERS_[geometry.getType()];
  if (segmentWriter) {
    var feature_uid = goog.getUid(feature);
    this.indexedFeaturesExtents_[feature_uid] = geometry.getExtent(
        ol.extent.createEmpty());
    segmentWriter.call(this, feature, geometry);

    if (listen) {
      this.geometryModifyListenerKeys_[feature_uid] = geometry.on(
          goog.events.EventType.CHANGE,
          this.handleGeometryModify_.bind(this, feature),
          this);
      this.geometryChangeListenerKeys_[feature_uid] = feature.on(
          ol.Object.getChangeEventType(feature.getGeometryName()),
          this.handleGeometryChange_, this);
    }
  }
};


/**
 * @param {ol.Feature} feature Feature.
 * @private
 */
ol.interaction.Snap.prototype.forEachFeatureAdd_ = function(feature) {
  this.addFeature(feature);
};


/**
 * @param {ol.Feature} feature Feature.
 * @private
 */
ol.interaction.Snap.prototype.forEachFeatureRemove_ = function(feature) {
  this.removeFeature(feature);
};


/**
 * @return {ol.Collection.<ol.Feature>|Array.<ol.Feature>} Features.
 * @private
 */
ol.interaction.Snap.prototype.getFeatures_ = function() {
  var features;
  if (this.features_) {
    features = this.features_;
  } else if (this.source_) {
    features = this.source_.getFeatures();
  }
  goog.asserts.assert(features !== undefined, 'features should be defined');
  return features;
};


/**
 * @param {ol.source.VectorEvent|ol.CollectionEvent} evt Event.
 * @private
 */
ol.interaction.Snap.prototype.handleFeatureAdd_ = function(evt) {
  var feature;
  if (evt instanceof ol.source.VectorEvent) {
    feature = evt.feature;
  } else if (evt instanceof ol.CollectionEvent) {
    feature = evt.element;
  }
  goog.asserts.assertInstanceof(feature, ol.Feature,
      'feature should be an ol.Feature');
  this.addFeature(feature);
};


/**
 * @param {ol.source.VectorEvent|ol.CollectionEvent} evt Event.
 * @private
 */
ol.interaction.Snap.prototype.handleFeatureRemove_ = function(evt) {
  var feature;
  if (evt instanceof ol.source.VectorEvent) {
    feature = evt.feature;
  } else if (evt instanceof ol.CollectionEvent) {
    feature = evt.element;
  }
  goog.asserts.assertInstanceof(feature, ol.Feature,
      'feature should be an ol.Feature');
  this.removeFeature(feature);
};


/**
 * @param {goog.events.Event} evt Event.
 * @private
 */
ol.interaction.Snap.prototype.handleGeometryChange_ = function(evt) {
  var feature = evt.currentTarget;
  goog.asserts.assertInstanceof(feature, ol.Feature);
  this.removeFeature(feature, true);
  this.addFeature(feature, true);
};


/**
 * @param {ol.Feature} feature Feature which geometry was modified.
 * @param {goog.events.Event} evt Event.
 * @private
 */
ol.interaction.Snap.prototype.handleGeometryModify_ = function(feature, evt) {
  if (this.handlingDownUpSequence) {
    var uid = goog.getUid(feature);
    if (!(uid in this.pendingFeatures_)) {
      this.pendingFeatures_[uid] = feature;
    }
  } else {
    this.updateFeature_(feature);
  }
};


/**
 * Remove a feature from the collection of features that we may snap to.
 * @param {ol.Feature} feature Feature
 * @param {boolean=} opt_unlisten Whether to unlisten to the geometry change
 *     or not. Defaults to `true`.
 * @api
 */
ol.interaction.Snap.prototype.removeFeature = function(feature, opt_unlisten) {
  var unlisten = opt_unlisten !== undefined ? opt_unlisten : true;
  var feature_uid = goog.getUid(feature);
  var extent = this.indexedFeaturesExtents_[feature_uid];
  if (extent) {
    var rBush = this.rBush_;
    var i, nodesToRemove = [];
    rBush.forEachInExtent(extent, function(node) {
      if (feature === node.feature) {
        nodesToRemove.push(node);
      }
    });
    for (i = nodesToRemove.length - 1; i >= 0; --i) {
      rBush.remove(nodesToRemove[i]);
    }
    if (unlisten) {
      ol.Observable.unByKey(this.geometryModifyListenerKeys_[feature_uid]);
      delete this.geometryModifyListenerKeys_[feature_uid];

      ol.Observable.unByKey(this.geometryChangeListenerKeys_[feature_uid]);
      delete this.geometryChangeListenerKeys_[feature_uid];
    }
  }
};


/**
 * @inheritDoc
 */
ol.interaction.Snap.prototype.setMap = function(map) {
  var currentMap = this.getMap();
  var keys = this.featuresListenerKeys_;
  var features = this.getFeatures_();

  if (currentMap) {
    keys.forEach(ol.Observable.unByKey);
    keys.length = 0;
    features.forEach(this.forEachFeatureRemove_, this);
  }

  goog.base(this, 'setMap', map);

  if (map) {
    if (this.features_) {
      keys.push(this.features_.on(ol.CollectionEventType.ADD,
          this.handleFeatureAdd_, this));
      keys.push(this.features_.on(ol.CollectionEventType.REMOVE,
          this.handleFeatureRemove_, this));
    } else if (this.source_) {
      keys.push(this.source_.on(ol.source.VectorEventType.ADDFEATURE,
          this.handleFeatureAdd_, this));
      keys.push(this.source_.on(ol.source.VectorEventType.REMOVEFEATURE,
          this.handleFeatureRemove_, this));
    }
    features.forEach(this.forEachFeatureAdd_, this);
  }
};


/**
 * @inheritDoc
 */
ol.interaction.Snap.prototype.shouldStopEvent = goog.functions.FALSE;


/**
 * @param {ol.Pixel} pixel Pixel
 * @param {ol.Coordinate} pixelCoordinate Coordinate
 * @param {ol.Map} map Map.
 * @return {ol.interaction.Snap.ResultType} Snap result
 */
ol.interaction.Snap.prototype.snapTo = function(pixel, pixelCoordinate, map) {

  var lowerLeft = map.getCoordinateFromPixel(
      [pixel[0] - this.pixelTolerance_, pixel[1] + this.pixelTolerance_]);
  var upperRight = map.getCoordinateFromPixel(
      [pixel[0] + this.pixelTolerance_, pixel[1] - this.pixelTolerance_]);
  var box = ol.extent.boundingExtent([lowerLeft, upperRight]);

  var segments = this.rBush_.getInExtent(box);
  var snappedToVertex = false;
  var snapped = false;
  var vertex = null;
  var vertexPixel = null;
  if (segments.length > 0) {
    this.pixelCoordinate_ = pixelCoordinate;
    segments.sort(this.sortByDistance_);
    var closestSegment = segments[0].segment;
    vertex = (ol.coordinate.closestOnSegment(pixelCoordinate,
        closestSegment));
    vertexPixel = map.getPixelFromCoordinate(vertex);
    if (Math.sqrt(ol.coordinate.squaredDistance(pixel, vertexPixel)) <=
        this.pixelTolerance_) {
      snapped = true;
      var pixel1 = map.getPixelFromCoordinate(closestSegment[0]);
      var pixel2 = map.getPixelFromCoordinate(closestSegment[1]);
      var squaredDist1 = ol.coordinate.squaredDistance(vertexPixel, pixel1);
      var squaredDist2 = ol.coordinate.squaredDistance(vertexPixel, pixel2);
      var dist = Math.sqrt(Math.min(squaredDist1, squaredDist2));
      snappedToVertex = dist <= this.pixelTolerance_;
      if (snappedToVertex) {
        vertex = squaredDist1 > squaredDist2 ?
            closestSegment[1] : closestSegment[0];
        vertexPixel = map.getPixelFromCoordinate(vertex);
        vertexPixel = [Math.round(vertexPixel[0]), Math.round(vertexPixel[1])];
      }
    }
  }
  return /** @type {ol.interaction.Snap.ResultType} */ ({
    snapped: snapped,
    vertex: vertex,
    vertexPixel: vertexPixel
  });
};


/**
 * @param {ol.Feature} feature Feature
 * @private
 */
ol.interaction.Snap.prototype.updateFeature_ = function(feature) {
  this.removeFeature(feature, false);
  this.addFeature(feature, false);
};


/**
 * @param {ol.Feature} feature Feature
 * @param {ol.geom.GeometryCollection} geometry Geometry.
 * @private
 */
ol.interaction.Snap.prototype.writeGeometryCollectionGeometry_ = function(feature, geometry) {
  var i, geometries = geometry.getGeometriesArray();
  for (i = 0; i < geometries.length; ++i) {
    this.SEGMENT_WRITERS_[geometries[i].getType()].call(
        this, feature, geometries[i]);
  }
};


/**
 * @param {ol.Feature} feature Feature
 * @param {ol.geom.LineString} geometry Geometry.
 * @private
 */
ol.interaction.Snap.prototype.writeLineStringGeometry_ = function(feature, geometry) {
  var coordinates = geometry.getCoordinates();
  var i, ii, segment, segmentData;
  for (i = 0, ii = coordinates.length - 1; i < ii; ++i) {
    segment = coordinates.slice(i, i + 2);
    segmentData = /** @type {ol.interaction.Snap.SegmentDataType} */ ({
      feature: feature,
      segment: segment
    });
    this.rBush_.insert(ol.extent.boundingExtent(segment), segmentData);
  }
};


/**
 * @param {ol.Feature} feature Feature
 * @param {ol.geom.MultiLineString} geometry Geometry.
 * @private
 */
ol.interaction.Snap.prototype.writeMultiLineStringGeometry_ = function(feature, geometry) {
  var lines = geometry.getCoordinates();
  var coordinates, i, ii, j, jj, segment, segmentData;
  for (j = 0, jj = lines.length; j < jj; ++j) {
    coordinates = lines[j];
    for (i = 0, ii = coordinates.length - 1; i < ii; ++i) {
      segment = coordinates.slice(i, i + 2);
      segmentData = /** @type {ol.interaction.Snap.SegmentDataType} */ ({
        feature: feature,
        segment: segment
      });
      this.rBush_.insert(ol.extent.boundingExtent(segment), segmentData);
    }
  }
};


/**
 * @param {ol.Feature} feature Feature
 * @param {ol.geom.MultiPoint} geometry Geometry.
 * @private
 */
ol.interaction.Snap.prototype.writeMultiPointGeometry_ = function(feature, geometry) {
  var points = geometry.getCoordinates();
  var coordinates, i, ii, segmentData;
  for (i = 0, ii = points.length; i < ii; ++i) {
    coordinates = points[i];
    segmentData = /** @type {ol.interaction.Snap.SegmentDataType} */ ({
      feature: feature,
      segment: [coordinates, coordinates]
    });
    this.rBush_.insert(geometry.getExtent(), segmentData);
  }
};


/**
 * @param {ol.Feature} feature Feature
 * @param {ol.geom.MultiPolygon} geometry Geometry.
 * @private
 */
ol.interaction.Snap.prototype.writeMultiPolygonGeometry_ = function(feature, geometry) {
  var polygons = geometry.getCoordinates();
  var coordinates, i, ii, j, jj, k, kk, rings, segment, segmentData;
  for (k = 0, kk = polygons.length; k < kk; ++k) {
    rings = polygons[k];
    for (j = 0, jj = rings.length; j < jj; ++j) {
      coordinates = rings[j];
      for (i = 0, ii = coordinates.length - 1; i < ii; ++i) {
        segment = coordinates.slice(i, i + 2);
        segmentData = /** @type {ol.interaction.Snap.SegmentDataType} */ ({
          feature: feature,
          segment: segment
        });
        this.rBush_.insert(ol.extent.boundingExtent(segment), segmentData);
      }
    }
  }
};


/**
 * @param {ol.Feature} feature Feature
 * @param {ol.geom.Point} geometry Geometry.
 * @private
 */
ol.interaction.Snap.prototype.writePointGeometry_ = function(feature, geometry) {
  var coordinates = geometry.getCoordinates();
  var segmentData = /** @type {ol.interaction.Snap.SegmentDataType} */ ({
    feature: feature,
    segment: [coordinates, coordinates]
  });
  this.rBush_.insert(geometry.getExtent(), segmentData);
};


/**
 * @param {ol.Feature} feature Feature
 * @param {ol.geom.Polygon} geometry Geometry.
 * @private
 */
ol.interaction.Snap.prototype.writePolygonGeometry_ = function(feature, geometry) {
  var rings = geometry.getCoordinates();
  var coordinates, i, ii, j, jj, segment, segmentData;
  for (j = 0, jj = rings.length; j < jj; ++j) {
    coordinates = rings[j];
    for (i = 0, ii = coordinates.length - 1; i < ii; ++i) {
      segment = coordinates.slice(i, i + 2);
      segmentData = /** @type {ol.interaction.Snap.SegmentDataType} */ ({
        feature: feature,
        segment: segment
      });
      this.rBush_.insert(ol.extent.boundingExtent(segment), segmentData);
    }
  }
};


/**
 * @typedef {{
 *     snapped: {boolean},
 *     vertex: (ol.Coordinate|null),
 *     vertexPixel: (ol.Pixel|null)
 * }}
 */
ol.interaction.Snap.ResultType;


/**
 * @typedef {{
 *     feature: ol.Feature,
 *     segment: Array.<ol.Coordinate>
 * }}
 */
ol.interaction.Snap.SegmentDataType;


/**
 * Handle all pointer events events.
 * @param {ol.MapBrowserEvent} evt A move event.
 * @return {boolean} Pass the event to other interactions.
 * @this {ol.interaction.Snap}
 * @private
 */
ol.interaction.Snap.handleEvent_ = function(evt) {
  var result = this.snapTo(evt.pixel, evt.coordinate, evt.map);
  if (result.snapped) {
    evt.coordinate = result.vertex.slice(0, 2);
    evt.pixel = result.vertexPixel;
  }
  return ol.interaction.Pointer.handleEvent.call(this, evt);
};


/**
 * @param {ol.MapBrowserPointerEvent} evt Event.
 * @return {boolean} Stop drag sequence?
 * @this {ol.interaction.Snap}
 * @private
 */
ol.interaction.Snap.handleUpEvent_ = function(evt) {
  var featuresToUpdate = goog.object.getValues(this.pendingFeatures_);
  if (featuresToUpdate.length) {
    featuresToUpdate.forEach(this.updateFeature_, this);
    this.pendingFeatures_ = {};
  }
  return false;
};


/**
 * Sort segments by distance, helper function
 * @param {ol.interaction.Snap.SegmentDataType} a The first segment data.
 * @param {ol.interaction.Snap.SegmentDataType} b The second segment data.
 * @return {number} The difference in distance.
 * @this {ol.interaction.Snap}
 */
ol.interaction.Snap.sortByDistance = function(a, b) {
  return ol.coordinate.squaredDistanceToSegment(
      this.pixelCoordinate_, a.segment) -
      ol.coordinate.squaredDistanceToSegment(
      this.pixelCoordinate_, b.segment);
};

goog.provide('ol.interaction.Translate');
goog.provide('ol.interaction.TranslateEvent');

goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('ol.array');
goog.require('ol.interaction.Pointer');


/**
 * @enum {string}
 */
ol.interaction.TranslateEventType = {
  /**
   * Triggered upon feature translation start.
   * @event ol.interaction.TranslateEvent#translatestart
   * @api
   */
  TRANSLATESTART: 'translatestart',
  /**
   * Triggered upon feature translation.
   * @event ol.interaction.TranslateEvent#translating
   * @api
   */
  TRANSLATING: 'translating',
  /**
   * Triggered upon feature translation end.
   * @event ol.interaction.TranslateEvent#translateend
   * @api
   */
  TRANSLATEEND: 'translateend'
};


/**
 * @classdesc
 * Events emitted by {@link ol.interaction.Translate} instances are instances of
 * this type.
 *
 * @constructor
 * @extends {goog.events.Event}
 * @implements {oli.interaction.TranslateEvent}
 * @param {ol.interaction.TranslateEventType} type Type.
 * @param {ol.Collection.<ol.Feature>} features The features translated.
 * @param {ol.Coordinate} coordinate The event coordinate.
 */
ol.interaction.TranslateEvent = function(type, features, coordinate) {

  goog.base(this, type);

  /**
   * The features being translated.
   * @type {ol.Collection.<ol.Feature>}
   * @api
   */
  this.features = features;

  /**
   * The coordinate of the drag event.
   * @const
   * @type {ol.Coordinate}
   * @api
   */
  this.coordinate = coordinate;
};
goog.inherits(ol.interaction.TranslateEvent, goog.events.Event);


/**
 * @classdesc
 * Interaction for translating (moving) features.
 *
 * @constructor
 * @extends {ol.interaction.Pointer}
 * @fires ol.interaction.TranslateEvent
 * @param {olx.interaction.TranslateOptions} options Options.
 * @api
 */
ol.interaction.Translate = function(options) {
  goog.base(this, {
    handleDownEvent: ol.interaction.Translate.handleDownEvent_,
    handleDragEvent: ol.interaction.Translate.handleDragEvent_,
    handleMoveEvent: ol.interaction.Translate.handleMoveEvent_,
    handleUpEvent: ol.interaction.Translate.handleUpEvent_
  });


  /**
   * @type {string|undefined}
   * @private
   */
  this.previousCursor_ = undefined;


  /**
   * The last position we translated to.
   * @type {ol.Coordinate}
   * @private
   */
  this.lastCoordinate_ = null;


  /**
   * @type {ol.Collection.<ol.Feature>}
   * @private
   */
  this.features_ = options.features !== undefined ? options.features : null;

  /**
   * @type {ol.Feature}
   * @private
   */
  this.lastFeature_ = null;
};
goog.inherits(ol.interaction.Translate, ol.interaction.Pointer);


/**
 * @param {ol.MapBrowserPointerEvent} event Event.
 * @return {boolean} Start drag sequence?
 * @this {ol.interaction.Translate}
 * @private
 */
ol.interaction.Translate.handleDownEvent_ = function(event) {
  this.lastFeature_ = this.featuresAtPixel_(event.pixel, event.map);
  if (!this.lastCoordinate_ && this.lastFeature_) {
    this.lastCoordinate_ = event.coordinate;
    ol.interaction.Translate.handleMoveEvent_.call(this, event);
    this.dispatchEvent(
        new ol.interaction.TranslateEvent(
            ol.interaction.TranslateEventType.TRANSLATESTART, this.features_,
            event.coordinate));
    return true;
  }
  return false;
};


/**
 * @param {ol.MapBrowserPointerEvent} event Event.
 * @return {boolean} Stop drag sequence?
 * @this {ol.interaction.Translate}
 * @private
 */
ol.interaction.Translate.handleUpEvent_ = function(event) {
  if (this.lastCoordinate_) {
    this.lastCoordinate_ = null;
    ol.interaction.Translate.handleMoveEvent_.call(this, event);
    this.dispatchEvent(
        new ol.interaction.TranslateEvent(
            ol.interaction.TranslateEventType.TRANSLATEEND, this.features_,
            event.coordinate));
    return true;
  }
  return false;
};


/**
 * @param {ol.MapBrowserPointerEvent} event Event.
 * @this {ol.interaction.Translate}
 * @private
 */
ol.interaction.Translate.handleDragEvent_ = function(event) {
  if (this.lastCoordinate_) {
    var newCoordinate = event.coordinate;
    var deltaX = newCoordinate[0] - this.lastCoordinate_[0];
    var deltaY = newCoordinate[1] - this.lastCoordinate_[1];

    if (this.features_) {
      this.features_.forEach(function(feature) {
        var geom = feature.getGeometry();
        geom.translate(deltaX, deltaY);
        feature.setGeometry(geom);
      });
    } else if (this.lastFeature_) {
      var geom = this.lastFeature_.getGeometry();
      geom.translate(deltaX, deltaY);
      this.lastFeature_.setGeometry(geom);
    }

    this.lastCoordinate_ = newCoordinate;
    this.dispatchEvent(
        new ol.interaction.TranslateEvent(
            ol.interaction.TranslateEventType.TRANSLATING, this.features_,
            newCoordinate));
  }
};


/**
 * @param {ol.MapBrowserEvent} event Event.
 * @this {ol.interaction.Translate}
 * @private
 */
ol.interaction.Translate.handleMoveEvent_ = function(event) {
  var elem = event.map.getTargetElement();
  var intersectingFeature = event.map.forEachFeatureAtPixel(event.pixel,
      function(feature) {
        return feature;
      });

  if (intersectingFeature) {
    var isSelected = false;

    if (this.features_ &&
        ol.array.includes(this.features_.getArray(), intersectingFeature)) {
      isSelected = true;
    }

    this.previousCursor_ = elem.style.cursor;

    // WebKit browsers don't support the grab icons without a prefix
    elem.style.cursor = this.lastCoordinate_ ?
        '-webkit-grabbing' : (isSelected ? '-webkit-grab' : 'pointer');

    // Thankfully, attempting to set the standard ones will silently fail,
    // keeping the prefixed icons
    elem.style.cursor = !this.lastCoordinate_ ?
        'grabbing' : (isSelected ? 'grab' : 'pointer');

  } else {
    elem.style.cursor = this.previousCursor_ !== undefined ?
        this.previousCursor_ : '';
    this.previousCursor_ = undefined;
  }
};


/**
 * Tests to see if the given coordinates intersects any of our selected
 * features.
 * @param {ol.Pixel} pixel Pixel coordinate to test for intersection.
 * @param {ol.Map} map Map to test the intersection on.
 * @return {ol.Feature} Returns the feature found at the specified pixel
 * coordinates.
 * @private
 */
ol.interaction.Translate.prototype.featuresAtPixel_ = function(pixel, map) {
  var found = null;

  var intersectingFeature = map.forEachFeatureAtPixel(pixel,
      function(feature) {
        return feature;
      });

  if (this.features_ &&
      ol.array.includes(this.features_.getArray(), intersectingFeature)) {
    found = intersectingFeature;
  }

  return found;
};

goog.provide('ol.layer.Heatmap');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.object');
goog.require('ol');
goog.require('ol.Object');
goog.require('ol.dom');
goog.require('ol.layer.Vector');
goog.require('ol.math');
goog.require('ol.render.EventType');
goog.require('ol.style.Icon');
goog.require('ol.style.Style');


/**
 * @enum {string}
 */
ol.layer.HeatmapLayerProperty = {
  BLUR: 'blur',
  GRADIENT: 'gradient',
  RADIUS: 'radius'
};


/**
 * @classdesc
 * Layer for rendering vector data as a heatmap.
 * Note that any property set in the options is set as a {@link ol.Object}
 * property on the layer object; for example, setting `title: 'My Title'` in the
 * options means that `title` is observable, and has get/set accessors.
 *
 * @constructor
 * @extends {ol.layer.Vector}
 * @fires ol.render.Event
 * @param {olx.layer.HeatmapOptions=} opt_options Options.
 * @api
 */
ol.layer.Heatmap = function(opt_options) {
  var options = opt_options ? opt_options : {};

  var baseOptions = goog.object.clone(options);

  delete baseOptions.gradient;
  delete baseOptions.radius;
  delete baseOptions.blur;
  delete baseOptions.shadow;
  delete baseOptions.weight;
  goog.base(this, /** @type {olx.layer.VectorOptions} */ (baseOptions));

  /**
   * @private
   * @type {Uint8ClampedArray}
   */
  this.gradient_ = null;

  /**
   * @private
   * @type {number}
   */
  this.shadow_ = options.shadow !== undefined ? options.shadow : 250;

  /**
   * @private
   * @type {string|undefined}
   */
  this.circleImage_ = undefined;

  /**
   * @private
   * @type {Array.<Array.<ol.style.Style>>}
   */
  this.styleCache_ = null;

  goog.events.listen(this,
      ol.Object.getChangeEventType(ol.layer.HeatmapLayerProperty.GRADIENT),
      this.handleGradientChanged_, false, this);

  this.setGradient(options.gradient ?
      options.gradient : ol.layer.Heatmap.DEFAULT_GRADIENT);

  this.setBlur(options.blur !== undefined ? options.blur : 15);

  this.setRadius(options.radius !== undefined ? options.radius : 8);

  goog.events.listen(this, [
    ol.Object.getChangeEventType(ol.layer.HeatmapLayerProperty.BLUR),
    ol.Object.getChangeEventType(ol.layer.HeatmapLayerProperty.RADIUS)
  ], this.handleStyleChanged_, false, this);

  this.handleStyleChanged_();

  var weight = options.weight ? options.weight : 'weight';
  var weightFunction;
  if (goog.isString(weight)) {
    weightFunction = function(feature) {
      return feature.get(weight);
    };
  } else {
    weightFunction = weight;
  }
  goog.asserts.assert(goog.isFunction(weightFunction),
      'weightFunction should be a function');

  this.setStyle(function(feature, resolution) {
    goog.asserts.assert(this.styleCache_, 'this.styleCache_ expected');
    goog.asserts.assert(this.circleImage_ !== undefined,
        'this.circleImage_ should be defined');
    var weight = weightFunction(feature);
    var opacity = weight !== undefined ? ol.math.clamp(weight, 0, 1) : 1;
    // cast to 8 bits
    var index = (255 * opacity) | 0;
    var style = this.styleCache_[index];
    if (!style) {
      style = [
        new ol.style.Style({
          image: new ol.style.Icon({
            opacity: opacity,
            src: this.circleImage_
          })
        })
      ];
      this.styleCache_[index] = style;
    }
    return style;
  }.bind(this));

  // For performance reasons, don't sort the features before rendering.
  // The render order is not relevant for a heatmap representation.
  this.setRenderOrder(null);

  goog.events.listen(this, ol.render.EventType.RENDER,
      this.handleRender_, false, this);

};
goog.inherits(ol.layer.Heatmap, ol.layer.Vector);


/**
 * @const
 * @type {Array.<string>}
 */
ol.layer.Heatmap.DEFAULT_GRADIENT = ['#00f', '#0ff', '#0f0', '#ff0', '#f00'];


/**
 * @param {Array.<string>} colors A list of colored.
 * @return {Uint8ClampedArray} An array.
 * @private
 */
ol.layer.Heatmap.createGradient_ = function(colors) {
  var width = 1;
  var height = 256;
  var context = ol.dom.createCanvasContext2D(width, height);

  var gradient = context.createLinearGradient(0, 0, width, height);
  var step = 1 / (colors.length - 1);
  for (var i = 0, ii = colors.length; i < ii; ++i) {
    gradient.addColorStop(i * step, colors[i]);
  }

  context.fillStyle = gradient;
  context.fillRect(0, 0, width, height);

  return context.getImageData(0, 0, width, height).data;
};


/**
 * @return {string} Data URL for a circle.
 * @private
 */
ol.layer.Heatmap.prototype.createCircle_ = function() {
  var radius = this.getRadius();
  var blur = this.getBlur();
  goog.asserts.assert(radius !== undefined && blur !== undefined,
      'radius and blur should be defined');
  var halfSize = radius + blur + 1;
  var size = 2 * halfSize;
  var context = ol.dom.createCanvasContext2D(size, size);
  context.shadowOffsetX = context.shadowOffsetY = this.shadow_;
  context.shadowBlur = blur;
  context.shadowColor = '#000';
  context.beginPath();
  var center = halfSize - this.shadow_;
  context.arc(center, center, radius, 0, Math.PI * 2, true);
  context.fill();
  return context.canvas.toDataURL();
};


/**
 * Return the blur size in pixels.
 * @return {number} Blur size in pixels.
 * @api
 * @observable
 */
ol.layer.Heatmap.prototype.getBlur = function() {
  return /** @type {number} */ (this.get(ol.layer.HeatmapLayerProperty.BLUR));
};


/**
 * Return the gradient colors as array of strings.
 * @return {Array.<string>} Colors.
 * @api
 * @observable
 */
ol.layer.Heatmap.prototype.getGradient = function() {
  return /** @type {Array.<string>} */ (
      this.get(ol.layer.HeatmapLayerProperty.GRADIENT));
};


/**
 * Return the size of the radius in pixels.
 * @return {number} Radius size in pixel.
 * @api
 * @observable
 */
ol.layer.Heatmap.prototype.getRadius = function() {
  return /** @type {number} */ (this.get(ol.layer.HeatmapLayerProperty.RADIUS));
};


/**
 * @private
 */
ol.layer.Heatmap.prototype.handleGradientChanged_ = function() {
  this.gradient_ = ol.layer.Heatmap.createGradient_(this.getGradient());
};


/**
 * @private
 */
ol.layer.Heatmap.prototype.handleStyleChanged_ = function() {
  this.circleImage_ = this.createCircle_();
  this.styleCache_ = new Array(256);
  this.changed();
};


/**
 * @param {ol.render.Event} event Post compose event
 * @private
 */
ol.layer.Heatmap.prototype.handleRender_ = function(event) {
  goog.asserts.assert(event.type == ol.render.EventType.RENDER,
      'event.type should be RENDER');
  goog.asserts.assert(this.gradient_, 'this.gradient_ expected');
  var context = event.context;
  var canvas = context.canvas;
  var image = context.getImageData(0, 0, canvas.width, canvas.height);
  var view8 = image.data;
  var i, ii, alpha;
  for (i = 0, ii = view8.length; i < ii; i += 4) {
    alpha = view8[i + 3] * 4;
    if (alpha) {
      view8[i] = this.gradient_[alpha];
      view8[i + 1] = this.gradient_[alpha + 1];
      view8[i + 2] = this.gradient_[alpha + 2];
    }
  }
  context.putImageData(image, 0, 0);
};


/**
 * Set the blur size in pixels.
 * @param {number} blur Blur size in pixels.
 * @api
 * @observable
 */
ol.layer.Heatmap.prototype.setBlur = function(blur) {
  this.set(ol.layer.HeatmapLayerProperty.BLUR, blur);
};


/**
 * Set the gradient colors as array of strings.
 * @param {Array.<string>} colors Gradient.
 * @api
 * @observable
 */
ol.layer.Heatmap.prototype.setGradient = function(colors) {
  this.set(ol.layer.HeatmapLayerProperty.GRADIENT, colors);
};


/**
 * Set the size of the radius in pixels.
 * @param {number} radius Radius size in pixel.
 * @api
 * @observable
 */
ol.layer.Heatmap.prototype.setRadius = function(radius) {
  this.set(ol.layer.HeatmapLayerProperty.RADIUS, radius);
};

goog.provide('ol.raster.Operation');
goog.provide('ol.raster.OperationType');


/**
 * Raster operation type. Supported values are `'pixel'` and `'image'`.
 * @enum {string}
 * @api
 */
ol.raster.OperationType = {
  PIXEL: 'pixel',
  IMAGE: 'image'
};


/**
 * A function that takes an array of input data, performs some operation, and
 * returns an array of ouput data.  For `'pixel'` type operations, functions
 * will be called with an array of {@link ol.raster.Pixel} data and should
 * return an array of the same.  For `'image'` type operations, functions will
 * be called with an array of {@link ImageData
 * https://developer.mozilla.org/en-US/docs/Web/API/ImageData} and should return
 * an array of the same.  The operations are called with a second "data"
 * argument, which can be used for storage.  The data object is accessible
 * from raster events, where it can be initialized in "beforeoperations" and
 * accessed again in "afteroperations".
 *
 * @typedef {function((Array.<ol.raster.Pixel>|Array.<ImageData>), Object):
 *     (Array.<ol.raster.Pixel>|Array.<ImageData>)}
 * @api
 */
ol.raster.Operation;

goog.provide('ol.raster.Pixel');


/**
 * An array of numbers representing pixel values.
 * @typedef {Array.<number>} ol.raster.Pixel
 * @api
 */
ol.raster.Pixel;

goog.provide('ol.render');

goog.require('goog.vec.Mat4');
goog.require('ol.render.canvas.Immediate');
goog.require('ol.vec.Mat4');


/**
 * Binds a Canvas Immediate API to a canvas context, to allow drawing geometries
 * to the context's canvas.
 *
 * The units for geometry coordinates are css pixels relative to the top left
 * corner of the canvas element.
 * ```js
 * var canvas = document.createElement('canvas');
 * var render = ol.render.toContext(canvas.getContext('2d'),
 *     { size: [100, 100] });
 * render.setFillStrokeStyle(new ol.style.Fill({ color: blue }));
 * render.drawPolygonGeometry(
 *     new ol.geom.Polygon([[[0, 0], [100, 100], [100, 0], [0, 0]]]));
 * ```
 *
 * Note that {@link ol.render.canvas.Immediate#drawAsync} and
 * {@link ol.render.canvas.Immediate#drawFeature} cannot be used.
 *
 * @param {CanvasRenderingContext2D} context Canvas context.
 * @param {olx.render.ToContextOptions=} opt_options Options.
 * @return {ol.render.canvas.Immediate} Canvas Immediate.
 * @api
 */
ol.render.toContext = function(context, opt_options) {
  var canvas = context.canvas;
  var options = opt_options ? opt_options : {};
  var pixelRatio = options.pixelRatio || ol.has.DEVICE_PIXEL_RATIO;
  var size = options.size;
  if (size) {
    canvas.width = size[0] * pixelRatio;
    canvas.height = size[1] * pixelRatio;
    canvas.style.width = size[0] + 'px';
    canvas.style.height = size[1] + 'px';
  }
  var extent = [0, 0, canvas.width, canvas.height];
  var transform = ol.vec.Mat4.makeTransform2D(goog.vec.Mat4.createNumber(),
      0, 0, pixelRatio, pixelRatio, 0, 0, 0);
  return new ol.render.canvas.Immediate(context, pixelRatio, extent, transform,
      0);
};

goog.provide('ol.reproj.Tile');
goog.provide('ol.reproj.TileFunctionType');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.math');
goog.require('goog.object');
goog.require('ol.Tile');
goog.require('ol.TileState');
goog.require('ol.extent');
goog.require('ol.math');
goog.require('ol.proj');
goog.require('ol.reproj');
goog.require('ol.reproj.Triangulation');


/**
 * @typedef {function(number, number, number, number) : ol.Tile}
 */
ol.reproj.TileFunctionType;


/**
 * @classdesc
 * Class encapsulating single reprojected tile.
 * See {@link ol.source.TileImage}.
 *
 * @constructor
 * @extends {ol.Tile}
 * @param {ol.proj.Projection} sourceProj Source projection.
 * @param {ol.tilegrid.TileGrid} sourceTileGrid Source tile grid.
 * @param {ol.proj.Projection} targetProj Target projection.
 * @param {ol.tilegrid.TileGrid} targetTileGrid Target tile grid.
 * @param {ol.TileCoord} tileCoord Coordinate of the tile.
 * @param {ol.TileCoord} wrappedTileCoord Coordinate of the tile wrapped in X.
 * @param {number} pixelRatio Pixel ratio.
 * @param {ol.reproj.TileFunctionType} getTileFunction
 *     Function returning source tiles (z, x, y, pixelRatio).
 * @param {number=} opt_errorThreshold Acceptable reprojection error (in px).
 * @param {boolean=} opt_renderEdges Render reprojection edges.
 */
ol.reproj.Tile = function(sourceProj, sourceTileGrid,
    targetProj, targetTileGrid, tileCoord, wrappedTileCoord,
    pixelRatio, getTileFunction,
    opt_errorThreshold,
    opt_renderEdges) {
  goog.base(this, tileCoord, ol.TileState.IDLE);

  /**
   * @private
   * @type {boolean}
   */
  this.renderEdges_ = opt_renderEdges !== undefined ? opt_renderEdges : false;

  /**
   * @private
   * @type {number}
   */
  this.pixelRatio_ = pixelRatio;

  /**
   * @private
   * @type {HTMLCanvasElement}
   */
  this.canvas_ = null;

  /**
   * @private
   * @type {Object.<number, HTMLCanvasElement>}
   */
  this.canvasByContext_ = {};

  /**
   * @private
   * @type {ol.tilegrid.TileGrid}
   */
  this.sourceTileGrid_ = sourceTileGrid;

  /**
   * @private
   * @type {ol.tilegrid.TileGrid}
   */
  this.targetTileGrid_ = targetTileGrid;

  /**
   * @private
   * @type {ol.TileCoord}
   */
  this.wrappedTileCoord_ = wrappedTileCoord ? wrappedTileCoord : tileCoord;

  /**
   * @private
   * @type {!Array.<ol.Tile>}
   */
  this.sourceTiles_ = [];

  /**
   * @private
   * @type {Array.<goog.events.Key>}
   */
  this.sourcesListenerKeys_ = null;

  /**
   * @private
   * @type {number}
   */
  this.sourceZ_ = 0;

  var targetExtent = targetTileGrid.getTileCoordExtent(this.wrappedTileCoord_);
  var maxTargetExtent = this.targetTileGrid_.getExtent();
  var maxSourceExtent = this.sourceTileGrid_.getExtent();

  var limitedTargetExtent = maxTargetExtent ?
      ol.extent.getIntersection(targetExtent, maxTargetExtent) : targetExtent;

  if (ol.extent.getArea(limitedTargetExtent) === 0) {
    // Tile is completely outside range -> EMPTY
    // TODO: is it actually correct that the source even creates the tile ?
    this.state = ol.TileState.EMPTY;
    return;
  }

  var sourceProjExtent = sourceProj.getExtent();
  if (sourceProjExtent) {
    if (!maxSourceExtent) {
      maxSourceExtent = sourceProjExtent;
    } else {
      maxSourceExtent = ol.extent.getIntersection(
          maxSourceExtent, sourceProjExtent);
    }
  }

  var targetResolution = targetTileGrid.getResolution(
      this.wrappedTileCoord_[0]);

  var targetCenter = ol.extent.getCenter(limitedTargetExtent);
  var sourceResolution = ol.reproj.calculateSourceResolution(
      sourceProj, targetProj, targetCenter, targetResolution);

  if (!goog.math.isFiniteNumber(sourceResolution) || sourceResolution <= 0) {
    // invalid sourceResolution -> EMPTY
    // probably edges of the projections when no extent is defined
    this.state = ol.TileState.EMPTY;
    return;
  }

  var errorThresholdInPixels = opt_errorThreshold !== undefined ?
      opt_errorThreshold : ol.DEFAULT_RASTER_REPROJECTION_ERROR_THRESHOLD;

  /**
   * @private
   * @type {!ol.reproj.Triangulation}
   */
  this.triangulation_ = new ol.reproj.Triangulation(
      sourceProj, targetProj, limitedTargetExtent, maxSourceExtent,
      sourceResolution * errorThresholdInPixels);

  if (this.triangulation_.getTriangles().length === 0) {
    // no valid triangles -> EMPTY
    this.state = ol.TileState.EMPTY;
    return;
  }

  this.sourceZ_ = sourceTileGrid.getZForResolution(sourceResolution);
  var sourceExtent = this.triangulation_.calculateSourceExtent();

  if (maxSourceExtent) {
    if (sourceProj.canWrapX()) {
      sourceExtent[1] = ol.math.clamp(
          sourceExtent[1], maxSourceExtent[1], maxSourceExtent[3]);
      sourceExtent[3] = ol.math.clamp(
          sourceExtent[3], maxSourceExtent[1], maxSourceExtent[3]);
    } else {
      sourceExtent = ol.extent.getIntersection(sourceExtent, maxSourceExtent);
    }
  }

  if (!ol.extent.getArea(sourceExtent)) {
    this.state = ol.TileState.EMPTY;
  } else {
    var sourceRange = sourceTileGrid.getTileRangeForExtentAndZ(
        sourceExtent, this.sourceZ_);

    var tilesRequired = sourceRange.getWidth() * sourceRange.getHeight();
    if (!goog.asserts.assert(
        tilesRequired < ol.RASTER_REPROJECTION_MAX_SOURCE_TILES,
        'reasonable number of tiles is required')) {
      this.state = ol.TileState.ERROR;
      return;
    }
    for (var srcX = sourceRange.minX; srcX <= sourceRange.maxX; srcX++) {
      for (var srcY = sourceRange.minY; srcY <= sourceRange.maxY; srcY++) {
        var tile = getTileFunction(this.sourceZ_, srcX, srcY, pixelRatio);
        if (tile) {
          this.sourceTiles_.push(tile);
        }
      }
    }

    if (this.sourceTiles_.length === 0) {
      this.state = ol.TileState.EMPTY;
    }
  }
};
goog.inherits(ol.reproj.Tile, ol.Tile);


/**
 * @inheritDoc
 */
ol.reproj.Tile.prototype.disposeInternal = function() {
  if (this.state == ol.TileState.LOADING) {
    this.unlistenSources_();
  }
  goog.base(this, 'disposeInternal');
};


/**
 * @inheritDoc
 */
ol.reproj.Tile.prototype.getImage = function(opt_context) {
  if (opt_context !== undefined) {
    var image;
    var key = goog.getUid(opt_context);
    if (key in this.canvasByContext_) {
      return this.canvasByContext_[key];
    } else if (goog.object.isEmpty(this.canvasByContext_)) {
      image = this.canvas_;
    } else {
      image = /** @type {HTMLCanvasElement} */ (this.canvas_.cloneNode(false));
    }
    this.canvasByContext_[key] = image;
    return image;
  } else {
    return this.canvas_;
  }
};


/**
 * @private
 */
ol.reproj.Tile.prototype.reproject_ = function() {
  var sources = [];
  this.sourceTiles_.forEach(function(tile, i, arr) {
    if (tile && tile.getState() == ol.TileState.LOADED) {
      sources.push({
        extent: this.sourceTileGrid_.getTileCoordExtent(tile.tileCoord),
        image: tile.getImage()
      });
    }
  }, this);
  this.sourceTiles_.length = 0;

  var z = this.wrappedTileCoord_[0];
  var size = this.targetTileGrid_.getTileSize(z);
  var width = goog.isNumber(size) ? size : size[0];
  var height = goog.isNumber(size) ? size : size[1];
  var targetResolution = this.targetTileGrid_.getResolution(z);
  var sourceResolution = this.sourceTileGrid_.getResolution(this.sourceZ_);

  var targetExtent = this.targetTileGrid_.getTileCoordExtent(
      this.wrappedTileCoord_);
  this.canvas_ = ol.reproj.render(width, height, this.pixelRatio_,
      sourceResolution, this.sourceTileGrid_.getExtent(),
      targetResolution, targetExtent, this.triangulation_, sources,
      this.renderEdges_);

  this.state = ol.TileState.LOADED;
  this.changed();
};


/**
 * @inheritDoc
 */
ol.reproj.Tile.prototype.load = function() {
  if (this.state == ol.TileState.IDLE) {
    this.state = ol.TileState.LOADING;
    this.changed();

    var leftToLoad = 0;

    goog.asserts.assert(!this.sourcesListenerKeys_,
        'this.sourcesListenerKeys_ should be null');

    this.sourcesListenerKeys_ = [];
    this.sourceTiles_.forEach(function(tile, i, arr) {
      var state = tile.getState();
      if (state == ol.TileState.IDLE || state == ol.TileState.LOADING) {
        leftToLoad++;

        var sourceListenKey;
        sourceListenKey = tile.listen(goog.events.EventType.CHANGE,
            function(e) {
              var state = tile.getState();
              if (state == ol.TileState.LOADED ||
                  state == ol.TileState.ERROR ||
                  state == ol.TileState.EMPTY) {
                goog.events.unlistenByKey(sourceListenKey);
                leftToLoad--;
                goog.asserts.assert(leftToLoad >= 0,
                    'leftToLoad should not be negative');
                if (leftToLoad === 0) {
                  this.unlistenSources_();
                  this.reproject_();
                }
              }
            }, false, this);
        this.sourcesListenerKeys_.push(sourceListenKey);
      }
    }, this);

    this.sourceTiles_.forEach(function(tile, i, arr) {
      var state = tile.getState();
      if (state == ol.TileState.IDLE) {
        tile.load();
      }
    });

    if (leftToLoad === 0) {
      goog.global.setTimeout(this.reproject_.bind(this), 0);
    }
  }
};


/**
 * @private
 */
ol.reproj.Tile.prototype.unlistenSources_ = function() {
  goog.asserts.assert(this.sourcesListenerKeys_,
      'this.sourcesListenerKeys_ should not be null');
  this.sourcesListenerKeys_.forEach(goog.events.unlistenByKey);
  this.sourcesListenerKeys_ = null;
};

// Copyright 2011 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview A utility to load JavaScript files via DOM script tags.
 * Refactored from goog.net.Jsonp. Works cross-domain.
 *
 */

goog.provide('goog.net.jsloader');
goog.provide('goog.net.jsloader.Error');
goog.provide('goog.net.jsloader.ErrorCode');
goog.provide('goog.net.jsloader.Options');

goog.require('goog.array');
goog.require('goog.async.Deferred');
goog.require('goog.debug.Error');
goog.require('goog.dom');
goog.require('goog.dom.TagName');
goog.require('goog.object');


/**
 * The name of the property of goog.global under which the JavaScript
 * verification object is stored by the loaded script.
 * @private {string}
 */
goog.net.jsloader.GLOBAL_VERIFY_OBJS_ = 'closure_verification';


/**
 * The default length of time, in milliseconds, we are prepared to wait for a
 * load request to complete.
 * @type {number}
 */
goog.net.jsloader.DEFAULT_TIMEOUT = 5000;


/**
 * Optional parameters for goog.net.jsloader.send.
 * timeout: The length of time, in milliseconds, we are prepared to wait
 *     for a load request to complete. Default it 5 seconds.
 * document: The HTML document under which to load the JavaScript. Default is
 *     the current document.
 * cleanupWhenDone: If true clean up the script tag after script completes to
 *     load. This is important if you just want to read data from the JavaScript
 *     and then throw it away. Default is false.
 * attributes: Additional attributes to set on the script tag.
 *
 * @typedef {{
 *   timeout: (number|undefined),
 *   document: (HTMLDocument|undefined),
 *   cleanupWhenDone: (boolean|undefined),
 *   attributes: (!Object<string, string>|undefined)
 * }}
 */
goog.net.jsloader.Options;


/**
 * Scripts (URIs) waiting to be loaded.
 * @private {!Array<string>}
 */
goog.net.jsloader.scriptsToLoad_ = [];


/**
 * The deferred result of loading the URIs in scriptsToLoad_.
 * We need to return this to a caller that wants to load URIs while
 * a deferred is already working on them.
 * @private {!goog.async.Deferred<null>}
 */
goog.net.jsloader.scriptLoadingDeferred_;


/**
 * Loads and evaluates the JavaScript files at the specified URIs, guaranteeing
 * the order of script loads.
 *
 * Because we have to load the scripts in serial (load script 1, exec script 1,
 * load script 2, exec script 2, and so on), this will be slower than doing
 * the network fetches in parallel.
 *
 * If you need to load a large number of scripts but dependency order doesn't
 * matter, you should just call goog.net.jsloader.load N times.
 *
 * If you need to load a large number of scripts on the same domain,
 * you may want to use goog.module.ModuleLoader.
 *
 * @param {Array<string>} uris The URIs to load.
 * @param {goog.net.jsloader.Options=} opt_options Optional parameters. See
 *     goog.net.jsloader.options documentation for details.
 * @return {!goog.async.Deferred} The deferred result, that may be used to add
 *     callbacks
 */
goog.net.jsloader.loadMany = function(uris, opt_options) {
  // Loading the scripts in serial introduces asynchronosity into the flow.
  // Therefore, there are race conditions where client A can kick off the load
  // sequence for client B, even though client A's scripts haven't all been
  // loaded yet.
  //
  // To work around this issue, all module loads share a queue.
  if (!uris.length) {
    return goog.async.Deferred.succeed(null);
  }

  var isAnotherModuleLoading = goog.net.jsloader.scriptsToLoad_.length;
  goog.array.extend(goog.net.jsloader.scriptsToLoad_, uris);
  if (isAnotherModuleLoading) {
    // jsloader is still loading some other scripts.
    // In order to prevent the race condition noted above, we just add
    // these URIs to the end of the scripts' queue and return the deferred
    // result of the ongoing script load, so the caller knows when they
    // finish loading.
    return goog.net.jsloader.scriptLoadingDeferred_;
  }

  uris = goog.net.jsloader.scriptsToLoad_;
  var popAndLoadNextScript = function() {
    var uri = uris.shift();
    var deferred = goog.net.jsloader.load(uri, opt_options);
    if (uris.length) {
      deferred.addBoth(popAndLoadNextScript);
    }
    return deferred;
  };
  goog.net.jsloader.scriptLoadingDeferred_ = popAndLoadNextScript();
  return goog.net.jsloader.scriptLoadingDeferred_;
};


/**
 * Loads and evaluates a JavaScript file.
 * When the script loads, a user callback is called.
 * It is the client's responsibility to verify that the script ran successfully.
 *
 * @param {string} uri The URI of the JavaScript.
 * @param {goog.net.jsloader.Options=} opt_options Optional parameters. See
 *     goog.net.jsloader.Options documentation for details.
 * @return {!goog.async.Deferred} The deferred result, that may be used to add
 *     callbacks and/or cancel the transmission.
 *     The error callback will be called with a single goog.net.jsloader.Error
 *     parameter.
 */
goog.net.jsloader.load = function(uri, opt_options) {
  var options = opt_options || {};
  var doc = options.document || document;

  var script = goog.dom.createElement(goog.dom.TagName.SCRIPT);
  var request = {script_: script, timeout_: undefined};
  var deferred = new goog.async.Deferred(goog.net.jsloader.cancel_, request);

  // Set a timeout.
  var timeout = null;
  var timeoutDuration = goog.isDefAndNotNull(options.timeout) ?
      options.timeout : goog.net.jsloader.DEFAULT_TIMEOUT;
  if (timeoutDuration > 0) {
    timeout = window.setTimeout(function() {
      goog.net.jsloader.cleanup_(script, true);
      deferred.errback(new goog.net.jsloader.Error(
          goog.net.jsloader.ErrorCode.TIMEOUT,
          'Timeout reached for loading script ' + uri));
    }, timeoutDuration);
    request.timeout_ = timeout;
  }

  // Hang the user callback to be called when the script completes to load.
  // NOTE(user): This callback will be called in IE even upon error. In any
  // case it is the client's responsibility to verify that the script ran
  // successfully.
  script.onload = script.onreadystatechange = function() {
    if (!script.readyState || script.readyState == 'loaded' ||
        script.readyState == 'complete') {
      var removeScriptNode = options.cleanupWhenDone || false;
      goog.net.jsloader.cleanup_(script, removeScriptNode, timeout);
      deferred.callback(null);
    }
  };

  // Add an error callback.
  // NOTE(user): Not supported in IE.
  script.onerror = function() {
    goog.net.jsloader.cleanup_(script, true, timeout);
    deferred.errback(new goog.net.jsloader.Error(
        goog.net.jsloader.ErrorCode.LOAD_ERROR,
        'Error while loading script ' + uri));
  };

  var properties = options.attributes || {};
  goog.object.extend(properties, {
    'type': 'text/javascript',
    'charset': 'UTF-8',
    // NOTE(user): Safari never loads the script if we don't set
    // the src attribute before appending.
    'src': uri
  });
  goog.dom.setProperties(script, properties);
  var scriptParent = goog.net.jsloader.getScriptParentElement_(doc);
  scriptParent.appendChild(script);

  return deferred;
};


/**
 * Loads a JavaScript file and verifies it was evaluated successfully, using a
 * verification object.
 * The verification object is set by the loaded JavaScript at the end of the
 * script.
 * We verify this object was set and return its value in the success callback.
 * If the object is not defined we trigger an error callback.
 *
 * @param {string} uri The URI of the JavaScript.
 * @param {string} verificationObjName The name of the verification object that
 *     the loaded script should set.
 * @param {goog.net.jsloader.Options} options Optional parameters. See
 *     goog.net.jsloader.Options documentation for details.
 * @return {!goog.async.Deferred} The deferred result, that may be used to add
 *     callbacks and/or cancel the transmission.
 *     The success callback will be called with a single parameter containing
 *     the value of the verification object.
 *     The error callback will be called with a single goog.net.jsloader.Error
 *     parameter.
 */
goog.net.jsloader.loadAndVerify = function(uri, verificationObjName, options) {
  // Define the global objects variable.
  if (!goog.global[goog.net.jsloader.GLOBAL_VERIFY_OBJS_]) {
    goog.global[goog.net.jsloader.GLOBAL_VERIFY_OBJS_] = {};
  }
  var verifyObjs = goog.global[goog.net.jsloader.GLOBAL_VERIFY_OBJS_];

  // Verify that the expected object does not exist yet.
  if (goog.isDef(verifyObjs[verificationObjName])) {
    // TODO(user): Error or reset variable?
    return goog.async.Deferred.fail(new goog.net.jsloader.Error(
        goog.net.jsloader.ErrorCode.VERIFY_OBJECT_ALREADY_EXISTS,
        'Verification object ' + verificationObjName + ' already defined.'));
  }

  // Send request to load the JavaScript.
  var sendDeferred = goog.net.jsloader.load(uri, options);

  // Create a deferred object wrapping the send result.
  var deferred = new goog.async.Deferred(
      goog.bind(sendDeferred.cancel, sendDeferred));

  // Call user back with object that was set by the script.
  sendDeferred.addCallback(function() {
    var result = verifyObjs[verificationObjName];
    if (goog.isDef(result)) {
      deferred.callback(result);
      delete verifyObjs[verificationObjName];
    } else {
      // Error: script was not loaded properly.
      deferred.errback(new goog.net.jsloader.Error(
          goog.net.jsloader.ErrorCode.VERIFY_ERROR,
          'Script ' + uri + ' loaded, but verification object ' +
          verificationObjName + ' was not defined.'));
    }
  });

  // Pass error to new deferred object.
  sendDeferred.addErrback(function(error) {
    if (goog.isDef(verifyObjs[verificationObjName])) {
      delete verifyObjs[verificationObjName];
    }
    deferred.errback(error);
  });

  return deferred;
};


/**
 * Gets the DOM element under which we should add new script elements.
 * How? Take the first head element, and if not found take doc.documentElement,
 * which always exists.
 *
 * @param {!HTMLDocument} doc The relevant document.
 * @return {!Element} The script parent element.
 * @private
 */
goog.net.jsloader.getScriptParentElement_ = function(doc) {
  var headElements = doc.getElementsByTagName(goog.dom.TagName.HEAD);
  if (!headElements || goog.array.isEmpty(headElements)) {
    return doc.documentElement;
  } else {
    return headElements[0];
  }
};


/**
 * Cancels a given request.
 * @this {{script_: Element, timeout_: number}} The request context.
 * @private
 */
goog.net.jsloader.cancel_ = function() {
  var request = this;
  if (request && request.script_) {
    var scriptNode = request.script_;
    if (scriptNode && scriptNode.tagName == goog.dom.TagName.SCRIPT) {
      goog.net.jsloader.cleanup_(scriptNode, true, request.timeout_);
    }
  }
};


/**
 * Removes the script node and the timeout.
 *
 * @param {Node} scriptNode The node to be cleaned up.
 * @param {boolean} removeScriptNode If true completely remove the script node.
 * @param {?number=} opt_timeout The timeout handler to cleanup.
 * @private
 */
goog.net.jsloader.cleanup_ = function(scriptNode, removeScriptNode,
                                      opt_timeout) {
  if (goog.isDefAndNotNull(opt_timeout)) {
    goog.global.clearTimeout(opt_timeout);
  }

  scriptNode.onload = goog.nullFunction;
  scriptNode.onerror = goog.nullFunction;
  scriptNode.onreadystatechange = goog.nullFunction;

  // Do this after a delay (removing the script node of a running script can
  // confuse older IEs).
  if (removeScriptNode) {
    window.setTimeout(function() {
      goog.dom.removeNode(scriptNode);
    }, 0);
  }
};


/**
 * Possible error codes for jsloader.
 * @enum {number}
 */
goog.net.jsloader.ErrorCode = {
  LOAD_ERROR: 0,
  TIMEOUT: 1,
  VERIFY_ERROR: 2,
  VERIFY_OBJECT_ALREADY_EXISTS: 3
};



/**
 * A jsloader error.
 *
 * @param {goog.net.jsloader.ErrorCode} code The error code.
 * @param {string=} opt_message Additional message.
 * @constructor
 * @extends {goog.debug.Error}
 * @final
 */
goog.net.jsloader.Error = function(code, opt_message) {
  var msg = 'Jsloader error (code #' + code + ')';
  if (opt_message) {
    msg += ': ' + opt_message;
  }
  goog.net.jsloader.Error.base(this, 'constructor', msg);

  /**
   * The code for this error.
   *
   * @type {goog.net.jsloader.ErrorCode}
   */
  this.code = code;
};
goog.inherits(goog.net.jsloader.Error, goog.debug.Error);

// Copyright 2006 The Closure Library Authors. All Rights Reserved.
//
// 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.

// The original file lives here: http://go/cross_domain_channel.js

/**
 * @fileoverview Implements a cross-domain communication channel. A
 * typical web page is prevented by browser security from sending
 * request, such as a XMLHttpRequest, to other servers than the ones
 * from which it came. The Jsonp class provides a workaround by
 * using dynamically generated script tags. Typical usage:.
 *
 * var jsonp = new goog.net.Jsonp(new goog.Uri('http://my.host.com/servlet'));
 * var payload = { 'foo': 1, 'bar': true };
 * jsonp.send(payload, function(reply) { alert(reply) });
 *
 * This script works in all browsers that are currently supported by
 * the Google Maps API, which is IE 6.0+, Firefox 0.8+, Safari 1.2.4+,
 * Netscape 7.1+, Mozilla 1.4+, Opera 8.02+.
 *
 */

goog.provide('goog.net.Jsonp');

goog.require('goog.Uri');
goog.require('goog.net.jsloader');

// WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
//
// This class allows us (Google) to send data from non-Google and thus
// UNTRUSTED pages to our servers. Under NO CIRCUMSTANCES return
// anything sensitive, such as session or cookie specific data. Return
// only data that you want parties external to Google to have. Also
// NEVER use this method to send data from web pages to untrusted
// servers, or redirects to unknown servers (www.google.com/cache,
// /q=xx&btnl, /url, www.googlepages.com, etc.)
//
// WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING



/**
 * Creates a new cross domain channel that sends data to the specified
 * host URL. By default, if no reply arrives within 5s, the channel
 * assumes the call failed to complete successfully.
 *
 * @param {goog.Uri|string} uri The Uri of the server side code that receives
 *     data posted through this channel (e.g.,
 *     "http://maps.google.com/maps/geo").
 *
 * @param {string=} opt_callbackParamName The parameter name that is used to
 *     specify the callback. Defaults to "callback".
 *
 * @constructor
 * @final
 */
goog.net.Jsonp = function(uri, opt_callbackParamName) {
  /**
   * The uri_ object will be used to encode the payload that is sent to the
   * server.
   * @type {goog.Uri}
   * @private
   */
  this.uri_ = new goog.Uri(uri);

  /**
   * This is the callback parameter name that is added to the uri.
   * @type {string}
   * @private
   */
  this.callbackParamName_ = opt_callbackParamName ?
      opt_callbackParamName : 'callback';

  /**
   * The length of time, in milliseconds, this channel is prepared
   * to wait for for a request to complete. The default value is 5 seconds.
   * @type {number}
   * @private
   */
  this.timeout_ = 5000;
};


/**
 * The name of the property of goog.global under which the callback is
 * stored.
 */
goog.net.Jsonp.CALLBACKS = '_callbacks_';


/**
 * Used to generate unique callback IDs. The counter must be global because
 * all channels share a common callback object.
 * @private
 */
goog.net.Jsonp.scriptCounter_ = 0;


/**
 * Sets the length of time, in milliseconds, this channel is prepared
 * to wait for for a request to complete. If the call is not competed
 * within the set time span, it is assumed to have failed. To wait
 * indefinitely for a request to complete set the timout to a negative
 * number.
 *
 * @param {number} timeout The length of time before calls are
 * interrupted.
 */
goog.net.Jsonp.prototype.setRequestTimeout = function(timeout) {
  this.timeout_ = timeout;
};


/**
 * Returns the current timeout value, in milliseconds.
 *
 * @return {number} The timeout value.
 */
goog.net.Jsonp.prototype.getRequestTimeout = function() {
  return this.timeout_;
};


/**
 * Sends the given payload to the URL specified at the construction
 * time. The reply is delivered to the given replyCallback. If the
 * errorCallback is specified and the reply does not arrive within the
 * timeout period set on this channel, the errorCallback is invoked
 * with the original payload.
 *
 * If no reply callback is specified, then the response is expected to
 * consist of calls to globally registered functions. No &callback=
 * URL parameter will be sent in the request, and the script element
 * will be cleaned up after the timeout.
 *
 * @param {Object=} opt_payload Name-value pairs.  If given, these will be
 *     added as parameters to the supplied URI as GET parameters to the
 *     given server URI.
 *
 * @param {Function=} opt_replyCallback A function expecting one
 *     argument, called when the reply arrives, with the response data.
 *
 * @param {Function=} opt_errorCallback A function expecting one
 *     argument, called on timeout, with the payload (if given), otherwise
 *     null.
 *
 * @param {string=} opt_callbackParamValue Value to be used as the
 *     parameter value for the callback parameter (callbackParamName).
 *     To be used when the value needs to be fixed by the client for a
 *     particular request, to make use of the cached responses for the request.
 *     NOTE: If multiple requests are made with the same
 *     opt_callbackParamValue, only the last call will work whenever the
 *     response comes back.
 *
 * @return {!Object} A request descriptor that may be used to cancel this
 *     transmission, or null, if the message may not be cancelled.
 */
goog.net.Jsonp.prototype.send = function(opt_payload,
                                         opt_replyCallback,
                                         opt_errorCallback,
                                         opt_callbackParamValue) {

  var payload = opt_payload || null;

  var id = opt_callbackParamValue ||
      '_' + (goog.net.Jsonp.scriptCounter_++).toString(36) +
      goog.now().toString(36);

  if (!goog.global[goog.net.Jsonp.CALLBACKS]) {
    goog.global[goog.net.Jsonp.CALLBACKS] = {};
  }

  // Create a new Uri object onto which this payload will be added
  var uri = this.uri_.clone();
  if (payload) {
    goog.net.Jsonp.addPayloadToUri_(payload, uri);
  }

  if (opt_replyCallback) {
    var reply = goog.net.Jsonp.newReplyHandler_(id, opt_replyCallback);
    goog.global[goog.net.Jsonp.CALLBACKS][id] = reply;

    uri.setParameterValues(this.callbackParamName_,
                           goog.net.Jsonp.CALLBACKS + '.' + id);
  }

  var deferred = goog.net.jsloader.load(uri.toString(),
      {timeout: this.timeout_, cleanupWhenDone: true});
  var error = goog.net.Jsonp.newErrorHandler_(id, payload, opt_errorCallback);
  deferred.addErrback(error);

  return {id_: id, deferred_: deferred};
};


/**
 * Cancels a given request. The request must be exactly the object returned by
 * the send method.
 *
 * @param {Object} request The request object returned by the send method.
 */
goog.net.Jsonp.prototype.cancel = function(request) {
  if (request) {
    if (request.deferred_) {
      request.deferred_.cancel();
    }
    if (request.id_) {
      goog.net.Jsonp.cleanup_(request.id_, false);
    }
  }
};


/**
 * Creates a timeout callback that calls the given timeoutCallback with the
 * original payload.
 *
 * @param {string} id The id of the script node.
 * @param {Object} payload The payload that was sent to the server.
 * @param {Function=} opt_errorCallback The function called on timeout.
 * @return {!Function} A zero argument function that handles callback duties.
 * @private
 */
goog.net.Jsonp.newErrorHandler_ = function(id,
                                           payload,
                                           opt_errorCallback) {
  /**
   * When we call across domains with a request, this function is the
   * timeout handler. Once it's done executing the user-specified
   * error-handler, it removes the script node and original function.
   */
  return function() {
    goog.net.Jsonp.cleanup_(id, false);
    if (opt_errorCallback) {
      opt_errorCallback(payload);
    }
  };
};


/**
 * Creates a reply callback that calls the given replyCallback with data
 * returned by the server.
 *
 * @param {string} id The id of the script node.
 * @param {Function} replyCallback The function called on reply.
 * @return {!Function} A reply callback function.
 * @private
 */
goog.net.Jsonp.newReplyHandler_ = function(id, replyCallback) {
  /**
   * This function is the handler for the all-is-well response. It
   * clears the error timeout handler, calls the user's handler, then
   * removes the script node and itself.
   *
   * @param {...Object} var_args The response data sent from the server.
   */
  var handler = function(var_args) {
    goog.net.Jsonp.cleanup_(id, true);
    replyCallback.apply(undefined, arguments);
  };
  return handler;
};


/**
 * Removes the script node and reply handler with the given id.
 *
 * @param {string} id The id of the script node to be removed.
 * @param {boolean} deleteReplyHandler If true, delete the reply handler
 *     instead of setting it to nullFunction (if we know the callback could
 *     never be called again).
 * @private
 */
goog.net.Jsonp.cleanup_ = function(id, deleteReplyHandler) {
  if (goog.global[goog.net.Jsonp.CALLBACKS][id]) {
    if (deleteReplyHandler) {
      delete goog.global[goog.net.Jsonp.CALLBACKS][id];
    } else {
      // Removing the script tag doesn't necessarily prevent the script
      // from firing, so we make the callback a noop.
      goog.global[goog.net.Jsonp.CALLBACKS][id] = goog.nullFunction;
    }
  }
};


/**
 * Returns URL encoded payload. The payload should be a map of name-value
 * pairs, in the form {"foo": 1, "bar": true, ...}.  If the map is empty,
 * the URI will be unchanged.
 *
 * <p>The method uses hasOwnProperty() to assure the properties are on the
 * object, not on its prototype.
 *
 * @param {!Object} payload A map of value name pairs to be encoded.
 *     A value may be specified as an array, in which case a query parameter
 *     will be created for each value, e.g.:
 *     {"foo": [1,2]} will encode to "foo=1&foo=2".
 *
 * @param {!goog.Uri} uri A Uri object onto which the payload key value pairs
 *     will be encoded.
 *
 * @return {!goog.Uri} A reference to the Uri sent as a parameter.
 * @private
 */
goog.net.Jsonp.addPayloadToUri_ = function(payload, uri) {
  for (var name in payload) {
    // NOTE(user): Safari/1.3 doesn't have hasOwnProperty(). In that
    // case, we iterate over all properties as a very lame workaround.
    if (!payload.hasOwnProperty || payload.hasOwnProperty(name)) {
      uri.setParameterValues(name, payload[name]);
    }
  }
  return uri;
};


// WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
//
// This class allows us (Google) to send data from non-Google and thus
// UNTRUSTED pages to our servers. Under NO CIRCUMSTANCES return
// anything sensitive, such as session or cookie specific data. Return
// only data that you want parties external to Google to have. Also
// NEVER use this method to send data from web pages to untrusted
// servers, or redirects to unknown servers (www.google.com/cache,
// /q=xx&btnl, /url, www.googlepages.com, etc.)
//
// WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING

goog.provide('ol.source.TileImage');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.object');
goog.require('ol.ImageTile');
goog.require('ol.TileCache');
goog.require('ol.TileState');
goog.require('ol.proj');
goog.require('ol.reproj.Tile');
goog.require('ol.source.UrlTile');


/**
 * @classdesc
 * Base class for sources providing images divided into a tile grid.
 *
 * @constructor
 * @fires ol.source.TileEvent
 * @extends {ol.source.UrlTile}
 * @param {olx.source.TileImageOptions} options Image tile options.
 * @api
 */
ol.source.TileImage = function(options) {

  goog.base(this, {
    attributions: options.attributions,
    extent: options.extent,
    logo: options.logo,
    opaque: options.opaque,
    projection: options.projection,
    state: options.state !== undefined ?
        /** @type {ol.source.State} */ (options.state) : undefined,
    tileGrid: options.tileGrid,
    tileLoadFunction: options.tileLoadFunction ?
        options.tileLoadFunction : ol.source.TileImage.defaultTileLoadFunction,
    tilePixelRatio: options.tilePixelRatio,
    tileUrlFunction: options.tileUrlFunction,
    url: options.url,
    urls: options.urls,
    wrapX: options.wrapX
  });

  /**
   * @protected
   * @type {?string}
   */
  this.crossOrigin =
      options.crossOrigin !== undefined ? options.crossOrigin : null;

  /**
   * @protected
   * @type {function(new: ol.ImageTile, ol.TileCoord, ol.TileState, string,
   *        ?string, ol.TileLoadFunctionType)}
   */
  this.tileClass = options.tileClass !== undefined ?
      options.tileClass : ol.ImageTile;

  /**
   * @protected
   * @type {Object.<string, ol.TileCache>}
   */
  this.tileCacheForProjection = {};

  /**
   * @protected
   * @type {Object.<string, ol.tilegrid.TileGrid>}
   */
  this.tileGridForProjection = {};

  /**
   * @private
   * @type {number|undefined}
   */
  this.reprojectionErrorThreshold_ = options.reprojectionErrorThreshold;

  /**
   * @private
   * @type {boolean}
   */
  this.renderReprojectionEdges_ = false;
};
goog.inherits(ol.source.TileImage, ol.source.UrlTile);


/**
 * @inheritDoc
 */
ol.source.TileImage.prototype.canExpireCache = function() {
  if (!ol.ENABLE_RASTER_REPROJECTION) {
    return goog.base(this, 'canExpireCache');
  }
  var canExpire = this.tileCache.canExpireCache();
  if (canExpire) {
    return true;
  } else {
    return goog.object.some(this.tileCacheForProjection, function(tileCache) {
      return tileCache.canExpireCache();
    });
  }
};


/**
 * @inheritDoc
 */
ol.source.TileImage.prototype.expireCache = function(projection, usedTiles) {
  if (!ol.ENABLE_RASTER_REPROJECTION) {
    goog.base(this, 'expireCache', projection, usedTiles);
    return;
  }
  var usedTileCache = this.getTileCacheForProjection(projection);

  this.tileCache.expireCache(this.tileCache == usedTileCache ? usedTiles : {});
  goog.object.forEach(this.tileCacheForProjection, function(tileCache) {
    tileCache.expireCache(tileCache == usedTileCache ? usedTiles : {});
  });
};


/**
 * @inheritDoc
 */
ol.source.TileImage.prototype.getOpaque = function(projection) {
  if (ol.ENABLE_RASTER_REPROJECTION &&
      this.getProjection() && projection &&
      !ol.proj.equivalent(this.getProjection(), projection)) {
    return false;
  } else {
    return goog.base(this, 'getOpaque', projection);
  }
};


/**
 * @inheritDoc
 */
ol.source.TileImage.prototype.getTileGridForProjection = function(projection) {
  if (!ol.ENABLE_RASTER_REPROJECTION) {
    return goog.base(this, 'getTileGridForProjection', projection);
  }
  var thisProj = this.getProjection();
  if (this.tileGrid &&
      (!thisProj || ol.proj.equivalent(thisProj, projection))) {
    return this.tileGrid;
  } else {
    var projKey = goog.getUid(projection).toString();
    if (!(projKey in this.tileGridForProjection)) {
      this.tileGridForProjection[projKey] =
          ol.tilegrid.getForProjection(projection);
    }
    return this.tileGridForProjection[projKey];
  }
};


/**
 * @inheritDoc
 */
ol.source.TileImage.prototype.getTileCacheForProjection = function(projection) {
  if (!ol.ENABLE_RASTER_REPROJECTION) {
    return goog.base(this, 'getTileCacheForProjection', projection);
  }
  var thisProj = this.getProjection();
  if (!thisProj || ol.proj.equivalent(thisProj, projection)) {
    return this.tileCache;
  } else {
    var projKey = goog.getUid(projection).toString();
    if (!(projKey in this.tileCacheForProjection)) {
      this.tileCacheForProjection[projKey] = new ol.TileCache();
    }
    return this.tileCacheForProjection[projKey];
  }
};


/**
 * @param {number} z Tile coordinate z.
 * @param {number} x Tile coordinate x.
 * @param {number} y Tile coordinate y.
 * @param {number} pixelRatio Pixel ratio.
 * @param {ol.proj.Projection} projection Projection.
 * @param {string} key The key set on the tile.
 * @return {ol.Tile} Tile.
 * @private
 */
ol.source.TileImage.prototype.createTile_ = function(z, x, y, pixelRatio, projection, key) {
  var tileCoord = [z, x, y];
  var urlTileCoord = this.getTileCoordForTileUrlFunction(
      tileCoord, projection);
  var tileUrl = urlTileCoord ?
      this.tileUrlFunction(urlTileCoord, pixelRatio, projection) : undefined;
  var tile = new this.tileClass(
      tileCoord,
      tileUrl !== undefined ? ol.TileState.IDLE : ol.TileState.EMPTY,
      tileUrl !== undefined ? tileUrl : '',
      this.crossOrigin,
      this.tileLoadFunction);
  tile.key = key;
  goog.events.listen(tile, goog.events.EventType.CHANGE,
      this.handleTileChange, false, this);
  return tile;
};


/**
 * @inheritDoc
 */
ol.source.TileImage.prototype.getTile = function(z, x, y, pixelRatio, projection) {
  if (!ol.ENABLE_RASTER_REPROJECTION ||
      !this.getProjection() ||
      !projection ||
      ol.proj.equivalent(this.getProjection(), projection)) {
    return this.getTileInternal(z, x, y, pixelRatio, projection);
  } else {
    var cache = this.getTileCacheForProjection(projection);
    var tileCoord = [z, x, y];
    var tileCoordKey = this.getKeyZXY.apply(this, tileCoord);
    if (cache.containsKey(tileCoordKey)) {
      return /** @type {!ol.Tile} */ (cache.get(tileCoordKey));
    } else {
      var sourceProjection = this.getProjection();
      var sourceTileGrid = this.getTileGridForProjection(sourceProjection);
      var targetTileGrid = this.getTileGridForProjection(projection);
      var wrappedTileCoord =
          this.getTileCoordForTileUrlFunction(tileCoord, projection);
      var tile = new ol.reproj.Tile(
          sourceProjection, sourceTileGrid,
          projection, targetTileGrid,
          tileCoord, wrappedTileCoord, this.getTilePixelRatio(pixelRatio),
          function(z, x, y, pixelRatio) {
            return this.getTileInternal(z, x, y, pixelRatio, sourceProjection);
          }.bind(this), this.reprojectionErrorThreshold_,
          this.renderReprojectionEdges_);

      cache.set(tileCoordKey, tile);
      return tile;
    }
  }
};


/**
 * @param {number} z Tile coordinate z.
 * @param {number} x Tile coordinate x.
 * @param {number} y Tile coordinate y.
 * @param {number} pixelRatio Pixel ratio.
 * @param {ol.proj.Projection} projection Projection.
 * @return {!ol.Tile} Tile.
 * @protected
 */
ol.source.TileImage.prototype.getTileInternal = function(z, x, y, pixelRatio, projection) {
  var /** @type {ol.Tile} */ tile = null;
  var tileCoordKey = this.getKeyZXY(z, x, y);
  var paramsKey = this.getKeyParams();
  if (!this.tileCache.containsKey(tileCoordKey)) {
    goog.asserts.assert(projection, 'argument projection is truthy');
    tile = this.createTile_(z, x, y, pixelRatio, projection, paramsKey);
    this.tileCache.set(tileCoordKey, tile);
  } else {
    tile = /** @type {!ol.Tile} */ (this.tileCache.get(tileCoordKey));
    if (tile.key != paramsKey) {
      // The source's params changed. If the tile has an interim tile and if we
      // can use it then we use it. Otherwise we create a new tile.  In both
      // cases we attempt to assign an interim tile to the new tile.
      var /** @type {ol.Tile} */ interimTile = tile;
      if (tile.interimTile && tile.interimTile.key == paramsKey) {
        goog.asserts.assert(tile.interimTile.getState() == ol.TileState.LOADED);
        goog.asserts.assert(tile.interimTile.interimTile === null);
        tile = tile.interimTile;
        if (interimTile.getState() == ol.TileState.LOADED) {
          tile.interimTile = interimTile;
        }
      } else {
        tile = this.createTile_(z, x, y, pixelRatio, projection, paramsKey);
        if (interimTile.getState() == ol.TileState.LOADED) {
          tile.interimTile = interimTile;
        } else if (interimTile.interimTile &&
            interimTile.interimTile.getState() == ol.TileState.LOADED) {
          tile.interimTile = interimTile.interimTile;
          interimTile.interimTile = null;
        }
      }
      if (tile.interimTile) {
        tile.interimTile.interimTile = null;
      }
      this.tileCache.replace(tileCoordKey, tile);
    }
  }
  goog.asserts.assert(tile);
  return tile;
};


/**
 * Sets whether to render reprojection edges or not (usually for debugging).
 * @param {boolean} render Render the edges.
 * @api
 */
ol.source.TileImage.prototype.setRenderReprojectionEdges = function(render) {
  if (!ol.ENABLE_RASTER_REPROJECTION ||
      this.renderReprojectionEdges_ == render) {
    return;
  }
  this.renderReprojectionEdges_ = render;
  goog.object.forEach(this.tileCacheForProjection, function(tileCache) {
    tileCache.clear();
  });
  this.changed();
};


/**
 * Sets the tile grid to use when reprojecting the tiles to the given
 * projection instead of the default tile grid for the projection.
 *
 * This can be useful when the default tile grid cannot be created
 * (e.g. projection has no extent defined) or
 * for optimization reasons (custom tile size, resolutions, ...).
 *
 * @param {ol.proj.ProjectionLike} projection Projection.
 * @param {ol.tilegrid.TileGrid} tilegrid Tile grid to use for the projection.
 * @api
 */
ol.source.TileImage.prototype.setTileGridForProjection = function(projection, tilegrid) {
  if (ol.ENABLE_RASTER_REPROJECTION) {
    var proj = ol.proj.get(projection);
    if (proj) {
      var projKey = goog.getUid(proj).toString();
      if (!(projKey in this.tileGridForProjection)) {
        this.tileGridForProjection[projKey] = tilegrid;
      }
    }
  }
};


/**
 * @param {ol.ImageTile} imageTile Image tile.
 * @param {string} src Source.
 */
ol.source.TileImage.defaultTileLoadFunction = function(imageTile, src) {
  imageTile.getImage().src = src;
};

goog.provide('ol.source.BingMaps');

goog.require('goog.Uri');
goog.require('goog.asserts');
goog.require('goog.net.Jsonp');
goog.require('ol.Attribution');
goog.require('ol.TileRange');
goog.require('ol.TileUrlFunction');
goog.require('ol.extent');
goog.require('ol.proj');
goog.require('ol.source.State');
goog.require('ol.source.TileImage');
goog.require('ol.tilecoord');


/**
 * @classdesc
 * Layer source for Bing Maps tile data.
 *
 * @constructor
 * @extends {ol.source.TileImage}
 * @param {olx.source.BingMapsOptions} options Bing Maps options.
 * @api stable
 */
ol.source.BingMaps = function(options) {

  goog.base(this, {
    crossOrigin: 'anonymous',
    opaque: true,
    projection: ol.proj.get('EPSG:3857'),
    reprojectionErrorThreshold: options.reprojectionErrorThreshold,
    state: ol.source.State.LOADING,
    tileLoadFunction: options.tileLoadFunction,
    wrapX: options.wrapX !== undefined ? options.wrapX : true
  });

  /**
   * @private
   * @type {string}
   */
  this.culture_ = options.culture !== undefined ? options.culture : 'en-us';

  /**
   * @private
   * @type {number}
   */
  this.maxZoom_ = options.maxZoom !== undefined ? options.maxZoom : -1;

  var uri = new goog.Uri(
      'https://dev.virtualearth.net/REST/v1/Imagery/Metadata/' +
      options.imagerySet);

  var jsonp = new goog.net.Jsonp(uri, 'jsonp');
  jsonp.send({
    'include': 'ImageryProviders',
    'uriScheme': 'https',
    'key': options.key
  }, this.handleImageryMetadataResponse.bind(this));

};
goog.inherits(ol.source.BingMaps, ol.source.TileImage);


/**
 * The attribution containing a link to the Microsoft® Bing™ Maps Platform APIs’
 * Terms Of Use.
 * @const
 * @type {ol.Attribution}
 * @api
 */
ol.source.BingMaps.TOS_ATTRIBUTION = new ol.Attribution({
  html: '<a class="ol-attribution-bing-tos" ' +
      'href="http://www.microsoft.com/maps/product/terms.html">' +
      'Terms of Use</a>'
});


/**
 * @param {BingMapsImageryMetadataResponse} response Response.
 */
ol.source.BingMaps.prototype.handleImageryMetadataResponse = function(response) {

  if (response.statusCode != 200 ||
      response.statusDescription != 'OK' ||
      response.authenticationResultCode != 'ValidCredentials' ||
      response.resourceSets.length != 1 ||
      response.resourceSets[0].resources.length != 1) {
    this.setState(ol.source.State.ERROR);
    return;
  }

  var brandLogoUri = response.brandLogoUri;
  if (brandLogoUri.indexOf('https') == -1) {
    brandLogoUri = brandLogoUri.replace('http', 'https');
  }
  //var copyright = response.copyright;  // FIXME do we need to display this?
  var resource = response.resourceSets[0].resources[0];
  goog.asserts.assert(resource.imageWidth == resource.imageHeight,
      'resource has imageWidth equal to imageHeight, i.e. is square');
  var maxZoom = this.maxZoom_ == -1 ? resource.zoomMax : this.maxZoom_;

  var sourceProjection = this.getProjection();
  var extent = ol.tilegrid.extentFromProjection(sourceProjection);
  var tileSize = resource.imageWidth == resource.imageHeight ?
      resource.imageWidth : [resource.imageWidth, resource.imageHeight];
  var tileGrid = ol.tilegrid.createXYZ({
    extent: extent,
    minZoom: resource.zoomMin,
    maxZoom: maxZoom,
    tileSize: tileSize
  });
  this.tileGrid = tileGrid;

  var culture = this.culture_;
  this.tileUrlFunction = ol.TileUrlFunction.createFromTileUrlFunctions(
      resource.imageUrlSubdomains.map(function(subdomain) {
        var quadKeyTileCoord = [0, 0, 0];
        var imageUrl = resource.imageUrl
            .replace('{subdomain}', subdomain)
            .replace('{culture}', culture);
        return (
            /**
             * @param {ol.TileCoord} tileCoord Tile coordinate.
             * @param {number} pixelRatio Pixel ratio.
             * @param {ol.proj.Projection} projection Projection.
             * @return {string|undefined} Tile URL.
             */
            function(tileCoord, pixelRatio, projection) {
              goog.asserts.assert(ol.proj.equivalent(
                  projection, sourceProjection),
                  'projections are equivalent');
              if (!tileCoord) {
                return undefined;
              } else {
                ol.tilecoord.createOrUpdate(tileCoord[0], tileCoord[1],
                    -tileCoord[2] - 1, quadKeyTileCoord);
                return imageUrl.replace('{quadkey}', ol.tilecoord.quadKey(
                    quadKeyTileCoord));
              }
            });
      }));

  if (resource.imageryProviders) {
    var transform = ol.proj.getTransformFromProjections(
        ol.proj.get('EPSG:4326'), this.getProjection());

    var attributions = resource.imageryProviders.map(function(imageryProvider) {
      var html = imageryProvider.attribution;
      /** @type {Object.<string, Array.<ol.TileRange>>} */
      var tileRanges = {};
      imageryProvider.coverageAreas.forEach(function(coverageArea) {
        var minZ = coverageArea.zoomMin;
        var maxZ = Math.min(coverageArea.zoomMax, maxZoom);
        var bbox = coverageArea.bbox;
        var epsg4326Extent = [bbox[1], bbox[0], bbox[3], bbox[2]];
        var extent = ol.extent.applyTransform(epsg4326Extent, transform);
        var tileRange, z, zKey;
        for (z = minZ; z <= maxZ; ++z) {
          zKey = z.toString();
          tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z);
          if (zKey in tileRanges) {
            tileRanges[zKey].push(tileRange);
          } else {
            tileRanges[zKey] = [tileRange];
          }
        }
      });
      return new ol.Attribution({html: html, tileRanges: tileRanges});
    });
    attributions.push(ol.source.BingMaps.TOS_ATTRIBUTION);
    this.setAttributions(attributions);
  }

  this.setLogo(brandLogoUri);

  this.setState(ol.source.State.READY);

};

// FIXME keep cluster cache by resolution ?
// FIXME distance not respected because of the centroid

goog.provide('ol.source.Cluster');

goog.require('goog.asserts');
goog.require('goog.events.EventType');
goog.require('goog.object');
goog.require('ol.Feature');
goog.require('ol.coordinate');
goog.require('ol.extent');
goog.require('ol.geom.Point');
goog.require('ol.source.Vector');


/**
 * @classdesc
 * Layer source to cluster vector data.
 *
 * @constructor
 * @param {olx.source.ClusterOptions} options Constructor options.
 * @extends {ol.source.Vector}
 * @api
 */
ol.source.Cluster = function(options) {
  goog.base(this, {
    attributions: options.attributions,
    extent: options.extent,
    logo: options.logo,
    projection: options.projection,
    wrapX: options.wrapX
  });

  /**
   * @type {number|undefined}
   * @private
   */
  this.resolution_ = undefined;

  /**
   * @type {number}
   * @private
   */
  this.distance_ = options.distance !== undefined ? options.distance : 20;

  /**
   * @type {Array.<ol.Feature>}
   * @private
   */
  this.features_ = [];

  /**
   * @type {ol.source.Vector}
   * @private
   */
  this.source_ = options.source;

  this.source_.on(goog.events.EventType.CHANGE,
      ol.source.Cluster.prototype.onSourceChange_, this);
};
goog.inherits(ol.source.Cluster, ol.source.Vector);


/**
 * Get a reference to the wrapped source.
 * @return {ol.source.Vector} Source.
 * @api
 */
ol.source.Cluster.prototype.getSource = function() {
  return this.source_;
};


/**
 * @inheritDoc
 */
ol.source.Cluster.prototype.loadFeatures = function(extent, resolution,
    projection) {
  this.source_.loadFeatures(extent, resolution, projection);
  if (resolution !== this.resolution_) {
    this.clear();
    this.resolution_ = resolution;
    this.cluster_();
    this.addFeatures(this.features_);
  }
};


/**
 * handle the source changing
 * @private
 */
ol.source.Cluster.prototype.onSourceChange_ = function() {
  this.clear();
  this.cluster_();
  this.addFeatures(this.features_);
  this.changed();
};


/**
 * @private
 */
ol.source.Cluster.prototype.cluster_ = function() {
  if (this.resolution_ === undefined) {
    return;
  }
  this.features_.length = 0;
  var extent = ol.extent.createEmpty();
  var mapDistance = this.distance_ * this.resolution_;
  var features = this.source_.getFeatures();

  /**
   * @type {Object.<string, boolean>}
   */
  var clustered = {};

  for (var i = 0, ii = features.length; i < ii; i++) {
    var feature = features[i];
    if (!(goog.getUid(feature).toString() in clustered)) {
      var geometry = feature.getGeometry();
      goog.asserts.assert(geometry instanceof ol.geom.Point,
          'feature geometry is a ol.geom.Point instance');
      var coordinates = geometry.getCoordinates();
      ol.extent.createOrUpdateFromCoordinate(coordinates, extent);
      ol.extent.buffer(extent, mapDistance, extent);

      var neighbors = this.source_.getFeaturesInExtent(extent);
      goog.asserts.assert(neighbors.length >= 1, 'at least one neighbor found');
      neighbors = neighbors.filter(function(neighbor) {
        var uid = goog.getUid(neighbor).toString();
        if (!(uid in clustered)) {
          clustered[uid] = true;
          return true;
        } else {
          return false;
        }
      });
      this.features_.push(this.createCluster_(neighbors));
    }
  }
  goog.asserts.assert(
      goog.object.getCount(clustered) == this.source_.getFeatures().length,
      'number of clustered equals number of features in the source');
};


/**
 * @param {Array.<ol.Feature>} features Features
 * @return {ol.Feature} The cluster feature.
 * @private
 */
ol.source.Cluster.prototype.createCluster_ = function(features) {
  var length = features.length;
  var centroid = [0, 0];
  for (var i = 0; i < length; i++) {
    var geometry = features[i].getGeometry();
    goog.asserts.assert(geometry instanceof ol.geom.Point,
        'feature geometry is a ol.geom.Point instance');
    var coordinates = geometry.getCoordinates();
    ol.coordinate.add(centroid, coordinates);
  }
  ol.coordinate.scale(centroid, 1 / length);

  var cluster = new ol.Feature(new ol.geom.Point(centroid));
  cluster.set('features', features);
  return cluster;
};

goog.provide('ol.source.ImageMapGuide');

goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.object');
goog.require('goog.uri.utils');
goog.require('ol.Image');
goog.require('ol.ImageLoadFunctionType');
goog.require('ol.extent');
goog.require('ol.source.Image');


/**
 * @classdesc
 * Source for images from Mapguide servers
 *
 * @constructor
 * @fires ol.source.ImageEvent
 * @extends {ol.source.Image}
 * @param {olx.source.ImageMapGuideOptions} options Options.
 * @api stable
 */
ol.source.ImageMapGuide = function(options) {

  goog.base(this, {
    projection: options.projection,
    resolutions: options.resolutions
  });

  /**
   * @private
   * @type {?string}
   */
  this.crossOrigin_ =
      options.crossOrigin !== undefined ? options.crossOrigin : null;

  /**
   * @private
   * @type {number}
   */
  this.displayDpi_ = options.displayDpi !== undefined ?
      options.displayDpi : 96;

  /**
   * @private
   * @type {Object}
   */
  this.params_ = options.params !== undefined ? options.params : {};

  /**
   * @private
   * @type {string|undefined}
   */
  this.url_ = options.url;

  /**
   * @private
   * @type {ol.ImageLoadFunctionType}
   */
  this.imageLoadFunction_ = options.imageLoadFunction !== undefined ?
      options.imageLoadFunction : ol.source.Image.defaultImageLoadFunction;

  /**
   * @private
   * @type {boolean}
   */
  this.hidpi_ = options.hidpi !== undefined ? options.hidpi : true;

  /**
   * @private
   * @type {number}
   */
  this.metersPerUnit_ = options.metersPerUnit !== undefined ?
      options.metersPerUnit : 1;

  /**
   * @private
   * @type {number}
   */
  this.ratio_ = options.ratio !== undefined ? options.ratio : 1;

  /**
   * @private
   * @type {boolean}
   */
  this.useOverlay_ = options.useOverlay !== undefined ?
      options.useOverlay : false;

  /**
   * @private
   * @type {ol.Image}
   */
  this.image_ = null;

  /**
   * @private
   * @type {number}
   */
  this.renderedRevision_ = 0;

};
goog.inherits(ol.source.ImageMapGuide, ol.source.Image);


/**
 * Get the user-provided params, i.e. those passed to the constructor through
 * the "params" option, and possibly updated using the updateParams method.
 * @return {Object} Params.
 * @api stable
 */
ol.source.ImageMapGuide.prototype.getParams = function() {
  return this.params_;
};


/**
 * @inheritDoc
 */
ol.source.ImageMapGuide.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) {
  resolution = this.findNearestResolution(resolution);
  pixelRatio = this.hidpi_ ? pixelRatio : 1;

  var image = this.image_;
  if (image &&
      this.renderedRevision_ == this.getRevision() &&
      image.getResolution() == resolution &&
      image.getPixelRatio() == pixelRatio &&
      ol.extent.containsExtent(image.getExtent(), extent)) {
    return image;
  }

  if (this.ratio_ != 1) {
    extent = extent.slice();
    ol.extent.scaleFromCenter(extent, this.ratio_);
  }
  var width = ol.extent.getWidth(extent) / resolution;
  var height = ol.extent.getHeight(extent) / resolution;
  var size = [width * pixelRatio, height * pixelRatio];

  if (this.url_ !== undefined) {
    var imageUrl = this.getUrl(this.url_, this.params_, extent, size,
        projection);
    image = new ol.Image(extent, resolution, pixelRatio,
        this.getAttributions(), imageUrl, this.crossOrigin_,
        this.imageLoadFunction_);
    goog.events.listen(image, goog.events.EventType.CHANGE,
        this.handleImageChange, false, this);
  } else {
    image = null;
  }
  this.image_ = image;
  this.renderedRevision_ = this.getRevision();

  return image;
};


/**
 * Return the image load function of the source.
 * @return {ol.ImageLoadFunctionType} The image load function.
 * @api
 */
ol.source.ImageMapGuide.prototype.getImageLoadFunction = function() {
  return this.imageLoadFunction_;
};


/**
 * @param {ol.Extent} extent The map extents.
 * @param {ol.Size} size The viewport size.
 * @param {number} metersPerUnit The meters-per-unit value.
 * @param {number} dpi The display resolution.
 * @return {number} The computed map scale.
 */
ol.source.ImageMapGuide.getScale = function(extent, size, metersPerUnit, dpi) {
  var mcsW = ol.extent.getWidth(extent);
  var mcsH = ol.extent.getHeight(extent);
  var devW = size[0];
  var devH = size[1];
  var mpp = 0.0254 / dpi;
  if (devH * mcsW > devW * mcsH) {
    return mcsW * metersPerUnit / (devW * mpp); // width limited
  } else {
    return mcsH * metersPerUnit / (devH * mpp); // height limited
  }
};


/**
 * Update the user-provided params.
 * @param {Object} params Params.
 * @api stable
 */
ol.source.ImageMapGuide.prototype.updateParams = function(params) {
  goog.object.extend(this.params_, params);
  this.changed();
};


/**
 * @param {string} baseUrl The mapagent url.
 * @param {Object.<string, string|number>} params Request parameters.
 * @param {ol.Extent} extent Extent.
 * @param {ol.Size} size Size.
 * @param {ol.proj.Projection} projection Projection.
 * @return {string} The mapagent map image request URL.
 */
ol.source.ImageMapGuide.prototype.getUrl = function(baseUrl, params, extent, size, projection) {
  var scale = ol.source.ImageMapGuide.getScale(extent, size,
      this.metersPerUnit_, this.displayDpi_);
  var center = ol.extent.getCenter(extent);
  var baseParams = {
    'OPERATION': this.useOverlay_ ? 'GETDYNAMICMAPOVERLAYIMAGE' : 'GETMAPIMAGE',
    'VERSION': '2.0.0',
    'LOCALE': 'en',
    'CLIENTAGENT': 'ol.source.ImageMapGuide source',
    'CLIP': '1',
    'SETDISPLAYDPI': this.displayDpi_,
    'SETDISPLAYWIDTH': Math.round(size[0]),
    'SETDISPLAYHEIGHT': Math.round(size[1]),
    'SETVIEWSCALE': scale,
    'SETVIEWCENTERX': center[0],
    'SETVIEWCENTERY': center[1]
  };
  goog.object.extend(baseParams, params);
  return goog.uri.utils.appendParamsFromMap(baseUrl, baseParams);
};


/**
 * Set the image load function of the MapGuide source.
 * @param {ol.ImageLoadFunctionType} imageLoadFunction Image load function.
 * @api
 */
ol.source.ImageMapGuide.prototype.setImageLoadFunction = function(
    imageLoadFunction) {
  this.image_ = null;
  this.imageLoadFunction_ = imageLoadFunction;
  this.changed();
};

goog.provide('ol.source.ImageStatic');

goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('ol.Image');
goog.require('ol.ImageLoadFunctionType');
goog.require('ol.ImageState');
goog.require('ol.extent');
goog.require('ol.proj');
goog.require('ol.source.Image');


/**
 * @classdesc
 * A layer source for displaying a single, static image.
 *
 * @constructor
 * @extends {ol.source.Image}
 * @param {olx.source.ImageStaticOptions} options Options.
 * @api stable
 */
ol.source.ImageStatic = function(options) {

  var attributions = options.attributions !== undefined ?
      options.attributions : null;

  var imageExtent = options.imageExtent;

  var crossOrigin = options.crossOrigin !== undefined ?
      options.crossOrigin : null;

  var /** @type {ol.ImageLoadFunctionType} */ imageLoadFunction =
      options.imageLoadFunction !== undefined ?
      options.imageLoadFunction : ol.source.Image.defaultImageLoadFunction;

  goog.base(this, {
    attributions: attributions,
    logo: options.logo,
    projection: ol.proj.get(options.projection)
  });

  /**
   * @private
   * @type {ol.Image}
   */
  this.image_ = new ol.Image(imageExtent, undefined, 1, attributions,
      options.url, crossOrigin, imageLoadFunction);

  /**
   * @private
   * @type {ol.Size}
   */
  this.imageSize_ = options.imageSize ? options.imageSize : null;

  goog.events.listen(this.image_, goog.events.EventType.CHANGE,
      this.handleImageChange, false, this);

};
goog.inherits(ol.source.ImageStatic, ol.source.Image);


/**
 * @inheritDoc
 */
ol.source.ImageStatic.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) {
  if (ol.extent.intersects(extent, this.image_.getExtent())) {
    return this.image_;
  }
  return null;
};


/**
 * @inheritDoc
 */
ol.source.ImageStatic.prototype.handleImageChange = function(evt) {
  if (this.image_.getState() == ol.ImageState.LOADED) {
    var imageExtent = this.image_.getExtent();
    var image = this.image_.getImage();
    var imageWidth, imageHeight;
    if (this.imageSize_) {
      imageWidth = this.imageSize_[0];
      imageHeight = this.imageSize_[1];
    } else {
      imageWidth = image.width;
      imageHeight = image.height;
    }
    var resolution = ol.extent.getHeight(imageExtent) / imageHeight;
    var targetWidth = Math.ceil(ol.extent.getWidth(imageExtent) / resolution);
    if (targetWidth != imageWidth) {
      var canvas = /** @type {HTMLCanvasElement} */
          (document.createElement('canvas'));
      canvas.width = targetWidth;
      canvas.height = /** @type {number} */ (imageHeight);
      var context = canvas.getContext('2d');
      context.drawImage(image, 0, 0, imageWidth, imageHeight,
          0, 0, canvas.width, canvas.height);
      this.image_.setImage(canvas);
    }
  }
  goog.base(this, 'handleImageChange', evt);
};

goog.provide('ol.source.wms');
goog.provide('ol.source.wms.ServerType');


/**
 * Available server types: `'carmentaserver'`, `'geoserver'`, `'mapserver'`,
 *     `'qgis'`. These are servers that have vendor parameters beyond the WMS
 *     specification that OpenLayers can make use of.
 * @enum {string}
 * @api
 */
ol.source.wms.ServerType = {
  CARMENTA_SERVER: 'carmentaserver',
  GEOSERVER: 'geoserver',
  MAPSERVER: 'mapserver',
  QGIS: 'qgis'
};

// FIXME cannot be shared between maps with different projections

goog.provide('ol.source.ImageWMS');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.uri.utils');
goog.require('ol');
goog.require('ol.Image');
goog.require('ol.ImageLoadFunctionType');
goog.require('ol.extent');
goog.require('ol.proj');
goog.require('ol.source.Image');
goog.require('ol.source.wms');
goog.require('ol.source.wms.ServerType');


/**
 * @classdesc
 * Source for WMS servers providing single, untiled images.
 *
 * @constructor
 * @fires ol.source.ImageEvent
 * @extends {ol.source.Image}
 * @param {olx.source.ImageWMSOptions=} opt_options Options.
 * @api stable
 */
ol.source.ImageWMS = function(opt_options) {

  var options = opt_options || {};

  goog.base(this, {
    attributions: options.attributions,
    logo: options.logo,
    projection: options.projection,
    resolutions: options.resolutions
  });

  /**
   * @private
   * @type {?string}
   */
  this.crossOrigin_ =
      options.crossOrigin !== undefined ? options.crossOrigin : null;

  /**
   * @private
   * @type {string|undefined}
   */
  this.url_ = options.url;

  /**
   * @private
   * @type {ol.ImageLoadFunctionType}
   */
  this.imageLoadFunction_ = options.imageLoadFunction !== undefined ?
      options.imageLoadFunction : ol.source.Image.defaultImageLoadFunction;

  /**
   * @private
   * @type {Object}
   */
  this.params_ = options.params;

  /**
   * @private
   * @type {boolean}
   */
  this.v13_ = true;
  this.updateV13_();

  /**
   * @private
   * @type {ol.source.wms.ServerType|undefined}
   */
  this.serverType_ =
      /** @type {ol.source.wms.ServerType|undefined} */ (options.serverType);

  /**
   * @private
   * @type {boolean}
   */
  this.hidpi_ = options.hidpi !== undefined ? options.hidpi : true;

  /**
   * @private
   * @type {ol.Image}
   */
  this.image_ = null;

  /**
   * @private
   * @type {ol.Size}
   */
  this.imageSize_ = [0, 0];

  /**
   * @private
   * @type {number}
   */
  this.renderedRevision_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.ratio_ = options.ratio !== undefined ? options.ratio : 1.5;

};
goog.inherits(ol.source.ImageWMS, ol.source.Image);


/**
 * @const
 * @type {ol.Size}
 * @private
 */
ol.source.ImageWMS.GETFEATUREINFO_IMAGE_SIZE_ = [101, 101];


/**
 * Return the GetFeatureInfo URL for the passed coordinate, resolution, and
 * projection. Return `undefined` if the GetFeatureInfo URL cannot be
 * constructed.
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {number} resolution Resolution.
 * @param {ol.proj.ProjectionLike} projection Projection.
 * @param {!Object} params GetFeatureInfo params. `INFO_FORMAT` at least should
 *     be provided. If `QUERY_LAYERS` is not provided then the layers specified
 *     in the `LAYERS` parameter will be used. `VERSION` should not be
 *     specified here.
 * @return {string|undefined} GetFeatureInfo URL.
 * @api stable
 */
ol.source.ImageWMS.prototype.getGetFeatureInfoUrl = function(coordinate, resolution, projection, params) {

  goog.asserts.assert(!('VERSION' in params),
      'key VERSION is not allowed in params');

  if (this.url_ === undefined) {
    return undefined;
  }

  var extent = ol.extent.getForViewAndSize(
      coordinate, resolution, 0,
      ol.source.ImageWMS.GETFEATUREINFO_IMAGE_SIZE_);

  var baseParams = {
    'SERVICE': 'WMS',
    'VERSION': ol.DEFAULT_WMS_VERSION,
    'REQUEST': 'GetFeatureInfo',
    'FORMAT': 'image/png',
    'TRANSPARENT': true,
    'QUERY_LAYERS': this.params_['LAYERS']
  };
  goog.object.extend(baseParams, this.params_, params);

  var x = Math.floor((coordinate[0] - extent[0]) / resolution);
  var y = Math.floor((extent[3] - coordinate[1]) / resolution);
  baseParams[this.v13_ ? 'I' : 'X'] = x;
  baseParams[this.v13_ ? 'J' : 'Y'] = y;

  return this.getRequestUrl_(
      extent, ol.source.ImageWMS.GETFEATUREINFO_IMAGE_SIZE_,
      1, ol.proj.get(projection), baseParams);
};


/**
 * Get the user-provided params, i.e. those passed to the constructor through
 * the "params" option, and possibly updated using the updateParams method.
 * @return {Object} Params.
 * @api stable
 */
ol.source.ImageWMS.prototype.getParams = function() {
  return this.params_;
};


/**
 * @inheritDoc
 */
ol.source.ImageWMS.prototype.getImageInternal = function(extent, resolution, pixelRatio, projection) {

  if (this.url_ === undefined) {
    return null;
  }

  resolution = this.findNearestResolution(resolution);

  if (pixelRatio != 1 && (!this.hidpi_ || this.serverType_ === undefined)) {
    pixelRatio = 1;
  }

  extent = extent.slice();
  var centerX = (extent[0] + extent[2]) / 2;
  var centerY = (extent[1] + extent[3]) / 2;

  var imageResolution = resolution / pixelRatio;
  var imageWidth = ol.extent.getWidth(extent) / imageResolution;
  var imageHeight = ol.extent.getHeight(extent) / imageResolution;

  var image = this.image_;
  if (image &&
      this.renderedRevision_ == this.getRevision() &&
      image.getResolution() == resolution &&
      image.getPixelRatio() == pixelRatio &&
      ol.extent.containsExtent(image.getExtent(), extent)) {
    return image;
  }

  if (this.ratio_ != 1) {
    var halfWidth = this.ratio_ * ol.extent.getWidth(extent) / 2;
    var halfHeight = this.ratio_ * ol.extent.getHeight(extent) / 2;
    extent[0] = centerX - halfWidth;
    extent[1] = centerY - halfHeight;
    extent[2] = centerX + halfWidth;
    extent[3] = centerY + halfHeight;
  }

  var params = {
    'SERVICE': 'WMS',
    'VERSION': ol.DEFAULT_WMS_VERSION,
    'REQUEST': 'GetMap',
    'FORMAT': 'image/png',
    'TRANSPARENT': true
  };
  goog.object.extend(params, this.params_);

  this.imageSize_[0] = Math.ceil(imageWidth * this.ratio_);
  this.imageSize_[1] = Math.ceil(imageHeight * this.ratio_);

  var url = this.getRequestUrl_(extent, this.imageSize_, pixelRatio,
      projection, params);

  this.image_ = new ol.Image(extent, resolution, pixelRatio,
      this.getAttributions(), url, this.crossOrigin_, this.imageLoadFunction_);

  this.renderedRevision_ = this.getRevision();

  goog.events.listen(this.image_, goog.events.EventType.CHANGE,
      this.handleImageChange, false, this);

  return this.image_;

};


/**
 * Return the image load function of the source.
 * @return {ol.ImageLoadFunctionType} The image load function.
 * @api
 */
ol.source.ImageWMS.prototype.getImageLoadFunction = function() {
  return this.imageLoadFunction_;
};


/**
 * @param {ol.Extent} extent Extent.
 * @param {ol.Size} size Size.
 * @param {number} pixelRatio Pixel ratio.
 * @param {ol.proj.Projection} projection Projection.
 * @param {Object} params Params.
 * @return {string} Request URL.
 * @private
 */
ol.source.ImageWMS.prototype.getRequestUrl_ = function(extent, size, pixelRatio, projection, params) {

  goog.asserts.assert(this.url_ !== undefined, 'url is defined');

  params[this.v13_ ? 'CRS' : 'SRS'] = projection.getCode();

  if (!('STYLES' in this.params_)) {
    params['STYLES'] = new String('');
  }

  if (pixelRatio != 1) {
    switch (this.serverType_) {
      case ol.source.wms.ServerType.GEOSERVER:
        var dpi = (90 * pixelRatio + 0.5) | 0;
        if ('FORMAT_OPTIONS' in params) {
          params['FORMAT_OPTIONS'] += ';dpi:' + dpi;
        } else {
          params['FORMAT_OPTIONS'] = 'dpi:' + dpi;
        }
        break;
      case ol.source.wms.ServerType.MAPSERVER:
        params['MAP_RESOLUTION'] = 90 * pixelRatio;
        break;
      case ol.source.wms.ServerType.CARMENTA_SERVER:
      case ol.source.wms.ServerType.QGIS:
        params['DPI'] = 90 * pixelRatio;
        break;
      default:
        goog.asserts.fail('unknown serverType configured');
        break;
    }
  }

  params['WIDTH'] = size[0];
  params['HEIGHT'] = size[1];

  var axisOrientation = projection.getAxisOrientation();
  var bbox;
  if (this.v13_ && axisOrientation.substr(0, 2) == 'ne') {
    bbox = [extent[1], extent[0], extent[3], extent[2]];
  } else {
    bbox = extent;
  }
  params['BBOX'] = bbox.join(',');

  return goog.uri.utils.appendParamsFromMap(this.url_, params);
};


/**
 * Return the URL used for this WMS source.
 * @return {string|undefined} URL.
 * @api stable
 */
ol.source.ImageWMS.prototype.getUrl = function() {
  return this.url_;
};


/**
 * Set the image load function of the source.
 * @param {ol.ImageLoadFunctionType} imageLoadFunction Image load function.
 * @api
 */
ol.source.ImageWMS.prototype.setImageLoadFunction = function(
    imageLoadFunction) {
  this.image_ = null;
  this.imageLoadFunction_ = imageLoadFunction;
  this.changed();
};


/**
 * Set the URL to use for requests.
 * @param {string|undefined} url URL.
 * @api stable
 */
ol.source.ImageWMS.prototype.setUrl = function(url) {
  if (url != this.url_) {
    this.url_ = url;
    this.image_ = null;
    this.changed();
  }
};


/**
 * Update the user-provided params.
 * @param {Object} params Params.
 * @api stable
 */
ol.source.ImageWMS.prototype.updateParams = function(params) {
  goog.object.extend(this.params_, params);
  this.updateV13_();
  this.image_ = null;
  this.changed();
};


/**
 * @private
 */
ol.source.ImageWMS.prototype.updateV13_ = function() {
  var version =
      goog.object.get(this.params_, 'VERSION', ol.DEFAULT_WMS_VERSION);
  this.v13_ = goog.string.compareVersions(version, '1.3') >= 0;
};

goog.provide('ol.source.XYZ');

goog.require('ol.source.TileImage');


/**
 * @classdesc
 * Layer source for tile data with URLs in a set XYZ format that are
 * defined in a URL template. By default, this follows the widely-used
 * Google grid where `x` 0 and `y` 0 are in the top left. Grids like
 * TMS where `x` 0 and `y` 0 are in the bottom left can be used by
 * using the `{-y}` placeholder in the URL template, so long as the
 * source does not have a custom tile grid. In this case,
 * {@link ol.source.TileImage} can be used with a `tileUrlFunction`
 * such as:
 *
 *  tileUrlFunction: function(coordinate) {
 *    return 'http://mapserver.com/' + coordinate[0] + '/' +
 *        coordinate[1] + '/' + coordinate[2] + '.png';
 *    }
 *
 *
 * @constructor
 * @extends {ol.source.TileImage}
 * @param {olx.source.XYZOptions} options XYZ options.
 * @api stable
 */
ol.source.XYZ = function(options) {
  var projection = options.projection !== undefined ?
      options.projection : 'EPSG:3857';

  var tileGrid = options.tileGrid !== undefined ? options.tileGrid :
      ol.tilegrid.createXYZ({
        extent: ol.tilegrid.extentFromProjection(projection),
        maxZoom: options.maxZoom,
        tileSize: options.tileSize
      });

  goog.base(this, {
    attributions: options.attributions,
    crossOrigin: options.crossOrigin,
    logo: options.logo,
    opaque: options.opaque,
    projection: projection,
    reprojectionErrorThreshold: options.reprojectionErrorThreshold,
    tileGrid: tileGrid,
    tileLoadFunction: options.tileLoadFunction,
    tilePixelRatio: options.tilePixelRatio,
    tileUrlFunction: options.tileUrlFunction,
    url: options.url,
    urls: options.urls,
    wrapX: options.wrapX !== undefined ? options.wrapX : true
  });

};
goog.inherits(ol.source.XYZ, ol.source.TileImage);

goog.provide('ol.source.OSM');

goog.require('ol.Attribution');
goog.require('ol.source.XYZ');


/**
 * @classdesc
 * Layer source for the OpenStreetMap tile server.
 *
 * @constructor
 * @extends {ol.source.XYZ}
 * @param {olx.source.OSMOptions=} opt_options Open Street Map options.
 * @api stable
 */
ol.source.OSM = function(opt_options) {

  var options = opt_options || {};

  var attributions;
  if (options.attributions !== undefined) {
    attributions = options.attributions;
  } else {
    attributions = [ol.source.OSM.ATTRIBUTION];
  }

  var crossOrigin = options.crossOrigin !== undefined ?
      options.crossOrigin : 'anonymous';

  var url = options.url !== undefined ?
      options.url : 'https://{a-c}.tile.openstreetmap.org/{z}/{x}/{y}.png';

  goog.base(this, {
    attributions: attributions,
    crossOrigin: crossOrigin,
    opaque: true,
    maxZoom: options.maxZoom !== undefined ? options.maxZoom : 19,
    reprojectionErrorThreshold: options.reprojectionErrorThreshold,
    tileLoadFunction: options.tileLoadFunction,
    url: url,
    wrapX: options.wrapX
  });

};
goog.inherits(ol.source.OSM, ol.source.XYZ);


/**
 * The attribution containing a link to the OpenStreetMap Copyright and License
 * page.
 * @const
 * @type {ol.Attribution}
 * @api
 */
ol.source.OSM.ATTRIBUTION = new ol.Attribution({
  html: '&copy; ' +
      '<a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a> ' +
      'contributors.'
});

goog.provide('ol.source.MapQuest');

goog.require('goog.asserts');
goog.require('ol.Attribution');
goog.require('ol.source.OSM');
goog.require('ol.source.XYZ');


/**
 * @classdesc
 * Layer source for the MapQuest tile server.
 *
 * @constructor
 * @extends {ol.source.XYZ}
 * @param {olx.source.MapQuestOptions=} opt_options MapQuest options.
 * @api stable
 */
ol.source.MapQuest = function(opt_options) {

  var options = opt_options || {};
  goog.asserts.assert(options.layer in ol.source.MapQuestConfig,
      'known layer configured');

  var layerConfig = ol.source.MapQuestConfig[options.layer];

  /**
   * Layer. Possible values are `osm`, `sat`, and `hyb`.
   * @type {string}
   * @private
   */
  this.layer_ = options.layer;

  var url = options.url !== undefined ? options.url :
      'https://otile{1-4}-s.mqcdn.com/tiles/1.0.0/' +
      this.layer_ + '/{z}/{x}/{y}.jpg';

  goog.base(this, {
    attributions: layerConfig.attributions,
    crossOrigin: 'anonymous',
    logo: 'https://developer.mapquest.com/content/osm/mq_logo.png',
    maxZoom: layerConfig.maxZoom,
    reprojectionErrorThreshold: options.reprojectionErrorThreshold,
    opaque: layerConfig.opaque,
    tileLoadFunction: options.tileLoadFunction,
    url: url
  });

};
goog.inherits(ol.source.MapQuest, ol.source.XYZ);


/**
 * @const
 * @type {ol.Attribution}
 */
ol.source.MapQuest.TILE_ATTRIBUTION = new ol.Attribution({
  html: 'Tiles Courtesy of <a href="http://www.mapquest.com/">MapQuest</a>'
});


/**
 * @type {Object.<string, {maxZoom: number, opaque: boolean, attributions: (Array.<ol.Attribution>)}>}
 */
ol.source.MapQuestConfig = {
  'osm': {
    maxZoom: 19,
    opaque: true,
    attributions: [
      ol.source.MapQuest.TILE_ATTRIBUTION,
      ol.source.OSM.ATTRIBUTION
    ]
  },
  'sat': {
    maxZoom: 18,
    opaque: true,
    attributions: [
      ol.source.MapQuest.TILE_ATTRIBUTION,
      new ol.Attribution({
        html: 'Portions Courtesy NASA/JPL-Caltech and ' +
            'U.S. Depart. of Agriculture, Farm Service Agency'
      })
    ]
  },
  'hyb': {
    maxZoom: 18,
    opaque: false,
    attributions: [
      ol.source.MapQuest.TILE_ATTRIBUTION,
      ol.source.OSM.ATTRIBUTION
    ]
  }
};


/**
 * Get the layer of the source, either `osm`, `sat`, or `hyb`.
 * @return {string} Layer.
 * @api
 */
ol.source.MapQuest.prototype.getLayer = function() {
  return this.layer_;
};

goog.provide('ol.ext.pixelworks');
/** @typedef {function(*)} */
ol.ext.pixelworks;
(function() {
var exports = {};
var module = {exports: exports};
var define;
/**
 * @fileoverview
 * @suppress {accessControls, ambiguousFunctionDecl, checkDebuggerStatement, checkRegExp, checkTypes, checkVars, const, constantProperty, deprecated, duplicate, es5Strict, fileoverviewTags, missingProperties, nonStandardJsDocs, strictModuleDepCheck, suspiciousCode, undefinedNames, undefinedVars, unknownDefines, uselessCode, visibility}
 */
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.pixelworks = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
var Processor = _dereq_('./processor');

exports.Processor = Processor;

},{"./processor":2}],2:[function(_dereq_,module,exports){
/* eslint-disable dot-notation */

/**
 * Create a function for running operations.
 * @param {function(Array, Object):*} operation The operation.
 * @return {function(Object):ArrayBuffer} A function that takes an object with
 * buffers, meta, imageOps, width, and height properties and returns an array
 * buffer.
 */
function createMinion(operation) {
  return function(data) {
    // bracket notation for minification support
    var buffers = data['buffers'];
    var meta = data['meta'];
    var imageOps = data['imageOps'];
    var width = data['width'];
    var height = data['height'];

    var numBuffers = buffers.length;
    var numBytes = buffers[0].byteLength;
    var output, b;

    if (imageOps) {
      var images = new Array(numBuffers);
      for (b = 0; b < numBuffers; ++b) {
        images[b] = new ImageData(
            new Uint8ClampedArray(buffers[b]), width, height);
      }
      output = operation(images, meta).data;
    } else {
      output = new Uint8ClampedArray(numBytes);
      var arrays = new Array(numBuffers);
      var pixels = new Array(numBuffers);
      for (b = 0; b < numBuffers; ++b) {
        arrays[b] = new Uint8ClampedArray(buffers[b]);
        pixels[b] = [0, 0, 0, 0];
      }
      for (var i = 0; i < numBytes; i += 4) {
        for (var j = 0; j < numBuffers; ++j) {
          var array = arrays[j];
          pixels[j][0] = array[i];
          pixels[j][1] = array[i + 1];
          pixels[j][2] = array[i + 2];
          pixels[j][3] = array[i + 3];
        }
        var pixel = operation(pixels, meta);
        output[i] = pixel[0];
        output[i + 1] = pixel[1];
        output[i + 2] = pixel[2];
        output[i + 3] = pixel[3];
      }
    }
    return output.buffer;
  };
}

/**
 * Create a worker for running operations.
 * @param {Object} config Configuration.
 * @param {function(Object)} onMessage Called with a message event.
 * @return {Worker} The worker.
 */
function createWorker(config, onMessage) {
  var lib = Object.keys(config.lib || {}).map(function(name) {
    return 'var ' + name + ' = ' + config.lib[name].toString() + ';';
  });

  var lines = lib.concat([
    'var __minion__ = (' + createMinion.toString() + ')(',
        config.operation.toString(),
    ');',
    'self.addEventListener("message", function(__event__) {',
      'var buffer = __minion__(__event__.data);',
      'self.postMessage({buffer: buffer, meta: __event__.data.meta}, [buffer]);',
    '});'
  ]);

  var blob = new Blob(lines, {type: 'text/javascript'});
  var source = URL.createObjectURL(blob);
  var worker = new Worker(source);
  worker.addEventListener('message', onMessage);
  return worker;
}

/**
 * Create a faux worker for running operations.
 * @param {Object} config Configuration.
 * @param {function(Object)} onMessage Called with a message event.
 * @return {Object} The faux worker.
 */
function createFauxWorker(config, onMessage) {
  var minion = createMinion(config.operation);
  return {
    postMessage: function(data) {
      setTimeout(function() {
        onMessage({data: {buffer: minion(data), meta: data.meta}});
      }, 0);
    }
  };
}

/**
 * A processor runs pixel or image operations in workers.
 * @param {Object} config Configuration.
 */
function Processor(config) {
  this._imageOps = !!config.imageOps;
  var threads;
  if (config.threads === 0) {
    threads = 0;
  } else if (this._imageOps) {
    threads = 1;
  } else {
    threads = config.threads || 1;
  }
  var workers = [];
  if (threads) {
    for (var i = 0; i < threads; ++i) {
      workers[i] = createWorker(config, this._onWorkerMessage.bind(this, i));
    }
  } else {
    workers[0] = createFauxWorker(config, this._onWorkerMessage.bind(this, 0));
  }
  this._workers = workers;
  this._queue = [];
  this._maxQueueLength = config.queue || Infinity;
  this._running = 0;
  this._dataLookup = {};
  this._job = null;
}

/**
 * Run operation on input data.
 * @param {Array.<Array|ImageData>} inputs Array of pixels or image data
 *     (depending on the operation type).
 * @param {Object} meta A user data object.  This is passed to all operations
 *     and must be serializable.
 * @param {function(Error, ImageData, Object)} callback Called when work
 *     completes.  The first argument is any error.  The second is the ImageData
 *     generated by operations.  The third is the user data object.
 */
Processor.prototype.process = function(inputs, meta, callback) {
  this._enqueue({
    inputs: inputs,
    meta: meta,
    callback: callback
  });
  this._dispatch();
};

/**
 * Stop responding to any completed work and destroy the processor.
 */
Processor.prototype.destroy = function() {
  for (var key in this) {
    this[key] = null;
  }
  this._destroyed = true;
};

/**
 * Add a job to the queue.
 * @param {Object} job The job.
 */
Processor.prototype._enqueue = function(job) {
  this._queue.push(job);
  while (this._queue.length > this._maxQueueLength) {
    this._queue.shift().callback(null, null);
  }
};

/**
 * Dispatch a job.
 */
Processor.prototype._dispatch = function() {
  if (this._running === 0 && this._queue.length > 0) {
    var job = this._job = this._queue.shift();
    var width = job.inputs[0].width;
    var height = job.inputs[0].height;
    var buffers = job.inputs.map(function(input) {
      return input.data.buffer;
    });
    var threads = this._workers.length;
    this._running = threads;
    if (threads === 1) {
      this._workers[0].postMessage({
        'buffers': buffers,
        'meta': job.meta,
        'imageOps': this._imageOps,
        'width': width,
        'height': height
      }, buffers);
    } else {
      var length = job.inputs[0].data.length;
      var segmentLength = 4 * Math.ceil(length / 4 / threads);
      for (var i = 0; i < threads; ++i) {
        var offset = i * segmentLength;
        var slices = [];
        for (var j = 0, jj = buffers.length; j < jj; ++j) {
          slices.push(buffers[i].slice(offset, offset + segmentLength));
        }
        this._workers[i].postMessage({
          'buffers': slices,
          'meta': job.meta,
          'imageOps': this._imageOps,
          'width': width,
          'height': height
        }, slices);
      }
    }
  }
};

/**
 * Handle messages from the worker.
 * @param {number} index The worker index.
 * @param {Object} event The message event.
 */
Processor.prototype._onWorkerMessage = function(index, event) {
  if (this._destroyed) {
    return;
  }
  this._dataLookup[index] = event.data;
  --this._running;
  if (this._running === 0) {
    this._resolveJob();
  }
};

/**
 * Resolve a job.  If there are no more worker threads, the processor callback
 * will be called.
 */
Processor.prototype._resolveJob = function() {
  var job = this._job;
  var threads = this._workers.length;
  var data, meta;
  if (threads === 1) {
    data = new Uint8ClampedArray(this._dataLookup[0]['buffer']);
    meta = this._dataLookup[0]['meta'];
  } else {
    var length = job.inputs[0].data.length;
    data = new Uint8ClampedArray(length);
    meta = new Array(length);
    var segmentLength = 4 * Math.ceil(length / 4 / threads);
    for (var i = 0; i < threads; ++i) {
      var buffer = this._dataLookup[i]['buffer'];
      var offset = i * segmentLength;
      data.set(new Uint8ClampedArray(buffer), offset);
      meta[i] = this._dataLookup[i]['meta'];
    }
  }
  this._job = null;
  this._dataLookup = {};
  job.callback(null,
      new ImageData(data, job.inputs[0].width, job.inputs[0].height), meta);
  this._dispatch();
};

module.exports = Processor;

},{}]},{},[1])(1)
});
ol.ext.pixelworks = module.exports;
})();

goog.provide('ol.source.Raster');
goog.provide('ol.source.RasterEvent');
goog.provide('ol.source.RasterEventType');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.events.Event');
goog.require('goog.events.EventType');
goog.require('goog.object');
goog.require('goog.vec.Mat4');
goog.require('ol.ImageCanvas');
goog.require('ol.TileQueue');
goog.require('ol.dom');
goog.require('ol.ext.pixelworks');
goog.require('ol.extent');
goog.require('ol.layer.Image');
goog.require('ol.layer.Tile');
goog.require('ol.raster.OperationType');
goog.require('ol.renderer.canvas.ImageLayer');
goog.require('ol.renderer.canvas.TileLayer');
goog.require('ol.source.Image');
goog.require('ol.source.State');
goog.require('ol.source.Tile');


/**
 * @classdesc
 * A source that transforms data from any number of input sources using an array
 * of {@link ol.raster.Operation} functions to transform input pixel values into
 * output pixel values.
 *
 * @constructor
 * @extends {ol.source.Image}
 * @param {olx.source.RasterOptions} options Options.
 * @api
 */
ol.source.Raster = function(options) {

  /**
   * @private
   * @type {*}
   */
  this.worker_ = null;

  /**
   * @private
   * @type {ol.raster.OperationType}
   */
  this.operationType_ = options.operationType !== undefined ?
      options.operationType : ol.raster.OperationType.PIXEL;

  /**
   * @private
   * @type {number}
   */
  this.threads_ = options.threads !== undefined ? options.threads : 1;

  /**
   * @private
   * @type {Array.<ol.renderer.canvas.Layer>}
   */
  this.renderers_ = ol.source.Raster.createRenderers_(options.sources);

  for (var r = 0, rr = this.renderers_.length; r < rr; ++r) {
    goog.events.listen(this.renderers_[r], goog.events.EventType.CHANGE,
        this.changed, false, this);
  }

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.canvasContext_ = ol.dom.createCanvasContext2D();

  /**
   * @private
   * @type {ol.TileQueue}
   */
  this.tileQueue_ = new ol.TileQueue(
      function() {
        return 1;
      },
      this.changed.bind(this));

  var layerStatesArray = ol.source.Raster.getLayerStatesArray_(this.renderers_);
  var layerStates = {};
  for (var i = 0, ii = layerStatesArray.length; i < ii; ++i) {
    layerStates[goog.getUid(layerStatesArray[i].layer)] = layerStatesArray[i];
  }

  /**
   * The most recently rendered state.
   * @type {?ol.source.Raster.RenderedState}
   * @private
   */
  this.renderedState_ = null;

  /**
   * The most recently rendered image canvas.
   * @type {ol.ImageCanvas}
   * @private
   */
  this.renderedImageCanvas_ = null;

  /**
   * @private
   * @type {olx.FrameState}
   */
  this.frameState_ = {
    animate: false,
    attributions: {},
    coordinateToPixelMatrix: goog.vec.Mat4.createNumber(),
    extent: null,
    focus: null,
    index: 0,
    layerStates: layerStates,
    layerStatesArray: layerStatesArray,
    logos: {},
    pixelRatio: 1,
    pixelToCoordinateMatrix: goog.vec.Mat4.createNumber(),
    postRenderFunctions: [],
    size: [0, 0],
    skippedFeatureUids: {},
    tileQueue: this.tileQueue_,
    time: Date.now(),
    usedTiles: {},
    viewState: /** @type {olx.ViewState} */ ({
      rotation: 0
    }),
    viewHints: [],
    wantedTiles: {}
  };

  goog.base(this, {});

  if (options.operation !== undefined) {
    this.setOperation(options.operation, options.lib);
  }

};
goog.inherits(ol.source.Raster, ol.source.Image);


/**
 * Set the operation.
 * @param {ol.raster.Operation} operation New operation.
 * @param {Object=} opt_lib Functions that will be available to operations run
 *     in a worker.
 * @api
 */
ol.source.Raster.prototype.setOperation = function(operation, opt_lib) {
  this.worker_ = new ol.ext.pixelworks.Processor({
    operation: operation,
    imageOps: this.operationType_ === ol.raster.OperationType.IMAGE,
    queue: 1,
    lib: opt_lib,
    threads: this.threads_
  });
  this.changed();
};


/**
 * Update the stored frame state.
 * @param {ol.Extent} extent The view extent (in map units).
 * @param {number} resolution The view resolution.
 * @param {ol.proj.Projection} projection The view projection.
 * @return {olx.FrameState} The updated frame state.
 * @private
 */
ol.source.Raster.prototype.updateFrameState_ = function(extent, resolution, projection) {

  var frameState = /** @type {olx.FrameState} */ (
      goog.object.clone(this.frameState_));

  frameState.viewState = /** @type {olx.ViewState} */ (
      goog.object.clone(frameState.viewState));

  var center = ol.extent.getCenter(extent);
  var width = Math.round(ol.extent.getWidth(extent) / resolution);
  var height = Math.round(ol.extent.getHeight(extent) / resolution);

  frameState.extent = extent;
  frameState.focus = ol.extent.getCenter(extent);
  frameState.size[0] = width;
  frameState.size[1] = height;

  var viewState = frameState.viewState;
  viewState.center = center;
  viewState.projection = projection;
  viewState.resolution = resolution;
  return frameState;
};


/**
 * Determine if the most recently rendered image canvas is dirty.
 * @param {ol.Extent} extent The requested extent.
 * @param {number} resolution The requested resolution.
 * @return {boolean} The image is dirty.
 * @private
 */
ol.source.Raster.prototype.isDirty_ = function(extent, resolution) {
  var state = this.renderedState_;
  return !state ||
      this.getRevision() !== state.revision ||
      resolution !== state.resolution ||
      !ol.extent.equals(extent, state.extent);
};


/**
 * @inheritDoc
 */
ol.source.Raster.prototype.getImage = function(extent, resolution, pixelRatio, projection) {

  if (!this.allSourcesReady_()) {
    return null;
  }

  if (!this.isDirty_(extent, resolution)) {
    return this.renderedImageCanvas_;
  }

  var context = this.canvasContext_;
  var canvas = context.canvas;

  var width = Math.round(ol.extent.getWidth(extent) / resolution);
  var height = Math.round(ol.extent.getHeight(extent) / resolution);

  if (width !== canvas.width ||
      height !== canvas.height) {
    canvas.width = width;
    canvas.height = height;
  }

  var frameState = this.updateFrameState_(extent, resolution, projection);

  var imageCanvas = new ol.ImageCanvas(
      extent, resolution, 1, this.getAttributions(), canvas,
      this.composeFrame_.bind(this, frameState));

  this.renderedImageCanvas_ = imageCanvas;

  this.renderedState_ = {
    extent: extent,
    resolution: resolution,
    revision: this.getRevision()
  };

  return imageCanvas;
};


/**
 * Determine if all sources are ready.
 * @return {boolean} All sources are ready.
 * @private
 */
ol.source.Raster.prototype.allSourcesReady_ = function() {
  var ready = true;
  var source;
  for (var i = 0, ii = this.renderers_.length; i < ii; ++i) {
    source = this.renderers_[i].getLayer().getSource();
    if (source.getState() !== ol.source.State.READY) {
      ready = false;
      break;
    }
  }
  return ready;
};


/**
 * Compose the frame.  This renders data from all sources, runs pixel-wise
 * operations, and renders the result to the stored canvas context.
 * @param {olx.FrameState} frameState The frame state.
 * @param {function(Error)} callback Called when composition is complete.
 * @private
 */
ol.source.Raster.prototype.composeFrame_ = function(frameState, callback) {
  var len = this.renderers_.length;
  var imageDatas = new Array(len);
  for (var i = 0; i < len; ++i) {
    var imageData = ol.source.Raster.getImageData_(
        this.renderers_[i], frameState, frameState.layerStatesArray[i]);
    if (imageData) {
      imageDatas[i] = imageData;
    } else {
      // image not yet ready
      return;
    }
  }

  var data = {};
  this.dispatchEvent(new ol.source.RasterEvent(
      ol.source.RasterEventType.BEFOREOPERATIONS, frameState, data));

  this.worker_.process(imageDatas, data,
      this.onWorkerComplete_.bind(this, frameState, callback));

  frameState.tileQueue.loadMoreTiles(16, 16);
};


/**
 * Called when pixel processing is complete.
 * @param {olx.FrameState} frameState The frame state.
 * @param {function(Error)} callback Called when rendering is complete.
 * @param {Error} err Any error during processing.
 * @param {ImageData} output The output image data.
 * @param {Object} data The user data.
 * @private
 */
ol.source.Raster.prototype.onWorkerComplete_ = function(frameState, callback, err, output, data) {
  if (err) {
    callback(err);
    return;
  }
  if (!output) {
    // job aborted
    return;
  }

  this.dispatchEvent(new ol.source.RasterEvent(
      ol.source.RasterEventType.AFTEROPERATIONS, frameState, data));

  var resolution = frameState.viewState.resolution / frameState.pixelRatio;
  if (!this.isDirty_(frameState.extent, resolution)) {
    this.canvasContext_.putImageData(output, 0, 0);
  }

  callback(null);
};


/**
 * Get image data from a renderer.
 * @param {ol.renderer.canvas.Layer} renderer Layer renderer.
 * @param {olx.FrameState} frameState The frame state.
 * @param {ol.layer.LayerState} layerState The layer state.
 * @return {ImageData} The image data.
 * @private
 */
ol.source.Raster.getImageData_ = function(renderer, frameState, layerState) {
  renderer.prepareFrame(frameState, layerState);
  // We should be able to call renderer.composeFrame(), but this is inefficient
  // for tiled sources (we've already rendered to an intermediate canvas in the
  // prepareFrame call and we don't need to render again to the output canvas).
  // TODO: make all canvas renderers render to a single canvas
  var image = renderer.getImage();
  if (!image) {
    return null;
  }
  var imageTransform = renderer.getImageTransform();
  var dx = Math.round(goog.vec.Mat4.getElement(imageTransform, 0, 3));
  var dy = Math.round(goog.vec.Mat4.getElement(imageTransform, 1, 3));
  var width = frameState.size[0];
  var height = frameState.size[1];
  if (image instanceof Image) {
    if (!ol.source.Raster.context_) {
      ol.source.Raster.context_ = ol.dom.createCanvasContext2D(width, height);
    } else {
      var canvas = ol.source.Raster.context_.canvas;
      if (canvas.width !== width || canvas.height !== height) {
        ol.source.Raster.context_ = ol.dom.createCanvasContext2D(width, height);
      } else {
        ol.source.Raster.context_.clearRect(0, 0, width, height);
      }
    }
    var dw = Math.round(
        image.width * goog.vec.Mat4.getElement(imageTransform, 0, 0));
    var dh = Math.round(
        image.height * goog.vec.Mat4.getElement(imageTransform, 1, 1));
    ol.source.Raster.context_.drawImage(image, dx, dy, dw, dh);
    return ol.source.Raster.context_.getImageData(0, 0, width, height);
  } else {
    return image.getContext('2d').getImageData(-dx, -dy, width, height);
  }
};


/**
 * A reusable canvas context.
 * @type {CanvasRenderingContext2D}
 * @private
 */
ol.source.Raster.context_ = null;


/**
 * Get a list of layer states from a list of renderers.
 * @param {Array.<ol.renderer.canvas.Layer>} renderers Layer renderers.
 * @return {Array.<ol.layer.LayerState>} The layer states.
 * @private
 */
ol.source.Raster.getLayerStatesArray_ = function(renderers) {
  return renderers.map(function(renderer) {
    return renderer.getLayer().getLayerState();
  });
};


/**
 * Create renderers for all sources.
 * @param {Array.<ol.source.Source>} sources The sources.
 * @return {Array.<ol.renderer.canvas.Layer>} Array of layer renderers.
 * @private
 */
ol.source.Raster.createRenderers_ = function(sources) {
  var len = sources.length;
  var renderers = new Array(len);
  for (var i = 0; i < len; ++i) {
    renderers[i] = ol.source.Raster.createRenderer_(sources[i]);
  }
  return renderers;
};


/**
 * Create a renderer for the provided source.
 * @param {ol.source.Source} source The source.
 * @return {ol.renderer.canvas.Layer} The renderer.
 * @private
 */
ol.source.Raster.createRenderer_ = function(source) {
  var renderer = null;
  if (source instanceof ol.source.Tile) {
    renderer = ol.source.Raster.createTileRenderer_(source);
  } else if (source instanceof ol.source.Image) {
    renderer = ol.source.Raster.createImageRenderer_(source);
  } else {
    goog.asserts.fail('Unsupported source type: ' + source);
  }
  return renderer;
};


/**
 * Create an image renderer for the provided source.
 * @param {ol.source.Image} source The source.
 * @return {ol.renderer.canvas.Layer} The renderer.
 * @private
 */
ol.source.Raster.createImageRenderer_ = function(source) {
  var layer = new ol.layer.Image({source: source});
  return new ol.renderer.canvas.ImageLayer(layer);
};


/**
 * Create a tile renderer for the provided source.
 * @param {ol.source.Tile} source The source.
 * @return {ol.renderer.canvas.Layer} The renderer.
 * @private
 */
ol.source.Raster.createTileRenderer_ = function(source) {
  var layer = new ol.layer.Tile({source: source});
  return new ol.renderer.canvas.TileLayer(layer);
};


/**
 * @typedef {{revision: number,
 *            resolution: number,
 *            extent: ol.Extent}}
 */
ol.source.Raster.RenderedState;


/**
 * @classdesc
 * Events emitted by {@link ol.source.Raster} instances are instances of this
 * type.
 *
 * @constructor
 * @extends {goog.events.Event}
 * @implements {oli.source.RasterEvent}
 * @param {string} type Type.
 * @param {olx.FrameState} frameState The frame state.
 * @param {Object} data An object made available to operations.
 */
ol.source.RasterEvent = function(type, frameState, data) {
  goog.base(this, type);

  /**
   * The raster extent.
   * @type {ol.Extent}
   * @api
   */
  this.extent = frameState.extent;

  /**
   * The pixel resolution (map units per pixel).
   * @type {number}
   * @api
   */
  this.resolution = frameState.viewState.resolution / frameState.pixelRatio;

  /**
   * An object made available to all operations.  This can be used by operations
   * as a storage object (e.g. for calculating statistics).
   * @type {Object}
   * @api
   */
  this.data = data;

};
goog.inherits(ol.source.RasterEvent, goog.events.Event);


/**
 * @enum {string}
 */
ol.source.RasterEventType = {
  /**
   * Triggered before operations are run.
   * @event ol.source.RasterEvent#beforeoperations
   * @api
   */
  BEFOREOPERATIONS: 'beforeoperations',

  /**
   * Triggered after operations are run.
   * @event ol.source.RasterEvent#afteroperations
   * @api
   */
  AFTEROPERATIONS: 'afteroperations'
};

goog.provide('ol.source.Stamen');

goog.require('goog.asserts');
goog.require('ol.Attribution');
goog.require('ol.source.OSM');
goog.require('ol.source.XYZ');


/**
 * @type {Object.<string, {extension: string, opaque: boolean}>}
 */
ol.source.StamenLayerConfig = {
  'terrain': {
    extension: 'jpg',
    opaque: true
  },
  'terrain-background': {
    extension: 'jpg',
    opaque: true
  },
  'terrain-labels': {
    extension: 'png',
    opaque: false
  },
  'terrain-lines': {
    extension: 'png',
    opaque: false
  },
  'toner-background': {
    extension: 'png',
    opaque: true
  },
  'toner': {
    extension: 'png',
    opaque: true
  },
  'toner-hybrid': {
    extension: 'png',
    opaque: false
  },
  'toner-labels': {
    extension: 'png',
    opaque: false
  },
  'toner-lines': {
    extension: 'png',
    opaque: false
  },
  'toner-lite': {
    extension: 'png',
    opaque: true
  },
  'watercolor': {
    extension: 'jpg',
    opaque: true
  }
};


/**
 * @type {Object.<string, {minZoom: number, maxZoom: number}>}
 */
ol.source.StamenProviderConfig = {
  'terrain': {
    minZoom: 4,
    maxZoom: 18
  },
  'toner': {
    minZoom: 0,
    maxZoom: 20
  },
  'watercolor': {
    minZoom: 3,
    maxZoom: 16
  }
};


/**
 * @classdesc
 * Layer source for the Stamen tile server.
 *
 * @constructor
 * @extends {ol.source.XYZ}
 * @param {olx.source.StamenOptions} options Stamen options.
 * @api stable
 */
ol.source.Stamen = function(options) {

  var i = options.layer.indexOf('-');
  var provider = i == -1 ? options.layer : options.layer.slice(0, i);
  goog.asserts.assert(provider in ol.source.StamenProviderConfig,
      'known provider configured');
  var providerConfig = ol.source.StamenProviderConfig[provider];

  goog.asserts.assert(options.layer in ol.source.StamenLayerConfig,
      'known layer configured');
  var layerConfig = ol.source.StamenLayerConfig[options.layer];

  var url = options.url !== undefined ? options.url :
      'https://stamen-tiles-{a-d}.a.ssl.fastly.net/' + options.layer +
      '/{z}/{x}/{y}.' + layerConfig.extension;

  goog.base(this, {
    attributions: ol.source.Stamen.ATTRIBUTIONS,
    crossOrigin: 'anonymous',
    maxZoom: providerConfig.maxZoom,
    // FIXME uncomment the following when tilegrid supports minZoom
    //minZoom: providerConfig.minZoom,
    opaque: layerConfig.opaque,
    reprojectionErrorThreshold: options.reprojectionErrorThreshold,
    tileLoadFunction: options.tileLoadFunction,
    url: url
  });

};
goog.inherits(ol.source.Stamen, ol.source.XYZ);


/**
 * @const
 * @type {Array.<ol.Attribution>}
 */
ol.source.Stamen.ATTRIBUTIONS = [
  new ol.Attribution({
    html: 'Map tiles by <a href="http://stamen.com/">Stamen Design</a>, ' +
        'under <a href="http://creativecommons.org/licenses/by/3.0/">CC BY' +
        ' 3.0</a>.'
  }),
  ol.source.OSM.ATTRIBUTION
];

goog.provide('ol.source.TileArcGISRest');

goog.require('goog.asserts');
goog.require('goog.math');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.uri.utils');
goog.require('ol');
goog.require('ol.TileCoord');
goog.require('ol.extent');
goog.require('ol.proj');
goog.require('ol.size');
goog.require('ol.source.TileImage');
goog.require('ol.tilecoord');


/**
 * @classdesc
 * Layer source for tile data from ArcGIS Rest services. Map and Image
 * Services are supported.
 *
 * For cached ArcGIS services, better performance is available using the
 * {@link ol.source.XYZ} data source.
 *
 * @constructor
 * @extends {ol.source.TileImage}
 * @param {olx.source.TileArcGISRestOptions=} opt_options Tile ArcGIS Rest
 *     options.
 * @api
 */
ol.source.TileArcGISRest = function(opt_options) {

  var options = opt_options || {};

  var params = options.params !== undefined ? options.params : {};

  goog.base(this, {
    attributions: options.attributions,
    crossOrigin: options.crossOrigin,
    logo: options.logo,
    projection: options.projection,
    reprojectionErrorThreshold: options.reprojectionErrorThreshold,
    tileGrid: options.tileGrid,
    tileLoadFunction: options.tileLoadFunction,
    url: options.url,
    urls: options.urls,
    wrapX: options.wrapX !== undefined ? options.wrapX : true
  });

  /**
   * @private
   * @type {Object}
   */
  this.params_ = params;

  /**
   * @private
   * @type {ol.Extent}
   */
  this.tmpExtent_ = ol.extent.createEmpty();

};
goog.inherits(ol.source.TileArcGISRest, ol.source.TileImage);


/**
 * Get the user-provided params, i.e. those passed to the constructor through
 * the "params" option, and possibly updated using the updateParams method.
 * @return {Object} Params.
 * @api
 */
ol.source.TileArcGISRest.prototype.getParams = function() {
  return this.params_;
};


/**
 * @param {ol.TileCoord} tileCoord Tile coordinate.
 * @param {ol.Size} tileSize Tile size.
 * @param {ol.Extent} tileExtent Tile extent.
 * @param {number} pixelRatio Pixel ratio.
 * @param {ol.proj.Projection} projection Projection.
 * @param {Object} params Params.
 * @return {string|undefined} Request URL.
 * @private
 */
ol.source.TileArcGISRest.prototype.getRequestUrl_ = function(tileCoord, tileSize, tileExtent,
        pixelRatio, projection, params) {

  var urls = this.urls;
  if (!urls) {
    return undefined;
  }

  // ArcGIS Server only wants the numeric portion of the projection ID.
  var srid = projection.getCode().split(':').pop();

  params['SIZE'] = tileSize[0] + ',' + tileSize[1];
  params['BBOX'] = tileExtent.join(',');
  params['BBOXSR'] = srid;
  params['IMAGESR'] = srid;
  params['DPI'] = Math.round(
      params['DPI'] ? params['DPI'] * pixelRatio : 90 * pixelRatio
      );

  var url;
  if (urls.length == 1) {
    url = urls[0];
  } else {
    var index = goog.math.modulo(ol.tilecoord.hash(tileCoord), urls.length);
    url = urls[index];
  }

  if (!goog.string.endsWith(url, '/')) {
    url = url + '/';
  }

  // If a MapServer, use export. If an ImageServer, use exportImage.
  if (goog.string.endsWith(url, 'MapServer/')) {
    url = url + 'export';
  } else if (goog.string.endsWith(url, 'ImageServer/')) {
    url = url + 'exportImage';
  } else {
    goog.asserts.fail('Unknown Rest Service', url);
  }

  return goog.uri.utils.appendParamsFromMap(url, params);
};


/**
 * @inheritDoc
 */
ol.source.TileArcGISRest.prototype.getTilePixelRatio = function(pixelRatio) {
  return pixelRatio;
};


/**
 * @inheritDoc
 */
ol.source.TileArcGISRest.prototype.fixedTileUrlFunction = function(tileCoord, pixelRatio, projection) {

  var tileGrid = this.getTileGrid();
  if (!tileGrid) {
    tileGrid = this.getTileGridForProjection(projection);
  }

  if (tileGrid.getResolutions().length <= tileCoord[0]) {
    return undefined;
  }

  var tileExtent = tileGrid.getTileCoordExtent(
      tileCoord, this.tmpExtent_);
  var tileSize = ol.size.toSize(
      tileGrid.getTileSize(tileCoord[0]), this.tmpSize);

  if (pixelRatio != 1) {
    tileSize = ol.size.scale(tileSize, pixelRatio, this.tmpSize);
  }

  // Apply default params and override with user specified values.
  var baseParams = {
    'F': 'image',
    'FORMAT': 'PNG32',
    'TRANSPARENT': true
  };
  goog.object.extend(baseParams, this.params_);

  return this.getRequestUrl_(tileCoord, tileSize, tileExtent,
      pixelRatio, projection, baseParams);
};


/**
 * Update the user-provided params.
 * @param {Object} params Params.
 * @api stable
 */
ol.source.TileArcGISRest.prototype.updateParams = function(params) {
  goog.object.extend(this.params_, params);
  this.changed();
};

goog.provide('ol.source.TileDebug');

goog.require('ol.Tile');
goog.require('ol.TileCoord');
goog.require('ol.TileState');
goog.require('ol.dom');
goog.require('ol.size');
goog.require('ol.source.Tile');


/**
 * @constructor
 * @extends {ol.Tile}
 * @param {ol.TileCoord} tileCoord Tile coordinate.
 * @param {ol.Size} tileSize Tile size.
 * @param {string} text Text.
 * @private
 */
ol.DebugTile_ = function(tileCoord, tileSize, text) {

  goog.base(this, tileCoord, ol.TileState.LOADED);

  /**
   * @private
   * @type {ol.Size}
   */
  this.tileSize_ = tileSize;

  /**
   * @private
   * @type {string}
   */
  this.text_ = text;

  /**
   * @private
   * @type {Object.<number, HTMLCanvasElement>}
   */
  this.canvasByContext_ = {};

};
goog.inherits(ol.DebugTile_, ol.Tile);


/**
 * Get the image element for this tile.
 * @param {Object=} opt_context Optional context. Only used by the DOM
 *     renderer.
 * @return {HTMLCanvasElement} Image.
 */
ol.DebugTile_.prototype.getImage = function(opt_context) {
  var key = opt_context !== undefined ? goog.getUid(opt_context) : -1;
  if (key in this.canvasByContext_) {
    return this.canvasByContext_[key];
  } else {

    var tileSize = this.tileSize_;
    var context = ol.dom.createCanvasContext2D(tileSize[0], tileSize[1]);

    context.strokeStyle = 'black';
    context.strokeRect(0.5, 0.5, tileSize[0] + 0.5, tileSize[1] + 0.5);

    context.fillStyle = 'black';
    context.textAlign = 'center';
    context.textBaseline = 'middle';
    context.font = '24px sans-serif';
    context.fillText(this.text_, tileSize[0] / 2, tileSize[1] / 2);

    this.canvasByContext_[key] = context.canvas;
    return context.canvas;

  }
};


/**
 * @classdesc
 * A pseudo tile source, which does not fetch tiles from a server, but renders
 * a grid outline for the tile grid/projection along with the coordinates for
 * each tile. See examples/canvas-tiles for an example.
 *
 * Uses Canvas context2d, so requires Canvas support.
 *
 * @constructor
 * @extends {ol.source.Tile}
 * @param {olx.source.TileDebugOptions} options Debug tile options.
 * @api
 */
ol.source.TileDebug = function(options) {

  goog.base(this, {
    opaque: false,
    projection: options.projection,
    tileGrid: options.tileGrid,
    wrapX: options.wrapX !== undefined ? options.wrapX : true
  });

};
goog.inherits(ol.source.TileDebug, ol.source.Tile);


/**
 * @inheritDoc
 */
ol.source.TileDebug.prototype.getTile = function(z, x, y) {
  var tileCoordKey = this.getKeyZXY(z, x, y);
  if (this.tileCache.containsKey(tileCoordKey)) {
    return /** @type {!ol.DebugTile_} */ (this.tileCache.get(tileCoordKey));
  } else {
    var tileSize = ol.size.toSize(this.tileGrid.getTileSize(z));
    var tileCoord = [z, x, y];
    var textTileCoord = this.getTileCoordForTileUrlFunction(tileCoord);
    var text = !textTileCoord ? '' :
        this.getTileCoordForTileUrlFunction(textTileCoord).toString();
    var tile = new ol.DebugTile_(tileCoord, tileSize, text);
    this.tileCache.set(tileCoordKey, tile);
    return tile;
  }
};

// Copyright 2013 The Closure Library Authors. All Rights Reserved.
//
// 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.

/**
 * @fileoverview This file contain classes that add support for cross-domain XHR
 * requests (see http://www.w3.org/TR/cors/). Most modern browsers are able to
 * use a regular XMLHttpRequest for that, but IE 8 use XDomainRequest object
 * instead. This file provides an adapter from this object to a goog.net.XhrLike
 * and a factory to allow using this with a goog.net.XhrIo instance.
 *
 * IE 7 and older versions are not supported (given that they do not support
 * CORS requests).
 */
goog.provide('goog.net.CorsXmlHttpFactory');
goog.provide('goog.net.IeCorsXhrAdapter');

goog.require('goog.net.HttpStatus');
goog.require('goog.net.XhrLike');
goog.require('goog.net.XmlHttp');
goog.require('goog.net.XmlHttpFactory');



/**
 * A factory of XML http request objects that supports cross domain requests.
 * This class should be instantiated and passed as the parameter of a
 * goog.net.XhrIo constructor to allow cross-domain requests in every browser.
 *
 * @extends {goog.net.XmlHttpFactory}
 * @constructor
 * @final
 */
goog.net.CorsXmlHttpFactory = function() {
  goog.net.XmlHttpFactory.call(this);
};
goog.inherits(goog.net.CorsXmlHttpFactory, goog.net.XmlHttpFactory);


/** @override */
goog.net.CorsXmlHttpFactory.prototype.createInstance = function() {
  var xhr = new XMLHttpRequest();
  if (('withCredentials' in xhr)) {
    return xhr;
  } else if (typeof XDomainRequest != 'undefined') {
    return new goog.net.IeCorsXhrAdapter();
  } else {
    throw Error('Unsupported browser');
  }
};


/** @override */
goog.net.CorsXmlHttpFactory.prototype.internalGetOptions = function() {
  return {};
};



/**
 * An adapter around Internet Explorer's XDomainRequest object that makes it
 * look like a standard XMLHttpRequest. This can be used instead of
 * XMLHttpRequest to support CORS.
 *
 * @implements {goog.net.XhrLike}
 * @constructor
 * @struct
 * @final
 */
goog.net.IeCorsXhrAdapter = function() {
  /**
   * The underlying XDomainRequest used to make the HTTP request.
   * @type {!XDomainRequest}
   * @private
   */
  this.xdr_ = new XDomainRequest();

  /**
   * The simulated ready state.
   * @type {number}
   */
  this.readyState = goog.net.XmlHttp.ReadyState.UNINITIALIZED;

  /**
   * The simulated ready state change callback function.
   * @type {Function}
   */
  this.onreadystatechange = null;

  /**
   * The simulated response text parameter.
   * @type {?string}
   */
  this.responseText = null;

  /**
   * The simulated status code
   * @type {number}
   */
  this.status = -1;

  /** @override */
  this.responseXML = null;

  /** @override */
  this.statusText = null;

  this.xdr_.onload = goog.bind(this.handleLoad_, this);
  this.xdr_.onerror = goog.bind(this.handleError_, this);
  this.xdr_.onprogress = goog.bind(this.handleProgress_, this);
  this.xdr_.ontimeout = goog.bind(this.handleTimeout_, this);
};


/**
 * Opens a connection to the provided URL.
 * @param {string} method The HTTP method to use. Valid methods include GET and
 *     POST.
 * @param {string} url The URL to contact. The authority of this URL must match
 *     the authority of the current page's URL (e.g. http or https).
 * @param {?boolean=} opt_async Whether the request is asynchronous, defaulting
 *     to true. XDomainRequest does not support syncronous requests, so setting
 *     it to false will actually raise an exception.
 * @override
 */
goog.net.IeCorsXhrAdapter.prototype.open = function(method, url, opt_async) {
  if (goog.isDefAndNotNull(opt_async) && (!opt_async)) {
    throw new Error('Only async requests are supported.');
  }
  this.xdr_.open(method, url);
};


/**
 * Sends the request to the remote server. Before calling this function, always
 * call {@link open}.
 * @param {(ArrayBuffer|ArrayBufferView|Blob|Document|FormData|null|string)=}
 *     opt_content The content to send as POSTDATA, if any. Only string data is
 *     supported by this implementation.
 * @override
 */
goog.net.IeCorsXhrAdapter.prototype.send = function(opt_content) {
  if (opt_content) {
    if (typeof opt_content == 'string') {
      this.xdr_.send(opt_content);
    } else {
      throw new Error('Only string data is supported');
    }
  } else {
    this.xdr_.send();
  }
};


/**
 * @override
 */
goog.net.IeCorsXhrAdapter.prototype.abort = function() {
  this.xdr_.abort();
};


/**
 * Sets a request header to send to the remote server. Because this
 * implementation does not support request headers, this function does nothing.
 * @param {string} key The name of the HTTP header to set. Ignored.
 * @param {string} value The value to set for the HTTP header. Ignored.
 * @override
 */
goog.net.IeCorsXhrAdapter.prototype.setRequestHeader = function(key, value) {
  // Unsupported; ignore the header.
};


/**
 * Returns the value of the response header identified by key. This
 * implementation only supports the 'content-type' header.
 * @param {string} key The request header to fetch. If this parameter is set to
 *     'content-type' (case-insensitive), this function returns the value of
 *     the 'content-type' request header. If this parameter is set to any other
 *     value, this function always returns an empty string.
 * @return {string} The value of the response header, or an empty string if key
 *     is not 'content-type' (case-insensitive).
 * @override
 */
goog.net.IeCorsXhrAdapter.prototype.getResponseHeader = function(key) {
  if (key.toLowerCase() == 'content-type') {
    return this.xdr_.contentType;
  }
  return '';
};


/**
 * Handles a request that has fully loaded successfully.
 * @private
 */
goog.net.IeCorsXhrAdapter.prototype.handleLoad_ = function() {
  // IE only calls onload if the status is 200, so the status code must be OK.
  this.status = goog.net.HttpStatus.OK;
  this.responseText = this.xdr_.responseText;
  this.setReadyState_(goog.net.XmlHttp.ReadyState.COMPLETE);
};


/**
 * Handles a request that has failed to load.
 * @private
 */
goog.net.IeCorsXhrAdapter.prototype.handleError_ = function() {
  // IE doesn't tell us what the status code actually is (other than the fact
  // that it is not 200), so simulate an INTERNAL_SERVER_ERROR.
  this.status = goog.net.HttpStatus.INTERNAL_SERVER_ERROR;
  this.responseText = null;
  this.setReadyState_(goog.net.XmlHttp.ReadyState.COMPLETE);
};


/**
 * Handles a request that timed out.
 * @private
 */
goog.net.IeCorsXhrAdapter.prototype.handleTimeout_ = function() {
  this.handleError_();
};


/**
 * Handles a request that is in the process of loading.
 * @private
 */
goog.net.IeCorsXhrAdapter.prototype.handleProgress_ = function() {
  // IE only calls onprogress if the status is 200, so the status code must be
  // OK.
  this.status = goog.net.HttpStatus.OK;
  this.setReadyState_(goog.net.XmlHttp.ReadyState.LOADING);
};


/**
 * Sets this XHR's ready state and fires the onreadystatechange listener (if one
 * is set).
 * @param {number} readyState The new ready state.
 * @private
 */
goog.net.IeCorsXhrAdapter.prototype.setReadyState_ = function(readyState) {
  this.readyState = readyState;
  if (this.onreadystatechange) {
    this.onreadystatechange();
  }
};


/**
 * Returns the response headers from the server. This implemntation only returns
 * the 'content-type' header.
 * @return {string} The headers returned from the server.
 * @override
 */
goog.net.IeCorsXhrAdapter.prototype.getAllResponseHeaders = function() {
  return 'content-type: ' + this.xdr_.contentType;
};

// FIXME check order of async callbacks

/**
 * @see http://mapbox.com/developers/api/
 */

goog.provide('ol.source.TileJSON');
goog.provide('ol.tilejson');

goog.require('goog.asserts');
goog.require('goog.events');
goog.require('goog.net.CorsXmlHttpFactory');
goog.require('goog.net.EventType');
goog.require('goog.net.Jsonp');
goog.require('goog.net.XhrIo');
goog.require('ol.Attribution');
goog.require('ol.TileRange');
goog.require('ol.TileUrlFunction');
goog.require('ol.extent');
goog.require('ol.proj');
goog.require('ol.source.State');
goog.require('ol.source.TileImage');


/**
 * @classdesc
 * Layer source for tile data in TileJSON format.
 *
 * @constructor
 * @extends {ol.source.TileImage}
 * @param {olx.source.TileJSONOptions} options TileJSON options.
 * @api stable
 */
ol.source.TileJSON = function(options) {

  goog.base(this, {
    attributions: options.attributions,
    crossOrigin: options.crossOrigin,
    projection: ol.proj.get('EPSG:3857'),
    reprojectionErrorThreshold: options.reprojectionErrorThreshold,
    state: ol.source.State.LOADING,
    tileLoadFunction: options.tileLoadFunction,
    wrapX: options.wrapX !== undefined ? options.wrapX : true
  });

  if (options.jsonp) {
    var request = new goog.net.Jsonp(options.url);
    request.send(undefined, this.handleTileJSONResponse.bind(this),
        this.handleTileJSONError.bind(this));
  } else {
    var xhr = new goog.net.XhrIo(new goog.net.CorsXmlHttpFactory());
    goog.events.listen(xhr, goog.net.EventType.COMPLETE, function(e) {
      if (xhr.isSuccess()) {
        var response = /** @type {TileJSON} */(xhr.getResponseJson());
        this.handleTileJSONResponse(response);
      } else {
        this.handleTileJSONError();
      }
      xhr.dispose();
    }, false, this);
    xhr.send(options.url);
  }

};
goog.inherits(ol.source.TileJSON, ol.source.TileImage);


/**
 * @protected
 * @param {TileJSON} tileJSON Tile JSON.
 */
ol.source.TileJSON.prototype.handleTileJSONResponse = function(tileJSON) {

  var epsg4326Projection = ol.proj.get('EPSG:4326');

  var sourceProjection = this.getProjection();
  var extent;
  if (tileJSON.bounds !== undefined) {
    var transform = ol.proj.getTransformFromProjections(
        epsg4326Projection, sourceProjection);
    extent = ol.extent.applyTransform(tileJSON.bounds, transform);
  }

  if (tileJSON.scheme !== undefined) {
    goog.asserts.assert(tileJSON.scheme == 'xyz', 'tileJSON-scheme is "xyz"');
  }
  var minZoom = tileJSON.minzoom || 0;
  var maxZoom = tileJSON.maxzoom || 22;
  var tileGrid = ol.tilegrid.createXYZ({
    extent: ol.tilegrid.extentFromProjection(sourceProjection),
    maxZoom: maxZoom,
    minZoom: minZoom
  });
  this.tileGrid = tileGrid;

  this.tileUrlFunction =
      ol.TileUrlFunction.createFromTemplates(tileJSON.tiles, tileGrid);

  if (tileJSON.attribution !== undefined && !this.getAttributions()) {
    var attributionExtent = extent !== undefined ?
        extent : epsg4326Projection.getExtent();
    /** @type {Object.<string, Array.<ol.TileRange>>} */
    var tileRanges = {};
    var z, zKey;
    for (z = minZoom; z <= maxZoom; ++z) {
      zKey = z.toString();
      tileRanges[zKey] =
          [tileGrid.getTileRangeForExtentAndZ(attributionExtent, z)];
    }
    this.setAttributions([
      new ol.Attribution({
        html: tileJSON.attribution,
        tileRanges: tileRanges
      })
    ]);
  }

  this.setState(ol.source.State.READY);

};


/**
 * @protected
 */
ol.source.TileJSON.prototype.handleTileJSONError = function() {
  this.setState(ol.source.State.ERROR);
};

goog.provide('ol.source.TileUTFGrid');

goog.require('goog.asserts');
goog.require('goog.async.nextTick');
goog.require('goog.events');
goog.require('goog.events.EventType');
goog.require('goog.net.Jsonp');
goog.require('ol.Attribution');
goog.require('ol.Tile');
goog.require('ol.TileState');
goog.require('ol.TileUrlFunction');
goog.require('ol.extent');
goog.require('ol.proj');
goog.require('ol.source.State');
goog.require('ol.source.Tile');


/**
 * @classdesc
 * Layer source for UTFGrid interaction data loaded from TileJSON format.
 *
 * @constructor
 * @extends {ol.source.Tile}
 * @param {olx.source.TileUTFGridOptions} options Source options.
 * @api
 */
ol.source.TileUTFGrid = function(options) {
  goog.base(this, {
    projection: ol.proj.get('EPSG:3857'),
    state: ol.source.State.LOADING
  });

  /**
   * @private
   * @type {boolean}
   */
  this.preemptive_ = options.preemptive !== undefined ?
      options.preemptive : true;

  /**
   * @private
   * @type {!ol.TileUrlFunctionType}
   */
  this.tileUrlFunction_ = ol.TileUrlFunction.nullTileUrlFunction;

  /**
   * @private
   * @type {string|undefined}
   */
  this.template_ = undefined;

  var request = new goog.net.Jsonp(options.url);
  request.send(undefined, this.handleTileJSONResponse.bind(this));
};
goog.inherits(ol.source.TileUTFGrid, ol.source.Tile);


/**
 * Return the template from TileJSON.
 * @return {string|undefined} The template from TileJSON.
 * @api
 */
ol.source.TileUTFGrid.prototype.getTemplate = function() {
  return this.template_;
};


/**
 * Calls the callback (synchronously by default) with the available data
 * for given coordinate and resolution (or `null` if not yet loaded or
 * in case of an error).
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {number} resolution Resolution.
 * @param {function(this: T, Object)} callback Callback.
 * @param {T=} opt_this The object to use as `this` in the callback.
 * @param {boolean=} opt_request If `true` the callback is always async.
 *                               The tile data is requested if not yet loaded.
 * @template T
 * @api
 */
ol.source.TileUTFGrid.prototype.forDataAtCoordinateAndResolution = function(
    coordinate, resolution, callback, opt_this, opt_request) {
  if (this.tileGrid) {
    var tileCoord = this.tileGrid.getTileCoordForCoordAndResolution(
        coordinate, resolution);
    var tile = /** @type {!ol.source.TileUTFGridTile_} */(this.getTile(
        tileCoord[0], tileCoord[1], tileCoord[2], 1, this.getProjection()));
    tile.forDataAtCoordinate(coordinate, callback, opt_this, opt_request);
  } else {
    if (opt_request === true) {
      goog.async.nextTick(function() {
        callback.call(opt_this, null);
      });
    } else {
      callback.call(opt_this, null);
    }
  }
};


/**
 * TODO: very similar to ol.source.TileJSON#handleTileJSONResponse
 * @protected
 * @param {TileJSON} tileJSON Tile JSON.
 */
ol.source.TileUTFGrid.prototype.handleTileJSONResponse = function(tileJSON) {

  var epsg4326Projection = ol.proj.get('EPSG:4326');

  var sourceProjection = this.getProjection();
  var extent;
  if (tileJSON.bounds !== undefined) {
    var transform = ol.proj.getTransformFromProjections(
        epsg4326Projection, sourceProjection);
    extent = ol.extent.applyTransform(tileJSON.bounds, transform);
  }

  if (tileJSON.scheme !== undefined) {
    goog.asserts.assert(tileJSON.scheme == 'xyz', 'tileJSON-scheme is "xyz"');
  }
  var minZoom = tileJSON.minzoom || 0;
  var maxZoom = tileJSON.maxzoom || 22;
  var tileGrid = ol.tilegrid.createXYZ({
    extent: ol.tilegrid.extentFromProjection(sourceProjection),
    maxZoom: maxZoom,
    minZoom: minZoom
  });
  this.tileGrid = tileGrid;

  this.template_ = tileJSON.template;

  var grids = tileJSON.grids;
  if (!grids) {
    this.setState(ol.source.State.ERROR);
    return;
  }

  this.tileUrlFunction_ =
      ol.TileUrlFunction.createFromTemplates(grids, tileGrid);

  if (tileJSON.attribution !== undefined) {
    var attributionExtent = extent !== undefined ?
        extent : epsg4326Projection.getExtent();
    /** @type {Object.<string, Array.<ol.TileRange>>} */
    var tileRanges = {};
    var z, zKey;
    for (z = minZoom; z <= maxZoom; ++z) {
      zKey = z.toString();
      tileRanges[zKey] =
          [tileGrid.getTileRangeForExtentAndZ(attributionExtent, z)];
    }
    this.setAttributions([
      new ol.Attribution({
        html: tileJSON.attribution,
        tileRanges: tileRanges
      })
    ]);
  }

  this.setState(ol.source.State.READY);

};


/**
 * @inheritDoc
 */
ol.source.TileUTFGrid.prototype.getTile = function(z, x, y, pixelRatio, projection) {
  var tileCoordKey = this.getKeyZXY(z, x, y);
  if (this.tileCache.containsKey(tileCoordKey)) {
    return /** @type {!ol.Tile} */ (this.tileCache.get(tileCoordKey));
  } else {
    goog.asserts.assert(projection, 'argument projection is truthy');
    var tileCoord = [z, x, y];
    var urlTileCoord =
        this.getTileCoordForTileUrlFunction(tileCoord, projection);
    var tileUrl = this.tileUrlFunction_(urlTileCoord, pixelRatio, projection);
    var tile = new ol.source.TileUTFGridTile_(
        tileCoord,
        tileUrl !== undefined ? ol.TileState.IDLE : ol.TileState.EMPTY,
        tileUrl !== undefined ? tileUrl : '',
        this.tileGrid.getTileCoordExtent(tileCoord),
        this.preemptive_);
    this.tileCache.set(tileCoordKey, tile);
    return tile;
  }
};


/**
 * @inheritDoc
 */
ol.source.TileUTFGrid.prototype.useTile = function(z, x, y) {
  var tileCoordKey = this.getKeyZXY(z, x, y);
  if (this.tileCache.containsKey(tileCoordKey)) {
    this.tileCache.get(tileCoordKey);
  }
};


/**
 * @constructor
 * @extends {ol.Tile}
 * @param {ol.TileCoord} tileCoord Tile coordinate.
 * @param {ol.TileState} state State.
 * @param {string} src Image source URI.
 * @param {ol.Extent} extent Extent of the tile.
 * @param {boolean} preemptive Load the tile when visible (before it's needed).
 * @private
 */
ol.source.TileUTFGridTile_ = function(tileCoord, state, src, extent, preemptive) {

  goog.base(this, tileCoord, state);

  /**
   * @private
   * @type {string}
   */
  this.src_ = src;

  /**
   * @private
   * @type {ol.Extent}
   */
  this.extent_ = extent;

  /**
   * @private
   * @type {boolean}
   */
  this.preemptive_ = preemptive;

  /**
   * @private
   * @type {Array.<string>}
   */
  this.grid_ = null;

  /**
   * @private
   * @type {Array.<string>}
   */
  this.keys_ = null;

  /**
   * @private
   * @type {Object.<string, Object>|undefined}
   */
  this.data_ = null;
};
goog.inherits(ol.source.TileUTFGridTile_, ol.Tile);


/**
 * Get the image element for this tile.
 * @param {Object=} opt_context Optional context. Only used for the DOM
 *     renderer.
 * @return {Image} Image.
 */
ol.source.TileUTFGridTile_.prototype.getImage = function(opt_context) {
  return null;
};


/**
 * Synchronously returns data at given coordinate (if available).
 * @param {ol.Coordinate} coordinate Coordinate.
 * @return {Object} The data.
 */
ol.source.TileUTFGridTile_.prototype.getData = function(coordinate) {
  if (!this.grid_ || !this.keys_ || !this.data_) {
    return null;
  }
  var xRelative = (coordinate[0] - this.extent_[0]) /
      (this.extent_[2] - this.extent_[0]);
  var yRelative = (coordinate[1] - this.extent_[1]) /
      (this.extent_[3] - this.extent_[1]);

  var row = this.grid_[Math.floor((1 - yRelative) * this.grid_.length)];

  if (!goog.isString(row)) {
    return null;
  }

  var code = row.charCodeAt(Math.floor(xRelative * row.length));
  if (code >= 93) {
    code--;
  }
  if (code >= 35) {
    code--;
  }
  code -= 32;

  return (code in this.keys_) ? this.data_[this.keys_[code]] : null;
};


/**
 * Calls the callback (synchronously by default) with the available data
 * for given coordinate (or `null` if not yet loaded).
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {function(this: T, Object)} callback Callback.
 * @param {T=} opt_this The object to use as `this` in the callback.
 * @param {boolean=} opt_request If `true` the callback is always async.
 *                               The tile data is requested if not yet loaded.
 * @template T
 */
ol.source.TileUTFGridTile_.prototype.forDataAtCoordinate = function(coordinate, callback, opt_this, opt_request) {
  if (this.state == ol.TileState.IDLE && opt_request === true) {
    goog.events.listenOnce(this, goog.events.EventType.CHANGE, function(e) {
      callback.call(opt_this, this.getData(coordinate));
    }, false, this);
    this.loadInternal_();
  } else {
    if (opt_request === true) {
      goog.async.nextTick(function() {
        callback.call(opt_this, this.getData(coordinate));
      }, this);
    } else {
      callback.call(opt_this, this.getData(coordinate));
    }
  }
};


/**
 * @inheritDoc
 */
ol.source.TileUTFGridTile_.prototype.getKey = function() {
  return this.src_;
};


/**
 * @private
 */
ol.source.TileUTFGridTile_.prototype.handleError_ = function() {
  this.state = ol.TileState.ERROR;
  this.changed();
};


/**
 * @param {!UTFGridJSON} json UTFGrid data.
 * @private
 */
ol.source.TileUTFGridTile_.prototype.handleLoad_ = function(json) {
  this.grid_ = json.grid;
  this.keys_ = json.keys;
  this.data_ = json.data;

  this.state = ol.TileState.EMPTY;
  this.changed();
};


/**
 * @private
 */
ol.source.TileUTFGridTile_.prototype.loadInternal_ = function() {
  if (this.state == ol.TileState.IDLE) {
    this.state = ol.TileState.LOADING;
    var request = new goog.net.Jsonp(this.src_);
    request.send(undefined, this.handleLoad_.bind(this),
                 this.handleError_.bind(this));
  }
};


/**
 * Load not yet loaded URI.
 */
ol.source.TileUTFGridTile_.prototype.load = function() {
  if (this.preemptive_) {
    this.loadInternal_();
  }
};

// FIXME add minZoom support
// FIXME add date line wrap (tile coord transform)
// FIXME cannot be shared between maps with different projections

goog.provide('ol.source.TileWMS');

goog.require('goog.asserts');
goog.require('goog.math');
goog.require('goog.object');
goog.require('goog.string');
goog.require('goog.uri.utils');
goog.require('ol');
goog.require('ol.TileCoord');
goog.require('ol.extent');
goog.require('ol.proj');
goog.require('ol.size');
goog.require('ol.source.TileImage');
goog.require('ol.source.wms');
goog.require('ol.source.wms.ServerType');
goog.require('ol.tilecoord');


/**
 * @classdesc
 * Layer source for tile data from WMS servers.
 *
 * @constructor
 * @extends {ol.source.TileImage}
 * @param {olx.source.TileWMSOptions=} opt_options Tile WMS options.
 * @api stable
 */
ol.source.TileWMS = function(opt_options) {

  var options = opt_options || {};

  var params = options.params !== undefined ? options.params : {};

  var transparent = goog.object.get(params, 'TRANSPARENT', true);

  goog.base(this, {
    attributions: options.attributions,
    crossOrigin: options.crossOrigin,
    logo: options.logo,
    opaque: !transparent,
    projection: options.projection,
    reprojectionErrorThreshold: options.reprojectionErrorThreshold,
    tileGrid: options.tileGrid,
    tileLoadFunction: options.tileLoadFunction,
    url: options.url,
    urls: options.urls,
    wrapX: options.wrapX !== undefined ? options.wrapX : true
  });

  /**
   * @private
   * @type {number}
   */
  this.gutter_ = options.gutter !== undefined ? options.gutter : 0;

  /**
   * @private
   * @type {Object}
   */
  this.params_ = params;

  /**
   * @private
   * @type {string}
   */
  this.paramsKey_ = '';
  this.resetParamsKey_();

  /**
   * @private
   * @type {boolean}
   */
  this.v13_ = true;

  /**
   * @private
   * @type {ol.source.wms.ServerType|undefined}
   */
  this.serverType_ =
      /** @type {ol.source.wms.ServerType|undefined} */ (options.serverType);

  /**
   * @private
   * @type {boolean}
   */
  this.hidpi_ = options.hidpi !== undefined ? options.hidpi : true;

  /**
   * @private
   * @type {string}
   */
  this.coordKeyPrefix_ = '';
  this.resetCoordKeyPrefix_();

  /**
   * @private
   * @type {ol.Extent}
   */
  this.tmpExtent_ = ol.extent.createEmpty();

  this.updateV13_();

};
goog.inherits(ol.source.TileWMS, ol.source.TileImage);


/**
 * Return the GetFeatureInfo URL for the passed coordinate, resolution, and
 * projection. Return `undefined` if the GetFeatureInfo URL cannot be
 * constructed.
 * @param {ol.Coordinate} coordinate Coordinate.
 * @param {number} resolution Resolution.
 * @param {ol.proj.ProjectionLike} projection Projection.
 * @param {!Object} params GetFeatureInfo params. `INFO_FORMAT` at least should
 *     be provided. If `QUERY_LAYERS` is not provided then the layers specified
 *     in the `LAYERS` parameter will be used. `VERSION` should not be
 *     specified here.
 * @return {string|undefined} GetFeatureInfo URL.
 * @api stable
 */
ol.source.TileWMS.prototype.getGetFeatureInfoUrl = function(coordinate, resolution, projection, params) {

  goog.asserts.assert(!('VERSION' in params),
      'key VERSION is not allowed in params');

  var projectionObj = ol.proj.get(projection);

  var tileGrid = this.getTileGrid();
  if (!tileGrid) {
    tileGrid = this.getTileGridForProjection(projectionObj);
  }

  var tileCoord = tileGrid.getTileCoordForCoordAndResolution(
      coordinate, resolution);

  if (tileGrid.getResolutions().length <= tileCoord[0]) {
    return undefined;
  }

  var tileResolution = tileGrid.getResolution(tileCoord[0]);
  var tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent_);
  var tileSize = ol.size.toSize(
      tileGrid.getTileSize(tileCoord[0]), this.tmpSize);

  var gutter = this.gutter_;
  if (gutter !== 0) {
    tileSize = ol.size.buffer(tileSize, gutter, this.tmpSize);
    tileExtent = ol.extent.buffer(tileExtent,
        tileResolution * gutter, tileExtent);
  }

  var baseParams = {
    'SERVICE': 'WMS',
    'VERSION': ol.DEFAULT_WMS_VERSION,
    'REQUEST': 'GetFeatureInfo',
    'FORMAT': 'image/png',
    'TRANSPARENT': true,
    'QUERY_LAYERS': this.params_['LAYERS']
  };
  goog.object.extend(baseParams, this.params_, params);

  var x = Math.floor((coordinate[0] - tileExtent[0]) / tileResolution);
  var y = Math.floor((tileExtent[3] - coordinate[1]) / tileResolution);

  baseParams[this.v13_ ? 'I' : 'X'] = x;
  baseParams[this.v13_ ? 'J' : 'Y'] = y;

  return this.getRequestUrl_(tileCoord, tileSize, tileExtent,
      1, projectionObj, baseParams);
};


/**
 * @inheritDoc
 */
ol.source.TileWMS.prototype.getGutter = function() {
  return this.gutter_;
};


/**
 * @inheritDoc
 */
ol.source.TileWMS.prototype.getKeyParams = function() {
  return this.paramsKey_;
};


/**
 * @inheritDoc
 */
ol.source.TileWMS.prototype.getKeyZXY = function(z, x, y) {
  return this.coordKeyPrefix_ + goog.base(this, 'getKeyZXY', z, x, y);
};


/**
 * Get the user-provided params, i.e. those passed to the constructor through
 * the "params" option, and possibly updated using the updateParams method.
 * @return {Object} Params.
 * @api stable
 */
ol.source.TileWMS.prototype.getParams = function() {
  return this.params_;
};


/**
 * @param {ol.TileCoord} tileCoord Tile coordinate.
 * @param {ol.Size} tileSize Tile size.
 * @param {ol.Extent} tileExtent Tile extent.
 * @param {number} pixelRatio Pixel ratio.
 * @param {ol.proj.Projection} projection Projection.
 * @param {Object} params Params.
 * @return {string|undefined} Request URL.
 * @private
 */
ol.source.TileWMS.prototype.getRequestUrl_ = function(tileCoord, tileSize, tileExtent,
        pixelRatio, projection, params) {

  var urls = this.urls;
  if (!urls) {
    return undefined;
  }

  params['WIDTH'] = tileSize[0];
  params['HEIGHT'] = tileSize[1];

  params[this.v13_ ? 'CRS' : 'SRS'] = projection.getCode();

  if (!('STYLES' in this.params_)) {
    params['STYLES'] = new String('');
  }

  if (pixelRatio != 1) {
    switch (this.serverType_) {
      case ol.source.wms.ServerType.GEOSERVER:
        var dpi = (90 * pixelRatio + 0.5) | 0;
        if ('FORMAT_OPTIONS' in params) {
          params['FORMAT_OPTIONS'] += ';dpi:' + dpi;
        } else {
          params['FORMAT_OPTIONS'] = 'dpi:' + dpi;
        }
        break;
      case ol.source.wms.ServerType.MAPSERVER:
        params['MAP_RESOLUTION'] = 90 * pixelRatio;
        break;
      case ol.source.wms.ServerType.CARMENTA_SERVER:
      case ol.source.wms.ServerType.QGIS:
        params['DPI'] = 90 * pixelRatio;
        break;
      default:
        goog.asserts.fail('unknown serverType configured');
        break;
    }
  }

  var axisOrientation = projection.getAxisOrientation();
  var bbox = tileExtent;
  if (this.v13_ && axisOrientation.substr(0, 2) == 'ne') {
    var tmp;
    tmp = tileExtent[0];
    bbox[0] = tileExtent[1];
    bbox[1] = tmp;
    tmp = tileExtent[2];
    bbox[2] = tileExtent[3];
    bbox[3] = tmp;
  }
  params['BBOX'] = bbox.join(',');

  var url;
  if (urls.length == 1) {
    url = urls[0];
  } else {
    var index = goog.math.modulo(ol.tilecoord.hash(tileCoord), urls.length);
    url = urls[index];
  }
  return goog.uri.utils.appendParamsFromMap(url, params);
};


/**
 * @inheritDoc
 */
ol.source.TileWMS.prototype.getTilePixelRatio = function(pixelRatio) {
  return (!this.hidpi_ || this.serverType_ === undefined) ? 1 : pixelRatio;
};


/**
 * @private
 */
ol.source.TileWMS.prototype.resetCoordKeyPrefix_ = function() {
  var i = 0;
  var res = [];

  if (this.urls) {
    var j, jj;
    for (j = 0, jj = this.urls.length; j < jj; ++j) {
      res[i++] = this.urls[j];
    }
  }

  this.coordKeyPrefix_ = res.join('#');
};


/**
 * @private
 */
ol.source.TileWMS.prototype.resetParamsKey_ = function() {
  var i = 0;
  var res = [];
  for (var key in this.params_) {
    res[i++] = key + '-' + this.params_[key];
  }
  this.paramsKey_ = res.join('/');
};


/**
 * @inheritDoc
 */
ol.source.TileWMS.prototype.fixedTileUrlFunction = function(tileCoord, pixelRatio, projection) {

  var tileGrid = this.getTileGrid();
  if (!tileGrid) {
    tileGrid = this.getTileGridForProjection(projection);
  }

  if (tileGrid.getResolutions().length <= tileCoord[0]) {
    return undefined;
  }

  if (pixelRatio != 1 && (!this.hidpi_ || this.serverType_ === undefined)) {
    pixelRatio = 1;
  }

  var tileResolution = tileGrid.getResolution(tileCoord[0]);
  var tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent_);
  var tileSize = ol.size.toSize(
      tileGrid.getTileSize(tileCoord[0]), this.tmpSize);

  var gutter = this.gutter_;
  if (gutter !== 0) {
    tileSize = ol.size.buffer(tileSize, gutter, this.tmpSize);
    tileExtent = ol.extent.buffer(tileExtent,
        tileResolution * gutter, tileExtent);
  }

  if (pixelRatio != 1) {
    tileSize = ol.size.scale(tileSize, pixelRatio, this.tmpSize);
  }

  var baseParams = {
    'SERVICE': 'WMS',
    'VERSION': ol.DEFAULT_WMS_VERSION,
    'REQUEST': 'GetMap',
    'FORMAT': 'image/png',
    'TRANSPARENT': true
  };
  goog.object.extend(baseParams, this.params_);

  return this.getRequestUrl_(tileCoord, tileSize, tileExtent,
      pixelRatio, projection, baseParams);
};


/**
 * Update the user-provided params.
 * @param {Object} params Params.
 * @api stable
 */
ol.source.TileWMS.prototype.updateParams = function(params) {
  goog.object.extend(this.params_, params);
  this.resetCoordKeyPrefix_();
  this.resetParamsKey_();
  this.updateV13_();
  this.changed();
};


/**
 * @private
 */
ol.source.TileWMS.prototype.updateV13_ = function() {
  var version =
      goog.object.get(this.params_, 'VERSION', ol.DEFAULT_WMS_VERSION);
  this.v13_ = goog.string.compareVersions(version, '1.3') >= 0;
};

goog.provide('ol.tilegrid.WMTS');

goog.require('goog.asserts');
goog.require('ol.proj');
goog.require('ol.tilegrid.TileGrid');


/**
 * @classdesc
 * Set the grid pattern for sources accessing WMTS tiled-image servers.
 *
 * @constructor
 * @extends {ol.tilegrid.TileGrid}
 * @param {olx.tilegrid.WMTSOptions} options WMTS options.
 * @struct
 * @api
 */
ol.tilegrid.WMTS = function(options) {

  goog.asserts.assert(
      options.resolutions.length == options.matrixIds.length,
      'options resolutions and matrixIds must have equal length (%s == %s)',
      options.resolutions.length, options.matrixIds.length);

  /**
   * @private
   * @type {!Array.<string>}
   */
  this.matrixIds_ = options.matrixIds;
  // FIXME: should the matrixIds become optionnal?

  goog.base(this, {
    extent: options.extent,
    origin: options.origin,
    origins: options.origins,
    resolutions: options.resolutions,
    tileSize: options.tileSize,
    tileSizes: options.tileSizes,
    sizes: options.sizes
  });

};
goog.inherits(ol.tilegrid.WMTS, ol.tilegrid.TileGrid);


/**
 * @param {number} z Z.
 * @return {string} MatrixId..
 */
ol.tilegrid.WMTS.prototype.getMatrixId = function(z) {
  goog.asserts.assert(0 <= z && z < this.matrixIds_.length,
      'attempted to retrive matrixId for illegal z (%s)', z);
  return this.matrixIds_[z];
};


/**
 * Get the list of matrix identifiers.
 * @return {Array.<string>} MatrixIds.
 * @api
 */
ol.tilegrid.WMTS.prototype.getMatrixIds = function() {
  return this.matrixIds_;
};


/**
 * Create a tile grid from a WMTS capabilities matrix set.
 * @param {Object} matrixSet An object representing a matrixSet in the
 *     capabilities document.
 * @param {ol.Extent=} opt_extent An optional extent to restrict the tile
 *     ranges the server provides.
 * @return {ol.tilegrid.WMTS} WMTS tileGrid instance.
 * @api
 */
ol.tilegrid.WMTS.createFromCapabilitiesMatrixSet = function(matrixSet, opt_extent) {

  /** @type {!Array.<number>} */
  var resolutions = [];
  /** @type {!Array.<string>} */
  var matrixIds = [];
  /** @type {!Array.<ol.Coordinate>} */
  var origins = [];
  /** @type {!Array.<ol.Size>} */
  var tileSizes = [];
  /** @type {!Array.<ol.Size>} */
  var sizes = [];

  var supportedCRSPropName = 'SupportedCRS';
  var matrixIdsPropName = 'TileMatrix';
  var identifierPropName = 'Identifier';
  var scaleDenominatorPropName = 'ScaleDenominator';
  var topLeftCornerPropName = 'TopLeftCorner';
  var tileWidthPropName = 'TileWidth';
  var tileHeightPropName = 'TileHeight';

  var projection;
  projection = ol.proj.get(matrixSet[supportedCRSPropName].replace(
      /urn:ogc:def:crs:(\w+):(.*:)?(\w+)$/, '$1:$3'));
  var metersPerUnit = projection.getMetersPerUnit();
  // swap origin x and y coordinates if axis orientation is lat/long
  var switchOriginXY = projection.getAxisOrientation().substr(0, 2) == 'ne';

  matrixSet[matrixIdsPropName].sort(function(a, b) {
    return b[scaleDenominatorPropName] - a[scaleDenominatorPropName];
  });

  matrixSet[matrixIdsPropName].forEach(function(elt, index, array) {
    matrixIds.push(elt[identifierPropName]);
    var resolution = elt[scaleDenominatorPropName] * 0.28E-3 / metersPerUnit;
    var tileWidth = elt[tileWidthPropName];
    var tileHeight = elt[tileHeightPropName];
    if (switchOriginXY) {
      origins.push([elt[topLeftCornerPropName][1],
        elt[topLeftCornerPropName][0]]);
    } else {
      origins.push(elt[topLeftCornerPropName]);
    }
    resolutions.push(resolution);
    tileSizes.push(tileWidth == tileHeight ?
        tileWidth : [tileWidth, tileHeight]);
    // top-left origin, so height is negative
    sizes.push([elt['MatrixWidth'], -elt['MatrixHeight']]);
  });

  return new ol.tilegrid.WMTS({
    extent: opt_extent,
    origins: origins,
    resolutions: resolutions,
    matrixIds: matrixIds,
    tileSizes: tileSizes,
    sizes: sizes
  });
};

goog.provide('ol.source.WMTS');
goog.provide('ol.source.WMTSRequestEncoding');

goog.require('goog.array');
goog.require('goog.asserts');
goog.require('goog.object');
goog.require('goog.uri.utils');
goog.require('ol.TileUrlFunction');
goog.require('ol.TileUrlFunctionType');
goog.require('ol.array');
goog.require('ol.extent');
goog.require('ol.proj');
goog.require('ol.source.TileImage');
goog.require('ol.tilegrid.WMTS');


/**
 * Request encoding. One of 'KVP', 'REST'.
 * @enum {string}
 * @api
 */
ol.source.WMTSRequestEncoding = {
  KVP: 'KVP',  // see spec §8
  REST: 'REST' // see spec §10
};


/**
 * @classdesc
 * Layer source for tile data from WMTS servers.
 *
 * @constructor
 * @extends {ol.source.TileImage}
 * @param {olx.source.WMTSOptions} options WMTS options.
 * @api stable
 */
ol.source.WMTS = function(options) {

  // TODO: add support for TileMatrixLimits

  /**
   * @private
   * @type {string}
   */
  this.version_ = options.version !== undefined ? options.version : '1.0.0';

  /**
   * @private
   * @type {string}
   */
  this.format_ = options.format !== undefined ? options.format : 'image/jpeg';

  /**
   * @private
   * @type {!Object}
   */
  this.dimensions_ = options.dimensions !== undefined ? options.dimensions : {};

  /**
   * @private
   * @type {string}
   */
  this.dimensionsKey_ = '';
  this.resetDimensionsKey_();

  /**
   * @private
   * @type {string}
   */
  this.layer_ = options.layer;

  /**
   * @private
   * @type {string}
   */
  this.matrixSet_ = options.matrixSet;

  /**
   * @private
   * @type {string}
   */
  this.style_ = options.style;

  var urls = options.urls;
  if (urls === undefined && options.url !== undefined) {
    urls = ol.TileUrlFunction.expandUrl(options.url);
  }

  // FIXME: should we guess this requestEncoding from options.url(s)
  //        structure? that would mean KVP only if a template is not provided.

  /**
   * @private
   * @type {ol.source.WMTSRequestEncoding}
   */
  this.requestEncoding_ = options.requestEncoding !== undefined ?
      /** @type {ol.source.WMTSRequestEncoding} */ (options.requestEncoding) :
      ol.source.WMTSRequestEncoding.KVP;

  var requestEncoding = this.requestEncoding_;

  // FIXME: should we create a default tileGrid?
  // we could issue a getCapabilities xhr to retrieve missing configuration
  var tileGrid = options.tileGrid;

  // context property names are lower case to allow for a case insensitive
  // replacement as some services use different naming conventions
  var context = {
    'layer': this.layer_,
    'style': this.style_,
    'tilematrixset': this.matrixSet_
  };

  if (requestEncoding == ol.source.WMTSRequestEncoding.KVP) {
    goog.object.extend(context, {
      'Service': 'WMTS',
      'Request': 'GetTile',
      'Version': this.version_,
      'Format': this.format_
    });
  }

  var dimensions = this.dimensions_;

  /**
   * @param {string} template Template.
   * @return {ol.TileUrlFunctionType} Tile URL function.
   */
  function createFromWMTSTemplate(template) {

    // TODO: we may want to create our own appendParams function so that params
    // order conforms to wmts spec guidance, and so that we can avoid to escape
    // special template params

    template = (requestEncoding == ol.source.WMTSRequestEncoding.KVP) ?
        goog.uri.utils.appendParamsFromMap(template, context) :
        template.replace(/\{(\w+?)\}/g, function(m, p) {
          return (p.toLowerCase() in context) ? context[p.toLowerCase()] : m;
        });

    return (
        /**
         * @param {ol.TileCoord} tileCoord Tile coordinate.
         * @param {number} pixelRatio Pixel ratio.
         * @param {ol.proj.Projection} projection Projection.
         * @return {string|undefined} Tile URL.
         */
        function(tileCoord, pixelRatio, projection) {
          if (!tileCoord) {
            return undefined;
          } else {
            var localContext = {
              'TileMatrix': tileGrid.getMatrixId(tileCoord[0]),
              'TileCol': tileCoord[1],
              'TileRow': -tileCoord[2] - 1
            };
            goog.object.extend(localContext, dimensions);
            var url = template;
            if (requestEncoding == ol.source.WMTSRequestEncoding.KVP) {
              url = goog.uri.utils.appendParamsFromMap(url, localContext);
            } else {
              url = url.replace(/\{(\w+?)\}/g, function(m, p) {
                return localContext[p];
              });
            }
            return url;
          }
        });
  }

  var tileUrlFunction = (urls && urls.length > 0) ?
      ol.TileUrlFunction.createFromTileUrlFunctions(
          urls.map(createFromWMTSTemplate)) :
      ol.TileUrlFunction.nullTileUrlFunction;

  goog.base(this, {
    attributions: options.attributions,
    crossOrigin: options.crossOrigin,
    logo: options.logo,
    projection: options.projection,
    reprojectionErrorThreshold: options.reprojectionErrorThreshold,
    tileClass: options.tileClass,
    tileGrid: tileGrid,
    tileLoadFunction: options.tileLoadFunction,
    tilePixelRatio: options.tilePixelRatio,
    tileUrlFunction: tileUrlFunction,
    urls: urls,
    wrapX: options.wrapX !== undefined ? options.wrapX : false
  });

};
goog.inherits(ol.source.WMTS, ol.source.TileImage);


/**
 * Get the dimensions, i.e. those passed to the constructor through the
 * "dimensions" option, and possibly updated using the updateDimensions
 * method.
 * @return {!Object} Dimensions.
 * @api
 */
ol.source.WMTS.prototype.getDimensions = function() {
  return this.dimensions_;
};


/**
 * Return the image format of the WMTS source.
 * @return {string} Format.
 * @api
 */
ol.source.WMTS.prototype.getFormat = function() {
  return this.format_;
};


/**
 * @inheritDoc
 */
ol.source.WMTS.prototype.getKeyParams = function() {
  return this.dimensionsKey_;
};


/**
 * Return the layer of the WMTS source.
 * @return {string} Layer.
 * @api
 */
ol.source.WMTS.prototype.getLayer = function() {
  return this.layer_;
};


/**
 * Return the matrix set of the WMTS source.
 * @return {string} MatrixSet.
 * @api
 */
ol.source.WMTS.prototype.getMatrixSet = function() {
  return this.matrixSet_;
};


/**
 * Return the request encoding, either "KVP" or "REST".
 * @return {ol.source.WMTSRequestEncoding} Request encoding.
 * @api
 */
ol.source.WMTS.prototype.getRequestEncoding = function() {
  return this.requestEncoding_;
};


/**
 * Return the style of the WMTS source.
 * @return {string} Style.
 * @api
 */
ol.source.WMTS.prototype.getStyle = function() {
  return this.style_;
};


/**
 * Return the version of the WMTS source.
 * @return {string} Version.
 * @api
 */
ol.source.WMTS.prototype.getVersion = function() {
  return this.version_;
};


/**
 * @private
 */
ol.source.WMTS.prototype.resetDimensionsKey_ = function() {
  var i = 0;
  var res = [];
  for (var key in this.dimensions_) {
    res[i++] = key + '-' + this.dimensions_[key];
  }
  this.dimensionsKey_ = res.join('/');
};


/**
 * Update the dimensions.
 * @param {Object} dimensions Dimensions.
 * @api
 */
ol.source.WMTS.prototype.updateDimensions = function(dimensions) {
  goog.object.extend(this.dimensions_, dimensions);
  this.resetDimensionsKey_();
  this.changed();
};


/**
 * Generate source options from a capabilities object.
 * @param {Object} wmtsCap An object representing the capabilities document.
 * @param {Object} config Configuration properties for the layer.  Defaults for
 *                  the layer will apply if not provided.
 *
 * Required config properties:
 *  - layer - {string} The layer identifier.
 *
 * Optional config properties:
 *  - matrixSet - {string} The matrix set identifier, required if there is
 *       more than one matrix set in the layer capabilities.
 *  - projection - {string} The desired CRS when no matrixSet is specified.
 *       eg: "EPSG:3857". If the desired projection is not available,
 *       an error is thrown.
 *  - requestEncoding - {string} url encoding format for the layer. Default is
 *       the first tile url format found in the GetCapabilities response.
 *  - style - {string} The name of the style
 *  - format - {string} Image format for the layer. Default is the first
 *       format returned in the GetCapabilities response.
 * @return {olx.source.WMTSOptions} WMTS source options object.
 * @api
 */
ol.source.WMTS.optionsFromCapabilities = function(wmtsCap, config) {

  // TODO: add support for TileMatrixLimits
  goog.asserts.assert(config['layer'],
      'config "layer" must not be null');

  var layers = wmtsCap['Contents']['Layer'];
  var l = goog.array.find(layers, function(elt, index, array) {
    return elt['Identifier'] == config['layer'];
  });
  goog.asserts.assert(l, 'found a matching layer in Contents/Layer');

  goog.asserts.assert(l['TileMatrixSetLink'].length > 0,
      'layer has TileMatrixSetLink');
  var tileMatrixSets = wmtsCap['Contents']['TileMatrixSet'];
  var idx, matrixSet;
  if (l['TileMatrixSetLink'].length > 1) {
    if ('projection' in config) {
      idx = goog.array.findIndex(l['TileMatrixSetLink'],
          function(elt, index, array) {
            var tileMatrixSet = goog.array.find(tileMatrixSets, function(el) {
              return el['Identifier'] == elt['TileMatrixSet'];
            });
            return tileMatrixSet['SupportedCRS'].replace(
                /urn:ogc:def:crs:(\w+):(.*:)?(\w+)$/, '$1:$3'
                   ) == config['projection'];
          });
    } else {
      idx = goog.array.findIndex(l['TileMatrixSetLink'],
          function(elt, index, array) {
            return elt['TileMatrixSet'] == config['matrixSet'];
          });
    }
  } else {
    idx = 0;
  }
  if (idx < 0) {
    idx = 0;
  }
  matrixSet = /** @type {string} */
      (l['TileMatrixSetLink'][idx]['TileMatrixSet']);

  goog.asserts.assert(matrixSet, 'TileMatrixSet must not be null');

  var format = /** @type {string} */ (l['Format'][0]);
  if ('format' in config) {
    format = config['format'];
  }
  idx = goog.array.findIndex(l['Style'], function(elt, index, array) {
    if ('style' in config) {
      return elt['Title'] == config['style'];
    } else {
      return elt['isDefault'];
    }
  });
  if (idx < 0) {
    idx = 0;
  }
  var style = /** @type {string} */ (l['Style'][idx]['Identifier']);

  var dimensions = {};
  if ('Dimension' in l) {
    l['Dimension'].forEach(function(elt, index, array) {
      var key = elt['Identifier'];
      var value = elt['Default'];
      if (value !== undefined) {
        goog.asserts.assert(ol.array.includes(elt['Value'], value),
            'default value contained in values');
      } else {
        value = elt['Value'][0];
      }
      goog.asserts.assert(value !== undefined, 'value could be found');
      dimensions[key] = value;
    });
  }

  var matrixSets = wmtsCap['Contents']['TileMatrixSet'];
  var matrixSetObj = goog.array.find(matrixSets, function(elt, index, array) {
    return elt['Identifier'] == matrixSet;
  });
  goog.asserts.assert(matrixSetObj,
      'found matrixSet in Contents/TileMatrixSet');

  var projection;
  if ('projection' in config) {
    projection = ol.proj.get(config['projection']);
  } else {
    projection = ol.proj.get(matrixSetObj['SupportedCRS'].replace(
        /urn:ogc:def:crs:(\w+):(.*:)?(\w+)$/, '$1:$3'));
  }

  var wgs84BoundingBox = l['WGS84BoundingBox'];
  var extent, wrapX;
  if (wgs84BoundingBox !== undefined) {
    var wgs84ProjectionExtent = ol.proj.get('EPSG:4326').getExtent();
    wrapX = (wgs84BoundingBox[0] == wgs84ProjectionExtent[0] &&
        wgs84BoundingBox[2] == wgs84ProjectionExtent[2]);
    extent = ol.proj.transformExtent(
        wgs84BoundingBox, 'EPSG:4326', projection);
    var projectionExtent = projection.getExtent();
    if (projectionExtent) {
      // If possible, do a sanity check on the extent - it should never be
      // bigger than the validity extent of the projection of a matrix set.
      if (!ol.extent.containsExtent(projectionExtent, extent)) {
        extent = undefined;
      }
    }
  }

  var tileGrid = ol.tilegrid.WMTS.createFromCapabilitiesMatrixSet(
      matrixSetObj, extent);

  /** @type {!Array.<string>} */
  var urls = [];
  var requestEncoding = config['requestEncoding'];
  requestEncoding = requestEncoding !== undefined ? requestEncoding : '';

  goog.asserts.assert(
      ol.array.includes(['REST', 'RESTful', 'KVP', ''], requestEncoding),
      'requestEncoding (%s) is one of "REST", "RESTful", "KVP" or ""',
      requestEncoding);

  if (!wmtsCap.hasOwnProperty('OperationsMetadata') ||
      !wmtsCap['OperationsMetadata'].hasOwnProperty('GetTile') ||
      requestEncoding.indexOf('REST') === 0) {
    // Add REST tile resource url
    requestEncoding = ol.source.WMTSRequestEncoding.REST;
    l['ResourceURL'].forEach(function(elt, index, array) {
      if (elt['resourceType'] == 'tile') {
        format = elt['format'];
        urls.push(/** @type {string} */ (elt['template']));
      }
    });
  } else {
    var gets = wmtsCap['OperationsMetadata']['GetTile']['DCP']['HTTP']['Get'];

    for (var i = 0, ii = gets.length; i < ii; ++i) {
      var constraint = goog.array.find(gets[i]['Constraint'],
          function(elt, index, array) {
            return elt['name'] == 'GetEncoding';
          });
      var encodings = constraint['AllowedValues']['Value'];
      if (encodings.length > 0 && ol.array.includes(encodings, 'KVP')) {
        requestEncoding = ol.source.WMTSRequestEncoding.KVP;
        urls.push(/** @type {string} */ (gets[i]['href']));
      }
    }
  }
  goog.asserts.assert(urls.length > 0, 'At least one URL found');

  return {
    urls: urls,
    layer: config['layer'],
    matrixSet: matrixSet,
    format: format,
    projection: projection,
    requestEncoding: requestEncoding,
    tileGrid: tileGrid,
    style: style,
    dimensions: dimensions,
    wrapX: wrapX
  };

};

goog.provide('ol.source.Zoomify');

goog.require('goog.asserts');
goog.require('ol');
goog.require('ol.ImageTile');
goog.require('ol.TileCoord');
goog.require('ol.TileState');
goog.require('ol.dom');
goog.require('ol.extent');
goog.require('ol.proj');
goog.require('ol.source.TileImage');
goog.require('ol.tilegrid.TileGrid');


/**
 * @enum {string}
 */
ol.source.ZoomifyTierSizeCalculation = {
  DEFAULT: 'default',
  TRUNCATED: 'truncated'
};


/**
 * @classdesc
 * Layer source for tile data in Zoomify format.
 *
 * @constructor
 * @extends {ol.source.TileImage}
 * @param {olx.source.ZoomifyOptions=} opt_options Options.
 * @api stable
 */
ol.source.Zoomify = function(opt_options) {

  var options = opt_options || {};

  var size = options.size;
  var tierSizeCalculation = options.tierSizeCalculation !== undefined ?
      options.tierSizeCalculation :
      ol.source.ZoomifyTierSizeCalculation.DEFAULT;

  var imageWidth = size[0];
  var imageHeight = size[1];
  var tierSizeInTiles = [];
  var tileSize = ol.DEFAULT_TILE_SIZE;

  switch (tierSizeCalculation) {
    case ol.source.ZoomifyTierSizeCalculation.DEFAULT:
      while (imageWidth > tileSize || imageHeight > tileSize) {
        tierSizeInTiles.push([
          Math.ceil(imageWidth / tileSize),
          Math.ceil(imageHeight / tileSize)
        ]);
        tileSize += tileSize;
      }
      break;
    case ol.source.ZoomifyTierSizeCalculation.TRUNCATED:
      var width = imageWidth;
      var height = imageHeight;
      while (width > tileSize || height > tileSize) {
        tierSizeInTiles.push([
          Math.ceil(width / tileSize),
          Math.ceil(height / tileSize)
        ]);
        width >>= 1;
        height >>= 1;
      }
      break;
    default:
      goog.asserts.fail();
      break;
  }

  tierSizeInTiles.push([1, 1]);
  tierSizeInTiles.reverse();

  var resolutions = [1];
  var tileCountUpToTier = [0];
  var i, ii;
  for (i = 1, ii = tierSizeInTiles.length; i < ii; i++) {
    resolutions.push(1 << i);
    tileCountUpToTier.push(
        tierSizeInTiles[i - 1][0] * tierSizeInTiles[i - 1][1] +
        tileCountUpToTier[i - 1]
    );
  }
  resolutions.reverse();

  var extent = [0, -size[1], size[0], 0];
  var tileGrid = new ol.tilegrid.TileGrid({
    extent: extent,
    origin: ol.extent.getTopLeft(extent),
    resolutions: resolutions
  });

  var url = options.url;

  /**
   * @this {ol.source.TileImage}
   * @param {ol.TileCoord} tileCoord Tile Coordinate.
   * @param {number} pixelRatio Pixel ratio.
   * @param {ol.proj.Projection} projection Projection.
   * @return {string|undefined} Tile URL.
   */
  function tileUrlFunction(tileCoord, pixelRatio, projection) {
    if (!tileCoord) {
      return undefined;
    } else {
      var tileCoordZ = tileCoord[0];
      var tileCoordX = tileCoord[1];
      var tileCoordY = -tileCoord[2] - 1;
      var tileIndex =
          tileCoordX +
          tileCoordY * tierSizeInTiles[tileCoordZ][0] +
          tileCountUpToTier[tileCoordZ];
      var tileGroup = (tileIndex / ol.DEFAULT_TILE_SIZE) | 0;
      return url + 'TileGroup' + tileGroup + '/' +
          tileCoordZ + '-' + tileCoordX + '-' + tileCoordY + '.jpg';
    }
  }

  goog.base(this, {
    attributions: options.attributions,
    crossOrigin: options.crossOrigin,
    logo: options.logo,
    reprojectionErrorThreshold: options.reprojectionErrorThreshold,
    tileClass: ol.source.ZoomifyTile_,
    tileGrid: tileGrid,
    tileUrlFunction: tileUrlFunction
  });

};
goog.inherits(ol.source.Zoomify, ol.source.TileImage);


/**
 * @constructor
 * @extends {ol.ImageTile}
 * @param {ol.TileCoord} tileCoord Tile coordinate.
 * @param {ol.TileState} state State.
 * @param {string} src Image source URI.
 * @param {?string} crossOrigin Cross origin.
 * @param {ol.TileLoadFunctionType} tileLoadFunction Tile load function.
 * @private
 */
ol.source.ZoomifyTile_ = function(
    tileCoord, state, src, crossOrigin, tileLoadFunction) {

  goog.base(this, tileCoord, state, src, crossOrigin, tileLoadFunction);

  /**
   * @private
   * @type {Object.<string,
   *                HTMLCanvasElement|HTMLImageElement|HTMLVideoElement>}
   */
  this.zoomifyImageByContext_ = {};

};
goog.inherits(ol.source.ZoomifyTile_, ol.ImageTile);


/**
 * @inheritDoc
 */
ol.source.ZoomifyTile_.prototype.getImage = function(opt_context) {
  var tileSize = ol.DEFAULT_TILE_SIZE;
  var key = opt_context !== undefined ?
      goog.getUid(opt_context).toString() : '';
  if (key in this.zoomifyImageByContext_) {
    return this.zoomifyImageByContext_[key];
  } else {
    var image = goog.base(this, 'getImage', opt_context);
    if (this.state == ol.TileState.LOADED) {
      if (image.width == tileSize && image.height == tileSize) {
        this.zoomifyImageByContext_[key] = image;
        return image;
      } else {
        var context = ol.dom.createCanvasContext2D(tileSize, tileSize);
        context.drawImage(image, 0, 0);
        this.zoomifyImageByContext_[key] = context.canvas;
        return context.canvas;
      }
    } else {
      return image;
    }
  }
};

goog.provide('ol.style.Atlas');
goog.provide('ol.style.AtlasManager');

goog.require('goog.asserts');
goog.require('goog.functions');
goog.require('goog.object');
goog.require('ol');


/**
 * Provides information for an image inside an atlas manager.
 * `offsetX` and `offsetY` is the position of the image inside
 * the atlas image `image` and the position of the hit-detection image
 * inside the hit-detection atlas image `hitImage`.
 * @typedef {{offsetX: number, offsetY: number, image: HTMLCanvasElement,
 *    hitImage: HTMLCanvasElement}}
 */
ol.style.AtlasManagerInfo;


/**
 * Manages the creation of image atlases.
 *
 * Images added to this manager will be inserted into an atlas, which
 * will be used for rendering.
 * The `size` given in the constructor is the size for the first
 * atlas. After that, when new atlases are created, they will have
 * twice the size as the latest atlas (until `maxSize` is reached).
 *
 * If an application uses many images or very large images, it is recommended
 * to set a higher `size` value to avoid the creation of too many atlases.
 *
 * @constructor
 * @struct
 * @api
 * @param {olx.style.AtlasManagerOptions=} opt_options Options.
 */
ol.style.AtlasManager = function(opt_options) {

  var options = opt_options || {};

  /**
   * The size in pixels of the latest atlas image.
   * @private
   * @type {number}
   */
  this.currentSize_ = options.initialSize !== undefined ?
      options.initialSize : ol.INITIAL_ATLAS_SIZE;

  /**
   * The maximum size in pixels of atlas images.
   * @private
   * @type {number}
   */
  this.maxSize_ = options.maxSize !== undefined ?
      options.maxSize : ol.MAX_ATLAS_SIZE != -1 ?
          ol.MAX_ATLAS_SIZE : ol.WEBGL_MAX_TEXTURE_SIZE !== undefined ?
              ol.WEBGL_MAX_TEXTURE_SIZE : 2048;

  /**
   * The size in pixels between images.
   * @private
   * @type {number}
   */
  this.space_ = options.space !== undefined ? options.space : 1;

  /**
   * @private
   * @type {Array.<ol.style.Atlas>}
   */
  this.atlases_ = [new ol.style.Atlas(this.currentSize_, this.space_)];

  /**
   * The size in pixels of the latest atlas image for hit-detection images.
   * @private
   * @type {number}
   */
  this.currentHitSize_ = this.currentSize_;

  /**
   * @private
   * @type {Array.<ol.style.Atlas>}
   */
  this.hitAtlases_ = [new ol.style.Atlas(this.currentHitSize_, this.space_)];
};


/**
 * @param {string} id The identifier of the entry to check.
 * @return {?ol.style.AtlasManagerInfo} The position and atlas image for the
 *    entry, or `null` if the entry is not part of the atlas manager.
 */
ol.style.AtlasManager.prototype.getInfo = function(id) {
  /** @type {?ol.style.AtlasInfo} */
  var info = this.getInfo_(this.atlases_, id);

  if (!info) {
    return null;
  }
  /** @type {?ol.style.AtlasInfo} */
  var hitInfo = this.getInfo_(this.hitAtlases_, id);
  goog.asserts.assert(hitInfo, 'hitInfo must not be null');

  return this.mergeInfos_(info, hitInfo);
};


/**
 * @private
 * @param {Array.<ol.style.Atlas>} atlases The atlases to search.
 * @param {string} id The identifier of the entry to check.
 * @return {?ol.style.AtlasInfo} The position and atlas image for the entry,
 *    or `null` if the entry is not part of the atlases.
 */
ol.style.AtlasManager.prototype.getInfo_ = function(atlases, id) {
  var atlas, info, i, ii;
  for (i = 0, ii = atlases.length; i < ii; ++i) {
    atlas = atlases[i];
    info = atlas.get(id);
    if (info) {
      return info;
    }
  }
  return null;
};


/**
 * @private
 * @param {ol.style.AtlasInfo} info The info for the real image.
 * @param {ol.style.AtlasInfo} hitInfo The info for the hit-detection
 *    image.
 * @return {?ol.style.AtlasManagerInfo} The position and atlas image for the
 *    entry, or `null` if the entry is not part of the atlases.
 */
ol.style.AtlasManager.prototype.mergeInfos_ = function(info, hitInfo) {
  goog.asserts.assert(info.offsetX === hitInfo.offsetX,
      'in order to merge, offsetX of info and hitInfo must be equal');
  goog.asserts.assert(info.offsetY === hitInfo.offsetY,
      'in order to merge, offsetY of info and hitInfo must be equal');
  return /** @type {ol.style.AtlasManagerInfo} */ ({
    offsetX: info.offsetX,
    offsetY: info.offsetY,
    image: info.image,
    hitImage: hitInfo.image
  });
};


/**
 * Add an image to the atlas manager.
 *
 * If an entry for the given id already exists, the entry will
 * be overridden (but the space on the atlas graphic will not be freed).
 *
 * If `renderHitCallback` is provided, the image (or the hit-detection version
 * of the image) will be rendered into a separate hit-detection atlas image.
 *
 * @param {string} id The identifier of the entry to add.
 * @param {number} width The width.
 * @param {number} height The height.
 * @param {function(CanvasRenderingContext2D, number, number)} renderCallback
 *    Called to render the new image onto an atlas image.
 * @param {function(CanvasRenderingContext2D, number, number)=}
 *    opt_renderHitCallback Called to render a hit-detection image onto a hit
 *    detection atlas image.
 * @param {Object=} opt_this Value to use as `this` when executing
 *    `renderCallback` and `renderHitCallback`.
 * @return {?ol.style.AtlasManagerInfo}  The position and atlas image for the
 *    entry, or `null` if the image is too big.
 */
ol.style.AtlasManager.prototype.add = function(id, width, height,
        renderCallback, opt_renderHitCallback, opt_this) {
  if (width + this.space_ > this.maxSize_ ||
      height + this.space_ > this.maxSize_) {
    return null;
  }

  /** @type {?ol.style.AtlasInfo} */
  var info = this.add_(false,
      id, width, height, renderCallback, opt_this);
  if (!info) {
    return null;
  }

  // even if no hit-detection entry is requested, we insert a fake entry into
  // the hit-detection atlas, to make sure that the offset is the same for
  // the original image and the hit-detection image.
  var renderHitCallback = opt_renderHitCallback !== undefined ?
      opt_renderHitCallback : goog.functions.NULL;

  /** @type {?ol.style.AtlasInfo} */
  var hitInfo = this.add_(true,
      id, width, height, renderHitCallback, opt_this);
  goog.asserts.assert(hitInfo, 'hitInfo must not be null');

  return this.mergeInfos_(info, hitInfo);
};


/**
 * @private
 * @param {boolean} isHitAtlas If the hit-detection atlases are used.
 * @param {string} id The identifier of the entry to add.
 * @param {number} width The width.
 * @param {number} height The height.
 * @param {function(CanvasRenderingContext2D, number, number)} renderCallback
 *    Called to render the new image onto an atlas image.
 * @param {Object=} opt_this Value to use as `this` when executing
 *    `renderCallback` and `renderHitCallback`.
 * @return {?ol.style.AtlasInfo}  The position and atlas image for the entry,
 *    or `null` if the image is too big.
 */
ol.style.AtlasManager.prototype.add_ = function(isHitAtlas, id, width, height,
        renderCallback, opt_this) {
  var atlases = (isHitAtlas) ? this.hitAtlases_ : this.atlases_;
  var atlas, info, i, ii;
  for (i = 0, ii = atlases.length; i < ii; ++i) {
    atlas = atlases[i];
    info = atlas.add(id, width, height, renderCallback, opt_this);
    if (info) {
      return info;
    } else if (!info && i === ii - 1) {
      // the entry could not be added to one of the existing atlases,
      // create a new atlas that is twice as big and try to add to this one.
      var size;
      if (isHitAtlas) {
        size = Math.min(this.currentHitSize_ * 2, this.maxSize_);
        this.currentHitSize_ = size;
      } else {
        size = Math.min(this.currentSize_ * 2, this.maxSize_);
        this.currentSize_ = size;
      }
      atlas = new ol.style.Atlas(size, this.space_);
      atlases.push(atlas);
      // run the loop another time
      ++ii;
    }
  }
  goog.asserts.fail('Failed to add to atlasmanager');
};


/**
 * Provides information for an image inside an atlas.
 * `offsetX` and `offsetY` are the position of the image inside
 * the atlas image `image`.
 * @typedef {{offsetX: number, offsetY: number, image: HTMLCanvasElement}}
 */
ol.style.AtlasInfo;


/**
 * This class facilitates the creation of image atlases.
 *
 * Images added to an atlas will be rendered onto a single
 * atlas canvas. The distribution of images on the canvas is
 * managed with the bin packing algorithm described in:
 * http://www.blackpawn.com/texts/lightmaps/
 *
 * @constructor
 * @struct
 * @param {number} size The size in pixels of the sprite image.
 * @param {number} space The space in pixels between images.
 *    Because texture coordinates are float values, the edges of
 *    images might not be completely correct (in a way that the
 *    edges overlap when being rendered). To avoid this we add a
 *    padding around each image.
 */
ol.style.Atlas = function(size, space) {

  /**
   * @private
   * @type {number}
   */
  this.space_ = space;

  /**
   * @private
   * @type {Array.<ol.style.Atlas.Block>}
   */
  this.emptyBlocks_ = [{x: 0, y: 0, width: size, height: size}];

  /**
   * @private
   * @type {Object.<string, ol.style.AtlasInfo>}
   */
  this.entries_ = {};

  /**
   * @private
   * @type {HTMLCanvasElement}
   */
  this.canvas_ = /** @type {HTMLCanvasElement} */
      (document.createElement('CANVAS'));
  this.canvas_.width = size;
  this.canvas_.height = size;

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.context_ = /** @type {CanvasRenderingContext2D} */
      (this.canvas_.getContext('2d'));
};


/**
 * @param {string} id The identifier of the entry to check.
 * @return {?ol.style.AtlasInfo} The atlas info.
 */
ol.style.Atlas.prototype.get = function(id) {
  return /** @type {?ol.style.AtlasInfo} */ (
      goog.object.get(this.entries_, id, null));
};


/**
 * @param {string} id The identifier of the entry to add.
 * @param {number} width The width.
 * @param {number} height The height.
 * @param {function(CanvasRenderingContext2D, number, number)} renderCallback
 *    Called to render the new image onto an atlas image.
 * @param {Object=} opt_this Value to use as `this` when executing
 *    `renderCallback`.
 * @return {?ol.style.AtlasInfo} The position and atlas image for the entry.
 */
ol.style.Atlas.prototype.add = function(id, width, height, renderCallback, opt_this) {
  var block, i, ii;
  for (i = 0, ii = this.emptyBlocks_.length; i < ii; ++i) {
    block = this.emptyBlocks_[i];
    if (block.width >= width + this.space_ &&
        block.height >= height + this.space_) {
      // we found a block that is big enough for our entry
      var entry = {
        offsetX: block.x + this.space_,
        offsetY: block.y + this.space_,
        image: this.canvas_
      };
      this.entries_[id] = entry;

      // render the image on the atlas image
      renderCallback.call(opt_this, this.context_,
          block.x + this.space_, block.y + this.space_);

      // split the block after the insertion, either horizontally or vertically
      this.split_(i, block, width + this.space_, height + this.space_);

      return entry;
    }
  }

  // there is no space for the new entry in this atlas
  return null;
};


/**
 * @private
 * @param {number} index The index of the block.
 * @param {ol.style.Atlas.Block} block The block to split.
 * @param {number} width The width of the entry to insert.
 * @param {number} height The height of the entry to insert.
 */
ol.style.Atlas.prototype.split_ = function(index, block, width, height) {
  var deltaWidth = block.width - width;
  var deltaHeight = block.height - height;

  /** @type {ol.style.Atlas.Block} */
  var newBlock1;
  /** @type {ol.style.Atlas.Block} */
  var newBlock2;

  if (deltaWidth > deltaHeight) {
    // split vertically
    // block right of the inserted entry
    newBlock1 = {
      x: block.x + width,
      y: block.y,
      width: block.width - width,
      height: block.height
    };

    // block below the inserted entry
    newBlock2 = {
      x: block.x,
      y: block.y + height,
      width: width,
      height: block.height - height
    };
    this.updateBlocks_(index, newBlock1, newBlock2);
  } else {
    // split horizontally
    // block right of the inserted entry
    newBlock1 = {
      x: block.x + width,
      y: block.y,
      width: block.width - width,
      height: height
    };

    // block below the inserted entry
    newBlock2 = {
      x: block.x,
      y: block.y + height,
      width: block.width,
      height: block.height - height
    };
    this.updateBlocks_(index, newBlock1, newBlock2);
  }
};


/**
 * Remove the old block and insert new blocks at the same array position.
 * The new blocks are inserted at the same position, so that splitted
 * blocks (that are potentially smaller) are filled first.
 * @private
 * @param {number} index The index of the block to remove.
 * @param {ol.style.Atlas.Block} newBlock1 The 1st block to add.
 * @param {ol.style.Atlas.Block} newBlock2 The 2nd block to add.
 */
ol.style.Atlas.prototype.updateBlocks_ = function(index, newBlock1, newBlock2) {
  var args = [index, 1];
  if (newBlock1.width > 0 && newBlock1.height > 0) {
    args.push(newBlock1);
  }
  if (newBlock2.width > 0 && newBlock2.height > 0) {
    args.push(newBlock2);
  }
  this.emptyBlocks_.splice.apply(this.emptyBlocks_, args);
};


/**
 * @typedef {{x: number, y: number, width: number, height: number}}
 */
ol.style.Atlas.Block;

goog.provide('ol.style.RegularShape');

goog.require('goog.asserts');
goog.require('ol');
goog.require('ol.color');
goog.require('ol.has');
goog.require('ol.render.canvas');
goog.require('ol.style.AtlasManager');
goog.require('ol.style.Fill');
goog.require('ol.style.Image');
goog.require('ol.style.ImageState');
goog.require('ol.style.Stroke');


/**
 * @classdesc
 * Set regular shape style for vector features. The resulting shape will be
 * a regular polygon when `radius` is provided, or a star when `radius1` and
 * `radius2` are provided.
 *
 * @constructor
 * @param {olx.style.RegularShapeOptions} options Options.
 * @extends {ol.style.Image}
 * @api
 */
ol.style.RegularShape = function(options) {

  goog.asserts.assert(
      options.radius !== undefined || options.radius1 !== undefined,
      'must provide either "radius" or "radius1"');

  /**
   * @private
   * @type {Array.<string>}
   */
  this.checksums_ = null;

  /**
   * @private
   * @type {HTMLCanvasElement}
   */
  this.canvas_ = null;

  /**
   * @private
   * @type {HTMLCanvasElement}
   */
  this.hitDetectionCanvas_ = null;

  /**
   * @private
   * @type {ol.style.Fill}
   */
  this.fill_ = options.fill !== undefined ? options.fill : null;

  /**
   * @private
   * @type {Array.<number>}
   */
  this.origin_ = [0, 0];

  /**
   * @private
   * @type {number}
   */
  this.points_ = options.points;

  /**
   * @private
   * @type {number}
   */
  this.radius_ = /** @type {number} */ (options.radius !== undefined ?
      options.radius : options.radius1);

  /**
   * @private
   * @type {number}
   */
  this.radius2_ =
      options.radius2 !== undefined ? options.radius2 : this.radius_;

  /**
   * @private
   * @type {number}
   */
  this.angle_ = options.angle !== undefined ? options.angle : 0;

  /**
   * @private
   * @type {ol.style.Stroke}
   */
  this.stroke_ = options.stroke !== undefined ? options.stroke : null;

  /**
   * @private
   * @type {Array.<number>}
   */
  this.anchor_ = null;

  /**
   * @private
   * @type {ol.Size}
   */
  this.size_ = null;

  /**
   * @private
   * @type {ol.Size}
   */
  this.imageSize_ = null;

  /**
   * @private
   * @type {ol.Size}
   */
  this.hitDetectionImageSize_ = null;

  this.render_(options.atlasManager);

  /**
   * @type {boolean}
   */
  var snapToPixel = options.snapToPixel !== undefined ?
      options.snapToPixel : true;

  goog.base(this, {
    opacity: 1,
    rotateWithView: false,
    rotation: options.rotation !== undefined ? options.rotation : 0,
    scale: 1,
    snapToPixel: snapToPixel
  });

};
goog.inherits(ol.style.RegularShape, ol.style.Image);


/**
 * @inheritDoc
 * @api
 */
ol.style.RegularShape.prototype.getAnchor = function() {
  return this.anchor_;
};


/**
 * Get the angle used in generating the shape.
 * @return {number} Shape's rotation in radians.
 * @api
 */
ol.style.RegularShape.prototype.getAngle = function() {
  return this.angle_;
};


/**
 * Get the fill style for the shape.
 * @return {ol.style.Fill} Fill style.
 * @api
 */
ol.style.RegularShape.prototype.getFill = function() {
  return this.fill_;
};


/**
 * @inheritDoc
 */
ol.style.RegularShape.prototype.getHitDetectionImage = function(pixelRatio) {
  return this.hitDetectionCanvas_;
};


/**
 * @inheritDoc
 * @api
 */
ol.style.RegularShape.prototype.getImage = function(pixelRatio) {
  return this.canvas_;
};


/**
 * @inheritDoc
 */
ol.style.RegularShape.prototype.getImageSize = function() {
  return this.imageSize_;
};


/**
 * @inheritDoc
 */
ol.style.RegularShape.prototype.getHitDetectionImageSize = function() {
  return this.hitDetectionImageSize_;
};


/**
 * @inheritDoc
 */
ol.style.RegularShape.prototype.getImageState = function() {
  return ol.style.ImageState.LOADED;
};


/**
 * @inheritDoc
 * @api
 */
ol.style.RegularShape.prototype.getOrigin = function() {
  return this.origin_;
};


/**
 * Get the number of points for generating the shape.
 * @return {number} Number of points for stars and regular polygons.
 * @api
 */
ol.style.RegularShape.prototype.getPoints = function() {
  return this.points_;
};


/**
 * Get the (primary) radius for the shape.
 * @return {number} Radius.
 * @api
 */
ol.style.RegularShape.prototype.getRadius = function() {
  return this.radius_;
};


/**
 * Get the secondary radius for the shape.
 * @return {number} Radius2.
 * @api
 */
ol.style.RegularShape.prototype.getRadius2 = function() {
  return this.radius2_;
};


/**
 * @inheritDoc
 * @api
 */
ol.style.RegularShape.prototype.getSize = function() {
  return this.size_;
};


/**
 * Get the stroke style for the shape.
 * @return {ol.style.Stroke} Stroke style.
 * @api
 */
ol.style.RegularShape.prototype.getStroke = function() {
  return this.stroke_;
};


/**
 * @inheritDoc
 */
ol.style.RegularShape.prototype.listenImageChange = ol.nullFunction;


/**
 * @inheritDoc
 */
ol.style.RegularShape.prototype.load = ol.nullFunction;


/**
 * @inheritDoc
 */
ol.style.RegularShape.prototype.unlistenImageChange = ol.nullFunction;


/**
 * @typedef {{
 *   strokeStyle: (string|undefined),
 *   strokeWidth: number,
 *   size: number,
 *   lineCap: string,
 *   lineDash: Array.<number>,
 *   lineJoin: string,
 *   miterLimit: number
 * }}
 */
ol.style.RegularShape.RenderOptions;


/**
 * @private
 * @param {ol.style.AtlasManager|undefined} atlasManager An atlas manager.
 */
ol.style.RegularShape.prototype.render_ = function(atlasManager) {
  var imageSize;
  var lineCap = '';
  var lineJoin = '';
  var miterLimit = 0;
  var lineDash = null;
  var strokeStyle;
  var strokeWidth = 0;

  if (this.stroke_) {
    strokeStyle = ol.color.asString(this.stroke_.getColor());
    strokeWidth = this.stroke_.getWidth();
    if (strokeWidth === undefined) {
      strokeWidth = ol.render.canvas.defaultLineWidth;
    }
    lineDash = this.stroke_.getLineDash();
    if (!ol.has.CANVAS_LINE_DASH) {
      lineDash = null;
    }
    lineJoin = this.stroke_.getLineJoin();
    if (lineJoin === undefined) {
      lineJoin = ol.render.canvas.defaultLineJoin;
    }
    lineCap = this.stroke_.getLineCap();
    if (lineCap === undefined) {
      lineCap = ol.render.canvas.defaultLineCap;
    }
    miterLimit = this.stroke_.getMiterLimit();
    if (miterLimit === undefined) {
      miterLimit = ol.render.canvas.defaultMiterLimit;
    }
  }

  var size = 2 * (this.radius_ + strokeWidth) + 1;

  /** @type {ol.style.RegularShape.RenderOptions} */
  var renderOptions = {
    strokeStyle: strokeStyle,
    strokeWidth: strokeWidth,
    size: size,
    lineCap: lineCap,
    lineDash: lineDash,
    lineJoin: lineJoin,
    miterLimit: miterLimit
  };

  if (atlasManager === undefined) {
    // no atlas manager is used, create a new canvas
    this.canvas_ = /** @type {HTMLCanvasElement} */
        (document.createElement('CANVAS'));

    this.canvas_.height = size;
    this.canvas_.width = size;

    // canvas.width and height are rounded to the closest integer
    size = this.canvas_.width;
    imageSize = size;

    var context = /** @type {CanvasRenderingContext2D} */
        (this.canvas_.getContext('2d'));
    this.draw_(renderOptions, context, 0, 0);

    this.createHitDetectionCanvas_(renderOptions);
  } else {
    // an atlas manager is used, add the symbol to an atlas
    size = Math.round(size);

    var hasCustomHitDetectionImage = !this.fill_;
    var renderHitDetectionCallback;
    if (hasCustomHitDetectionImage) {
      // render the hit-detection image into a separate atlas image
      renderHitDetectionCallback =
          this.drawHitDetectionCanvas_.bind(this, renderOptions);
    }

    var id = this.getChecksum();
    var info = atlasManager.add(
        id, size, size, this.draw_.bind(this, renderOptions),
        renderHitDetectionCallback);
    goog.asserts.assert(info, 'shape size is too large');

    this.canvas_ = info.image;
    this.origin_ = [info.offsetX, info.offsetY];
    imageSize = info.image.width;

    if (hasCustomHitDetectionImage) {
      this.hitDetectionCanvas_ = info.hitImage;
      this.hitDetectionImageSize_ =
          [info.hitImage.width, info.hitImage.height];
    } else {
      this.hitDetectionCanvas_ = this.canvas_;
      this.hitDetectionImageSize_ = [imageSize, imageSize];
    }
  }

  this.anchor_ = [size / 2, size / 2];
  this.size_ = [size, size];
  this.imageSize_ = [imageSize, imageSize];
};


/**
 * @private
 * @param {ol.style.RegularShape.RenderOptions} renderOptions Render options.
 * @param {CanvasRenderingContext2D} context The rendering context.
 * @param {number} x The origin for the symbol (x).
 * @param {number} y The origin for the symbol (y).
 */
ol.style.RegularShape.prototype.draw_ = function(renderOptions, context, x, y) {
  var i, angle0, radiusC;
  // reset transform
  context.setTransform(1, 0, 0, 1, 0, 0);

  // then move to (x, y)
  context.translate(x, y);

  context.beginPath();
  if (this.radius2_ !== this.radius_) {
    this.points_ = 2 * this.points_;
  }
  for (i = 0; i <= this.points_; i++) {
    angle0 = i * 2 * Math.PI / this.points_ - Math.PI / 2 + this.angle_;
    radiusC = i % 2 === 0 ? this.radius_ : this.radius2_;
    context.lineTo(renderOptions.size / 2 + radiusC * Math.cos(angle0),
                   renderOptions.size / 2 + radiusC * Math.sin(angle0));
  }

  if (this.fill_) {
    context.fillStyle = ol.color.asString(this.fill_.getColor());
    context.fill();
  }
  if (this.stroke_) {
    context.strokeStyle = renderOptions.strokeStyle;
    context.lineWidth = renderOptions.strokeWidth;
    if (renderOptions.lineDash) {
      context.setLineDash(renderOptions.lineDash);
    }
    context.lineCap = renderOptions.lineCap;
    context.lineJoin = renderOptions.lineJoin;
    context.miterLimit = renderOptions.miterLimit;
    context.stroke();
  }
  context.closePath();
};


/**
 * @private
 * @param {ol.style.RegularShape.RenderOptions} renderOptions Render options.
 */
ol.style.RegularShape.prototype.createHitDetectionCanvas_ = function(renderOptions) {
  this.hitDetectionImageSize_ = [renderOptions.size, renderOptions.size];
  if (this.fill_) {
    this.hitDetectionCanvas_ = this.canvas_;
    return;
  }

  // if no fill style is set, create an extra hit-detection image with a
  // default fill style
  this.hitDetectionCanvas_ = /** @type {HTMLCanvasElement} */
      (document.createElement('CANVAS'));
  var canvas = this.hitDetectionCanvas_;

  canvas.height = renderOptions.size;
  canvas.width = renderOptions.size;

  var context = /** @type {CanvasRenderingContext2D} */
      (canvas.getContext('2d'));
  this.drawHitDetectionCanvas_(renderOptions, context, 0, 0);
};


/**
 * @private
 * @param {ol.style.RegularShape.RenderOptions} renderOptions Render options.
 * @param {CanvasRenderingContext2D} context The context.
 * @param {number} x The origin for the symbol (x).
 * @param {number} y The origin for the symbol (y).
 */
ol.style.RegularShape.prototype.drawHitDetectionCanvas_ = function(renderOptions, context, x, y) {
  // reset transform
  context.setTransform(1, 0, 0, 1, 0, 0);

  // then move to (x, y)
  context.translate(x, y);

  context.beginPath();
  if (this.radius2_ !== this.radius_) {
    this.points_ = 2 * this.points_;
  }
  var i, radiusC, angle0;
  for (i = 0; i <= this.points_; i++) {
    angle0 = i * 2 * Math.PI / this.points_ - Math.PI / 2 + this.angle_;
    radiusC = i % 2 === 0 ? this.radius_ : this.radius2_;
    context.lineTo(renderOptions.size / 2 + radiusC * Math.cos(angle0),
                   renderOptions.size / 2 + radiusC * Math.sin(angle0));
  }

  context.fillStyle = ol.render.canvas.defaultFillStyle;
  context.fill();
  if (this.stroke_) {
    context.strokeStyle = renderOptions.strokeStyle;
    context.lineWidth = renderOptions.strokeWidth;
    if (renderOptions.lineDash) {
      context.setLineDash(renderOptions.lineDash);
    }
    context.stroke();
  }
  context.closePath();
};


/**
 * @return {string} The checksum.
 */
ol.style.RegularShape.prototype.getChecksum = function() {
  var strokeChecksum = this.stroke_ ?
      this.stroke_.getChecksum() : '-';
  var fillChecksum = this.fill_ ?
      this.fill_.getChecksum() : '-';

  var recalculate = !this.checksums_ ||
      (strokeChecksum != this.checksums_[1] ||
      fillChecksum != this.checksums_[2] ||
      this.radius_ != this.checksums_[3] ||
      this.radius2_ != this.checksums_[4] ||
      this.angle_ != this.checksums_[5] ||
      this.points_ != this.checksums_[6]);

  if (recalculate) {
    var checksum = 'r' + strokeChecksum + fillChecksum +
        (this.radius_ !== undefined ? this.radius_.toString() : '-') +
        (this.radius2_ !== undefined ? this.radius2_.toString() : '-') +
        (this.angle_ !== undefined ? this.angle_.toString() : '-') +
        (this.points_ !== undefined ? this.points_.toString() : '-');
    this.checksums_ = [checksum, strokeChecksum, fillChecksum,
      this.radius_, this.radius2_, this.angle_, this.points_];
  }

  return this.checksums_[0];
};

// Copyright 2009 The Closure Library Authors.
// All Rights Reserved.
//
// 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.
//
// This file has been auto-generated by GenJsDeps, please do not edit.

goog.addDependency('demos/editor/equationeditor.js', ['goog.demos.editor.EquationEditor'], ['goog.ui.equation.EquationEditorDialog']);
goog.addDependency('demos/editor/helloworld.js', ['goog.demos.editor.HelloWorld'], ['goog.dom', 'goog.dom.TagName', 'goog.editor.Plugin']);
goog.addDependency('demos/editor/helloworlddialog.js', ['goog.demos.editor.HelloWorldDialog', 'goog.demos.editor.HelloWorldDialog.OkEvent'], ['goog.dom.TagName', 'goog.events.Event', 'goog.string', 'goog.ui.editor.AbstractDialog', 'goog.ui.editor.AbstractDialog.Builder', 'goog.ui.editor.AbstractDialog.EventType']);
goog.addDependency('demos/editor/helloworlddialogplugin.js', ['goog.demos.editor.HelloWorldDialogPlugin', 'goog.demos.editor.HelloWorldDialogPlugin.Command'], ['goog.demos.editor.HelloWorldDialog', 'goog.dom.TagName', 'goog.editor.plugins.AbstractDialogPlugin', 'goog.editor.range', 'goog.functions', 'goog.ui.editor.AbstractDialog.EventType']);

/**
 * @fileoverview Custom exports file.
 * @suppress {checkVars,extraRequire}
 */

goog.require('ol');
goog.require('ol.Attribution');
goog.require('ol.Collection');
goog.require('ol.CollectionEvent');
goog.require('ol.CollectionEventType');
goog.require('ol.Color');
goog.require('ol.Coordinate');
goog.require('ol.CoordinateFormatType');
goog.require('ol.DeviceOrientation');
goog.require('ol.DeviceOrientationProperty');
goog.require('ol.DragBoxEvent');
goog.require('ol.Extent');
goog.require('ol.Feature');
goog.require('ol.FeatureLoader');
goog.require('ol.FeatureStyleFunction');
goog.require('ol.FeatureUrlFunction');
goog.require('ol.Geolocation');
goog.require('ol.GeolocationProperty');
goog.require('ol.Graticule');
goog.require('ol.Image');
goog.require('ol.ImageTile');
goog.require('ol.Kinetic');
goog.require('ol.LoadingStrategy');
goog.require('ol.Map');
goog.require('ol.MapBrowserEvent');
goog.require('ol.MapBrowserEvent.EventType');
goog.require('ol.MapBrowserEventHandler');
goog.require('ol.MapBrowserPointerEvent');
goog.require('ol.MapEvent');
goog.require('ol.MapEventType');
goog.require('ol.MapProperty');
goog.require('ol.Object');
goog.require('ol.ObjectEvent');
goog.require('ol.ObjectEventType');
goog.require('ol.Observable');
goog.require('ol.Overlay');
goog.require('ol.OverlayPositioning');
goog.require('ol.OverlayProperty');
goog.require('ol.Size');
goog.require('ol.Sphere');
goog.require('ol.Tile');
goog.require('ol.TileState');
goog.require('ol.VectorTile');
goog.require('ol.View');
goog.require('ol.ViewHint');
goog.require('ol.ViewProperty');
goog.require('ol.animation');
goog.require('ol.color');
goog.require('ol.control');
goog.require('ol.control.Attribution');
goog.require('ol.control.Control');
goog.require('ol.control.FullScreen');
goog.require('ol.control.MousePosition');
goog.require('ol.control.OverviewMap');
goog.require('ol.control.Rotate');
goog.require('ol.control.ScaleLine');
goog.require('ol.control.ScaleLineProperty');
goog.require('ol.control.ScaleLineUnits');
goog.require('ol.control.Zoom');
goog.require('ol.control.ZoomSlider');
goog.require('ol.control.ZoomToExtent');
goog.require('ol.coordinate');
goog.require('ol.easing');
goog.require('ol.events.ConditionType');
goog.require('ol.events.condition');
goog.require('ol.extent');
goog.require('ol.extent.Corner');
goog.require('ol.extent.Relationship');
goog.require('ol.featureloader');
goog.require('ol.format.EsriJSON');
goog.require('ol.format.Feature');
goog.require('ol.format.GML');
goog.require('ol.format.GML2');
goog.require('ol.format.GML3');
goog.require('ol.format.GMLBase');
goog.require('ol.format.GPX');
goog.require('ol.format.GeoJSON');
goog.require('ol.format.IGC');
goog.require('ol.format.IGCZ');
goog.require('ol.format.KML');
goog.require('ol.format.MVT');
goog.require('ol.format.OSMXML');
goog.require('ol.format.Polyline');
goog.require('ol.format.TopoJSON');
goog.require('ol.format.WFS');
goog.require('ol.format.WKT');
goog.require('ol.format.WMSCapabilities');
goog.require('ol.format.WMSGetFeatureInfo');
goog.require('ol.format.WMTSCapabilities');
goog.require('ol.geom.Circle');
goog.require('ol.geom.Geometry');
goog.require('ol.geom.GeometryCollection');
goog.require('ol.geom.GeometryLayout');
goog.require('ol.geom.GeometryType');
goog.require('ol.geom.LineString');
goog.require('ol.geom.LinearRing');
goog.require('ol.geom.MultiLineString');
goog.require('ol.geom.MultiPoint');
goog.require('ol.geom.MultiPolygon');
goog.require('ol.geom.Point');
goog.require('ol.geom.Polygon');
goog.require('ol.geom.SimpleGeometry');
goog.require('ol.has');
goog.require('ol.interaction');
goog.require('ol.interaction.DoubleClickZoom');
goog.require('ol.interaction.DragAndDrop');
goog.require('ol.interaction.DragAndDropEvent');
goog.require('ol.interaction.DragBox');
goog.require('ol.interaction.DragPan');
goog.require('ol.interaction.DragRotate');
goog.require('ol.interaction.DragRotateAndZoom');
goog.require('ol.interaction.DragZoom');
goog.require('ol.interaction.Draw');
goog.require('ol.interaction.DrawEvent');
goog.require('ol.interaction.DrawEventType');
goog.require('ol.interaction.DrawGeometryFunctionType');
goog.require('ol.interaction.DrawMode');
goog.require('ol.interaction.Interaction');
goog.require('ol.interaction.InteractionProperty');
goog.require('ol.interaction.KeyboardPan');
goog.require('ol.interaction.KeyboardZoom');
goog.require('ol.interaction.Modify');
goog.require('ol.interaction.ModifyEvent');
goog.require('ol.interaction.MouseWheelZoom');
goog.require('ol.interaction.PinchRotate');
goog.require('ol.interaction.PinchZoom');
goog.require('ol.interaction.Pointer');
goog.require('ol.interaction.Select');
goog.require('ol.interaction.SelectEvent');
goog.require('ol.interaction.SelectEventType');
goog.require('ol.interaction.SelectFilterFunction');
goog.require('ol.interaction.Snap');
goog.require('ol.interaction.SnapProperty');
goog.require('ol.interaction.Translate');
goog.require('ol.interaction.TranslateEvent');
goog.require('ol.layer.Base');
goog.require('ol.layer.Group');
goog.require('ol.layer.Heatmap');
goog.require('ol.layer.Image');
goog.require('ol.layer.Layer');
goog.require('ol.layer.LayerProperty');
goog.require('ol.layer.LayerState');
goog.require('ol.layer.Tile');
goog.require('ol.layer.Vector');
goog.require('ol.layer.VectorTile');
goog.require('ol.loadingstrategy');
goog.require('ol.proj');
goog.require('ol.proj.METERS_PER_UNIT');
goog.require('ol.proj.Projection');
goog.require('ol.proj.ProjectionLike');
goog.require('ol.proj.Units');
goog.require('ol.proj.common');
goog.require('ol.render');
goog.require('ol.render.Event');
goog.require('ol.render.EventType');
goog.require('ol.render.Feature');
goog.require('ol.render.VectorContext');
goog.require('ol.render.canvas.Immediate');
goog.require('ol.render.webgl.Immediate');
goog.require('ol.size');
goog.require('ol.source.BingMaps');
goog.require('ol.source.Cluster');
goog.require('ol.source.Image');
goog.require('ol.source.ImageCanvas');
goog.require('ol.source.ImageEvent');
goog.require('ol.source.ImageMapGuide');
goog.require('ol.source.ImageStatic');
goog.require('ol.source.ImageVector');
goog.require('ol.source.ImageWMS');
goog.require('ol.source.MapQuest');
goog.require('ol.source.OSM');
goog.require('ol.source.Raster');
goog.require('ol.source.RasterEvent');
goog.require('ol.source.RasterEventType');
goog.require('ol.source.Source');
goog.require('ol.source.Stamen');
goog.require('ol.source.State');
goog.require('ol.source.Tile');
goog.require('ol.source.TileArcGISRest');
goog.require('ol.source.TileDebug');
goog.require('ol.source.TileEvent');
goog.require('ol.source.TileImage');
goog.require('ol.source.TileJSON');
goog.require('ol.source.TileOptions');
goog.require('ol.source.TileUTFGrid');
goog.require('ol.source.TileWMS');
goog.require('ol.source.UrlTile');
goog.require('ol.source.Vector');
goog.require('ol.source.VectorEvent');
goog.require('ol.source.VectorEventType');
goog.require('ol.source.VectorTile');
goog.require('ol.source.WMTS');
goog.require('ol.source.WMTSRequestEncoding');
goog.require('ol.source.XYZ');
goog.require('ol.source.Zoomify');
goog.require('ol.style.Atlas');
goog.require('ol.style.AtlasManager');
goog.require('ol.style.Circle');
goog.require('ol.style.Fill');
goog.require('ol.style.GeometryFunction');
goog.require('ol.style.Icon');
goog.require('ol.style.IconAnchorUnits');
goog.require('ol.style.IconImageCache');
goog.require('ol.style.IconOrigin');
goog.require('ol.style.Image');
goog.require('ol.style.ImageState');
goog.require('ol.style.RegularShape');
goog.require('ol.style.Stroke');
goog.require('ol.style.Style');
goog.require('ol.style.StyleFunction');
goog.require('ol.style.Text');
goog.require('ol.style.defaultGeometryFunction');
goog.require('ol.tilegrid.TileGrid');
goog.require('ol.tilegrid.WMTS');
goog.require('ol.tilejson');
goog.require('ol.webgl.Context');
goog.require('ol.xml');


goog.exportSymbol(
    'ol.animation.bounce',
    ol.animation.bounce,
    OPENLAYERS);

goog.exportSymbol(
    'ol.animation.pan',
    ol.animation.pan,
    OPENLAYERS);

goog.exportSymbol(
    'ol.animation.rotate',
    ol.animation.rotate,
    OPENLAYERS);

goog.exportSymbol(
    'ol.animation.zoom',
    ol.animation.zoom,
    OPENLAYERS);

goog.exportSymbol(
    'ol.Attribution',
    ol.Attribution,
    OPENLAYERS);

goog.exportProperty(
    ol.Attribution.prototype,
    'getHTML',
    ol.Attribution.prototype.getHTML);

goog.exportProperty(
    ol.CollectionEvent.prototype,
    'element',
    ol.CollectionEvent.prototype.element);

goog.exportSymbol(
    'ol.Collection',
    ol.Collection,
    OPENLAYERS);

goog.exportProperty(
    ol.Collection.prototype,
    'clear',
    ol.Collection.prototype.clear);

goog.exportProperty(
    ol.Collection.prototype,
    'extend',
    ol.Collection.prototype.extend);

goog.exportProperty(
    ol.Collection.prototype,
    'forEach',
    ol.Collection.prototype.forEach);

goog.exportProperty(
    ol.Collection.prototype,
    'getArray',
    ol.Collection.prototype.getArray);

goog.exportProperty(
    ol.Collection.prototype,
    'item',
    ol.Collection.prototype.item);

goog.exportProperty(
    ol.Collection.prototype,
    'getLength',
    ol.Collection.prototype.getLength);

goog.exportProperty(
    ol.Collection.prototype,
    'insertAt',
    ol.Collection.prototype.insertAt);

goog.exportProperty(
    ol.Collection.prototype,
    'pop',
    ol.Collection.prototype.pop);

goog.exportProperty(
    ol.Collection.prototype,
    'push',
    ol.Collection.prototype.push);

goog.exportProperty(
    ol.Collection.prototype,
    'remove',
    ol.Collection.prototype.remove);

goog.exportProperty(
    ol.Collection.prototype,
    'removeAt',
    ol.Collection.prototype.removeAt);

goog.exportProperty(
    ol.Collection.prototype,
    'setAt',
    ol.Collection.prototype.setAt);

goog.exportSymbol(
    'ol.coordinate.add',
    ol.coordinate.add,
    OPENLAYERS);

goog.exportSymbol(
    'ol.coordinate.createStringXY',
    ol.coordinate.createStringXY,
    OPENLAYERS);

goog.exportSymbol(
    'ol.coordinate.format',
    ol.coordinate.format,
    OPENLAYERS);

goog.exportSymbol(
    'ol.coordinate.rotate',
    ol.coordinate.rotate,
    OPENLAYERS);

goog.exportSymbol(
    'ol.coordinate.toStringHDMS',
    ol.coordinate.toStringHDMS,
    OPENLAYERS);

goog.exportSymbol(
    'ol.coordinate.toStringXY',
    ol.coordinate.toStringXY,
    OPENLAYERS);

goog.exportSymbol(
    'ol.DeviceOrientation',
    ol.DeviceOrientation,
    OPENLAYERS);

goog.exportProperty(
    ol.DeviceOrientation.prototype,
    'getAlpha',
    ol.DeviceOrientation.prototype.getAlpha);

goog.exportProperty(
    ol.DeviceOrientation.prototype,
    'getBeta',
    ol.DeviceOrientation.prototype.getBeta);

goog.exportProperty(
    ol.DeviceOrientation.prototype,
    'getGamma',
    ol.DeviceOrientation.prototype.getGamma);

goog.exportProperty(
    ol.DeviceOrientation.prototype,
    'getHeading',
    ol.DeviceOrientation.prototype.getHeading);

goog.exportProperty(
    ol.DeviceOrientation.prototype,
    'getTracking',
    ol.DeviceOrientation.prototype.getTracking);

goog.exportProperty(
    ol.DeviceOrientation.prototype,
    'setTracking',
    ol.DeviceOrientation.prototype.setTracking);

goog.exportSymbol(
    'ol.easing.easeIn',
    ol.easing.easeIn,
    OPENLAYERS);

goog.exportSymbol(
    'ol.easing.easeOut',
    ol.easing.easeOut,
    OPENLAYERS);

goog.exportSymbol(
    'ol.easing.inAndOut',
    ol.easing.inAndOut,
    OPENLAYERS);

goog.exportSymbol(
    'ol.easing.linear',
    ol.easing.linear,
    OPENLAYERS);

goog.exportSymbol(
    'ol.easing.upAndDown',
    ol.easing.upAndDown,
    OPENLAYERS);

goog.exportSymbol(
    'ol.extent.boundingExtent',
    ol.extent.boundingExtent,
    OPENLAYERS);

goog.exportSymbol(
    'ol.extent.buffer',
    ol.extent.buffer,
    OPENLAYERS);

goog.exportSymbol(
    'ol.extent.containsCoordinate',
    ol.extent.containsCoordinate,
    OPENLAYERS);

goog.exportSymbol(
    'ol.extent.containsExtent',
    ol.extent.containsExtent,
    OPENLAYERS);

goog.exportSymbol(
    'ol.extent.containsXY',
    ol.extent.containsXY,
    OPENLAYERS);

goog.exportSymbol(
    'ol.extent.createEmpty',
    ol.extent.createEmpty,
    OPENLAYERS);

goog.exportSymbol(
    'ol.extent.equals',
    ol.extent.equals,
    OPENLAYERS);

goog.exportSymbol(
    'ol.extent.extend',
    ol.extent.extend,
    OPENLAYERS);

goog.exportSymbol(
    'ol.extent.getBottomLeft',
    ol.extent.getBottomLeft,
    OPENLAYERS);

goog.exportSymbol(
    'ol.extent.getBottomRight',
    ol.extent.getBottomRight,
    OPENLAYERS);

goog.exportSymbol(
    'ol.extent.getCenter',
    ol.extent.getCenter,
    OPENLAYERS);

goog.exportSymbol(
    'ol.extent.getHeight',
    ol.extent.getHeight,
    OPENLAYERS);

goog.exportSymbol(
    'ol.extent.getIntersection',
    ol.extent.getIntersection,
    OPENLAYERS);

goog.exportSymbol(
    'ol.extent.getSize',
    ol.extent.getSize,
    OPENLAYERS);

goog.exportSymbol(
    'ol.extent.getTopLeft',
    ol.extent.getTopLeft,
    OPENLAYERS);

goog.exportSymbol(
    'ol.extent.getTopRight',
    ol.extent.getTopRight,
    OPENLAYERS);

goog.exportSymbol(
    'ol.extent.getWidth',
    ol.extent.getWidth,
    OPENLAYERS);

goog.exportSymbol(
    'ol.extent.intersects',
    ol.extent.intersects,
    OPENLAYERS);

goog.exportSymbol(
    'ol.extent.isEmpty',
    ol.extent.isEmpty,
    OPENLAYERS);

goog.exportSymbol(
    'ol.extent.applyTransform',
    ol.extent.applyTransform,
    OPENLAYERS);

goog.exportSymbol(
    'ol.Feature',
    ol.Feature,
    OPENLAYERS);

goog.exportProperty(
    ol.Feature.prototype,
    'clone',
    ol.Feature.prototype.clone);

goog.exportProperty(
    ol.Feature.prototype,
    'getGeometry',
    ol.Feature.prototype.getGeometry);

goog.exportProperty(
    ol.Feature.prototype,
    'getId',
    ol.Feature.prototype.getId);

goog.exportProperty(
    ol.Feature.prototype,
    'getGeometryName',
    ol.Feature.prototype.getGeometryName);

goog.exportProperty(
    ol.Feature.prototype,
    'getStyle',
    ol.Feature.prototype.getStyle);

goog.exportProperty(
    ol.Feature.prototype,
    'getStyleFunction',
    ol.Feature.prototype.getStyleFunction);

goog.exportProperty(
    ol.Feature.prototype,
    'setGeometry',
    ol.Feature.prototype.setGeometry);

goog.exportProperty(
    ol.Feature.prototype,
    'setStyle',
    ol.Feature.prototype.setStyle);

goog.exportProperty(
    ol.Feature.prototype,
    'setId',
    ol.Feature.prototype.setId);

goog.exportProperty(
    ol.Feature.prototype,
    'setGeometryName',
    ol.Feature.prototype.setGeometryName);

goog.exportSymbol(
    'ol.featureloader.tile',
    ol.featureloader.tile,
    OPENLAYERS);

goog.exportSymbol(
    'ol.featureloader.xhr',
    ol.featureloader.xhr,
    OPENLAYERS);

goog.exportSymbol(
    'ol.Geolocation',
    ol.Geolocation,
    OPENLAYERS);

goog.exportProperty(
    ol.Geolocation.prototype,
    'getAccuracy',
    ol.Geolocation.prototype.getAccuracy);

goog.exportProperty(
    ol.Geolocation.prototype,
    'getAccuracyGeometry',
    ol.Geolocation.prototype.getAccuracyGeometry);

goog.exportProperty(
    ol.Geolocation.prototype,
    'getAltitude',
    ol.Geolocation.prototype.getAltitude);

goog.exportProperty(
    ol.Geolocation.prototype,
    'getAltitudeAccuracy',
    ol.Geolocation.prototype.getAltitudeAccuracy);

goog.exportProperty(
    ol.Geolocation.prototype,
    'getHeading',
    ol.Geolocation.prototype.getHeading);

goog.exportProperty(
    ol.Geolocation.prototype,
    'getPosition',
    ol.Geolocation.prototype.getPosition);

goog.exportProperty(
    ol.Geolocation.prototype,
    'getProjection',
    ol.Geolocation.prototype.getProjection);

goog.exportProperty(
    ol.Geolocation.prototype,
    'getSpeed',
    ol.Geolocation.prototype.getSpeed);

goog.exportProperty(
    ol.Geolocation.prototype,
    'getTracking',
    ol.Geolocation.prototype.getTracking);

goog.exportProperty(
    ol.Geolocation.prototype,
    'getTrackingOptions',
    ol.Geolocation.prototype.getTrackingOptions);

goog.exportProperty(
    ol.Geolocation.prototype,
    'setProjection',
    ol.Geolocation.prototype.setProjection);

goog.exportProperty(
    ol.Geolocation.prototype,
    'setTracking',
    ol.Geolocation.prototype.setTracking);

goog.exportProperty(
    ol.Geolocation.prototype,
    'setTrackingOptions',
    ol.Geolocation.prototype.setTrackingOptions);

goog.exportSymbol(
    'ol.Graticule',
    ol.Graticule,
    OPENLAYERS);

goog.exportProperty(
    ol.Graticule.prototype,
    'getMap',
    ol.Graticule.prototype.getMap);

goog.exportProperty(
    ol.Graticule.prototype,
    'getMeridians',
    ol.Graticule.prototype.getMeridians);

goog.exportProperty(
    ol.Graticule.prototype,
    'getParallels',
    ol.Graticule.prototype.getParallels);

goog.exportProperty(
    ol.Graticule.prototype,
    'setMap',
    ol.Graticule.prototype.setMap);

goog.exportSymbol(
    'ol.has.DEVICE_PIXEL_RATIO',
    ol.has.DEVICE_PIXEL_RATIO,
    OPENLAYERS);

goog.exportSymbol(
    'ol.has.CANVAS',
    ol.has.CANVAS,
    OPENLAYERS);

goog.exportSymbol(
    'ol.has.DEVICE_ORIENTATION',
    ol.has.DEVICE_ORIENTATION,
    OPENLAYERS);

goog.exportSymbol(
    'ol.has.GEOLOCATION',
    ol.has.GEOLOCATION,
    OPENLAYERS);

goog.exportSymbol(
    'ol.has.TOUCH',
    ol.has.TOUCH,
    OPENLAYERS);

goog.exportSymbol(
    'ol.has.WEBGL',
    ol.has.WEBGL,
    OPENLAYERS);

goog.exportProperty(
    ol.Image.prototype,
    'getImage',
    ol.Image.prototype.getImage);

goog.exportProperty(
    ol.ImageTile.prototype,
    'getImage',
    ol.ImageTile.prototype.getImage);

goog.exportSymbol(
    'ol.Kinetic',
    ol.Kinetic,
    OPENLAYERS);

goog.exportSymbol(
    'ol.loadingstrategy.all',
    ol.loadingstrategy.all,
    OPENLAYERS);

goog.exportSymbol(
    'ol.loadingstrategy.bbox',
    ol.loadingstrategy.bbox,
    OPENLAYERS);

goog.exportSymbol(
    'ol.loadingstrategy.tile',
    ol.loadingstrategy.tile,
    OPENLAYERS);

goog.exportSymbol(
    'ol.Map',
    ol.Map,
    OPENLAYERS);

goog.exportProperty(
    ol.Map.prototype,
    'addControl',
    ol.Map.prototype.addControl);

goog.exportProperty(
    ol.Map.prototype,
    'addInteraction',
    ol.Map.prototype.addInteraction);

goog.exportProperty(
    ol.Map.prototype,
    'addLayer',
    ol.Map.prototype.addLayer);

goog.exportProperty(
    ol.Map.prototype,
    'addOverlay',
    ol.Map.prototype.addOverlay);

goog.exportProperty(
    ol.Map.prototype,
    'beforeRender',
    ol.Map.prototype.beforeRender);

goog.exportProperty(
    ol.Map.prototype,
    'forEachFeatureAtPixel',
    ol.Map.prototype.forEachFeatureAtPixel);

goog.exportProperty(
    ol.Map.prototype,
    'forEachLayerAtPixel',
    ol.Map.prototype.forEachLayerAtPixel);

goog.exportProperty(
    ol.Map.prototype,
    'hasFeatureAtPixel',
    ol.Map.prototype.hasFeatureAtPixel);

goog.exportProperty(
    ol.Map.prototype,
    'getEventCoordinate',
    ol.Map.prototype.getEventCoordinate);

goog.exportProperty(
    ol.Map.prototype,
    'getEventPixel',
    ol.Map.prototype.getEventPixel);

goog.exportProperty(
    ol.Map.prototype,
    'getTarget',
    ol.Map.prototype.getTarget);

goog.exportProperty(
    ol.Map.prototype,
    'getTargetElement',
    ol.Map.prototype.getTargetElement);

goog.exportProperty(
    ol.Map.prototype,
    'getCoordinateFromPixel',
    ol.Map.prototype.getCoordinateFromPixel);

goog.exportProperty(
    ol.Map.prototype,
    'getControls',
    ol.Map.prototype.getControls);

goog.exportProperty(
    ol.Map.prototype,
    'getOverlays',
    ol.Map.prototype.getOverlays);

goog.exportProperty(
    ol.Map.prototype,
    'getOverlayById',
    ol.Map.prototype.getOverlayById);

goog.exportProperty(
    ol.Map.prototype,
    'getInteractions',
    ol.Map.prototype.getInteractions);

goog.exportProperty(
    ol.Map.prototype,
    'getLayerGroup',
    ol.Map.prototype.getLayerGroup);

goog.exportProperty(
    ol.Map.prototype,
    'getLayers',
    ol.Map.prototype.getLayers);

goog.exportProperty(
    ol.Map.prototype,
    'getPixelFromCoordinate',
    ol.Map.prototype.getPixelFromCoordinate);

goog.exportProperty(
    ol.Map.prototype,
    'getSize',
    ol.Map.prototype.getSize);

goog.exportProperty(
    ol.Map.prototype,
    'getView',
    ol.Map.prototype.getView);

goog.exportProperty(
    ol.Map.prototype,
    'getViewport',
    ol.Map.prototype.getViewport);

goog.exportProperty(
    ol.Map.prototype,
    'renderSync',
    ol.Map.prototype.renderSync);

goog.exportProperty(
    ol.Map.prototype,
    'render',
    ol.Map.prototype.render);

goog.exportProperty(
    ol.Map.prototype,
    'removeControl',
    ol.Map.prototype.removeControl);

goog.exportProperty(
    ol.Map.prototype,
    'removeInteraction',
    ol.Map.prototype.removeInteraction);

goog.exportProperty(
    ol.Map.prototype,
    'removeLayer',
    ol.Map.prototype.removeLayer);

goog.exportProperty(
    ol.Map.prototype,
    'removeOverlay',
    ol.Map.prototype.removeOverlay);

goog.exportProperty(
    ol.Map.prototype,
    'setLayerGroup',
    ol.Map.prototype.setLayerGroup);

goog.exportProperty(
    ol.Map.prototype,
    'setSize',
    ol.Map.prototype.setSize);

goog.exportProperty(
    ol.Map.prototype,
    'setTarget',
    ol.Map.prototype.setTarget);

goog.exportProperty(
    ol.Map.prototype,
    'setView',
    ol.Map.prototype.setView);

goog.exportProperty(
    ol.Map.prototype,
    'updateSize',
    ol.Map.prototype.updateSize);

goog.exportProperty(
    ol.MapBrowserEvent.prototype,
    'originalEvent',
    ol.MapBrowserEvent.prototype.originalEvent);

goog.exportProperty(
    ol.MapBrowserEvent.prototype,
    'pixel',
    ol.MapBrowserEvent.prototype.pixel);

goog.exportProperty(
    ol.MapBrowserEvent.prototype,
    'coordinate',
    ol.MapBrowserEvent.prototype.coordinate);

goog.exportProperty(
    ol.MapBrowserEvent.prototype,
    'dragging',
    ol.MapBrowserEvent.prototype.dragging);

goog.exportProperty(
    ol.MapBrowserEvent.prototype,
    'preventDefault',
    ol.MapBrowserEvent.prototype.preventDefault);

goog.exportProperty(
    ol.MapBrowserEvent.prototype,
    'stopPropagation',
    ol.MapBrowserEvent.prototype.stopPropagation);

goog.exportProperty(
    ol.MapEvent.prototype,
    'map',
    ol.MapEvent.prototype.map);

goog.exportProperty(
    ol.MapEvent.prototype,
    'frameState',
    ol.MapEvent.prototype.frameState);

goog.exportProperty(
    ol.ObjectEvent.prototype,
    'key',
    ol.ObjectEvent.prototype.key);

goog.exportProperty(
    ol.ObjectEvent.prototype,
    'oldValue',
    ol.ObjectEvent.prototype.oldValue);

goog.exportSymbol(
    'ol.Object',
    ol.Object,
    OPENLAYERS);

goog.exportProperty(
    ol.Object.prototype,
    'get',
    ol.Object.prototype.get);

goog.exportProperty(
    ol.Object.prototype,
    'getKeys',
    ol.Object.prototype.getKeys);

goog.exportProperty(
    ol.Object.prototype,
    'getProperties',
    ol.Object.prototype.getProperties);

goog.exportProperty(
    ol.Object.prototype,
    'set',
    ol.Object.prototype.set);

goog.exportProperty(
    ol.Object.prototype,
    'setProperties',
    ol.Object.prototype.setProperties);

goog.exportProperty(
    ol.Object.prototype,
    'unset',
    ol.Object.prototype.unset);

goog.exportSymbol(
    'ol.Observable',
    ol.Observable,
    OPENLAYERS);

goog.exportSymbol(
    'ol.Observable.unByKey',
    ol.Observable.unByKey,
    OPENLAYERS);

goog.exportProperty(
    ol.Observable.prototype,
    'changed',
    ol.Observable.prototype.changed);

goog.exportProperty(
    ol.Observable.prototype,
    'dispatchEvent',
    ol.Observable.prototype.dispatchEvent);

goog.exportProperty(
    ol.Observable.prototype,
    'getRevision',
    ol.Observable.prototype.getRevision);

goog.exportProperty(
    ol.Observable.prototype,
    'on',
    ol.Observable.prototype.on);

goog.exportProperty(
    ol.Observable.prototype,
    'once',
    ol.Observable.prototype.once);

goog.exportProperty(
    ol.Observable.prototype,
    'un',
    ol.Observable.prototype.un);

goog.exportProperty(
    ol.Observable.prototype,
    'unByKey',
    ol.Observable.prototype.unByKey);

goog.exportSymbol(
    'ol.inherits',
    ol.inherits,
    OPENLAYERS);

goog.exportSymbol(
    'ol.Overlay',
    ol.Overlay,
    OPENLAYERS);

goog.exportProperty(
    ol.Overlay.prototype,
    'getElement',
    ol.Overlay.prototype.getElement);

goog.exportProperty(
    ol.Overlay.prototype,
    'getId',
    ol.Overlay.prototype.getId);

goog.exportProperty(
    ol.Overlay.prototype,
    'getMap',
    ol.Overlay.prototype.getMap);

goog.exportProperty(
    ol.Overlay.prototype,
    'getOffset',
    ol.Overlay.prototype.getOffset);

goog.exportProperty(
    ol.Overlay.prototype,
    'getPosition',
    ol.Overlay.prototype.getPosition);

goog.exportProperty(
    ol.Overlay.prototype,
    'getPositioning',
    ol.Overlay.prototype.getPositioning);

goog.exportProperty(
    ol.Overlay.prototype,
    'setElement',
    ol.Overlay.prototype.setElement);

goog.exportProperty(
    ol.Overlay.prototype,
    'setMap',
    ol.Overlay.prototype.setMap);

goog.exportProperty(
    ol.Overlay.prototype,
    'setOffset',
    ol.Overlay.prototype.setOffset);

goog.exportProperty(
    ol.Overlay.prototype,
    'setPosition',
    ol.Overlay.prototype.setPosition);

goog.exportProperty(
    ol.Overlay.prototype,
    'setPositioning',
    ol.Overlay.prototype.setPositioning);

goog.exportSymbol(
    'ol.render.toContext',
    ol.render.toContext,
    OPENLAYERS);

goog.exportSymbol(
    'ol.size.toSize',
    ol.size.toSize,
    OPENLAYERS);

goog.exportProperty(
    ol.Tile.prototype,
    'getTileCoord',
    ol.Tile.prototype.getTileCoord);

goog.exportProperty(
    ol.VectorTile.prototype,
    'getFormat',
    ol.VectorTile.prototype.getFormat);

goog.exportProperty(
    ol.VectorTile.prototype,
    'setFeatures',
    ol.VectorTile.prototype.setFeatures);

goog.exportProperty(
    ol.VectorTile.prototype,
    'setLoader',
    ol.VectorTile.prototype.setLoader);

goog.exportSymbol(
    'ol.View',
    ol.View,
    OPENLAYERS);

goog.exportProperty(
    ol.View.prototype,
    'constrainCenter',
    ol.View.prototype.constrainCenter);

goog.exportProperty(
    ol.View.prototype,
    'constrainResolution',
    ol.View.prototype.constrainResolution);

goog.exportProperty(
    ol.View.prototype,
    'constrainRotation',
    ol.View.prototype.constrainRotation);

goog.exportProperty(
    ol.View.prototype,
    'getCenter',
    ol.View.prototype.getCenter);

goog.exportProperty(
    ol.View.prototype,
    'calculateExtent',
    ol.View.prototype.calculateExtent);

goog.exportProperty(
    ol.View.prototype,
    'getProjection',
    ol.View.prototype.getProjection);

goog.exportProperty(
    ol.View.prototype,
    'getResolution',
    ol.View.prototype.getResolution);

goog.exportProperty(
    ol.View.prototype,
    'getRotation',
    ol.View.prototype.getRotation);

goog.exportProperty(
    ol.View.prototype,
    'getZoom',
    ol.View.prototype.getZoom);

goog.exportProperty(
    ol.View.prototype,
    'fit',
    ol.View.prototype.fit);

goog.exportProperty(
    ol.View.prototype,
    'centerOn',
    ol.View.prototype.centerOn);

goog.exportProperty(
    ol.View.prototype,
    'rotate',
    ol.View.prototype.rotate);

goog.exportProperty(
    ol.View.prototype,
    'setCenter',
    ol.View.prototype.setCenter);

goog.exportProperty(
    ol.View.prototype,
    'setResolution',
    ol.View.prototype.setResolution);

goog.exportProperty(
    ol.View.prototype,
    'setRotation',
    ol.View.prototype.setRotation);

goog.exportProperty(
    ol.View.prototype,
    'setZoom',
    ol.View.prototype.setZoom);

goog.exportSymbol(
    'ol.xml.getAllTextContent',
    ol.xml.getAllTextContent,
    OPENLAYERS);

goog.exportSymbol(
    'ol.xml.parse',
    ol.xml.parse,
    OPENLAYERS);

goog.exportProperty(
    ol.webgl.Context.prototype,
    'getGL',
    ol.webgl.Context.prototype.getGL);

goog.exportProperty(
    ol.webgl.Context.prototype,
    'useProgram',
    ol.webgl.Context.prototype.useProgram);

goog.exportSymbol(
    'ol.tilegrid.TileGrid',
    ol.tilegrid.TileGrid,
    OPENLAYERS);

goog.exportProperty(
    ol.tilegrid.TileGrid.prototype,
    'getMaxZoom',
    ol.tilegrid.TileGrid.prototype.getMaxZoom);

goog.exportProperty(
    ol.tilegrid.TileGrid.prototype,
    'getMinZoom',
    ol.tilegrid.TileGrid.prototype.getMinZoom);

goog.exportProperty(
    ol.tilegrid.TileGrid.prototype,
    'getOrigin',
    ol.tilegrid.TileGrid.prototype.getOrigin);

goog.exportProperty(
    ol.tilegrid.TileGrid.prototype,
    'getResolution',
    ol.tilegrid.TileGrid.prototype.getResolution);

goog.exportProperty(
    ol.tilegrid.TileGrid.prototype,
    'getResolutions',
    ol.tilegrid.TileGrid.prototype.getResolutions);

goog.exportProperty(
    ol.tilegrid.TileGrid.prototype,
    'getTileCoordExtent',
    ol.tilegrid.TileGrid.prototype.getTileCoordExtent);

goog.exportProperty(
    ol.tilegrid.TileGrid.prototype,
    'getTileCoordForCoordAndResolution',
    ol.tilegrid.TileGrid.prototype.getTileCoordForCoordAndResolution);

goog.exportProperty(
    ol.tilegrid.TileGrid.prototype,
    'getTileCoordForCoordAndZ',
    ol.tilegrid.TileGrid.prototype.getTileCoordForCoordAndZ);

goog.exportProperty(
    ol.tilegrid.TileGrid.prototype,
    'getTileSize',
    ol.tilegrid.TileGrid.prototype.getTileSize);

goog.exportSymbol(
    'ol.tilegrid.createXYZ',
    ol.tilegrid.createXYZ,
    OPENLAYERS);

goog.exportSymbol(
    'ol.tilegrid.WMTS',
    ol.tilegrid.WMTS,
    OPENLAYERS);

goog.exportProperty(
    ol.tilegrid.WMTS.prototype,
    'getMatrixIds',
    ol.tilegrid.WMTS.prototype.getMatrixIds);

goog.exportSymbol(
    'ol.tilegrid.WMTS.createFromCapabilitiesMatrixSet',
    ol.tilegrid.WMTS.createFromCapabilitiesMatrixSet,
    OPENLAYERS);

goog.exportSymbol(
    'ol.style.AtlasManager',
    ol.style.AtlasManager,
    OPENLAYERS);

goog.exportSymbol(
    'ol.style.Circle',
    ol.style.Circle,
    OPENLAYERS);

goog.exportProperty(
    ol.style.Circle.prototype,
    'getFill',
    ol.style.Circle.prototype.getFill);

goog.exportProperty(
    ol.style.Circle.prototype,
    'getImage',
    ol.style.Circle.prototype.getImage);

goog.exportProperty(
    ol.style.Circle.prototype,
    'getRadius',
    ol.style.Circle.prototype.getRadius);

goog.exportProperty(
    ol.style.Circle.prototype,
    'getStroke',
    ol.style.Circle.prototype.getStroke);

goog.exportSymbol(
    'ol.style.Fill',
    ol.style.Fill,
    OPENLAYERS);

goog.exportProperty(
    ol.style.Fill.prototype,
    'getColor',
    ol.style.Fill.prototype.getColor);

goog.exportProperty(
    ol.style.Fill.prototype,
    'setColor',
    ol.style.Fill.prototype.setColor);

goog.exportSymbol(
    'ol.style.Icon',
    ol.style.Icon,
    OPENLAYERS);

goog.exportProperty(
    ol.style.Icon.prototype,
    'getAnchor',
    ol.style.Icon.prototype.getAnchor);

goog.exportProperty(
    ol.style.Icon.prototype,
    'getImage',
    ol.style.Icon.prototype.getImage);

goog.exportProperty(
    ol.style.Icon.prototype,
    'getOrigin',
    ol.style.Icon.prototype.getOrigin);

goog.exportProperty(
    ol.style.Icon.prototype,
    'getSrc',
    ol.style.Icon.prototype.getSrc);

goog.exportProperty(
    ol.style.Icon.prototype,
    'getSize',
    ol.style.Icon.prototype.getSize);

goog.exportProperty(
    ol.style.Icon.prototype,
    'load',
    ol.style.Icon.prototype.load);

goog.exportSymbol(
    'ol.style.Image',
    ol.style.Image,
    OPENLAYERS);

goog.exportProperty(
    ol.style.Image.prototype,
    'getOpacity',
    ol.style.Image.prototype.getOpacity);

goog.exportProperty(
    ol.style.Image.prototype,
    'getRotateWithView',
    ol.style.Image.prototype.getRotateWithView);

goog.exportProperty(
    ol.style.Image.prototype,
    'getRotation',
    ol.style.Image.prototype.getRotation);

goog.exportProperty(
    ol.style.Image.prototype,
    'getScale',
    ol.style.Image.prototype.getScale);

goog.exportProperty(
    ol.style.Image.prototype,
    'getSnapToPixel',
    ol.style.Image.prototype.getSnapToPixel);

goog.exportProperty(
    ol.style.Image.prototype,
    'setOpacity',
    ol.style.Image.prototype.setOpacity);

goog.exportProperty(
    ol.style.Image.prototype,
    'setRotation',
    ol.style.Image.prototype.setRotation);

goog.exportProperty(
    ol.style.Image.prototype,
    'setScale',
    ol.style.Image.prototype.setScale);

goog.exportSymbol(
    'ol.style.RegularShape',
    ol.style.RegularShape,
    OPENLAYERS);

goog.exportProperty(
    ol.style.RegularShape.prototype,
    'getAnchor',
    ol.style.RegularShape.prototype.getAnchor);

goog.exportProperty(
    ol.style.RegularShape.prototype,
    'getAngle',
    ol.style.RegularShape.prototype.getAngle);

goog.exportProperty(
    ol.style.RegularShape.prototype,
    'getFill',
    ol.style.RegularShape.prototype.getFill);

goog.exportProperty(
    ol.style.RegularShape.prototype,
    'getImage',
    ol.style.RegularShape.prototype.getImage);

goog.exportProperty(
    ol.style.RegularShape.prototype,
    'getOrigin',
    ol.style.RegularShape.prototype.getOrigin);

goog.exportProperty(
    ol.style.RegularShape.prototype,
    'getPoints',
    ol.style.RegularShape.prototype.getPoints);

goog.exportProperty(
    ol.style.RegularShape.prototype,
    'getRadius',
    ol.style.RegularShape.prototype.getRadius);

goog.exportProperty(
    ol.style.RegularShape.prototype,
    'getRadius2',
    ol.style.RegularShape.prototype.getRadius2);

goog.exportProperty(
    ol.style.RegularShape.prototype,
    'getSize',
    ol.style.RegularShape.prototype.getSize);

goog.exportProperty(
    ol.style.RegularShape.prototype,
    'getStroke',
    ol.style.RegularShape.prototype.getStroke);

goog.exportSymbol(
    'ol.style.Stroke',
    ol.style.Stroke,
    OPENLAYERS);

goog.exportProperty(
    ol.style.Stroke.prototype,
    'getColor',
    ol.style.Stroke.prototype.getColor);

goog.exportProperty(
    ol.style.Stroke.prototype,
    'getLineCap',
    ol.style.Stroke.prototype.getLineCap);

goog.exportProperty(
    ol.style.Stroke.prototype,
    'getLineDash',
    ol.style.Stroke.prototype.getLineDash);

goog.exportProperty(
    ol.style.Stroke.prototype,
    'getLineJoin',
    ol.style.Stroke.prototype.getLineJoin);

goog.exportProperty(
    ol.style.Stroke.prototype,
    'getMiterLimit',
    ol.style.Stroke.prototype.getMiterLimit);

goog.exportProperty(
    ol.style.Stroke.prototype,
    'getWidth',
    ol.style.Stroke.prototype.getWidth);

goog.exportProperty(
    ol.style.Stroke.prototype,
    'setColor',
    ol.style.Stroke.prototype.setColor);

goog.exportProperty(
    ol.style.Stroke.prototype,
    'setLineCap',
    ol.style.Stroke.prototype.setLineCap);

goog.exportProperty(
    ol.style.Stroke.prototype,
    'setLineDash',
    ol.style.Stroke.prototype.setLineDash);

goog.exportProperty(
    ol.style.Stroke.prototype,
    'setLineJoin',
    ol.style.Stroke.prototype.setLineJoin);

goog.exportProperty(
    ol.style.Stroke.prototype,
    'setMiterLimit',
    ol.style.Stroke.prototype.setMiterLimit);

goog.exportProperty(
    ol.style.Stroke.prototype,
    'setWidth',
    ol.style.Stroke.prototype.setWidth);

goog.exportSymbol(
    'ol.style.Style',
    ol.style.Style,
    OPENLAYERS);

goog.exportProperty(
    ol.style.Style.prototype,
    'getGeometry',
    ol.style.Style.prototype.getGeometry);

goog.exportProperty(
    ol.style.Style.prototype,
    'getGeometryFunction',
    ol.style.Style.prototype.getGeometryFunction);

goog.exportProperty(
    ol.style.Style.prototype,
    'getFill',
    ol.style.Style.prototype.getFill);

goog.exportProperty(
    ol.style.Style.prototype,
    'getImage',
    ol.style.Style.prototype.getImage);

goog.exportProperty(
    ol.style.Style.prototype,
    'getStroke',
    ol.style.Style.prototype.getStroke);

goog.exportProperty(
    ol.style.Style.prototype,
    'getText',
    ol.style.Style.prototype.getText);

goog.exportProperty(
    ol.style.Style.prototype,
    'getZIndex',
    ol.style.Style.prototype.getZIndex);

goog.exportProperty(
    ol.style.Style.prototype,
    'setGeometry',
    ol.style.Style.prototype.setGeometry);

goog.exportProperty(
    ol.style.Style.prototype,
    'setZIndex',
    ol.style.Style.prototype.setZIndex);

goog.exportSymbol(
    'ol.style.Text',
    ol.style.Text,
    OPENLAYERS);

goog.exportProperty(
    ol.style.Text.prototype,
    'getFont',
    ol.style.Text.prototype.getFont);

goog.exportProperty(
    ol.style.Text.prototype,
    'getOffsetX',
    ol.style.Text.prototype.getOffsetX);

goog.exportProperty(
    ol.style.Text.prototype,
    'getOffsetY',
    ol.style.Text.prototype.getOffsetY);

goog.exportProperty(
    ol.style.Text.prototype,
    'getFill',
    ol.style.Text.prototype.getFill);

goog.exportProperty(
    ol.style.Text.prototype,
    'getRotation',
    ol.style.Text.prototype.getRotation);

goog.exportProperty(
    ol.style.Text.prototype,
    'getScale',
    ol.style.Text.prototype.getScale);

goog.exportProperty(
    ol.style.Text.prototype,
    'getStroke',
    ol.style.Text.prototype.getStroke);

goog.exportProperty(
    ol.style.Text.prototype,
    'getText',
    ol.style.Text.prototype.getText);

goog.exportProperty(
    ol.style.Text.prototype,
    'getTextAlign',
    ol.style.Text.prototype.getTextAlign);

goog.exportProperty(
    ol.style.Text.prototype,
    'getTextBaseline',
    ol.style.Text.prototype.getTextBaseline);

goog.exportProperty(
    ol.style.Text.prototype,
    'setFont',
    ol.style.Text.prototype.setFont);

goog.exportProperty(
    ol.style.Text.prototype,
    'setOffsetX',
    ol.style.Text.prototype.setOffsetX);

goog.exportProperty(
    ol.style.Text.prototype,
    'setOffsetY',
    ol.style.Text.prototype.setOffsetY);

goog.exportProperty(
    ol.style.Text.prototype,
    'setFill',
    ol.style.Text.prototype.setFill);

goog.exportProperty(
    ol.style.Text.prototype,
    'setRotation',
    ol.style.Text.prototype.setRotation);

goog.exportProperty(
    ol.style.Text.prototype,
    'setScale',
    ol.style.Text.prototype.setScale);

goog.exportProperty(
    ol.style.Text.prototype,
    'setStroke',
    ol.style.Text.prototype.setStroke);

goog.exportProperty(
    ol.style.Text.prototype,
    'setText',
    ol.style.Text.prototype.setText);

goog.exportProperty(
    ol.style.Text.prototype,
    'setTextAlign',
    ol.style.Text.prototype.setTextAlign);

goog.exportProperty(
    ol.style.Text.prototype,
    'setTextBaseline',
    ol.style.Text.prototype.setTextBaseline);

goog.exportSymbol(
    'ol.Sphere',
    ol.Sphere,
    OPENLAYERS);

goog.exportProperty(
    ol.Sphere.prototype,
    'geodesicArea',
    ol.Sphere.prototype.geodesicArea);

goog.exportProperty(
    ol.Sphere.prototype,
    'haversineDistance',
    ol.Sphere.prototype.haversineDistance);

goog.exportSymbol(
    'ol.source.BingMaps',
    ol.source.BingMaps,
    OPENLAYERS);

goog.exportSymbol(
    'ol.source.BingMaps.TOS_ATTRIBUTION',
    ol.source.BingMaps.TOS_ATTRIBUTION,
    OPENLAYERS);

goog.exportSymbol(
    'ol.source.Cluster',
    ol.source.Cluster,
    OPENLAYERS);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'getSource',
    ol.source.Cluster.prototype.getSource);

goog.exportSymbol(
    'ol.source.ImageCanvas',
    ol.source.ImageCanvas,
    OPENLAYERS);

goog.exportSymbol(
    'ol.source.ImageMapGuide',
    ol.source.ImageMapGuide,
    OPENLAYERS);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'getParams',
    ol.source.ImageMapGuide.prototype.getParams);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'getImageLoadFunction',
    ol.source.ImageMapGuide.prototype.getImageLoadFunction);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'updateParams',
    ol.source.ImageMapGuide.prototype.updateParams);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'setImageLoadFunction',
    ol.source.ImageMapGuide.prototype.setImageLoadFunction);

goog.exportSymbol(
    'ol.source.Image',
    ol.source.Image,
    OPENLAYERS);

goog.exportProperty(
    ol.source.ImageEvent.prototype,
    'image',
    ol.source.ImageEvent.prototype.image);

goog.exportSymbol(
    'ol.source.ImageStatic',
    ol.source.ImageStatic,
    OPENLAYERS);

goog.exportSymbol(
    'ol.source.ImageVector',
    ol.source.ImageVector,
    OPENLAYERS);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'getSource',
    ol.source.ImageVector.prototype.getSource);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'getStyle',
    ol.source.ImageVector.prototype.getStyle);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'getStyleFunction',
    ol.source.ImageVector.prototype.getStyleFunction);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'setStyle',
    ol.source.ImageVector.prototype.setStyle);

goog.exportSymbol(
    'ol.source.ImageWMS',
    ol.source.ImageWMS,
    OPENLAYERS);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'getGetFeatureInfoUrl',
    ol.source.ImageWMS.prototype.getGetFeatureInfoUrl);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'getParams',
    ol.source.ImageWMS.prototype.getParams);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'getImageLoadFunction',
    ol.source.ImageWMS.prototype.getImageLoadFunction);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'getUrl',
    ol.source.ImageWMS.prototype.getUrl);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'setImageLoadFunction',
    ol.source.ImageWMS.prototype.setImageLoadFunction);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'setUrl',
    ol.source.ImageWMS.prototype.setUrl);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'updateParams',
    ol.source.ImageWMS.prototype.updateParams);

goog.exportSymbol(
    'ol.source.MapQuest',
    ol.source.MapQuest,
    OPENLAYERS);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'getLayer',
    ol.source.MapQuest.prototype.getLayer);

goog.exportSymbol(
    'ol.source.OSM',
    ol.source.OSM,
    OPENLAYERS);

goog.exportSymbol(
    'ol.source.OSM.ATTRIBUTION',
    ol.source.OSM.ATTRIBUTION,
    OPENLAYERS);

goog.exportSymbol(
    'ol.source.Raster',
    ol.source.Raster,
    OPENLAYERS);

goog.exportProperty(
    ol.source.Raster.prototype,
    'setOperation',
    ol.source.Raster.prototype.setOperation);

goog.exportProperty(
    ol.source.RasterEvent.prototype,
    'extent',
    ol.source.RasterEvent.prototype.extent);

goog.exportProperty(
    ol.source.RasterEvent.prototype,
    'resolution',
    ol.source.RasterEvent.prototype.resolution);

goog.exportProperty(
    ol.source.RasterEvent.prototype,
    'data',
    ol.source.RasterEvent.prototype.data);

goog.exportSymbol(
    'ol.source.Source',
    ol.source.Source,
    OPENLAYERS);

goog.exportProperty(
    ol.source.Source.prototype,
    'getAttributions',
    ol.source.Source.prototype.getAttributions);

goog.exportProperty(
    ol.source.Source.prototype,
    'getLogo',
    ol.source.Source.prototype.getLogo);

goog.exportProperty(
    ol.source.Source.prototype,
    'getProjection',
    ol.source.Source.prototype.getProjection);

goog.exportProperty(
    ol.source.Source.prototype,
    'getState',
    ol.source.Source.prototype.getState);

goog.exportProperty(
    ol.source.Source.prototype,
    'setAttributions',
    ol.source.Source.prototype.setAttributions);

goog.exportSymbol(
    'ol.source.Stamen',
    ol.source.Stamen,
    OPENLAYERS);

goog.exportSymbol(
    'ol.source.TileArcGISRest',
    ol.source.TileArcGISRest,
    OPENLAYERS);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'getParams',
    ol.source.TileArcGISRest.prototype.getParams);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'updateParams',
    ol.source.TileArcGISRest.prototype.updateParams);

goog.exportSymbol(
    'ol.source.TileDebug',
    ol.source.TileDebug,
    OPENLAYERS);

goog.exportSymbol(
    'ol.source.TileImage',
    ol.source.TileImage,
    OPENLAYERS);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'setRenderReprojectionEdges',
    ol.source.TileImage.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'setTileGridForProjection',
    ol.source.TileImage.prototype.setTileGridForProjection);

goog.exportSymbol(
    'ol.source.TileJSON',
    ol.source.TileJSON,
    OPENLAYERS);

goog.exportSymbol(
    'ol.source.Tile',
    ol.source.Tile,
    OPENLAYERS);

goog.exportProperty(
    ol.source.Tile.prototype,
    'getTileGrid',
    ol.source.Tile.prototype.getTileGrid);

goog.exportProperty(
    ol.source.TileEvent.prototype,
    'tile',
    ol.source.TileEvent.prototype.tile);

goog.exportSymbol(
    'ol.source.TileUTFGrid',
    ol.source.TileUTFGrid,
    OPENLAYERS);

goog.exportProperty(
    ol.source.TileUTFGrid.prototype,
    'getTemplate',
    ol.source.TileUTFGrid.prototype.getTemplate);

goog.exportProperty(
    ol.source.TileUTFGrid.prototype,
    'forDataAtCoordinateAndResolution',
    ol.source.TileUTFGrid.prototype.forDataAtCoordinateAndResolution);

goog.exportSymbol(
    'ol.source.TileWMS',
    ol.source.TileWMS,
    OPENLAYERS);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'getGetFeatureInfoUrl',
    ol.source.TileWMS.prototype.getGetFeatureInfoUrl);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'getParams',
    ol.source.TileWMS.prototype.getParams);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'updateParams',
    ol.source.TileWMS.prototype.updateParams);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'getTileLoadFunction',
    ol.source.UrlTile.prototype.getTileLoadFunction);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'getTileUrlFunction',
    ol.source.UrlTile.prototype.getTileUrlFunction);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'getUrls',
    ol.source.UrlTile.prototype.getUrls);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'setTileLoadFunction',
    ol.source.UrlTile.prototype.setTileLoadFunction);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'setTileUrlFunction',
    ol.source.UrlTile.prototype.setTileUrlFunction);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'setUrl',
    ol.source.UrlTile.prototype.setUrl);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'setUrls',
    ol.source.UrlTile.prototype.setUrls);

goog.exportSymbol(
    'ol.source.Vector',
    ol.source.Vector,
    OPENLAYERS);

goog.exportProperty(
    ol.source.Vector.prototype,
    'addFeature',
    ol.source.Vector.prototype.addFeature);

goog.exportProperty(
    ol.source.Vector.prototype,
    'addFeatures',
    ol.source.Vector.prototype.addFeatures);

goog.exportProperty(
    ol.source.Vector.prototype,
    'clear',
    ol.source.Vector.prototype.clear);

goog.exportProperty(
    ol.source.Vector.prototype,
    'forEachFeature',
    ol.source.Vector.prototype.forEachFeature);

goog.exportProperty(
    ol.source.Vector.prototype,
    'forEachFeatureInExtent',
    ol.source.Vector.prototype.forEachFeatureInExtent);

goog.exportProperty(
    ol.source.Vector.prototype,
    'forEachFeatureIntersectingExtent',
    ol.source.Vector.prototype.forEachFeatureIntersectingExtent);

goog.exportProperty(
    ol.source.Vector.prototype,
    'getFeaturesCollection',
    ol.source.Vector.prototype.getFeaturesCollection);

goog.exportProperty(
    ol.source.Vector.prototype,
    'getFeatures',
    ol.source.Vector.prototype.getFeatures);

goog.exportProperty(
    ol.source.Vector.prototype,
    'getFeaturesAtCoordinate',
    ol.source.Vector.prototype.getFeaturesAtCoordinate);

goog.exportProperty(
    ol.source.Vector.prototype,
    'getFeaturesInExtent',
    ol.source.Vector.prototype.getFeaturesInExtent);

goog.exportProperty(
    ol.source.Vector.prototype,
    'getClosestFeatureToCoordinate',
    ol.source.Vector.prototype.getClosestFeatureToCoordinate);

goog.exportProperty(
    ol.source.Vector.prototype,
    'getExtent',
    ol.source.Vector.prototype.getExtent);

goog.exportProperty(
    ol.source.Vector.prototype,
    'getFeatureById',
    ol.source.Vector.prototype.getFeatureById);

goog.exportProperty(
    ol.source.Vector.prototype,
    'removeFeature',
    ol.source.Vector.prototype.removeFeature);

goog.exportProperty(
    ol.source.VectorEvent.prototype,
    'feature',
    ol.source.VectorEvent.prototype.feature);

goog.exportSymbol(
    'ol.source.VectorTile',
    ol.source.VectorTile,
    OPENLAYERS);

goog.exportSymbol(
    'ol.source.WMTS',
    ol.source.WMTS,
    OPENLAYERS);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'getDimensions',
    ol.source.WMTS.prototype.getDimensions);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'getFormat',
    ol.source.WMTS.prototype.getFormat);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'getLayer',
    ol.source.WMTS.prototype.getLayer);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'getMatrixSet',
    ol.source.WMTS.prototype.getMatrixSet);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'getRequestEncoding',
    ol.source.WMTS.prototype.getRequestEncoding);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'getStyle',
    ol.source.WMTS.prototype.getStyle);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'getVersion',
    ol.source.WMTS.prototype.getVersion);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'updateDimensions',
    ol.source.WMTS.prototype.updateDimensions);

goog.exportSymbol(
    'ol.source.WMTS.optionsFromCapabilities',
    ol.source.WMTS.optionsFromCapabilities,
    OPENLAYERS);

goog.exportSymbol(
    'ol.source.XYZ',
    ol.source.XYZ,
    OPENLAYERS);

goog.exportSymbol(
    'ol.source.Zoomify',
    ol.source.Zoomify,
    OPENLAYERS);

goog.exportProperty(
    ol.render.Event.prototype,
    'vectorContext',
    ol.render.Event.prototype.vectorContext);

goog.exportProperty(
    ol.render.Event.prototype,
    'frameState',
    ol.render.Event.prototype.frameState);

goog.exportProperty(
    ol.render.Event.prototype,
    'context',
    ol.render.Event.prototype.context);

goog.exportProperty(
    ol.render.Event.prototype,
    'glContext',
    ol.render.Event.prototype.glContext);

goog.exportProperty(
    ol.render.Feature.prototype,
    'get',
    ol.render.Feature.prototype.get);

goog.exportProperty(
    ol.render.Feature.prototype,
    'getExtent',
    ol.render.Feature.prototype.getExtent);

goog.exportProperty(
    ol.render.Feature.prototype,
    'getGeometry',
    ol.render.Feature.prototype.getGeometry);

goog.exportProperty(
    ol.render.Feature.prototype,
    'getProperties',
    ol.render.Feature.prototype.getProperties);

goog.exportProperty(
    ol.render.Feature.prototype,
    'getType',
    ol.render.Feature.prototype.getType);

goog.exportSymbol(
    'ol.render.VectorContext',
    ol.render.VectorContext,
    OPENLAYERS);

goog.exportProperty(
    ol.render.webgl.Immediate.prototype,
    'drawAsync',
    ol.render.webgl.Immediate.prototype.drawAsync);

goog.exportProperty(
    ol.render.webgl.Immediate.prototype,
    'drawCircleGeometry',
    ol.render.webgl.Immediate.prototype.drawCircleGeometry);

goog.exportProperty(
    ol.render.webgl.Immediate.prototype,
    'drawFeature',
    ol.render.webgl.Immediate.prototype.drawFeature);

goog.exportProperty(
    ol.render.webgl.Immediate.prototype,
    'drawGeometryCollectionGeometry',
    ol.render.webgl.Immediate.prototype.drawGeometryCollectionGeometry);

goog.exportProperty(
    ol.render.webgl.Immediate.prototype,
    'drawPointGeometry',
    ol.render.webgl.Immediate.prototype.drawPointGeometry);

goog.exportProperty(
    ol.render.webgl.Immediate.prototype,
    'drawLineStringGeometry',
    ol.render.webgl.Immediate.prototype.drawLineStringGeometry);

goog.exportProperty(
    ol.render.webgl.Immediate.prototype,
    'drawMultiLineStringGeometry',
    ol.render.webgl.Immediate.prototype.drawMultiLineStringGeometry);

goog.exportProperty(
    ol.render.webgl.Immediate.prototype,
    'drawMultiPointGeometry',
    ol.render.webgl.Immediate.prototype.drawMultiPointGeometry);

goog.exportProperty(
    ol.render.webgl.Immediate.prototype,
    'drawMultiPolygonGeometry',
    ol.render.webgl.Immediate.prototype.drawMultiPolygonGeometry);

goog.exportProperty(
    ol.render.webgl.Immediate.prototype,
    'drawPolygonGeometry',
    ol.render.webgl.Immediate.prototype.drawPolygonGeometry);

goog.exportProperty(
    ol.render.webgl.Immediate.prototype,
    'drawText',
    ol.render.webgl.Immediate.prototype.drawText);

goog.exportProperty(
    ol.render.webgl.Immediate.prototype,
    'setFillStrokeStyle',
    ol.render.webgl.Immediate.prototype.setFillStrokeStyle);

goog.exportProperty(
    ol.render.webgl.Immediate.prototype,
    'setImageStyle',
    ol.render.webgl.Immediate.prototype.setImageStyle);

goog.exportProperty(
    ol.render.webgl.Immediate.prototype,
    'setTextStyle',
    ol.render.webgl.Immediate.prototype.setTextStyle);

goog.exportProperty(
    ol.render.canvas.Immediate.prototype,
    'drawAsync',
    ol.render.canvas.Immediate.prototype.drawAsync);

goog.exportProperty(
    ol.render.canvas.Immediate.prototype,
    'drawCircleGeometry',
    ol.render.canvas.Immediate.prototype.drawCircleGeometry);

goog.exportProperty(
    ol.render.canvas.Immediate.prototype,
    'drawFeature',
    ol.render.canvas.Immediate.prototype.drawFeature);

goog.exportProperty(
    ol.render.canvas.Immediate.prototype,
    'drawPointGeometry',
    ol.render.canvas.Immediate.prototype.drawPointGeometry);

goog.exportProperty(
    ol.render.canvas.Immediate.prototype,
    'drawMultiPointGeometry',
    ol.render.canvas.Immediate.prototype.drawMultiPointGeometry);

goog.exportProperty(
    ol.render.canvas.Immediate.prototype,
    'drawLineStringGeometry',
    ol.render.canvas.Immediate.prototype.drawLineStringGeometry);

goog.exportProperty(
    ol.render.canvas.Immediate.prototype,
    'drawMultiLineStringGeometry',
    ol.render.canvas.Immediate.prototype.drawMultiLineStringGeometry);

goog.exportProperty(
    ol.render.canvas.Immediate.prototype,
    'drawPolygonGeometry',
    ol.render.canvas.Immediate.prototype.drawPolygonGeometry);

goog.exportProperty(
    ol.render.canvas.Immediate.prototype,
    'drawMultiPolygonGeometry',
    ol.render.canvas.Immediate.prototype.drawMultiPolygonGeometry);

goog.exportProperty(
    ol.render.canvas.Immediate.prototype,
    'setFillStrokeStyle',
    ol.render.canvas.Immediate.prototype.setFillStrokeStyle);

goog.exportProperty(
    ol.render.canvas.Immediate.prototype,
    'setImageStyle',
    ol.render.canvas.Immediate.prototype.setImageStyle);

goog.exportProperty(
    ol.render.canvas.Immediate.prototype,
    'setTextStyle',
    ol.render.canvas.Immediate.prototype.setTextStyle);

goog.exportSymbol(
    'ol.proj.common.add',
    ol.proj.common.add,
    OPENLAYERS);

goog.exportSymbol(
    'ol.proj.METERS_PER_UNIT',
    ol.proj.METERS_PER_UNIT,
    OPENLAYERS);

goog.exportSymbol(
    'ol.proj.Projection',
    ol.proj.Projection,
    OPENLAYERS);

goog.exportProperty(
    ol.proj.Projection.prototype,
    'getCode',
    ol.proj.Projection.prototype.getCode);

goog.exportProperty(
    ol.proj.Projection.prototype,
    'getExtent',
    ol.proj.Projection.prototype.getExtent);

goog.exportProperty(
    ol.proj.Projection.prototype,
    'getUnits',
    ol.proj.Projection.prototype.getUnits);

goog.exportProperty(
    ol.proj.Projection.prototype,
    'getMetersPerUnit',
    ol.proj.Projection.prototype.getMetersPerUnit);

goog.exportProperty(
    ol.proj.Projection.prototype,
    'getWorldExtent',
    ol.proj.Projection.prototype.getWorldExtent);

goog.exportProperty(
    ol.proj.Projection.prototype,
    'isGlobal',
    ol.proj.Projection.prototype.isGlobal);

goog.exportProperty(
    ol.proj.Projection.prototype,
    'setGlobal',
    ol.proj.Projection.prototype.setGlobal);

goog.exportProperty(
    ol.proj.Projection.prototype,
    'setExtent',
    ol.proj.Projection.prototype.setExtent);

goog.exportProperty(
    ol.proj.Projection.prototype,
    'setWorldExtent',
    ol.proj.Projection.prototype.setWorldExtent);

goog.exportProperty(
    ol.proj.Projection.prototype,
    'setGetPointResolution',
    ol.proj.Projection.prototype.setGetPointResolution);

goog.exportProperty(
    ol.proj.Projection.prototype,
    'getPointResolution',
    ol.proj.Projection.prototype.getPointResolution);

goog.exportSymbol(
    'ol.proj.setProj4',
    ol.proj.setProj4,
    OPENLAYERS);

goog.exportSymbol(
    'ol.proj.addEquivalentProjections',
    ol.proj.addEquivalentProjections,
    OPENLAYERS);

goog.exportSymbol(
    'ol.proj.addProjection',
    ol.proj.addProjection,
    OPENLAYERS);

goog.exportSymbol(
    'ol.proj.addCoordinateTransforms',
    ol.proj.addCoordinateTransforms,
    OPENLAYERS);

goog.exportSymbol(
    'ol.proj.fromLonLat',
    ol.proj.fromLonLat,
    OPENLAYERS);

goog.exportSymbol(
    'ol.proj.toLonLat',
    ol.proj.toLonLat,
    OPENLAYERS);

goog.exportSymbol(
    'ol.proj.get',
    ol.proj.get,
    OPENLAYERS);

goog.exportSymbol(
    'ol.proj.getTransform',
    ol.proj.getTransform,
    OPENLAYERS);

goog.exportSymbol(
    'ol.proj.transform',
    ol.proj.transform,
    OPENLAYERS);

goog.exportSymbol(
    'ol.proj.transformExtent',
    ol.proj.transformExtent,
    OPENLAYERS);

goog.exportSymbol(
    'ol.layer.Heatmap',
    ol.layer.Heatmap,
    OPENLAYERS);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'getBlur',
    ol.layer.Heatmap.prototype.getBlur);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'getGradient',
    ol.layer.Heatmap.prototype.getGradient);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'getRadius',
    ol.layer.Heatmap.prototype.getRadius);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'setBlur',
    ol.layer.Heatmap.prototype.setBlur);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'setGradient',
    ol.layer.Heatmap.prototype.setGradient);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'setRadius',
    ol.layer.Heatmap.prototype.setRadius);

goog.exportSymbol(
    'ol.layer.Image',
    ol.layer.Image,
    OPENLAYERS);

goog.exportProperty(
    ol.layer.Image.prototype,
    'getSource',
    ol.layer.Image.prototype.getSource);

goog.exportSymbol(
    'ol.layer.Layer',
    ol.layer.Layer,
    OPENLAYERS);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'getSource',
    ol.layer.Layer.prototype.getSource);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'setMap',
    ol.layer.Layer.prototype.setMap);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'setSource',
    ol.layer.Layer.prototype.setSource);

goog.exportSymbol(
    'ol.layer.Base',
    ol.layer.Base,
    OPENLAYERS);

goog.exportProperty(
    ol.layer.Base.prototype,
    'getExtent',
    ol.layer.Base.prototype.getExtent);

goog.exportProperty(
    ol.layer.Base.prototype,
    'getMaxResolution',
    ol.layer.Base.prototype.getMaxResolution);

goog.exportProperty(
    ol.layer.Base.prototype,
    'getMinResolution',
    ol.layer.Base.prototype.getMinResolution);

goog.exportProperty(
    ol.layer.Base.prototype,
    'getOpacity',
    ol.layer.Base.prototype.getOpacity);

goog.exportProperty(
    ol.layer.Base.prototype,
    'getVisible',
    ol.layer.Base.prototype.getVisible);

goog.exportProperty(
    ol.layer.Base.prototype,
    'getZIndex',
    ol.layer.Base.prototype.getZIndex);

goog.exportProperty(
    ol.layer.Base.prototype,
    'setExtent',
    ol.layer.Base.prototype.setExtent);

goog.exportProperty(
    ol.layer.Base.prototype,
    'setMaxResolution',
    ol.layer.Base.prototype.setMaxResolution);

goog.exportProperty(
    ol.layer.Base.prototype,
    'setMinResolution',
    ol.layer.Base.prototype.setMinResolution);

goog.exportProperty(
    ol.layer.Base.prototype,
    'setOpacity',
    ol.layer.Base.prototype.setOpacity);

goog.exportProperty(
    ol.layer.Base.prototype,
    'setVisible',
    ol.layer.Base.prototype.setVisible);

goog.exportProperty(
    ol.layer.Base.prototype,
    'setZIndex',
    ol.layer.Base.prototype.setZIndex);

goog.exportSymbol(
    'ol.layer.Group',
    ol.layer.Group,
    OPENLAYERS);

goog.exportProperty(
    ol.layer.Group.prototype,
    'getLayers',
    ol.layer.Group.prototype.getLayers);

goog.exportProperty(
    ol.layer.Group.prototype,
    'setLayers',
    ol.layer.Group.prototype.setLayers);

goog.exportSymbol(
    'ol.layer.Tile',
    ol.layer.Tile,
    OPENLAYERS);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'getPreload',
    ol.layer.Tile.prototype.getPreload);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'getSource',
    ol.layer.Tile.prototype.getSource);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'setPreload',
    ol.layer.Tile.prototype.setPreload);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'getUseInterimTilesOnError',
    ol.layer.Tile.prototype.getUseInterimTilesOnError);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'setUseInterimTilesOnError',
    ol.layer.Tile.prototype.setUseInterimTilesOnError);

goog.exportSymbol(
    'ol.layer.Vector',
    ol.layer.Vector,
    OPENLAYERS);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'getSource',
    ol.layer.Vector.prototype.getSource);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'getStyle',
    ol.layer.Vector.prototype.getStyle);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'getStyleFunction',
    ol.layer.Vector.prototype.getStyleFunction);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'setStyle',
    ol.layer.Vector.prototype.setStyle);

goog.exportSymbol(
    'ol.layer.VectorTile',
    ol.layer.VectorTile,
    OPENLAYERS);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'getPreload',
    ol.layer.VectorTile.prototype.getPreload);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'getSource',
    ol.layer.VectorTile.prototype.getSource);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'getUseInterimTilesOnError',
    ol.layer.VectorTile.prototype.getUseInterimTilesOnError);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'setPreload',
    ol.layer.VectorTile.prototype.setPreload);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'setUseInterimTilesOnError',
    ol.layer.VectorTile.prototype.setUseInterimTilesOnError);

goog.exportSymbol(
    'ol.interaction.DoubleClickZoom',
    ol.interaction.DoubleClickZoom,
    OPENLAYERS);

goog.exportSymbol(
    'ol.interaction.DoubleClickZoom.handleEvent',
    ol.interaction.DoubleClickZoom.handleEvent,
    OPENLAYERS);

goog.exportSymbol(
    'ol.interaction.DragAndDrop',
    ol.interaction.DragAndDrop,
    OPENLAYERS);

goog.exportSymbol(
    'ol.interaction.DragAndDrop.handleEvent',
    ol.interaction.DragAndDrop.handleEvent,
    OPENLAYERS);

goog.exportProperty(
    ol.interaction.DragAndDropEvent.prototype,
    'features',
    ol.interaction.DragAndDropEvent.prototype.features);

goog.exportProperty(
    ol.interaction.DragAndDropEvent.prototype,
    'file',
    ol.interaction.DragAndDropEvent.prototype.file);

goog.exportProperty(
    ol.interaction.DragAndDropEvent.prototype,
    'projection',
    ol.interaction.DragAndDropEvent.prototype.projection);

goog.exportProperty(
    ol.DragBoxEvent.prototype,
    'coordinate',
    ol.DragBoxEvent.prototype.coordinate);

goog.exportProperty(
    ol.DragBoxEvent.prototype,
    'mapBrowserEvent',
    ol.DragBoxEvent.prototype.mapBrowserEvent);

goog.exportSymbol(
    'ol.interaction.DragBox',
    ol.interaction.DragBox,
    OPENLAYERS);

goog.exportProperty(
    ol.interaction.DragBox.prototype,
    'getGeometry',
    ol.interaction.DragBox.prototype.getGeometry);

goog.exportSymbol(
    'ol.interaction.DragPan',
    ol.interaction.DragPan,
    OPENLAYERS);

goog.exportSymbol(
    'ol.interaction.DragRotateAndZoom',
    ol.interaction.DragRotateAndZoom,
    OPENLAYERS);

goog.exportSymbol(
    'ol.interaction.DragRotate',
    ol.interaction.DragRotate,
    OPENLAYERS);

goog.exportSymbol(
    'ol.interaction.DragZoom',
    ol.interaction.DragZoom,
    OPENLAYERS);

goog.exportProperty(
    ol.interaction.DrawEvent.prototype,
    'feature',
    ol.interaction.DrawEvent.prototype.feature);

goog.exportSymbol(
    'ol.interaction.Draw',
    ol.interaction.Draw,
    OPENLAYERS);

goog.exportSymbol(
    'ol.interaction.Draw.handleEvent',
    ol.interaction.Draw.handleEvent,
    OPENLAYERS);

goog.exportProperty(
    ol.interaction.Draw.prototype,
    'removeLastPoint',
    ol.interaction.Draw.prototype.removeLastPoint);

goog.exportProperty(
    ol.interaction.Draw.prototype,
    'finishDrawing',
    ol.interaction.Draw.prototype.finishDrawing);

goog.exportProperty(
    ol.interaction.Draw.prototype,
    'extend',
    ol.interaction.Draw.prototype.extend);

goog.exportSymbol(
    'ol.interaction.Draw.createRegularPolygon',
    ol.interaction.Draw.createRegularPolygon,
    OPENLAYERS);

goog.exportSymbol(
    'ol.interaction.Interaction',
    ol.interaction.Interaction,
    OPENLAYERS);

goog.exportProperty(
    ol.interaction.Interaction.prototype,
    'getActive',
    ol.interaction.Interaction.prototype.getActive);

goog.exportProperty(
    ol.interaction.Interaction.prototype,
    'getMap',
    ol.interaction.Interaction.prototype.getMap);

goog.exportProperty(
    ol.interaction.Interaction.prototype,
    'setActive',
    ol.interaction.Interaction.prototype.setActive);

goog.exportSymbol(
    'ol.interaction.defaults',
    ol.interaction.defaults,
    OPENLAYERS);

goog.exportSymbol(
    'ol.interaction.KeyboardPan',
    ol.interaction.KeyboardPan,
    OPENLAYERS);

goog.exportSymbol(
    'ol.interaction.KeyboardPan.handleEvent',
    ol.interaction.KeyboardPan.handleEvent,
    OPENLAYERS);

goog.exportSymbol(
    'ol.interaction.KeyboardZoom',
    ol.interaction.KeyboardZoom,
    OPENLAYERS);

goog.exportSymbol(
    'ol.interaction.KeyboardZoom.handleEvent',
    ol.interaction.KeyboardZoom.handleEvent,
    OPENLAYERS);

goog.exportProperty(
    ol.interaction.ModifyEvent.prototype,
    'features',
    ol.interaction.ModifyEvent.prototype.features);

goog.exportProperty(
    ol.interaction.ModifyEvent.prototype,
    'mapBrowserPointerEvent',
    ol.interaction.ModifyEvent.prototype.mapBrowserPointerEvent);

goog.exportSymbol(
    'ol.interaction.Modify',
    ol.interaction.Modify,
    OPENLAYERS);

goog.exportSymbol(
    'ol.interaction.Modify.handleEvent',
    ol.interaction.Modify.handleEvent,
    OPENLAYERS);

goog.exportSymbol(
    'ol.interaction.MouseWheelZoom',
    ol.interaction.MouseWheelZoom,
    OPENLAYERS);

goog.exportSymbol(
    'ol.interaction.MouseWheelZoom.handleEvent',
    ol.interaction.MouseWheelZoom.handleEvent,
    OPENLAYERS);

goog.exportProperty(
    ol.interaction.MouseWheelZoom.prototype,
    'setMouseAnchor',
    ol.interaction.MouseWheelZoom.prototype.setMouseAnchor);

goog.exportSymbol(
    'ol.interaction.PinchRotate',
    ol.interaction.PinchRotate,
    OPENLAYERS);

goog.exportSymbol(
    'ol.interaction.PinchZoom',
    ol.interaction.PinchZoom,
    OPENLAYERS);

goog.exportSymbol(
    'ol.interaction.Pointer',
    ol.interaction.Pointer,
    OPENLAYERS);

goog.exportSymbol(
    'ol.interaction.Pointer.handleEvent',
    ol.interaction.Pointer.handleEvent,
    OPENLAYERS);

goog.exportProperty(
    ol.interaction.SelectEvent.prototype,
    'selected',
    ol.interaction.SelectEvent.prototype.selected);

goog.exportProperty(
    ol.interaction.SelectEvent.prototype,
    'deselected',
    ol.interaction.SelectEvent.prototype.deselected);

goog.exportProperty(
    ol.interaction.SelectEvent.prototype,
    'mapBrowserEvent',
    ol.interaction.SelectEvent.prototype.mapBrowserEvent);

goog.exportSymbol(
    'ol.interaction.Select',
    ol.interaction.Select,
    OPENLAYERS);

goog.exportProperty(
    ol.interaction.Select.prototype,
    'getFeatures',
    ol.interaction.Select.prototype.getFeatures);

goog.exportProperty(
    ol.interaction.Select.prototype,
    'getLayer',
    ol.interaction.Select.prototype.getLayer);

goog.exportSymbol(
    'ol.interaction.Select.handleEvent',
    ol.interaction.Select.handleEvent,
    OPENLAYERS);

goog.exportProperty(
    ol.interaction.Select.prototype,
    'setMap',
    ol.interaction.Select.prototype.setMap);

goog.exportSymbol(
    'ol.interaction.Snap',
    ol.interaction.Snap,
    OPENLAYERS);

goog.exportProperty(
    ol.interaction.Snap.prototype,
    'addFeature',
    ol.interaction.Snap.prototype.addFeature);

goog.exportProperty(
    ol.interaction.Snap.prototype,
    'removeFeature',
    ol.interaction.Snap.prototype.removeFeature);

goog.exportProperty(
    ol.interaction.TranslateEvent.prototype,
    'features',
    ol.interaction.TranslateEvent.prototype.features);

goog.exportProperty(
    ol.interaction.TranslateEvent.prototype,
    'coordinate',
    ol.interaction.TranslateEvent.prototype.coordinate);

goog.exportSymbol(
    'ol.interaction.Translate',
    ol.interaction.Translate,
    OPENLAYERS);

goog.exportSymbol(
    'ol.geom.Circle',
    ol.geom.Circle,
    OPENLAYERS);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'clone',
    ol.geom.Circle.prototype.clone);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'getCenter',
    ol.geom.Circle.prototype.getCenter);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'getRadius',
    ol.geom.Circle.prototype.getRadius);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'getType',
    ol.geom.Circle.prototype.getType);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'intersectsExtent',
    ol.geom.Circle.prototype.intersectsExtent);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'setCenter',
    ol.geom.Circle.prototype.setCenter);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'setCenterAndRadius',
    ol.geom.Circle.prototype.setCenterAndRadius);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'setRadius',
    ol.geom.Circle.prototype.setRadius);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'transform',
    ol.geom.Circle.prototype.transform);

goog.exportSymbol(
    'ol.geom.Geometry',
    ol.geom.Geometry,
    OPENLAYERS);

goog.exportProperty(
    ol.geom.Geometry.prototype,
    'getClosestPoint',
    ol.geom.Geometry.prototype.getClosestPoint);

goog.exportProperty(
    ol.geom.Geometry.prototype,
    'getExtent',
    ol.geom.Geometry.prototype.getExtent);

goog.exportProperty(
    ol.geom.Geometry.prototype,
    'simplify',
    ol.geom.Geometry.prototype.simplify);

goog.exportProperty(
    ol.geom.Geometry.prototype,
    'transform',
    ol.geom.Geometry.prototype.transform);

goog.exportSymbol(
    'ol.geom.GeometryCollection',
    ol.geom.GeometryCollection,
    OPENLAYERS);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'clone',
    ol.geom.GeometryCollection.prototype.clone);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'getGeometries',
    ol.geom.GeometryCollection.prototype.getGeometries);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'getType',
    ol.geom.GeometryCollection.prototype.getType);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'intersectsExtent',
    ol.geom.GeometryCollection.prototype.intersectsExtent);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'setGeometries',
    ol.geom.GeometryCollection.prototype.setGeometries);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'applyTransform',
    ol.geom.GeometryCollection.prototype.applyTransform);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'translate',
    ol.geom.GeometryCollection.prototype.translate);

goog.exportSymbol(
    'ol.geom.LinearRing',
    ol.geom.LinearRing,
    OPENLAYERS);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'clone',
    ol.geom.LinearRing.prototype.clone);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'getArea',
    ol.geom.LinearRing.prototype.getArea);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'getCoordinates',
    ol.geom.LinearRing.prototype.getCoordinates);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'getType',
    ol.geom.LinearRing.prototype.getType);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'setCoordinates',
    ol.geom.LinearRing.prototype.setCoordinates);

goog.exportSymbol(
    'ol.geom.LineString',
    ol.geom.LineString,
    OPENLAYERS);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'appendCoordinate',
    ol.geom.LineString.prototype.appendCoordinate);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'clone',
    ol.geom.LineString.prototype.clone);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'forEachSegment',
    ol.geom.LineString.prototype.forEachSegment);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'getCoordinateAtM',
    ol.geom.LineString.prototype.getCoordinateAtM);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'getCoordinates',
    ol.geom.LineString.prototype.getCoordinates);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'getCoordinateAt',
    ol.geom.LineString.prototype.getCoordinateAt);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'getLength',
    ol.geom.LineString.prototype.getLength);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'getType',
    ol.geom.LineString.prototype.getType);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'intersectsExtent',
    ol.geom.LineString.prototype.intersectsExtent);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'setCoordinates',
    ol.geom.LineString.prototype.setCoordinates);

goog.exportSymbol(
    'ol.geom.MultiLineString',
    ol.geom.MultiLineString,
    OPENLAYERS);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'appendLineString',
    ol.geom.MultiLineString.prototype.appendLineString);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'clone',
    ol.geom.MultiLineString.prototype.clone);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'getCoordinateAtM',
    ol.geom.MultiLineString.prototype.getCoordinateAtM);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'getCoordinates',
    ol.geom.MultiLineString.prototype.getCoordinates);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'getLineString',
    ol.geom.MultiLineString.prototype.getLineString);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'getLineStrings',
    ol.geom.MultiLineString.prototype.getLineStrings);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'getType',
    ol.geom.MultiLineString.prototype.getType);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'intersectsExtent',
    ol.geom.MultiLineString.prototype.intersectsExtent);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'setCoordinates',
    ol.geom.MultiLineString.prototype.setCoordinates);

goog.exportSymbol(
    'ol.geom.MultiPoint',
    ol.geom.MultiPoint,
    OPENLAYERS);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'appendPoint',
    ol.geom.MultiPoint.prototype.appendPoint);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'clone',
    ol.geom.MultiPoint.prototype.clone);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'getCoordinates',
    ol.geom.MultiPoint.prototype.getCoordinates);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'getPoint',
    ol.geom.MultiPoint.prototype.getPoint);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'getPoints',
    ol.geom.MultiPoint.prototype.getPoints);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'getType',
    ol.geom.MultiPoint.prototype.getType);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'intersectsExtent',
    ol.geom.MultiPoint.prototype.intersectsExtent);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'setCoordinates',
    ol.geom.MultiPoint.prototype.setCoordinates);

goog.exportSymbol(
    'ol.geom.MultiPolygon',
    ol.geom.MultiPolygon,
    OPENLAYERS);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'appendPolygon',
    ol.geom.MultiPolygon.prototype.appendPolygon);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'clone',
    ol.geom.MultiPolygon.prototype.clone);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'getArea',
    ol.geom.MultiPolygon.prototype.getArea);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'getCoordinates',
    ol.geom.MultiPolygon.prototype.getCoordinates);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'getInteriorPoints',
    ol.geom.MultiPolygon.prototype.getInteriorPoints);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'getPolygon',
    ol.geom.MultiPolygon.prototype.getPolygon);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'getPolygons',
    ol.geom.MultiPolygon.prototype.getPolygons);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'getType',
    ol.geom.MultiPolygon.prototype.getType);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'intersectsExtent',
    ol.geom.MultiPolygon.prototype.intersectsExtent);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'setCoordinates',
    ol.geom.MultiPolygon.prototype.setCoordinates);

goog.exportSymbol(
    'ol.geom.Point',
    ol.geom.Point,
    OPENLAYERS);

goog.exportProperty(
    ol.geom.Point.prototype,
    'clone',
    ol.geom.Point.prototype.clone);

goog.exportProperty(
    ol.geom.Point.prototype,
    'getCoordinates',
    ol.geom.Point.prototype.getCoordinates);

goog.exportProperty(
    ol.geom.Point.prototype,
    'getType',
    ol.geom.Point.prototype.getType);

goog.exportProperty(
    ol.geom.Point.prototype,
    'intersectsExtent',
    ol.geom.Point.prototype.intersectsExtent);

goog.exportProperty(
    ol.geom.Point.prototype,
    'setCoordinates',
    ol.geom.Point.prototype.setCoordinates);

goog.exportSymbol(
    'ol.geom.Polygon',
    ol.geom.Polygon,
    OPENLAYERS);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'appendLinearRing',
    ol.geom.Polygon.prototype.appendLinearRing);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'clone',
    ol.geom.Polygon.prototype.clone);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'getArea',
    ol.geom.Polygon.prototype.getArea);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'getCoordinates',
    ol.geom.Polygon.prototype.getCoordinates);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'getInteriorPoint',
    ol.geom.Polygon.prototype.getInteriorPoint);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'getLinearRingCount',
    ol.geom.Polygon.prototype.getLinearRingCount);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'getLinearRing',
    ol.geom.Polygon.prototype.getLinearRing);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'getLinearRings',
    ol.geom.Polygon.prototype.getLinearRings);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'getType',
    ol.geom.Polygon.prototype.getType);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'intersectsExtent',
    ol.geom.Polygon.prototype.intersectsExtent);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'setCoordinates',
    ol.geom.Polygon.prototype.setCoordinates);

goog.exportSymbol(
    'ol.geom.Polygon.circular',
    ol.geom.Polygon.circular,
    OPENLAYERS);

goog.exportSymbol(
    'ol.geom.Polygon.fromExtent',
    ol.geom.Polygon.fromExtent,
    OPENLAYERS);

goog.exportSymbol(
    'ol.geom.Polygon.fromCircle',
    ol.geom.Polygon.fromCircle,
    OPENLAYERS);

goog.exportSymbol(
    'ol.geom.SimpleGeometry',
    ol.geom.SimpleGeometry,
    OPENLAYERS);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'getFirstCoordinate',
    ol.geom.SimpleGeometry.prototype.getFirstCoordinate);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'getLastCoordinate',
    ol.geom.SimpleGeometry.prototype.getLastCoordinate);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'getLayout',
    ol.geom.SimpleGeometry.prototype.getLayout);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'applyTransform',
    ol.geom.SimpleGeometry.prototype.applyTransform);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'translate',
    ol.geom.SimpleGeometry.prototype.translate);

goog.exportSymbol(
    'ol.format.EsriJSON',
    ol.format.EsriJSON,
    OPENLAYERS);

goog.exportProperty(
    ol.format.EsriJSON.prototype,
    'readFeature',
    ol.format.EsriJSON.prototype.readFeature);

goog.exportProperty(
    ol.format.EsriJSON.prototype,
    'readFeatures',
    ol.format.EsriJSON.prototype.readFeatures);

goog.exportProperty(
    ol.format.EsriJSON.prototype,
    'readGeometry',
    ol.format.EsriJSON.prototype.readGeometry);

goog.exportProperty(
    ol.format.EsriJSON.prototype,
    'readProjection',
    ol.format.EsriJSON.prototype.readProjection);

goog.exportProperty(
    ol.format.EsriJSON.prototype,
    'writeGeometry',
    ol.format.EsriJSON.prototype.writeGeometry);

goog.exportProperty(
    ol.format.EsriJSON.prototype,
    'writeGeometryObject',
    ol.format.EsriJSON.prototype.writeGeometryObject);

goog.exportProperty(
    ol.format.EsriJSON.prototype,
    'writeFeature',
    ol.format.EsriJSON.prototype.writeFeature);

goog.exportProperty(
    ol.format.EsriJSON.prototype,
    'writeFeatureObject',
    ol.format.EsriJSON.prototype.writeFeatureObject);

goog.exportProperty(
    ol.format.EsriJSON.prototype,
    'writeFeatures',
    ol.format.EsriJSON.prototype.writeFeatures);

goog.exportProperty(
    ol.format.EsriJSON.prototype,
    'writeFeaturesObject',
    ol.format.EsriJSON.prototype.writeFeaturesObject);

goog.exportSymbol(
    'ol.format.Feature',
    ol.format.Feature,
    OPENLAYERS);

goog.exportSymbol(
    'ol.format.GeoJSON',
    ol.format.GeoJSON,
    OPENLAYERS);

goog.exportProperty(
    ol.format.GeoJSON.prototype,
    'readFeature',
    ol.format.GeoJSON.prototype.readFeature);

goog.exportProperty(
    ol.format.GeoJSON.prototype,
    'readFeatures',
    ol.format.GeoJSON.prototype.readFeatures);

goog.exportProperty(
    ol.format.GeoJSON.prototype,
    'readGeometry',
    ol.format.GeoJSON.prototype.readGeometry);

goog.exportProperty(
    ol.format.GeoJSON.prototype,
    'readProjection',
    ol.format.GeoJSON.prototype.readProjection);

goog.exportProperty(
    ol.format.GeoJSON.prototype,
    'writeFeature',
    ol.format.GeoJSON.prototype.writeFeature);

goog.exportProperty(
    ol.format.GeoJSON.prototype,
    'writeFeatureObject',
    ol.format.GeoJSON.prototype.writeFeatureObject);

goog.exportProperty(
    ol.format.GeoJSON.prototype,
    'writeFeatures',
    ol.format.GeoJSON.prototype.writeFeatures);

goog.exportProperty(
    ol.format.GeoJSON.prototype,
    'writeFeaturesObject',
    ol.format.GeoJSON.prototype.writeFeaturesObject);

goog.exportProperty(
    ol.format.GeoJSON.prototype,
    'writeGeometry',
    ol.format.GeoJSON.prototype.writeGeometry);

goog.exportProperty(
    ol.format.GeoJSON.prototype,
    'writeGeometryObject',
    ol.format.GeoJSON.prototype.writeGeometryObject);

goog.exportSymbol(
    'ol.format.GPX',
    ol.format.GPX,
    OPENLAYERS);

goog.exportProperty(
    ol.format.GPX.prototype,
    'readFeature',
    ol.format.GPX.prototype.readFeature);

goog.exportProperty(
    ol.format.GPX.prototype,
    'readFeatures',
    ol.format.GPX.prototype.readFeatures);

goog.exportProperty(
    ol.format.GPX.prototype,
    'readProjection',
    ol.format.GPX.prototype.readProjection);

goog.exportProperty(
    ol.format.GPX.prototype,
    'writeFeatures',
    ol.format.GPX.prototype.writeFeatures);

goog.exportProperty(
    ol.format.GPX.prototype,
    'writeFeaturesNode',
    ol.format.GPX.prototype.writeFeaturesNode);

goog.exportSymbol(
    'ol.format.IGC',
    ol.format.IGC,
    OPENLAYERS);

goog.exportProperty(
    ol.format.IGC.prototype,
    'readFeature',
    ol.format.IGC.prototype.readFeature);

goog.exportProperty(
    ol.format.IGC.prototype,
    'readFeatures',
    ol.format.IGC.prototype.readFeatures);

goog.exportProperty(
    ol.format.IGC.prototype,
    'readProjection',
    ol.format.IGC.prototype.readProjection);

goog.exportSymbol(
    'ol.format.KML',
    ol.format.KML,
    OPENLAYERS);

goog.exportProperty(
    ol.format.KML.prototype,
    'readFeature',
    ol.format.KML.prototype.readFeature);

goog.exportProperty(
    ol.format.KML.prototype,
    'readFeatures',
    ol.format.KML.prototype.readFeatures);

goog.exportProperty(
    ol.format.KML.prototype,
    'readName',
    ol.format.KML.prototype.readName);

goog.exportProperty(
    ol.format.KML.prototype,
    'readNetworkLinks',
    ol.format.KML.prototype.readNetworkLinks);

goog.exportProperty(
    ol.format.KML.prototype,
    'readProjection',
    ol.format.KML.prototype.readProjection);

goog.exportProperty(
    ol.format.KML.prototype,
    'writeFeatures',
    ol.format.KML.prototype.writeFeatures);

goog.exportProperty(
    ol.format.KML.prototype,
    'writeFeaturesNode',
    ol.format.KML.prototype.writeFeaturesNode);

goog.exportSymbol(
    'ol.format.MVT',
    ol.format.MVT,
    OPENLAYERS);

goog.exportProperty(
    ol.format.MVT.prototype,
    'setLayers',
    ol.format.MVT.prototype.setLayers);

goog.exportSymbol(
    'ol.format.OSMXML',
    ol.format.OSMXML,
    OPENLAYERS);

goog.exportProperty(
    ol.format.OSMXML.prototype,
    'readFeatures',
    ol.format.OSMXML.prototype.readFeatures);

goog.exportProperty(
    ol.format.OSMXML.prototype,
    'readProjection',
    ol.format.OSMXML.prototype.readProjection);

goog.exportSymbol(
    'ol.format.Polyline',
    ol.format.Polyline,
    OPENLAYERS);

goog.exportSymbol(
    'ol.format.Polyline.encodeDeltas',
    ol.format.Polyline.encodeDeltas,
    OPENLAYERS);

goog.exportSymbol(
    'ol.format.Polyline.decodeDeltas',
    ol.format.Polyline.decodeDeltas,
    OPENLAYERS);

goog.exportSymbol(
    'ol.format.Polyline.encodeFloats',
    ol.format.Polyline.encodeFloats,
    OPENLAYERS);

goog.exportSymbol(
    'ol.format.Polyline.decodeFloats',
    ol.format.Polyline.decodeFloats,
    OPENLAYERS);

goog.exportProperty(
    ol.format.Polyline.prototype,
    'readFeature',
    ol.format.Polyline.prototype.readFeature);

goog.exportProperty(
    ol.format.Polyline.prototype,
    'readFeatures',
    ol.format.Polyline.prototype.readFeatures);

goog.exportProperty(
    ol.format.Polyline.prototype,
    'readGeometry',
    ol.format.Polyline.prototype.readGeometry);

goog.exportProperty(
    ol.format.Polyline.prototype,
    'readProjection',
    ol.format.Polyline.prototype.readProjection);

goog.exportProperty(
    ol.format.Polyline.prototype,
    'writeGeometry',
    ol.format.Polyline.prototype.writeGeometry);

goog.exportSymbol(
    'ol.format.TopoJSON',
    ol.format.TopoJSON,
    OPENLAYERS);

goog.exportProperty(
    ol.format.TopoJSON.prototype,
    'readFeatures',
    ol.format.TopoJSON.prototype.readFeatures);

goog.exportProperty(
    ol.format.TopoJSON.prototype,
    'readProjection',
    ol.format.TopoJSON.prototype.readProjection);

goog.exportSymbol(
    'ol.format.WFS',
    ol.format.WFS,
    OPENLAYERS);

goog.exportProperty(
    ol.format.WFS.prototype,
    'readFeatures',
    ol.format.WFS.prototype.readFeatures);

goog.exportProperty(
    ol.format.WFS.prototype,
    'readTransactionResponse',
    ol.format.WFS.prototype.readTransactionResponse);

goog.exportProperty(
    ol.format.WFS.prototype,
    'readFeatureCollectionMetadata',
    ol.format.WFS.prototype.readFeatureCollectionMetadata);

goog.exportProperty(
    ol.format.WFS.prototype,
    'writeGetFeature',
    ol.format.WFS.prototype.writeGetFeature);

goog.exportProperty(
    ol.format.WFS.prototype,
    'writeTransaction',
    ol.format.WFS.prototype.writeTransaction);

goog.exportProperty(
    ol.format.WFS.prototype,
    'readProjection',
    ol.format.WFS.prototype.readProjection);

goog.exportSymbol(
    'ol.format.WKT',
    ol.format.WKT,
    OPENLAYERS);

goog.exportProperty(
    ol.format.WKT.prototype,
    'readFeature',
    ol.format.WKT.prototype.readFeature);

goog.exportProperty(
    ol.format.WKT.prototype,
    'readFeatures',
    ol.format.WKT.prototype.readFeatures);

goog.exportProperty(
    ol.format.WKT.prototype,
    'readGeometry',
    ol.format.WKT.prototype.readGeometry);

goog.exportProperty(
    ol.format.WKT.prototype,
    'writeFeature',
    ol.format.WKT.prototype.writeFeature);

goog.exportProperty(
    ol.format.WKT.prototype,
    'writeFeatures',
    ol.format.WKT.prototype.writeFeatures);

goog.exportProperty(
    ol.format.WKT.prototype,
    'writeGeometry',
    ol.format.WKT.prototype.writeGeometry);

goog.exportSymbol(
    'ol.format.WMSCapabilities',
    ol.format.WMSCapabilities,
    OPENLAYERS);

goog.exportProperty(
    ol.format.WMSCapabilities.prototype,
    'read',
    ol.format.WMSCapabilities.prototype.read);

goog.exportSymbol(
    'ol.format.WMSGetFeatureInfo',
    ol.format.WMSGetFeatureInfo,
    OPENLAYERS);

goog.exportProperty(
    ol.format.WMSGetFeatureInfo.prototype,
    'readFeatures',
    ol.format.WMSGetFeatureInfo.prototype.readFeatures);

goog.exportSymbol(
    'ol.format.WMTSCapabilities',
    ol.format.WMTSCapabilities,
    OPENLAYERS);

goog.exportProperty(
    ol.format.WMTSCapabilities.prototype,
    'read',
    ol.format.WMTSCapabilities.prototype.read);

goog.exportSymbol(
    'ol.format.GML2',
    ol.format.GML2,
    OPENLAYERS);

goog.exportSymbol(
    'ol.format.GML3',
    ol.format.GML3,
    OPENLAYERS);

goog.exportProperty(
    ol.format.GML3.prototype,
    'writeGeometryNode',
    ol.format.GML3.prototype.writeGeometryNode);

goog.exportProperty(
    ol.format.GML3.prototype,
    'writeFeatures',
    ol.format.GML3.prototype.writeFeatures);

goog.exportProperty(
    ol.format.GML3.prototype,
    'writeFeaturesNode',
    ol.format.GML3.prototype.writeFeaturesNode);

goog.exportSymbol(
    'ol.format.GML',
    ol.format.GML,
    OPENLAYERS);

goog.exportProperty(
    ol.format.GML.prototype,
    'writeFeatures',
    ol.format.GML.prototype.writeFeatures);

goog.exportProperty(
    ol.format.GML.prototype,
    'writeFeaturesNode',
    ol.format.GML.prototype.writeFeaturesNode);

goog.exportProperty(
    ol.format.GMLBase.prototype,
    'readFeatures',
    ol.format.GMLBase.prototype.readFeatures);

goog.exportSymbol(
    'ol.events.condition.altKeyOnly',
    ol.events.condition.altKeyOnly,
    OPENLAYERS);

goog.exportSymbol(
    'ol.events.condition.altShiftKeysOnly',
    ol.events.condition.altShiftKeysOnly,
    OPENLAYERS);

goog.exportSymbol(
    'ol.events.condition.always',
    ol.events.condition.always,
    OPENLAYERS);

goog.exportSymbol(
    'ol.events.condition.click',
    ol.events.condition.click,
    OPENLAYERS);

goog.exportSymbol(
    'ol.events.condition.never',
    ol.events.condition.never,
    OPENLAYERS);

goog.exportSymbol(
    'ol.events.condition.pointerMove',
    ol.events.condition.pointerMove,
    OPENLAYERS);

goog.exportSymbol(
    'ol.events.condition.singleClick',
    ol.events.condition.singleClick,
    OPENLAYERS);

goog.exportSymbol(
    'ol.events.condition.doubleClick',
    ol.events.condition.doubleClick,
    OPENLAYERS);

goog.exportSymbol(
    'ol.events.condition.noModifierKeys',
    ol.events.condition.noModifierKeys,
    OPENLAYERS);

goog.exportSymbol(
    'ol.events.condition.platformModifierKeyOnly',
    ol.events.condition.platformModifierKeyOnly,
    OPENLAYERS);

goog.exportSymbol(
    'ol.events.condition.shiftKeyOnly',
    ol.events.condition.shiftKeyOnly,
    OPENLAYERS);

goog.exportSymbol(
    'ol.events.condition.targetNotEditable',
    ol.events.condition.targetNotEditable,
    OPENLAYERS);

goog.exportSymbol(
    'ol.events.condition.mouseOnly',
    ol.events.condition.mouseOnly,
    OPENLAYERS);

goog.exportSymbol(
    'ol.control.Attribution',
    ol.control.Attribution,
    OPENLAYERS);

goog.exportSymbol(
    'ol.control.Attribution.render',
    ol.control.Attribution.render,
    OPENLAYERS);

goog.exportProperty(
    ol.control.Attribution.prototype,
    'getCollapsible',
    ol.control.Attribution.prototype.getCollapsible);

goog.exportProperty(
    ol.control.Attribution.prototype,
    'setCollapsible',
    ol.control.Attribution.prototype.setCollapsible);

goog.exportProperty(
    ol.control.Attribution.prototype,
    'setCollapsed',
    ol.control.Attribution.prototype.setCollapsed);

goog.exportProperty(
    ol.control.Attribution.prototype,
    'getCollapsed',
    ol.control.Attribution.prototype.getCollapsed);

goog.exportSymbol(
    'ol.control.Control',
    ol.control.Control,
    OPENLAYERS);

goog.exportProperty(
    ol.control.Control.prototype,
    'getMap',
    ol.control.Control.prototype.getMap);

goog.exportProperty(
    ol.control.Control.prototype,
    'setMap',
    ol.control.Control.prototype.setMap);

goog.exportProperty(
    ol.control.Control.prototype,
    'setTarget',
    ol.control.Control.prototype.setTarget);

goog.exportSymbol(
    'ol.control.defaults',
    ol.control.defaults,
    OPENLAYERS);

goog.exportSymbol(
    'ol.control.FullScreen',
    ol.control.FullScreen,
    OPENLAYERS);

goog.exportSymbol(
    'ol.control.MousePosition',
    ol.control.MousePosition,
    OPENLAYERS);

goog.exportSymbol(
    'ol.control.MousePosition.render',
    ol.control.MousePosition.render,
    OPENLAYERS);

goog.exportProperty(
    ol.control.MousePosition.prototype,
    'getCoordinateFormat',
    ol.control.MousePosition.prototype.getCoordinateFormat);

goog.exportProperty(
    ol.control.MousePosition.prototype,
    'getProjection',
    ol.control.MousePosition.prototype.getProjection);

goog.exportProperty(
    ol.control.MousePosition.prototype,
    'setCoordinateFormat',
    ol.control.MousePosition.prototype.setCoordinateFormat);

goog.exportProperty(
    ol.control.MousePosition.prototype,
    'setProjection',
    ol.control.MousePosition.prototype.setProjection);

goog.exportSymbol(
    'ol.control.OverviewMap',
    ol.control.OverviewMap,
    OPENLAYERS);

goog.exportSymbol(
    'ol.control.OverviewMap.render',
    ol.control.OverviewMap.render,
    OPENLAYERS);

goog.exportProperty(
    ol.control.OverviewMap.prototype,
    'getCollapsible',
    ol.control.OverviewMap.prototype.getCollapsible);

goog.exportProperty(
    ol.control.OverviewMap.prototype,
    'setCollapsible',
    ol.control.OverviewMap.prototype.setCollapsible);

goog.exportProperty(
    ol.control.OverviewMap.prototype,
    'setCollapsed',
    ol.control.OverviewMap.prototype.setCollapsed);

goog.exportProperty(
    ol.control.OverviewMap.prototype,
    'getCollapsed',
    ol.control.OverviewMap.prototype.getCollapsed);

goog.exportProperty(
    ol.control.OverviewMap.prototype,
    'getOverviewMap',
    ol.control.OverviewMap.prototype.getOverviewMap);

goog.exportSymbol(
    'ol.control.Rotate',
    ol.control.Rotate,
    OPENLAYERS);

goog.exportSymbol(
    'ol.control.Rotate.render',
    ol.control.Rotate.render,
    OPENLAYERS);

goog.exportSymbol(
    'ol.control.ScaleLine',
    ol.control.ScaleLine,
    OPENLAYERS);

goog.exportProperty(
    ol.control.ScaleLine.prototype,
    'getUnits',
    ol.control.ScaleLine.prototype.getUnits);

goog.exportSymbol(
    'ol.control.ScaleLine.render',
    ol.control.ScaleLine.render,
    OPENLAYERS);

goog.exportProperty(
    ol.control.ScaleLine.prototype,
    'setUnits',
    ol.control.ScaleLine.prototype.setUnits);

goog.exportSymbol(
    'ol.control.Zoom',
    ol.control.Zoom,
    OPENLAYERS);

goog.exportSymbol(
    'ol.control.ZoomSlider',
    ol.control.ZoomSlider,
    OPENLAYERS);

goog.exportSymbol(
    'ol.control.ZoomSlider.render',
    ol.control.ZoomSlider.render,
    OPENLAYERS);

goog.exportSymbol(
    'ol.control.ZoomToExtent',
    ol.control.ZoomToExtent,
    OPENLAYERS);

goog.exportSymbol(
    'ol.color.asArray',
    ol.color.asArray,
    OPENLAYERS);

goog.exportSymbol(
    'ol.color.asString',
    ol.color.asString,
    OPENLAYERS);

goog.exportProperty(
    ol.Object.prototype,
    'changed',
    ol.Object.prototype.changed);

goog.exportProperty(
    ol.Object.prototype,
    'dispatchEvent',
    ol.Object.prototype.dispatchEvent);

goog.exportProperty(
    ol.Object.prototype,
    'getRevision',
    ol.Object.prototype.getRevision);

goog.exportProperty(
    ol.Object.prototype,
    'on',
    ol.Object.prototype.on);

goog.exportProperty(
    ol.Object.prototype,
    'once',
    ol.Object.prototype.once);

goog.exportProperty(
    ol.Object.prototype,
    'un',
    ol.Object.prototype.un);

goog.exportProperty(
    ol.Object.prototype,
    'unByKey',
    ol.Object.prototype.unByKey);

goog.exportProperty(
    ol.Collection.prototype,
    'get',
    ol.Collection.prototype.get);

goog.exportProperty(
    ol.Collection.prototype,
    'getKeys',
    ol.Collection.prototype.getKeys);

goog.exportProperty(
    ol.Collection.prototype,
    'getProperties',
    ol.Collection.prototype.getProperties);

goog.exportProperty(
    ol.Collection.prototype,
    'set',
    ol.Collection.prototype.set);

goog.exportProperty(
    ol.Collection.prototype,
    'setProperties',
    ol.Collection.prototype.setProperties);

goog.exportProperty(
    ol.Collection.prototype,
    'unset',
    ol.Collection.prototype.unset);

goog.exportProperty(
    ol.Collection.prototype,
    'changed',
    ol.Collection.prototype.changed);

goog.exportProperty(
    ol.Collection.prototype,
    'dispatchEvent',
    ol.Collection.prototype.dispatchEvent);

goog.exportProperty(
    ol.Collection.prototype,
    'getRevision',
    ol.Collection.prototype.getRevision);

goog.exportProperty(
    ol.Collection.prototype,
    'on',
    ol.Collection.prototype.on);

goog.exportProperty(
    ol.Collection.prototype,
    'once',
    ol.Collection.prototype.once);

goog.exportProperty(
    ol.Collection.prototype,
    'un',
    ol.Collection.prototype.un);

goog.exportProperty(
    ol.Collection.prototype,
    'unByKey',
    ol.Collection.prototype.unByKey);

goog.exportProperty(
    ol.DeviceOrientation.prototype,
    'get',
    ol.DeviceOrientation.prototype.get);

goog.exportProperty(
    ol.DeviceOrientation.prototype,
    'getKeys',
    ol.DeviceOrientation.prototype.getKeys);

goog.exportProperty(
    ol.DeviceOrientation.prototype,
    'getProperties',
    ol.DeviceOrientation.prototype.getProperties);

goog.exportProperty(
    ol.DeviceOrientation.prototype,
    'set',
    ol.DeviceOrientation.prototype.set);

goog.exportProperty(
    ol.DeviceOrientation.prototype,
    'setProperties',
    ol.DeviceOrientation.prototype.setProperties);

goog.exportProperty(
    ol.DeviceOrientation.prototype,
    'unset',
    ol.DeviceOrientation.prototype.unset);

goog.exportProperty(
    ol.DeviceOrientation.prototype,
    'changed',
    ol.DeviceOrientation.prototype.changed);

goog.exportProperty(
    ol.DeviceOrientation.prototype,
    'dispatchEvent',
    ol.DeviceOrientation.prototype.dispatchEvent);

goog.exportProperty(
    ol.DeviceOrientation.prototype,
    'getRevision',
    ol.DeviceOrientation.prototype.getRevision);

goog.exportProperty(
    ol.DeviceOrientation.prototype,
    'on',
    ol.DeviceOrientation.prototype.on);

goog.exportProperty(
    ol.DeviceOrientation.prototype,
    'once',
    ol.DeviceOrientation.prototype.once);

goog.exportProperty(
    ol.DeviceOrientation.prototype,
    'un',
    ol.DeviceOrientation.prototype.un);

goog.exportProperty(
    ol.DeviceOrientation.prototype,
    'unByKey',
    ol.DeviceOrientation.prototype.unByKey);

goog.exportProperty(
    ol.Feature.prototype,
    'get',
    ol.Feature.prototype.get);

goog.exportProperty(
    ol.Feature.prototype,
    'getKeys',
    ol.Feature.prototype.getKeys);

goog.exportProperty(
    ol.Feature.prototype,
    'getProperties',
    ol.Feature.prototype.getProperties);

goog.exportProperty(
    ol.Feature.prototype,
    'set',
    ol.Feature.prototype.set);

goog.exportProperty(
    ol.Feature.prototype,
    'setProperties',
    ol.Feature.prototype.setProperties);

goog.exportProperty(
    ol.Feature.prototype,
    'unset',
    ol.Feature.prototype.unset);

goog.exportProperty(
    ol.Feature.prototype,
    'changed',
    ol.Feature.prototype.changed);

goog.exportProperty(
    ol.Feature.prototype,
    'dispatchEvent',
    ol.Feature.prototype.dispatchEvent);

goog.exportProperty(
    ol.Feature.prototype,
    'getRevision',
    ol.Feature.prototype.getRevision);

goog.exportProperty(
    ol.Feature.prototype,
    'on',
    ol.Feature.prototype.on);

goog.exportProperty(
    ol.Feature.prototype,
    'once',
    ol.Feature.prototype.once);

goog.exportProperty(
    ol.Feature.prototype,
    'un',
    ol.Feature.prototype.un);

goog.exportProperty(
    ol.Feature.prototype,
    'unByKey',
    ol.Feature.prototype.unByKey);

goog.exportProperty(
    ol.Geolocation.prototype,
    'get',
    ol.Geolocation.prototype.get);

goog.exportProperty(
    ol.Geolocation.prototype,
    'getKeys',
    ol.Geolocation.prototype.getKeys);

goog.exportProperty(
    ol.Geolocation.prototype,
    'getProperties',
    ol.Geolocation.prototype.getProperties);

goog.exportProperty(
    ol.Geolocation.prototype,
    'set',
    ol.Geolocation.prototype.set);

goog.exportProperty(
    ol.Geolocation.prototype,
    'setProperties',
    ol.Geolocation.prototype.setProperties);

goog.exportProperty(
    ol.Geolocation.prototype,
    'unset',
    ol.Geolocation.prototype.unset);

goog.exportProperty(
    ol.Geolocation.prototype,
    'changed',
    ol.Geolocation.prototype.changed);

goog.exportProperty(
    ol.Geolocation.prototype,
    'dispatchEvent',
    ol.Geolocation.prototype.dispatchEvent);

goog.exportProperty(
    ol.Geolocation.prototype,
    'getRevision',
    ol.Geolocation.prototype.getRevision);

goog.exportProperty(
    ol.Geolocation.prototype,
    'on',
    ol.Geolocation.prototype.on);

goog.exportProperty(
    ol.Geolocation.prototype,
    'once',
    ol.Geolocation.prototype.once);

goog.exportProperty(
    ol.Geolocation.prototype,
    'un',
    ol.Geolocation.prototype.un);

goog.exportProperty(
    ol.Geolocation.prototype,
    'unByKey',
    ol.Geolocation.prototype.unByKey);

goog.exportProperty(
    ol.ImageTile.prototype,
    'getTileCoord',
    ol.ImageTile.prototype.getTileCoord);

goog.exportProperty(
    ol.Map.prototype,
    'get',
    ol.Map.prototype.get);

goog.exportProperty(
    ol.Map.prototype,
    'getKeys',
    ol.Map.prototype.getKeys);

goog.exportProperty(
    ol.Map.prototype,
    'getProperties',
    ol.Map.prototype.getProperties);

goog.exportProperty(
    ol.Map.prototype,
    'set',
    ol.Map.prototype.set);

goog.exportProperty(
    ol.Map.prototype,
    'setProperties',
    ol.Map.prototype.setProperties);

goog.exportProperty(
    ol.Map.prototype,
    'unset',
    ol.Map.prototype.unset);

goog.exportProperty(
    ol.Map.prototype,
    'changed',
    ol.Map.prototype.changed);

goog.exportProperty(
    ol.Map.prototype,
    'dispatchEvent',
    ol.Map.prototype.dispatchEvent);

goog.exportProperty(
    ol.Map.prototype,
    'getRevision',
    ol.Map.prototype.getRevision);

goog.exportProperty(
    ol.Map.prototype,
    'on',
    ol.Map.prototype.on);

goog.exportProperty(
    ol.Map.prototype,
    'once',
    ol.Map.prototype.once);

goog.exportProperty(
    ol.Map.prototype,
    'un',
    ol.Map.prototype.un);

goog.exportProperty(
    ol.Map.prototype,
    'unByKey',
    ol.Map.prototype.unByKey);

goog.exportProperty(
    ol.MapBrowserEvent.prototype,
    'map',
    ol.MapBrowserEvent.prototype.map);

goog.exportProperty(
    ol.MapBrowserEvent.prototype,
    'frameState',
    ol.MapBrowserEvent.prototype.frameState);

goog.exportProperty(
    ol.MapBrowserPointerEvent.prototype,
    'originalEvent',
    ol.MapBrowserPointerEvent.prototype.originalEvent);

goog.exportProperty(
    ol.MapBrowserPointerEvent.prototype,
    'pixel',
    ol.MapBrowserPointerEvent.prototype.pixel);

goog.exportProperty(
    ol.MapBrowserPointerEvent.prototype,
    'coordinate',
    ol.MapBrowserPointerEvent.prototype.coordinate);

goog.exportProperty(
    ol.MapBrowserPointerEvent.prototype,
    'dragging',
    ol.MapBrowserPointerEvent.prototype.dragging);

goog.exportProperty(
    ol.MapBrowserPointerEvent.prototype,
    'preventDefault',
    ol.MapBrowserPointerEvent.prototype.preventDefault);

goog.exportProperty(
    ol.MapBrowserPointerEvent.prototype,
    'stopPropagation',
    ol.MapBrowserPointerEvent.prototype.stopPropagation);

goog.exportProperty(
    ol.MapBrowserPointerEvent.prototype,
    'map',
    ol.MapBrowserPointerEvent.prototype.map);

goog.exportProperty(
    ol.MapBrowserPointerEvent.prototype,
    'frameState',
    ol.MapBrowserPointerEvent.prototype.frameState);

goog.exportProperty(
    ol.Overlay.prototype,
    'get',
    ol.Overlay.prototype.get);

goog.exportProperty(
    ol.Overlay.prototype,
    'getKeys',
    ol.Overlay.prototype.getKeys);

goog.exportProperty(
    ol.Overlay.prototype,
    'getProperties',
    ol.Overlay.prototype.getProperties);

goog.exportProperty(
    ol.Overlay.prototype,
    'set',
    ol.Overlay.prototype.set);

goog.exportProperty(
    ol.Overlay.prototype,
    'setProperties',
    ol.Overlay.prototype.setProperties);

goog.exportProperty(
    ol.Overlay.prototype,
    'unset',
    ol.Overlay.prototype.unset);

goog.exportProperty(
    ol.Overlay.prototype,
    'changed',
    ol.Overlay.prototype.changed);

goog.exportProperty(
    ol.Overlay.prototype,
    'dispatchEvent',
    ol.Overlay.prototype.dispatchEvent);

goog.exportProperty(
    ol.Overlay.prototype,
    'getRevision',
    ol.Overlay.prototype.getRevision);

goog.exportProperty(
    ol.Overlay.prototype,
    'on',
    ol.Overlay.prototype.on);

goog.exportProperty(
    ol.Overlay.prototype,
    'once',
    ol.Overlay.prototype.once);

goog.exportProperty(
    ol.Overlay.prototype,
    'un',
    ol.Overlay.prototype.un);

goog.exportProperty(
    ol.Overlay.prototype,
    'unByKey',
    ol.Overlay.prototype.unByKey);

goog.exportProperty(
    ol.VectorTile.prototype,
    'getTileCoord',
    ol.VectorTile.prototype.getTileCoord);

goog.exportProperty(
    ol.View.prototype,
    'get',
    ol.View.prototype.get);

goog.exportProperty(
    ol.View.prototype,
    'getKeys',
    ol.View.prototype.getKeys);

goog.exportProperty(
    ol.View.prototype,
    'getProperties',
    ol.View.prototype.getProperties);

goog.exportProperty(
    ol.View.prototype,
    'set',
    ol.View.prototype.set);

goog.exportProperty(
    ol.View.prototype,
    'setProperties',
    ol.View.prototype.setProperties);

goog.exportProperty(
    ol.View.prototype,
    'unset',
    ol.View.prototype.unset);

goog.exportProperty(
    ol.View.prototype,
    'changed',
    ol.View.prototype.changed);

goog.exportProperty(
    ol.View.prototype,
    'dispatchEvent',
    ol.View.prototype.dispatchEvent);

goog.exportProperty(
    ol.View.prototype,
    'getRevision',
    ol.View.prototype.getRevision);

goog.exportProperty(
    ol.View.prototype,
    'on',
    ol.View.prototype.on);

goog.exportProperty(
    ol.View.prototype,
    'once',
    ol.View.prototype.once);

goog.exportProperty(
    ol.View.prototype,
    'un',
    ol.View.prototype.un);

goog.exportProperty(
    ol.View.prototype,
    'unByKey',
    ol.View.prototype.unByKey);

goog.exportProperty(
    ol.tilegrid.WMTS.prototype,
    'getMaxZoom',
    ol.tilegrid.WMTS.prototype.getMaxZoom);

goog.exportProperty(
    ol.tilegrid.WMTS.prototype,
    'getMinZoom',
    ol.tilegrid.WMTS.prototype.getMinZoom);

goog.exportProperty(
    ol.tilegrid.WMTS.prototype,
    'getOrigin',
    ol.tilegrid.WMTS.prototype.getOrigin);

goog.exportProperty(
    ol.tilegrid.WMTS.prototype,
    'getResolution',
    ol.tilegrid.WMTS.prototype.getResolution);

goog.exportProperty(
    ol.tilegrid.WMTS.prototype,
    'getResolutions',
    ol.tilegrid.WMTS.prototype.getResolutions);

goog.exportProperty(
    ol.tilegrid.WMTS.prototype,
    'getTileCoordExtent',
    ol.tilegrid.WMTS.prototype.getTileCoordExtent);

goog.exportProperty(
    ol.tilegrid.WMTS.prototype,
    'getTileCoordForCoordAndResolution',
    ol.tilegrid.WMTS.prototype.getTileCoordForCoordAndResolution);

goog.exportProperty(
    ol.tilegrid.WMTS.prototype,
    'getTileCoordForCoordAndZ',
    ol.tilegrid.WMTS.prototype.getTileCoordForCoordAndZ);

goog.exportProperty(
    ol.tilegrid.WMTS.prototype,
    'getTileSize',
    ol.tilegrid.WMTS.prototype.getTileSize);

goog.exportProperty(
    ol.style.Circle.prototype,
    'getOpacity',
    ol.style.Circle.prototype.getOpacity);

goog.exportProperty(
    ol.style.Circle.prototype,
    'getRotateWithView',
    ol.style.Circle.prototype.getRotateWithView);

goog.exportProperty(
    ol.style.Circle.prototype,
    'getRotation',
    ol.style.Circle.prototype.getRotation);

goog.exportProperty(
    ol.style.Circle.prototype,
    'getScale',
    ol.style.Circle.prototype.getScale);

goog.exportProperty(
    ol.style.Circle.prototype,
    'getSnapToPixel',
    ol.style.Circle.prototype.getSnapToPixel);

goog.exportProperty(
    ol.style.Circle.prototype,
    'setOpacity',
    ol.style.Circle.prototype.setOpacity);

goog.exportProperty(
    ol.style.Circle.prototype,
    'setRotation',
    ol.style.Circle.prototype.setRotation);

goog.exportProperty(
    ol.style.Circle.prototype,
    'setScale',
    ol.style.Circle.prototype.setScale);

goog.exportProperty(
    ol.style.Icon.prototype,
    'getOpacity',
    ol.style.Icon.prototype.getOpacity);

goog.exportProperty(
    ol.style.Icon.prototype,
    'getRotateWithView',
    ol.style.Icon.prototype.getRotateWithView);

goog.exportProperty(
    ol.style.Icon.prototype,
    'getRotation',
    ol.style.Icon.prototype.getRotation);

goog.exportProperty(
    ol.style.Icon.prototype,
    'getScale',
    ol.style.Icon.prototype.getScale);

goog.exportProperty(
    ol.style.Icon.prototype,
    'getSnapToPixel',
    ol.style.Icon.prototype.getSnapToPixel);

goog.exportProperty(
    ol.style.Icon.prototype,
    'setOpacity',
    ol.style.Icon.prototype.setOpacity);

goog.exportProperty(
    ol.style.Icon.prototype,
    'setRotation',
    ol.style.Icon.prototype.setRotation);

goog.exportProperty(
    ol.style.Icon.prototype,
    'setScale',
    ol.style.Icon.prototype.setScale);

goog.exportProperty(
    ol.style.RegularShape.prototype,
    'getOpacity',
    ol.style.RegularShape.prototype.getOpacity);

goog.exportProperty(
    ol.style.RegularShape.prototype,
    'getRotateWithView',
    ol.style.RegularShape.prototype.getRotateWithView);

goog.exportProperty(
    ol.style.RegularShape.prototype,
    'getRotation',
    ol.style.RegularShape.prototype.getRotation);

goog.exportProperty(
    ol.style.RegularShape.prototype,
    'getScale',
    ol.style.RegularShape.prototype.getScale);

goog.exportProperty(
    ol.style.RegularShape.prototype,
    'getSnapToPixel',
    ol.style.RegularShape.prototype.getSnapToPixel);

goog.exportProperty(
    ol.style.RegularShape.prototype,
    'setOpacity',
    ol.style.RegularShape.prototype.setOpacity);

goog.exportProperty(
    ol.style.RegularShape.prototype,
    'setRotation',
    ol.style.RegularShape.prototype.setRotation);

goog.exportProperty(
    ol.style.RegularShape.prototype,
    'setScale',
    ol.style.RegularShape.prototype.setScale);

goog.exportProperty(
    ol.source.Source.prototype,
    'get',
    ol.source.Source.prototype.get);

goog.exportProperty(
    ol.source.Source.prototype,
    'getKeys',
    ol.source.Source.prototype.getKeys);

goog.exportProperty(
    ol.source.Source.prototype,
    'getProperties',
    ol.source.Source.prototype.getProperties);

goog.exportProperty(
    ol.source.Source.prototype,
    'set',
    ol.source.Source.prototype.set);

goog.exportProperty(
    ol.source.Source.prototype,
    'setProperties',
    ol.source.Source.prototype.setProperties);

goog.exportProperty(
    ol.source.Source.prototype,
    'unset',
    ol.source.Source.prototype.unset);

goog.exportProperty(
    ol.source.Source.prototype,
    'changed',
    ol.source.Source.prototype.changed);

goog.exportProperty(
    ol.source.Source.prototype,
    'dispatchEvent',
    ol.source.Source.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.Source.prototype,
    'getRevision',
    ol.source.Source.prototype.getRevision);

goog.exportProperty(
    ol.source.Source.prototype,
    'on',
    ol.source.Source.prototype.on);

goog.exportProperty(
    ol.source.Source.prototype,
    'once',
    ol.source.Source.prototype.once);

goog.exportProperty(
    ol.source.Source.prototype,
    'un',
    ol.source.Source.prototype.un);

goog.exportProperty(
    ol.source.Source.prototype,
    'unByKey',
    ol.source.Source.prototype.unByKey);

goog.exportProperty(
    ol.source.Tile.prototype,
    'getAttributions',
    ol.source.Tile.prototype.getAttributions);

goog.exportProperty(
    ol.source.Tile.prototype,
    'getLogo',
    ol.source.Tile.prototype.getLogo);

goog.exportProperty(
    ol.source.Tile.prototype,
    'getProjection',
    ol.source.Tile.prototype.getProjection);

goog.exportProperty(
    ol.source.Tile.prototype,
    'getState',
    ol.source.Tile.prototype.getState);

goog.exportProperty(
    ol.source.Tile.prototype,
    'setAttributions',
    ol.source.Tile.prototype.setAttributions);

goog.exportProperty(
    ol.source.Tile.prototype,
    'get',
    ol.source.Tile.prototype.get);

goog.exportProperty(
    ol.source.Tile.prototype,
    'getKeys',
    ol.source.Tile.prototype.getKeys);

goog.exportProperty(
    ol.source.Tile.prototype,
    'getProperties',
    ol.source.Tile.prototype.getProperties);

goog.exportProperty(
    ol.source.Tile.prototype,
    'set',
    ol.source.Tile.prototype.set);

goog.exportProperty(
    ol.source.Tile.prototype,
    'setProperties',
    ol.source.Tile.prototype.setProperties);

goog.exportProperty(
    ol.source.Tile.prototype,
    'unset',
    ol.source.Tile.prototype.unset);

goog.exportProperty(
    ol.source.Tile.prototype,
    'changed',
    ol.source.Tile.prototype.changed);

goog.exportProperty(
    ol.source.Tile.prototype,
    'dispatchEvent',
    ol.source.Tile.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.Tile.prototype,
    'getRevision',
    ol.source.Tile.prototype.getRevision);

goog.exportProperty(
    ol.source.Tile.prototype,
    'on',
    ol.source.Tile.prototype.on);

goog.exportProperty(
    ol.source.Tile.prototype,
    'once',
    ol.source.Tile.prototype.once);

goog.exportProperty(
    ol.source.Tile.prototype,
    'un',
    ol.source.Tile.prototype.un);

goog.exportProperty(
    ol.source.Tile.prototype,
    'unByKey',
    ol.source.Tile.prototype.unByKey);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'getTileGrid',
    ol.source.UrlTile.prototype.getTileGrid);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'getAttributions',
    ol.source.UrlTile.prototype.getAttributions);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'getLogo',
    ol.source.UrlTile.prototype.getLogo);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'getProjection',
    ol.source.UrlTile.prototype.getProjection);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'getState',
    ol.source.UrlTile.prototype.getState);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'setAttributions',
    ol.source.UrlTile.prototype.setAttributions);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'get',
    ol.source.UrlTile.prototype.get);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'getKeys',
    ol.source.UrlTile.prototype.getKeys);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'getProperties',
    ol.source.UrlTile.prototype.getProperties);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'set',
    ol.source.UrlTile.prototype.set);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'setProperties',
    ol.source.UrlTile.prototype.setProperties);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'unset',
    ol.source.UrlTile.prototype.unset);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'changed',
    ol.source.UrlTile.prototype.changed);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'dispatchEvent',
    ol.source.UrlTile.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'getRevision',
    ol.source.UrlTile.prototype.getRevision);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'on',
    ol.source.UrlTile.prototype.on);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'once',
    ol.source.UrlTile.prototype.once);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'un',
    ol.source.UrlTile.prototype.un);

goog.exportProperty(
    ol.source.UrlTile.prototype,
    'unByKey',
    ol.source.UrlTile.prototype.unByKey);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'getTileLoadFunction',
    ol.source.TileImage.prototype.getTileLoadFunction);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'getTileUrlFunction',
    ol.source.TileImage.prototype.getTileUrlFunction);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'getUrls',
    ol.source.TileImage.prototype.getUrls);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'setTileLoadFunction',
    ol.source.TileImage.prototype.setTileLoadFunction);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'setTileUrlFunction',
    ol.source.TileImage.prototype.setTileUrlFunction);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'setUrl',
    ol.source.TileImage.prototype.setUrl);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'setUrls',
    ol.source.TileImage.prototype.setUrls);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'getTileGrid',
    ol.source.TileImage.prototype.getTileGrid);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'getAttributions',
    ol.source.TileImage.prototype.getAttributions);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'getLogo',
    ol.source.TileImage.prototype.getLogo);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'getProjection',
    ol.source.TileImage.prototype.getProjection);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'getState',
    ol.source.TileImage.prototype.getState);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'setAttributions',
    ol.source.TileImage.prototype.setAttributions);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'get',
    ol.source.TileImage.prototype.get);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'getKeys',
    ol.source.TileImage.prototype.getKeys);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'getProperties',
    ol.source.TileImage.prototype.getProperties);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'set',
    ol.source.TileImage.prototype.set);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'setProperties',
    ol.source.TileImage.prototype.setProperties);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'unset',
    ol.source.TileImage.prototype.unset);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'changed',
    ol.source.TileImage.prototype.changed);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'dispatchEvent',
    ol.source.TileImage.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'getRevision',
    ol.source.TileImage.prototype.getRevision);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'on',
    ol.source.TileImage.prototype.on);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'once',
    ol.source.TileImage.prototype.once);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'un',
    ol.source.TileImage.prototype.un);

goog.exportProperty(
    ol.source.TileImage.prototype,
    'unByKey',
    ol.source.TileImage.prototype.unByKey);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'setRenderReprojectionEdges',
    ol.source.BingMaps.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'setTileGridForProjection',
    ol.source.BingMaps.prototype.setTileGridForProjection);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'getTileLoadFunction',
    ol.source.BingMaps.prototype.getTileLoadFunction);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'getTileUrlFunction',
    ol.source.BingMaps.prototype.getTileUrlFunction);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'getUrls',
    ol.source.BingMaps.prototype.getUrls);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'setTileLoadFunction',
    ol.source.BingMaps.prototype.setTileLoadFunction);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'setTileUrlFunction',
    ol.source.BingMaps.prototype.setTileUrlFunction);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'setUrl',
    ol.source.BingMaps.prototype.setUrl);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'setUrls',
    ol.source.BingMaps.prototype.setUrls);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'getTileGrid',
    ol.source.BingMaps.prototype.getTileGrid);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'getAttributions',
    ol.source.BingMaps.prototype.getAttributions);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'getLogo',
    ol.source.BingMaps.prototype.getLogo);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'getProjection',
    ol.source.BingMaps.prototype.getProjection);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'getState',
    ol.source.BingMaps.prototype.getState);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'setAttributions',
    ol.source.BingMaps.prototype.setAttributions);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'get',
    ol.source.BingMaps.prototype.get);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'getKeys',
    ol.source.BingMaps.prototype.getKeys);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'getProperties',
    ol.source.BingMaps.prototype.getProperties);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'set',
    ol.source.BingMaps.prototype.set);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'setProperties',
    ol.source.BingMaps.prototype.setProperties);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'unset',
    ol.source.BingMaps.prototype.unset);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'changed',
    ol.source.BingMaps.prototype.changed);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'dispatchEvent',
    ol.source.BingMaps.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'getRevision',
    ol.source.BingMaps.prototype.getRevision);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'on',
    ol.source.BingMaps.prototype.on);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'once',
    ol.source.BingMaps.prototype.once);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'un',
    ol.source.BingMaps.prototype.un);

goog.exportProperty(
    ol.source.BingMaps.prototype,
    'unByKey',
    ol.source.BingMaps.prototype.unByKey);

goog.exportProperty(
    ol.source.Vector.prototype,
    'getAttributions',
    ol.source.Vector.prototype.getAttributions);

goog.exportProperty(
    ol.source.Vector.prototype,
    'getLogo',
    ol.source.Vector.prototype.getLogo);

goog.exportProperty(
    ol.source.Vector.prototype,
    'getProjection',
    ol.source.Vector.prototype.getProjection);

goog.exportProperty(
    ol.source.Vector.prototype,
    'getState',
    ol.source.Vector.prototype.getState);

goog.exportProperty(
    ol.source.Vector.prototype,
    'setAttributions',
    ol.source.Vector.prototype.setAttributions);

goog.exportProperty(
    ol.source.Vector.prototype,
    'get',
    ol.source.Vector.prototype.get);

goog.exportProperty(
    ol.source.Vector.prototype,
    'getKeys',
    ol.source.Vector.prototype.getKeys);

goog.exportProperty(
    ol.source.Vector.prototype,
    'getProperties',
    ol.source.Vector.prototype.getProperties);

goog.exportProperty(
    ol.source.Vector.prototype,
    'set',
    ol.source.Vector.prototype.set);

goog.exportProperty(
    ol.source.Vector.prototype,
    'setProperties',
    ol.source.Vector.prototype.setProperties);

goog.exportProperty(
    ol.source.Vector.prototype,
    'unset',
    ol.source.Vector.prototype.unset);

goog.exportProperty(
    ol.source.Vector.prototype,
    'changed',
    ol.source.Vector.prototype.changed);

goog.exportProperty(
    ol.source.Vector.prototype,
    'dispatchEvent',
    ol.source.Vector.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.Vector.prototype,
    'getRevision',
    ol.source.Vector.prototype.getRevision);

goog.exportProperty(
    ol.source.Vector.prototype,
    'on',
    ol.source.Vector.prototype.on);

goog.exportProperty(
    ol.source.Vector.prototype,
    'once',
    ol.source.Vector.prototype.once);

goog.exportProperty(
    ol.source.Vector.prototype,
    'un',
    ol.source.Vector.prototype.un);

goog.exportProperty(
    ol.source.Vector.prototype,
    'unByKey',
    ol.source.Vector.prototype.unByKey);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'addFeature',
    ol.source.Cluster.prototype.addFeature);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'addFeatures',
    ol.source.Cluster.prototype.addFeatures);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'clear',
    ol.source.Cluster.prototype.clear);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'forEachFeature',
    ol.source.Cluster.prototype.forEachFeature);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'forEachFeatureInExtent',
    ol.source.Cluster.prototype.forEachFeatureInExtent);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'forEachFeatureIntersectingExtent',
    ol.source.Cluster.prototype.forEachFeatureIntersectingExtent);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'getFeaturesCollection',
    ol.source.Cluster.prototype.getFeaturesCollection);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'getFeatures',
    ol.source.Cluster.prototype.getFeatures);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'getFeaturesAtCoordinate',
    ol.source.Cluster.prototype.getFeaturesAtCoordinate);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'getFeaturesInExtent',
    ol.source.Cluster.prototype.getFeaturesInExtent);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'getClosestFeatureToCoordinate',
    ol.source.Cluster.prototype.getClosestFeatureToCoordinate);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'getExtent',
    ol.source.Cluster.prototype.getExtent);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'getFeatureById',
    ol.source.Cluster.prototype.getFeatureById);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'removeFeature',
    ol.source.Cluster.prototype.removeFeature);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'getAttributions',
    ol.source.Cluster.prototype.getAttributions);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'getLogo',
    ol.source.Cluster.prototype.getLogo);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'getProjection',
    ol.source.Cluster.prototype.getProjection);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'getState',
    ol.source.Cluster.prototype.getState);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'setAttributions',
    ol.source.Cluster.prototype.setAttributions);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'get',
    ol.source.Cluster.prototype.get);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'getKeys',
    ol.source.Cluster.prototype.getKeys);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'getProperties',
    ol.source.Cluster.prototype.getProperties);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'set',
    ol.source.Cluster.prototype.set);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'setProperties',
    ol.source.Cluster.prototype.setProperties);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'unset',
    ol.source.Cluster.prototype.unset);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'changed',
    ol.source.Cluster.prototype.changed);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'dispatchEvent',
    ol.source.Cluster.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'getRevision',
    ol.source.Cluster.prototype.getRevision);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'on',
    ol.source.Cluster.prototype.on);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'once',
    ol.source.Cluster.prototype.once);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'un',
    ol.source.Cluster.prototype.un);

goog.exportProperty(
    ol.source.Cluster.prototype,
    'unByKey',
    ol.source.Cluster.prototype.unByKey);

goog.exportProperty(
    ol.source.Image.prototype,
    'getAttributions',
    ol.source.Image.prototype.getAttributions);

goog.exportProperty(
    ol.source.Image.prototype,
    'getLogo',
    ol.source.Image.prototype.getLogo);

goog.exportProperty(
    ol.source.Image.prototype,
    'getProjection',
    ol.source.Image.prototype.getProjection);

goog.exportProperty(
    ol.source.Image.prototype,
    'getState',
    ol.source.Image.prototype.getState);

goog.exportProperty(
    ol.source.Image.prototype,
    'setAttributions',
    ol.source.Image.prototype.setAttributions);

goog.exportProperty(
    ol.source.Image.prototype,
    'get',
    ol.source.Image.prototype.get);

goog.exportProperty(
    ol.source.Image.prototype,
    'getKeys',
    ol.source.Image.prototype.getKeys);

goog.exportProperty(
    ol.source.Image.prototype,
    'getProperties',
    ol.source.Image.prototype.getProperties);

goog.exportProperty(
    ol.source.Image.prototype,
    'set',
    ol.source.Image.prototype.set);

goog.exportProperty(
    ol.source.Image.prototype,
    'setProperties',
    ol.source.Image.prototype.setProperties);

goog.exportProperty(
    ol.source.Image.prototype,
    'unset',
    ol.source.Image.prototype.unset);

goog.exportProperty(
    ol.source.Image.prototype,
    'changed',
    ol.source.Image.prototype.changed);

goog.exportProperty(
    ol.source.Image.prototype,
    'dispatchEvent',
    ol.source.Image.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.Image.prototype,
    'getRevision',
    ol.source.Image.prototype.getRevision);

goog.exportProperty(
    ol.source.Image.prototype,
    'on',
    ol.source.Image.prototype.on);

goog.exportProperty(
    ol.source.Image.prototype,
    'once',
    ol.source.Image.prototype.once);

goog.exportProperty(
    ol.source.Image.prototype,
    'un',
    ol.source.Image.prototype.un);

goog.exportProperty(
    ol.source.Image.prototype,
    'unByKey',
    ol.source.Image.prototype.unByKey);

goog.exportProperty(
    ol.source.ImageCanvas.prototype,
    'getAttributions',
    ol.source.ImageCanvas.prototype.getAttributions);

goog.exportProperty(
    ol.source.ImageCanvas.prototype,
    'getLogo',
    ol.source.ImageCanvas.prototype.getLogo);

goog.exportProperty(
    ol.source.ImageCanvas.prototype,
    'getProjection',
    ol.source.ImageCanvas.prototype.getProjection);

goog.exportProperty(
    ol.source.ImageCanvas.prototype,
    'getState',
    ol.source.ImageCanvas.prototype.getState);

goog.exportProperty(
    ol.source.ImageCanvas.prototype,
    'setAttributions',
    ol.source.ImageCanvas.prototype.setAttributions);

goog.exportProperty(
    ol.source.ImageCanvas.prototype,
    'get',
    ol.source.ImageCanvas.prototype.get);

goog.exportProperty(
    ol.source.ImageCanvas.prototype,
    'getKeys',
    ol.source.ImageCanvas.prototype.getKeys);

goog.exportProperty(
    ol.source.ImageCanvas.prototype,
    'getProperties',
    ol.source.ImageCanvas.prototype.getProperties);

goog.exportProperty(
    ol.source.ImageCanvas.prototype,
    'set',
    ol.source.ImageCanvas.prototype.set);

goog.exportProperty(
    ol.source.ImageCanvas.prototype,
    'setProperties',
    ol.source.ImageCanvas.prototype.setProperties);

goog.exportProperty(
    ol.source.ImageCanvas.prototype,
    'unset',
    ol.source.ImageCanvas.prototype.unset);

goog.exportProperty(
    ol.source.ImageCanvas.prototype,
    'changed',
    ol.source.ImageCanvas.prototype.changed);

goog.exportProperty(
    ol.source.ImageCanvas.prototype,
    'dispatchEvent',
    ol.source.ImageCanvas.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.ImageCanvas.prototype,
    'getRevision',
    ol.source.ImageCanvas.prototype.getRevision);

goog.exportProperty(
    ol.source.ImageCanvas.prototype,
    'on',
    ol.source.ImageCanvas.prototype.on);

goog.exportProperty(
    ol.source.ImageCanvas.prototype,
    'once',
    ol.source.ImageCanvas.prototype.once);

goog.exportProperty(
    ol.source.ImageCanvas.prototype,
    'un',
    ol.source.ImageCanvas.prototype.un);

goog.exportProperty(
    ol.source.ImageCanvas.prototype,
    'unByKey',
    ol.source.ImageCanvas.prototype.unByKey);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'getAttributions',
    ol.source.ImageMapGuide.prototype.getAttributions);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'getLogo',
    ol.source.ImageMapGuide.prototype.getLogo);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'getProjection',
    ol.source.ImageMapGuide.prototype.getProjection);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'getState',
    ol.source.ImageMapGuide.prototype.getState);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'setAttributions',
    ol.source.ImageMapGuide.prototype.setAttributions);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'get',
    ol.source.ImageMapGuide.prototype.get);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'getKeys',
    ol.source.ImageMapGuide.prototype.getKeys);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'getProperties',
    ol.source.ImageMapGuide.prototype.getProperties);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'set',
    ol.source.ImageMapGuide.prototype.set);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'setProperties',
    ol.source.ImageMapGuide.prototype.setProperties);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'unset',
    ol.source.ImageMapGuide.prototype.unset);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'changed',
    ol.source.ImageMapGuide.prototype.changed);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'dispatchEvent',
    ol.source.ImageMapGuide.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'getRevision',
    ol.source.ImageMapGuide.prototype.getRevision);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'on',
    ol.source.ImageMapGuide.prototype.on);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'once',
    ol.source.ImageMapGuide.prototype.once);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'un',
    ol.source.ImageMapGuide.prototype.un);

goog.exportProperty(
    ol.source.ImageMapGuide.prototype,
    'unByKey',
    ol.source.ImageMapGuide.prototype.unByKey);

goog.exportProperty(
    ol.source.ImageStatic.prototype,
    'getAttributions',
    ol.source.ImageStatic.prototype.getAttributions);

goog.exportProperty(
    ol.source.ImageStatic.prototype,
    'getLogo',
    ol.source.ImageStatic.prototype.getLogo);

goog.exportProperty(
    ol.source.ImageStatic.prototype,
    'getProjection',
    ol.source.ImageStatic.prototype.getProjection);

goog.exportProperty(
    ol.source.ImageStatic.prototype,
    'getState',
    ol.source.ImageStatic.prototype.getState);

goog.exportProperty(
    ol.source.ImageStatic.prototype,
    'setAttributions',
    ol.source.ImageStatic.prototype.setAttributions);

goog.exportProperty(
    ol.source.ImageStatic.prototype,
    'get',
    ol.source.ImageStatic.prototype.get);

goog.exportProperty(
    ol.source.ImageStatic.prototype,
    'getKeys',
    ol.source.ImageStatic.prototype.getKeys);

goog.exportProperty(
    ol.source.ImageStatic.prototype,
    'getProperties',
    ol.source.ImageStatic.prototype.getProperties);

goog.exportProperty(
    ol.source.ImageStatic.prototype,
    'set',
    ol.source.ImageStatic.prototype.set);

goog.exportProperty(
    ol.source.ImageStatic.prototype,
    'setProperties',
    ol.source.ImageStatic.prototype.setProperties);

goog.exportProperty(
    ol.source.ImageStatic.prototype,
    'unset',
    ol.source.ImageStatic.prototype.unset);

goog.exportProperty(
    ol.source.ImageStatic.prototype,
    'changed',
    ol.source.ImageStatic.prototype.changed);

goog.exportProperty(
    ol.source.ImageStatic.prototype,
    'dispatchEvent',
    ol.source.ImageStatic.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.ImageStatic.prototype,
    'getRevision',
    ol.source.ImageStatic.prototype.getRevision);

goog.exportProperty(
    ol.source.ImageStatic.prototype,
    'on',
    ol.source.ImageStatic.prototype.on);

goog.exportProperty(
    ol.source.ImageStatic.prototype,
    'once',
    ol.source.ImageStatic.prototype.once);

goog.exportProperty(
    ol.source.ImageStatic.prototype,
    'un',
    ol.source.ImageStatic.prototype.un);

goog.exportProperty(
    ol.source.ImageStatic.prototype,
    'unByKey',
    ol.source.ImageStatic.prototype.unByKey);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'getAttributions',
    ol.source.ImageVector.prototype.getAttributions);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'getLogo',
    ol.source.ImageVector.prototype.getLogo);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'getProjection',
    ol.source.ImageVector.prototype.getProjection);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'getState',
    ol.source.ImageVector.prototype.getState);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'setAttributions',
    ol.source.ImageVector.prototype.setAttributions);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'get',
    ol.source.ImageVector.prototype.get);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'getKeys',
    ol.source.ImageVector.prototype.getKeys);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'getProperties',
    ol.source.ImageVector.prototype.getProperties);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'set',
    ol.source.ImageVector.prototype.set);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'setProperties',
    ol.source.ImageVector.prototype.setProperties);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'unset',
    ol.source.ImageVector.prototype.unset);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'changed',
    ol.source.ImageVector.prototype.changed);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'dispatchEvent',
    ol.source.ImageVector.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'getRevision',
    ol.source.ImageVector.prototype.getRevision);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'on',
    ol.source.ImageVector.prototype.on);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'once',
    ol.source.ImageVector.prototype.once);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'un',
    ol.source.ImageVector.prototype.un);

goog.exportProperty(
    ol.source.ImageVector.prototype,
    'unByKey',
    ol.source.ImageVector.prototype.unByKey);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'getAttributions',
    ol.source.ImageWMS.prototype.getAttributions);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'getLogo',
    ol.source.ImageWMS.prototype.getLogo);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'getProjection',
    ol.source.ImageWMS.prototype.getProjection);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'getState',
    ol.source.ImageWMS.prototype.getState);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'setAttributions',
    ol.source.ImageWMS.prototype.setAttributions);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'get',
    ol.source.ImageWMS.prototype.get);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'getKeys',
    ol.source.ImageWMS.prototype.getKeys);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'getProperties',
    ol.source.ImageWMS.prototype.getProperties);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'set',
    ol.source.ImageWMS.prototype.set);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'setProperties',
    ol.source.ImageWMS.prototype.setProperties);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'unset',
    ol.source.ImageWMS.prototype.unset);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'changed',
    ol.source.ImageWMS.prototype.changed);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'dispatchEvent',
    ol.source.ImageWMS.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'getRevision',
    ol.source.ImageWMS.prototype.getRevision);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'on',
    ol.source.ImageWMS.prototype.on);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'once',
    ol.source.ImageWMS.prototype.once);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'un',
    ol.source.ImageWMS.prototype.un);

goog.exportProperty(
    ol.source.ImageWMS.prototype,
    'unByKey',
    ol.source.ImageWMS.prototype.unByKey);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'setRenderReprojectionEdges',
    ol.source.XYZ.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'setTileGridForProjection',
    ol.source.XYZ.prototype.setTileGridForProjection);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'getTileLoadFunction',
    ol.source.XYZ.prototype.getTileLoadFunction);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'getTileUrlFunction',
    ol.source.XYZ.prototype.getTileUrlFunction);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'getUrls',
    ol.source.XYZ.prototype.getUrls);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'setTileLoadFunction',
    ol.source.XYZ.prototype.setTileLoadFunction);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'setTileUrlFunction',
    ol.source.XYZ.prototype.setTileUrlFunction);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'setUrl',
    ol.source.XYZ.prototype.setUrl);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'setUrls',
    ol.source.XYZ.prototype.setUrls);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'getTileGrid',
    ol.source.XYZ.prototype.getTileGrid);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'getAttributions',
    ol.source.XYZ.prototype.getAttributions);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'getLogo',
    ol.source.XYZ.prototype.getLogo);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'getProjection',
    ol.source.XYZ.prototype.getProjection);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'getState',
    ol.source.XYZ.prototype.getState);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'setAttributions',
    ol.source.XYZ.prototype.setAttributions);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'get',
    ol.source.XYZ.prototype.get);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'getKeys',
    ol.source.XYZ.prototype.getKeys);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'getProperties',
    ol.source.XYZ.prototype.getProperties);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'set',
    ol.source.XYZ.prototype.set);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'setProperties',
    ol.source.XYZ.prototype.setProperties);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'unset',
    ol.source.XYZ.prototype.unset);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'changed',
    ol.source.XYZ.prototype.changed);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'dispatchEvent',
    ol.source.XYZ.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'getRevision',
    ol.source.XYZ.prototype.getRevision);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'on',
    ol.source.XYZ.prototype.on);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'once',
    ol.source.XYZ.prototype.once);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'un',
    ol.source.XYZ.prototype.un);

goog.exportProperty(
    ol.source.XYZ.prototype,
    'unByKey',
    ol.source.XYZ.prototype.unByKey);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'setRenderReprojectionEdges',
    ol.source.MapQuest.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'setTileGridForProjection',
    ol.source.MapQuest.prototype.setTileGridForProjection);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'getTileLoadFunction',
    ol.source.MapQuest.prototype.getTileLoadFunction);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'getTileUrlFunction',
    ol.source.MapQuest.prototype.getTileUrlFunction);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'getUrls',
    ol.source.MapQuest.prototype.getUrls);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'setTileLoadFunction',
    ol.source.MapQuest.prototype.setTileLoadFunction);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'setTileUrlFunction',
    ol.source.MapQuest.prototype.setTileUrlFunction);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'setUrl',
    ol.source.MapQuest.prototype.setUrl);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'setUrls',
    ol.source.MapQuest.prototype.setUrls);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'getTileGrid',
    ol.source.MapQuest.prototype.getTileGrid);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'getAttributions',
    ol.source.MapQuest.prototype.getAttributions);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'getLogo',
    ol.source.MapQuest.prototype.getLogo);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'getProjection',
    ol.source.MapQuest.prototype.getProjection);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'getState',
    ol.source.MapQuest.prototype.getState);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'setAttributions',
    ol.source.MapQuest.prototype.setAttributions);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'get',
    ol.source.MapQuest.prototype.get);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'getKeys',
    ol.source.MapQuest.prototype.getKeys);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'getProperties',
    ol.source.MapQuest.prototype.getProperties);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'set',
    ol.source.MapQuest.prototype.set);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'setProperties',
    ol.source.MapQuest.prototype.setProperties);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'unset',
    ol.source.MapQuest.prototype.unset);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'changed',
    ol.source.MapQuest.prototype.changed);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'dispatchEvent',
    ol.source.MapQuest.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'getRevision',
    ol.source.MapQuest.prototype.getRevision);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'on',
    ol.source.MapQuest.prototype.on);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'once',
    ol.source.MapQuest.prototype.once);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'un',
    ol.source.MapQuest.prototype.un);

goog.exportProperty(
    ol.source.MapQuest.prototype,
    'unByKey',
    ol.source.MapQuest.prototype.unByKey);

goog.exportProperty(
    ol.source.OSM.prototype,
    'setRenderReprojectionEdges',
    ol.source.OSM.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    ol.source.OSM.prototype,
    'setTileGridForProjection',
    ol.source.OSM.prototype.setTileGridForProjection);

goog.exportProperty(
    ol.source.OSM.prototype,
    'getTileLoadFunction',
    ol.source.OSM.prototype.getTileLoadFunction);

goog.exportProperty(
    ol.source.OSM.prototype,
    'getTileUrlFunction',
    ol.source.OSM.prototype.getTileUrlFunction);

goog.exportProperty(
    ol.source.OSM.prototype,
    'getUrls',
    ol.source.OSM.prototype.getUrls);

goog.exportProperty(
    ol.source.OSM.prototype,
    'setTileLoadFunction',
    ol.source.OSM.prototype.setTileLoadFunction);

goog.exportProperty(
    ol.source.OSM.prototype,
    'setTileUrlFunction',
    ol.source.OSM.prototype.setTileUrlFunction);

goog.exportProperty(
    ol.source.OSM.prototype,
    'setUrl',
    ol.source.OSM.prototype.setUrl);

goog.exportProperty(
    ol.source.OSM.prototype,
    'setUrls',
    ol.source.OSM.prototype.setUrls);

goog.exportProperty(
    ol.source.OSM.prototype,
    'getTileGrid',
    ol.source.OSM.prototype.getTileGrid);

goog.exportProperty(
    ol.source.OSM.prototype,
    'getAttributions',
    ol.source.OSM.prototype.getAttributions);

goog.exportProperty(
    ol.source.OSM.prototype,
    'getLogo',
    ol.source.OSM.prototype.getLogo);

goog.exportProperty(
    ol.source.OSM.prototype,
    'getProjection',
    ol.source.OSM.prototype.getProjection);

goog.exportProperty(
    ol.source.OSM.prototype,
    'getState',
    ol.source.OSM.prototype.getState);

goog.exportProperty(
    ol.source.OSM.prototype,
    'setAttributions',
    ol.source.OSM.prototype.setAttributions);

goog.exportProperty(
    ol.source.OSM.prototype,
    'get',
    ol.source.OSM.prototype.get);

goog.exportProperty(
    ol.source.OSM.prototype,
    'getKeys',
    ol.source.OSM.prototype.getKeys);

goog.exportProperty(
    ol.source.OSM.prototype,
    'getProperties',
    ol.source.OSM.prototype.getProperties);

goog.exportProperty(
    ol.source.OSM.prototype,
    'set',
    ol.source.OSM.prototype.set);

goog.exportProperty(
    ol.source.OSM.prototype,
    'setProperties',
    ol.source.OSM.prototype.setProperties);

goog.exportProperty(
    ol.source.OSM.prototype,
    'unset',
    ol.source.OSM.prototype.unset);

goog.exportProperty(
    ol.source.OSM.prototype,
    'changed',
    ol.source.OSM.prototype.changed);

goog.exportProperty(
    ol.source.OSM.prototype,
    'dispatchEvent',
    ol.source.OSM.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.OSM.prototype,
    'getRevision',
    ol.source.OSM.prototype.getRevision);

goog.exportProperty(
    ol.source.OSM.prototype,
    'on',
    ol.source.OSM.prototype.on);

goog.exportProperty(
    ol.source.OSM.prototype,
    'once',
    ol.source.OSM.prototype.once);

goog.exportProperty(
    ol.source.OSM.prototype,
    'un',
    ol.source.OSM.prototype.un);

goog.exportProperty(
    ol.source.OSM.prototype,
    'unByKey',
    ol.source.OSM.prototype.unByKey);

goog.exportProperty(
    ol.source.Raster.prototype,
    'getAttributions',
    ol.source.Raster.prototype.getAttributions);

goog.exportProperty(
    ol.source.Raster.prototype,
    'getLogo',
    ol.source.Raster.prototype.getLogo);

goog.exportProperty(
    ol.source.Raster.prototype,
    'getProjection',
    ol.source.Raster.prototype.getProjection);

goog.exportProperty(
    ol.source.Raster.prototype,
    'getState',
    ol.source.Raster.prototype.getState);

goog.exportProperty(
    ol.source.Raster.prototype,
    'setAttributions',
    ol.source.Raster.prototype.setAttributions);

goog.exportProperty(
    ol.source.Raster.prototype,
    'get',
    ol.source.Raster.prototype.get);

goog.exportProperty(
    ol.source.Raster.prototype,
    'getKeys',
    ol.source.Raster.prototype.getKeys);

goog.exportProperty(
    ol.source.Raster.prototype,
    'getProperties',
    ol.source.Raster.prototype.getProperties);

goog.exportProperty(
    ol.source.Raster.prototype,
    'set',
    ol.source.Raster.prototype.set);

goog.exportProperty(
    ol.source.Raster.prototype,
    'setProperties',
    ol.source.Raster.prototype.setProperties);

goog.exportProperty(
    ol.source.Raster.prototype,
    'unset',
    ol.source.Raster.prototype.unset);

goog.exportProperty(
    ol.source.Raster.prototype,
    'changed',
    ol.source.Raster.prototype.changed);

goog.exportProperty(
    ol.source.Raster.prototype,
    'dispatchEvent',
    ol.source.Raster.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.Raster.prototype,
    'getRevision',
    ol.source.Raster.prototype.getRevision);

goog.exportProperty(
    ol.source.Raster.prototype,
    'on',
    ol.source.Raster.prototype.on);

goog.exportProperty(
    ol.source.Raster.prototype,
    'once',
    ol.source.Raster.prototype.once);

goog.exportProperty(
    ol.source.Raster.prototype,
    'un',
    ol.source.Raster.prototype.un);

goog.exportProperty(
    ol.source.Raster.prototype,
    'unByKey',
    ol.source.Raster.prototype.unByKey);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'setRenderReprojectionEdges',
    ol.source.Stamen.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'setTileGridForProjection',
    ol.source.Stamen.prototype.setTileGridForProjection);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'getTileLoadFunction',
    ol.source.Stamen.prototype.getTileLoadFunction);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'getTileUrlFunction',
    ol.source.Stamen.prototype.getTileUrlFunction);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'getUrls',
    ol.source.Stamen.prototype.getUrls);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'setTileLoadFunction',
    ol.source.Stamen.prototype.setTileLoadFunction);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'setTileUrlFunction',
    ol.source.Stamen.prototype.setTileUrlFunction);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'setUrl',
    ol.source.Stamen.prototype.setUrl);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'setUrls',
    ol.source.Stamen.prototype.setUrls);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'getTileGrid',
    ol.source.Stamen.prototype.getTileGrid);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'getAttributions',
    ol.source.Stamen.prototype.getAttributions);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'getLogo',
    ol.source.Stamen.prototype.getLogo);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'getProjection',
    ol.source.Stamen.prototype.getProjection);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'getState',
    ol.source.Stamen.prototype.getState);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'setAttributions',
    ol.source.Stamen.prototype.setAttributions);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'get',
    ol.source.Stamen.prototype.get);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'getKeys',
    ol.source.Stamen.prototype.getKeys);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'getProperties',
    ol.source.Stamen.prototype.getProperties);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'set',
    ol.source.Stamen.prototype.set);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'setProperties',
    ol.source.Stamen.prototype.setProperties);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'unset',
    ol.source.Stamen.prototype.unset);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'changed',
    ol.source.Stamen.prototype.changed);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'dispatchEvent',
    ol.source.Stamen.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'getRevision',
    ol.source.Stamen.prototype.getRevision);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'on',
    ol.source.Stamen.prototype.on);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'once',
    ol.source.Stamen.prototype.once);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'un',
    ol.source.Stamen.prototype.un);

goog.exportProperty(
    ol.source.Stamen.prototype,
    'unByKey',
    ol.source.Stamen.prototype.unByKey);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'setRenderReprojectionEdges',
    ol.source.TileArcGISRest.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'setTileGridForProjection',
    ol.source.TileArcGISRest.prototype.setTileGridForProjection);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'getTileLoadFunction',
    ol.source.TileArcGISRest.prototype.getTileLoadFunction);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'getTileUrlFunction',
    ol.source.TileArcGISRest.prototype.getTileUrlFunction);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'getUrls',
    ol.source.TileArcGISRest.prototype.getUrls);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'setTileLoadFunction',
    ol.source.TileArcGISRest.prototype.setTileLoadFunction);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'setTileUrlFunction',
    ol.source.TileArcGISRest.prototype.setTileUrlFunction);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'setUrl',
    ol.source.TileArcGISRest.prototype.setUrl);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'setUrls',
    ol.source.TileArcGISRest.prototype.setUrls);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'getTileGrid',
    ol.source.TileArcGISRest.prototype.getTileGrid);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'getAttributions',
    ol.source.TileArcGISRest.prototype.getAttributions);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'getLogo',
    ol.source.TileArcGISRest.prototype.getLogo);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'getProjection',
    ol.source.TileArcGISRest.prototype.getProjection);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'getState',
    ol.source.TileArcGISRest.prototype.getState);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'setAttributions',
    ol.source.TileArcGISRest.prototype.setAttributions);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'get',
    ol.source.TileArcGISRest.prototype.get);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'getKeys',
    ol.source.TileArcGISRest.prototype.getKeys);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'getProperties',
    ol.source.TileArcGISRest.prototype.getProperties);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'set',
    ol.source.TileArcGISRest.prototype.set);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'setProperties',
    ol.source.TileArcGISRest.prototype.setProperties);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'unset',
    ol.source.TileArcGISRest.prototype.unset);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'changed',
    ol.source.TileArcGISRest.prototype.changed);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'dispatchEvent',
    ol.source.TileArcGISRest.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'getRevision',
    ol.source.TileArcGISRest.prototype.getRevision);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'on',
    ol.source.TileArcGISRest.prototype.on);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'once',
    ol.source.TileArcGISRest.prototype.once);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'un',
    ol.source.TileArcGISRest.prototype.un);

goog.exportProperty(
    ol.source.TileArcGISRest.prototype,
    'unByKey',
    ol.source.TileArcGISRest.prototype.unByKey);

goog.exportProperty(
    ol.source.TileDebug.prototype,
    'getTileGrid',
    ol.source.TileDebug.prototype.getTileGrid);

goog.exportProperty(
    ol.source.TileDebug.prototype,
    'getAttributions',
    ol.source.TileDebug.prototype.getAttributions);

goog.exportProperty(
    ol.source.TileDebug.prototype,
    'getLogo',
    ol.source.TileDebug.prototype.getLogo);

goog.exportProperty(
    ol.source.TileDebug.prototype,
    'getProjection',
    ol.source.TileDebug.prototype.getProjection);

goog.exportProperty(
    ol.source.TileDebug.prototype,
    'getState',
    ol.source.TileDebug.prototype.getState);

goog.exportProperty(
    ol.source.TileDebug.prototype,
    'setAttributions',
    ol.source.TileDebug.prototype.setAttributions);

goog.exportProperty(
    ol.source.TileDebug.prototype,
    'get',
    ol.source.TileDebug.prototype.get);

goog.exportProperty(
    ol.source.TileDebug.prototype,
    'getKeys',
    ol.source.TileDebug.prototype.getKeys);

goog.exportProperty(
    ol.source.TileDebug.prototype,
    'getProperties',
    ol.source.TileDebug.prototype.getProperties);

goog.exportProperty(
    ol.source.TileDebug.prototype,
    'set',
    ol.source.TileDebug.prototype.set);

goog.exportProperty(
    ol.source.TileDebug.prototype,
    'setProperties',
    ol.source.TileDebug.prototype.setProperties);

goog.exportProperty(
    ol.source.TileDebug.prototype,
    'unset',
    ol.source.TileDebug.prototype.unset);

goog.exportProperty(
    ol.source.TileDebug.prototype,
    'changed',
    ol.source.TileDebug.prototype.changed);

goog.exportProperty(
    ol.source.TileDebug.prototype,
    'dispatchEvent',
    ol.source.TileDebug.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.TileDebug.prototype,
    'getRevision',
    ol.source.TileDebug.prototype.getRevision);

goog.exportProperty(
    ol.source.TileDebug.prototype,
    'on',
    ol.source.TileDebug.prototype.on);

goog.exportProperty(
    ol.source.TileDebug.prototype,
    'once',
    ol.source.TileDebug.prototype.once);

goog.exportProperty(
    ol.source.TileDebug.prototype,
    'un',
    ol.source.TileDebug.prototype.un);

goog.exportProperty(
    ol.source.TileDebug.prototype,
    'unByKey',
    ol.source.TileDebug.prototype.unByKey);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'setRenderReprojectionEdges',
    ol.source.TileJSON.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'setTileGridForProjection',
    ol.source.TileJSON.prototype.setTileGridForProjection);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'getTileLoadFunction',
    ol.source.TileJSON.prototype.getTileLoadFunction);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'getTileUrlFunction',
    ol.source.TileJSON.prototype.getTileUrlFunction);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'getUrls',
    ol.source.TileJSON.prototype.getUrls);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'setTileLoadFunction',
    ol.source.TileJSON.prototype.setTileLoadFunction);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'setTileUrlFunction',
    ol.source.TileJSON.prototype.setTileUrlFunction);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'setUrl',
    ol.source.TileJSON.prototype.setUrl);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'setUrls',
    ol.source.TileJSON.prototype.setUrls);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'getTileGrid',
    ol.source.TileJSON.prototype.getTileGrid);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'getAttributions',
    ol.source.TileJSON.prototype.getAttributions);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'getLogo',
    ol.source.TileJSON.prototype.getLogo);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'getProjection',
    ol.source.TileJSON.prototype.getProjection);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'getState',
    ol.source.TileJSON.prototype.getState);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'setAttributions',
    ol.source.TileJSON.prototype.setAttributions);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'get',
    ol.source.TileJSON.prototype.get);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'getKeys',
    ol.source.TileJSON.prototype.getKeys);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'getProperties',
    ol.source.TileJSON.prototype.getProperties);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'set',
    ol.source.TileJSON.prototype.set);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'setProperties',
    ol.source.TileJSON.prototype.setProperties);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'unset',
    ol.source.TileJSON.prototype.unset);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'changed',
    ol.source.TileJSON.prototype.changed);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'dispatchEvent',
    ol.source.TileJSON.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'getRevision',
    ol.source.TileJSON.prototype.getRevision);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'on',
    ol.source.TileJSON.prototype.on);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'once',
    ol.source.TileJSON.prototype.once);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'un',
    ol.source.TileJSON.prototype.un);

goog.exportProperty(
    ol.source.TileJSON.prototype,
    'unByKey',
    ol.source.TileJSON.prototype.unByKey);

goog.exportProperty(
    ol.source.TileUTFGrid.prototype,
    'getTileGrid',
    ol.source.TileUTFGrid.prototype.getTileGrid);

goog.exportProperty(
    ol.source.TileUTFGrid.prototype,
    'getAttributions',
    ol.source.TileUTFGrid.prototype.getAttributions);

goog.exportProperty(
    ol.source.TileUTFGrid.prototype,
    'getLogo',
    ol.source.TileUTFGrid.prototype.getLogo);

goog.exportProperty(
    ol.source.TileUTFGrid.prototype,
    'getProjection',
    ol.source.TileUTFGrid.prototype.getProjection);

goog.exportProperty(
    ol.source.TileUTFGrid.prototype,
    'getState',
    ol.source.TileUTFGrid.prototype.getState);

goog.exportProperty(
    ol.source.TileUTFGrid.prototype,
    'setAttributions',
    ol.source.TileUTFGrid.prototype.setAttributions);

goog.exportProperty(
    ol.source.TileUTFGrid.prototype,
    'get',
    ol.source.TileUTFGrid.prototype.get);

goog.exportProperty(
    ol.source.TileUTFGrid.prototype,
    'getKeys',
    ol.source.TileUTFGrid.prototype.getKeys);

goog.exportProperty(
    ol.source.TileUTFGrid.prototype,
    'getProperties',
    ol.source.TileUTFGrid.prototype.getProperties);

goog.exportProperty(
    ol.source.TileUTFGrid.prototype,
    'set',
    ol.source.TileUTFGrid.prototype.set);

goog.exportProperty(
    ol.source.TileUTFGrid.prototype,
    'setProperties',
    ol.source.TileUTFGrid.prototype.setProperties);

goog.exportProperty(
    ol.source.TileUTFGrid.prototype,
    'unset',
    ol.source.TileUTFGrid.prototype.unset);

goog.exportProperty(
    ol.source.TileUTFGrid.prototype,
    'changed',
    ol.source.TileUTFGrid.prototype.changed);

goog.exportProperty(
    ol.source.TileUTFGrid.prototype,
    'dispatchEvent',
    ol.source.TileUTFGrid.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.TileUTFGrid.prototype,
    'getRevision',
    ol.source.TileUTFGrid.prototype.getRevision);

goog.exportProperty(
    ol.source.TileUTFGrid.prototype,
    'on',
    ol.source.TileUTFGrid.prototype.on);

goog.exportProperty(
    ol.source.TileUTFGrid.prototype,
    'once',
    ol.source.TileUTFGrid.prototype.once);

goog.exportProperty(
    ol.source.TileUTFGrid.prototype,
    'un',
    ol.source.TileUTFGrid.prototype.un);

goog.exportProperty(
    ol.source.TileUTFGrid.prototype,
    'unByKey',
    ol.source.TileUTFGrid.prototype.unByKey);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'setRenderReprojectionEdges',
    ol.source.TileWMS.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'setTileGridForProjection',
    ol.source.TileWMS.prototype.setTileGridForProjection);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'getTileLoadFunction',
    ol.source.TileWMS.prototype.getTileLoadFunction);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'getTileUrlFunction',
    ol.source.TileWMS.prototype.getTileUrlFunction);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'getUrls',
    ol.source.TileWMS.prototype.getUrls);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'setTileLoadFunction',
    ol.source.TileWMS.prototype.setTileLoadFunction);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'setTileUrlFunction',
    ol.source.TileWMS.prototype.setTileUrlFunction);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'setUrl',
    ol.source.TileWMS.prototype.setUrl);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'setUrls',
    ol.source.TileWMS.prototype.setUrls);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'getTileGrid',
    ol.source.TileWMS.prototype.getTileGrid);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'getAttributions',
    ol.source.TileWMS.prototype.getAttributions);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'getLogo',
    ol.source.TileWMS.prototype.getLogo);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'getProjection',
    ol.source.TileWMS.prototype.getProjection);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'getState',
    ol.source.TileWMS.prototype.getState);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'setAttributions',
    ol.source.TileWMS.prototype.setAttributions);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'get',
    ol.source.TileWMS.prototype.get);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'getKeys',
    ol.source.TileWMS.prototype.getKeys);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'getProperties',
    ol.source.TileWMS.prototype.getProperties);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'set',
    ol.source.TileWMS.prototype.set);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'setProperties',
    ol.source.TileWMS.prototype.setProperties);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'unset',
    ol.source.TileWMS.prototype.unset);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'changed',
    ol.source.TileWMS.prototype.changed);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'dispatchEvent',
    ol.source.TileWMS.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'getRevision',
    ol.source.TileWMS.prototype.getRevision);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'on',
    ol.source.TileWMS.prototype.on);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'once',
    ol.source.TileWMS.prototype.once);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'un',
    ol.source.TileWMS.prototype.un);

goog.exportProperty(
    ol.source.TileWMS.prototype,
    'unByKey',
    ol.source.TileWMS.prototype.unByKey);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'getTileLoadFunction',
    ol.source.VectorTile.prototype.getTileLoadFunction);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'getTileUrlFunction',
    ol.source.VectorTile.prototype.getTileUrlFunction);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'getUrls',
    ol.source.VectorTile.prototype.getUrls);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'setTileLoadFunction',
    ol.source.VectorTile.prototype.setTileLoadFunction);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'setTileUrlFunction',
    ol.source.VectorTile.prototype.setTileUrlFunction);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'setUrl',
    ol.source.VectorTile.prototype.setUrl);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'setUrls',
    ol.source.VectorTile.prototype.setUrls);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'getTileGrid',
    ol.source.VectorTile.prototype.getTileGrid);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'getAttributions',
    ol.source.VectorTile.prototype.getAttributions);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'getLogo',
    ol.source.VectorTile.prototype.getLogo);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'getProjection',
    ol.source.VectorTile.prototype.getProjection);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'getState',
    ol.source.VectorTile.prototype.getState);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'setAttributions',
    ol.source.VectorTile.prototype.setAttributions);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'get',
    ol.source.VectorTile.prototype.get);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'getKeys',
    ol.source.VectorTile.prototype.getKeys);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'getProperties',
    ol.source.VectorTile.prototype.getProperties);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'set',
    ol.source.VectorTile.prototype.set);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'setProperties',
    ol.source.VectorTile.prototype.setProperties);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'unset',
    ol.source.VectorTile.prototype.unset);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'changed',
    ol.source.VectorTile.prototype.changed);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'dispatchEvent',
    ol.source.VectorTile.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'getRevision',
    ol.source.VectorTile.prototype.getRevision);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'on',
    ol.source.VectorTile.prototype.on);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'once',
    ol.source.VectorTile.prototype.once);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'un',
    ol.source.VectorTile.prototype.un);

goog.exportProperty(
    ol.source.VectorTile.prototype,
    'unByKey',
    ol.source.VectorTile.prototype.unByKey);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'setRenderReprojectionEdges',
    ol.source.WMTS.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'setTileGridForProjection',
    ol.source.WMTS.prototype.setTileGridForProjection);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'getTileLoadFunction',
    ol.source.WMTS.prototype.getTileLoadFunction);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'getTileUrlFunction',
    ol.source.WMTS.prototype.getTileUrlFunction);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'getUrls',
    ol.source.WMTS.prototype.getUrls);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'setTileLoadFunction',
    ol.source.WMTS.prototype.setTileLoadFunction);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'setTileUrlFunction',
    ol.source.WMTS.prototype.setTileUrlFunction);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'setUrl',
    ol.source.WMTS.prototype.setUrl);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'setUrls',
    ol.source.WMTS.prototype.setUrls);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'getTileGrid',
    ol.source.WMTS.prototype.getTileGrid);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'getAttributions',
    ol.source.WMTS.prototype.getAttributions);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'getLogo',
    ol.source.WMTS.prototype.getLogo);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'getProjection',
    ol.source.WMTS.prototype.getProjection);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'getState',
    ol.source.WMTS.prototype.getState);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'setAttributions',
    ol.source.WMTS.prototype.setAttributions);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'get',
    ol.source.WMTS.prototype.get);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'getKeys',
    ol.source.WMTS.prototype.getKeys);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'getProperties',
    ol.source.WMTS.prototype.getProperties);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'set',
    ol.source.WMTS.prototype.set);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'setProperties',
    ol.source.WMTS.prototype.setProperties);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'unset',
    ol.source.WMTS.prototype.unset);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'changed',
    ol.source.WMTS.prototype.changed);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'dispatchEvent',
    ol.source.WMTS.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'getRevision',
    ol.source.WMTS.prototype.getRevision);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'on',
    ol.source.WMTS.prototype.on);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'once',
    ol.source.WMTS.prototype.once);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'un',
    ol.source.WMTS.prototype.un);

goog.exportProperty(
    ol.source.WMTS.prototype,
    'unByKey',
    ol.source.WMTS.prototype.unByKey);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'setRenderReprojectionEdges',
    ol.source.Zoomify.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'setTileGridForProjection',
    ol.source.Zoomify.prototype.setTileGridForProjection);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'getTileLoadFunction',
    ol.source.Zoomify.prototype.getTileLoadFunction);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'getTileUrlFunction',
    ol.source.Zoomify.prototype.getTileUrlFunction);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'getUrls',
    ol.source.Zoomify.prototype.getUrls);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'setTileLoadFunction',
    ol.source.Zoomify.prototype.setTileLoadFunction);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'setTileUrlFunction',
    ol.source.Zoomify.prototype.setTileUrlFunction);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'setUrl',
    ol.source.Zoomify.prototype.setUrl);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'setUrls',
    ol.source.Zoomify.prototype.setUrls);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'getTileGrid',
    ol.source.Zoomify.prototype.getTileGrid);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'getAttributions',
    ol.source.Zoomify.prototype.getAttributions);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'getLogo',
    ol.source.Zoomify.prototype.getLogo);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'getProjection',
    ol.source.Zoomify.prototype.getProjection);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'getState',
    ol.source.Zoomify.prototype.getState);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'setAttributions',
    ol.source.Zoomify.prototype.setAttributions);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'get',
    ol.source.Zoomify.prototype.get);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'getKeys',
    ol.source.Zoomify.prototype.getKeys);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'getProperties',
    ol.source.Zoomify.prototype.getProperties);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'set',
    ol.source.Zoomify.prototype.set);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'setProperties',
    ol.source.Zoomify.prototype.setProperties);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'unset',
    ol.source.Zoomify.prototype.unset);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'changed',
    ol.source.Zoomify.prototype.changed);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'dispatchEvent',
    ol.source.Zoomify.prototype.dispatchEvent);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'getRevision',
    ol.source.Zoomify.prototype.getRevision);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'on',
    ol.source.Zoomify.prototype.on);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'once',
    ol.source.Zoomify.prototype.once);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'un',
    ol.source.Zoomify.prototype.un);

goog.exportProperty(
    ol.source.Zoomify.prototype,
    'unByKey',
    ol.source.Zoomify.prototype.unByKey);

goog.exportProperty(
    ol.reproj.Tile.prototype,
    'getTileCoord',
    ol.reproj.Tile.prototype.getTileCoord);

goog.exportProperty(
    ol.renderer.Layer.prototype,
    'changed',
    ol.renderer.Layer.prototype.changed);

goog.exportProperty(
    ol.renderer.Layer.prototype,
    'dispatchEvent',
    ol.renderer.Layer.prototype.dispatchEvent);

goog.exportProperty(
    ol.renderer.Layer.prototype,
    'getRevision',
    ol.renderer.Layer.prototype.getRevision);

goog.exportProperty(
    ol.renderer.Layer.prototype,
    'on',
    ol.renderer.Layer.prototype.on);

goog.exportProperty(
    ol.renderer.Layer.prototype,
    'once',
    ol.renderer.Layer.prototype.once);

goog.exportProperty(
    ol.renderer.Layer.prototype,
    'un',
    ol.renderer.Layer.prototype.un);

goog.exportProperty(
    ol.renderer.Layer.prototype,
    'unByKey',
    ol.renderer.Layer.prototype.unByKey);

goog.exportProperty(
    ol.renderer.webgl.Layer.prototype,
    'changed',
    ol.renderer.webgl.Layer.prototype.changed);

goog.exportProperty(
    ol.renderer.webgl.Layer.prototype,
    'dispatchEvent',
    ol.renderer.webgl.Layer.prototype.dispatchEvent);

goog.exportProperty(
    ol.renderer.webgl.Layer.prototype,
    'getRevision',
    ol.renderer.webgl.Layer.prototype.getRevision);

goog.exportProperty(
    ol.renderer.webgl.Layer.prototype,
    'on',
    ol.renderer.webgl.Layer.prototype.on);

goog.exportProperty(
    ol.renderer.webgl.Layer.prototype,
    'once',
    ol.renderer.webgl.Layer.prototype.once);

goog.exportProperty(
    ol.renderer.webgl.Layer.prototype,
    'un',
    ol.renderer.webgl.Layer.prototype.un);

goog.exportProperty(
    ol.renderer.webgl.Layer.prototype,
    'unByKey',
    ol.renderer.webgl.Layer.prototype.unByKey);

goog.exportProperty(
    ol.renderer.webgl.ImageLayer.prototype,
    'changed',
    ol.renderer.webgl.ImageLayer.prototype.changed);

goog.exportProperty(
    ol.renderer.webgl.ImageLayer.prototype,
    'dispatchEvent',
    ol.renderer.webgl.ImageLayer.prototype.dispatchEvent);

goog.exportProperty(
    ol.renderer.webgl.ImageLayer.prototype,
    'getRevision',
    ol.renderer.webgl.ImageLayer.prototype.getRevision);

goog.exportProperty(
    ol.renderer.webgl.ImageLayer.prototype,
    'on',
    ol.renderer.webgl.ImageLayer.prototype.on);

goog.exportProperty(
    ol.renderer.webgl.ImageLayer.prototype,
    'once',
    ol.renderer.webgl.ImageLayer.prototype.once);

goog.exportProperty(
    ol.renderer.webgl.ImageLayer.prototype,
    'un',
    ol.renderer.webgl.ImageLayer.prototype.un);

goog.exportProperty(
    ol.renderer.webgl.ImageLayer.prototype,
    'unByKey',
    ol.renderer.webgl.ImageLayer.prototype.unByKey);

goog.exportProperty(
    ol.renderer.webgl.TileLayer.prototype,
    'changed',
    ol.renderer.webgl.TileLayer.prototype.changed);

goog.exportProperty(
    ol.renderer.webgl.TileLayer.prototype,
    'dispatchEvent',
    ol.renderer.webgl.TileLayer.prototype.dispatchEvent);

goog.exportProperty(
    ol.renderer.webgl.TileLayer.prototype,
    'getRevision',
    ol.renderer.webgl.TileLayer.prototype.getRevision);

goog.exportProperty(
    ol.renderer.webgl.TileLayer.prototype,
    'on',
    ol.renderer.webgl.TileLayer.prototype.on);

goog.exportProperty(
    ol.renderer.webgl.TileLayer.prototype,
    'once',
    ol.renderer.webgl.TileLayer.prototype.once);

goog.exportProperty(
    ol.renderer.webgl.TileLayer.prototype,
    'un',
    ol.renderer.webgl.TileLayer.prototype.un);

goog.exportProperty(
    ol.renderer.webgl.TileLayer.prototype,
    'unByKey',
    ol.renderer.webgl.TileLayer.prototype.unByKey);

goog.exportProperty(
    ol.renderer.webgl.VectorLayer.prototype,
    'changed',
    ol.renderer.webgl.VectorLayer.prototype.changed);

goog.exportProperty(
    ol.renderer.webgl.VectorLayer.prototype,
    'dispatchEvent',
    ol.renderer.webgl.VectorLayer.prototype.dispatchEvent);

goog.exportProperty(
    ol.renderer.webgl.VectorLayer.prototype,
    'getRevision',
    ol.renderer.webgl.VectorLayer.prototype.getRevision);

goog.exportProperty(
    ol.renderer.webgl.VectorLayer.prototype,
    'on',
    ol.renderer.webgl.VectorLayer.prototype.on);

goog.exportProperty(
    ol.renderer.webgl.VectorLayer.prototype,
    'once',
    ol.renderer.webgl.VectorLayer.prototype.once);

goog.exportProperty(
    ol.renderer.webgl.VectorLayer.prototype,
    'un',
    ol.renderer.webgl.VectorLayer.prototype.un);

goog.exportProperty(
    ol.renderer.webgl.VectorLayer.prototype,
    'unByKey',
    ol.renderer.webgl.VectorLayer.prototype.unByKey);

goog.exportProperty(
    ol.renderer.dom.Layer.prototype,
    'changed',
    ol.renderer.dom.Layer.prototype.changed);

goog.exportProperty(
    ol.renderer.dom.Layer.prototype,
    'dispatchEvent',
    ol.renderer.dom.Layer.prototype.dispatchEvent);

goog.exportProperty(
    ol.renderer.dom.Layer.prototype,
    'getRevision',
    ol.renderer.dom.Layer.prototype.getRevision);

goog.exportProperty(
    ol.renderer.dom.Layer.prototype,
    'on',
    ol.renderer.dom.Layer.prototype.on);

goog.exportProperty(
    ol.renderer.dom.Layer.prototype,
    'once',
    ol.renderer.dom.Layer.prototype.once);

goog.exportProperty(
    ol.renderer.dom.Layer.prototype,
    'un',
    ol.renderer.dom.Layer.prototype.un);

goog.exportProperty(
    ol.renderer.dom.Layer.prototype,
    'unByKey',
    ol.renderer.dom.Layer.prototype.unByKey);

goog.exportProperty(
    ol.renderer.dom.ImageLayer.prototype,
    'changed',
    ol.renderer.dom.ImageLayer.prototype.changed);

goog.exportProperty(
    ol.renderer.dom.ImageLayer.prototype,
    'dispatchEvent',
    ol.renderer.dom.ImageLayer.prototype.dispatchEvent);

goog.exportProperty(
    ol.renderer.dom.ImageLayer.prototype,
    'getRevision',
    ol.renderer.dom.ImageLayer.prototype.getRevision);

goog.exportProperty(
    ol.renderer.dom.ImageLayer.prototype,
    'on',
    ol.renderer.dom.ImageLayer.prototype.on);

goog.exportProperty(
    ol.renderer.dom.ImageLayer.prototype,
    'once',
    ol.renderer.dom.ImageLayer.prototype.once);

goog.exportProperty(
    ol.renderer.dom.ImageLayer.prototype,
    'un',
    ol.renderer.dom.ImageLayer.prototype.un);

goog.exportProperty(
    ol.renderer.dom.ImageLayer.prototype,
    'unByKey',
    ol.renderer.dom.ImageLayer.prototype.unByKey);

goog.exportProperty(
    ol.renderer.dom.TileLayer.prototype,
    'changed',
    ol.renderer.dom.TileLayer.prototype.changed);

goog.exportProperty(
    ol.renderer.dom.TileLayer.prototype,
    'dispatchEvent',
    ol.renderer.dom.TileLayer.prototype.dispatchEvent);

goog.exportProperty(
    ol.renderer.dom.TileLayer.prototype,
    'getRevision',
    ol.renderer.dom.TileLayer.prototype.getRevision);

goog.exportProperty(
    ol.renderer.dom.TileLayer.prototype,
    'on',
    ol.renderer.dom.TileLayer.prototype.on);

goog.exportProperty(
    ol.renderer.dom.TileLayer.prototype,
    'once',
    ol.renderer.dom.TileLayer.prototype.once);

goog.exportProperty(
    ol.renderer.dom.TileLayer.prototype,
    'un',
    ol.renderer.dom.TileLayer.prototype.un);

goog.exportProperty(
    ol.renderer.dom.TileLayer.prototype,
    'unByKey',
    ol.renderer.dom.TileLayer.prototype.unByKey);

goog.exportProperty(
    ol.renderer.dom.VectorLayer.prototype,
    'changed',
    ol.renderer.dom.VectorLayer.prototype.changed);

goog.exportProperty(
    ol.renderer.dom.VectorLayer.prototype,
    'dispatchEvent',
    ol.renderer.dom.VectorLayer.prototype.dispatchEvent);

goog.exportProperty(
    ol.renderer.dom.VectorLayer.prototype,
    'getRevision',
    ol.renderer.dom.VectorLayer.prototype.getRevision);

goog.exportProperty(
    ol.renderer.dom.VectorLayer.prototype,
    'on',
    ol.renderer.dom.VectorLayer.prototype.on);

goog.exportProperty(
    ol.renderer.dom.VectorLayer.prototype,
    'once',
    ol.renderer.dom.VectorLayer.prototype.once);

goog.exportProperty(
    ol.renderer.dom.VectorLayer.prototype,
    'un',
    ol.renderer.dom.VectorLayer.prototype.un);

goog.exportProperty(
    ol.renderer.dom.VectorLayer.prototype,
    'unByKey',
    ol.renderer.dom.VectorLayer.prototype.unByKey);

goog.exportProperty(
    ol.renderer.canvas.Layer.prototype,
    'changed',
    ol.renderer.canvas.Layer.prototype.changed);

goog.exportProperty(
    ol.renderer.canvas.Layer.prototype,
    'dispatchEvent',
    ol.renderer.canvas.Layer.prototype.dispatchEvent);

goog.exportProperty(
    ol.renderer.canvas.Layer.prototype,
    'getRevision',
    ol.renderer.canvas.Layer.prototype.getRevision);

goog.exportProperty(
    ol.renderer.canvas.Layer.prototype,
    'on',
    ol.renderer.canvas.Layer.prototype.on);

goog.exportProperty(
    ol.renderer.canvas.Layer.prototype,
    'once',
    ol.renderer.canvas.Layer.prototype.once);

goog.exportProperty(
    ol.renderer.canvas.Layer.prototype,
    'un',
    ol.renderer.canvas.Layer.prototype.un);

goog.exportProperty(
    ol.renderer.canvas.Layer.prototype,
    'unByKey',
    ol.renderer.canvas.Layer.prototype.unByKey);

goog.exportProperty(
    ol.renderer.canvas.ImageLayer.prototype,
    'changed',
    ol.renderer.canvas.ImageLayer.prototype.changed);

goog.exportProperty(
    ol.renderer.canvas.ImageLayer.prototype,
    'dispatchEvent',
    ol.renderer.canvas.ImageLayer.prototype.dispatchEvent);

goog.exportProperty(
    ol.renderer.canvas.ImageLayer.prototype,
    'getRevision',
    ol.renderer.canvas.ImageLayer.prototype.getRevision);

goog.exportProperty(
    ol.renderer.canvas.ImageLayer.prototype,
    'on',
    ol.renderer.canvas.ImageLayer.prototype.on);

goog.exportProperty(
    ol.renderer.canvas.ImageLayer.prototype,
    'once',
    ol.renderer.canvas.ImageLayer.prototype.once);

goog.exportProperty(
    ol.renderer.canvas.ImageLayer.prototype,
    'un',
    ol.renderer.canvas.ImageLayer.prototype.un);

goog.exportProperty(
    ol.renderer.canvas.ImageLayer.prototype,
    'unByKey',
    ol.renderer.canvas.ImageLayer.prototype.unByKey);

goog.exportProperty(
    ol.renderer.canvas.TileLayer.prototype,
    'changed',
    ol.renderer.canvas.TileLayer.prototype.changed);

goog.exportProperty(
    ol.renderer.canvas.TileLayer.prototype,
    'dispatchEvent',
    ol.renderer.canvas.TileLayer.prototype.dispatchEvent);

goog.exportProperty(
    ol.renderer.canvas.TileLayer.prototype,
    'getRevision',
    ol.renderer.canvas.TileLayer.prototype.getRevision);

goog.exportProperty(
    ol.renderer.canvas.TileLayer.prototype,
    'on',
    ol.renderer.canvas.TileLayer.prototype.on);

goog.exportProperty(
    ol.renderer.canvas.TileLayer.prototype,
    'once',
    ol.renderer.canvas.TileLayer.prototype.once);

goog.exportProperty(
    ol.renderer.canvas.TileLayer.prototype,
    'un',
    ol.renderer.canvas.TileLayer.prototype.un);

goog.exportProperty(
    ol.renderer.canvas.TileLayer.prototype,
    'unByKey',
    ol.renderer.canvas.TileLayer.prototype.unByKey);

goog.exportProperty(
    ol.renderer.canvas.VectorLayer.prototype,
    'changed',
    ol.renderer.canvas.VectorLayer.prototype.changed);

goog.exportProperty(
    ol.renderer.canvas.VectorLayer.prototype,
    'dispatchEvent',
    ol.renderer.canvas.VectorLayer.prototype.dispatchEvent);

goog.exportProperty(
    ol.renderer.canvas.VectorLayer.prototype,
    'getRevision',
    ol.renderer.canvas.VectorLayer.prototype.getRevision);

goog.exportProperty(
    ol.renderer.canvas.VectorLayer.prototype,
    'on',
    ol.renderer.canvas.VectorLayer.prototype.on);

goog.exportProperty(
    ol.renderer.canvas.VectorLayer.prototype,
    'once',
    ol.renderer.canvas.VectorLayer.prototype.once);

goog.exportProperty(
    ol.renderer.canvas.VectorLayer.prototype,
    'un',
    ol.renderer.canvas.VectorLayer.prototype.un);

goog.exportProperty(
    ol.renderer.canvas.VectorLayer.prototype,
    'unByKey',
    ol.renderer.canvas.VectorLayer.prototype.unByKey);

goog.exportProperty(
    ol.renderer.canvas.VectorTileLayer.prototype,
    'changed',
    ol.renderer.canvas.VectorTileLayer.prototype.changed);

goog.exportProperty(
    ol.renderer.canvas.VectorTileLayer.prototype,
    'dispatchEvent',
    ol.renderer.canvas.VectorTileLayer.prototype.dispatchEvent);

goog.exportProperty(
    ol.renderer.canvas.VectorTileLayer.prototype,
    'getRevision',
    ol.renderer.canvas.VectorTileLayer.prototype.getRevision);

goog.exportProperty(
    ol.renderer.canvas.VectorTileLayer.prototype,
    'on',
    ol.renderer.canvas.VectorTileLayer.prototype.on);

goog.exportProperty(
    ol.renderer.canvas.VectorTileLayer.prototype,
    'once',
    ol.renderer.canvas.VectorTileLayer.prototype.once);

goog.exportProperty(
    ol.renderer.canvas.VectorTileLayer.prototype,
    'un',
    ol.renderer.canvas.VectorTileLayer.prototype.un);

goog.exportProperty(
    ol.renderer.canvas.VectorTileLayer.prototype,
    'unByKey',
    ol.renderer.canvas.VectorTileLayer.prototype.unByKey);

goog.exportProperty(
    ol.layer.Base.prototype,
    'get',
    ol.layer.Base.prototype.get);

goog.exportProperty(
    ol.layer.Base.prototype,
    'getKeys',
    ol.layer.Base.prototype.getKeys);

goog.exportProperty(
    ol.layer.Base.prototype,
    'getProperties',
    ol.layer.Base.prototype.getProperties);

goog.exportProperty(
    ol.layer.Base.prototype,
    'set',
    ol.layer.Base.prototype.set);

goog.exportProperty(
    ol.layer.Base.prototype,
    'setProperties',
    ol.layer.Base.prototype.setProperties);

goog.exportProperty(
    ol.layer.Base.prototype,
    'unset',
    ol.layer.Base.prototype.unset);

goog.exportProperty(
    ol.layer.Base.prototype,
    'changed',
    ol.layer.Base.prototype.changed);

goog.exportProperty(
    ol.layer.Base.prototype,
    'dispatchEvent',
    ol.layer.Base.prototype.dispatchEvent);

goog.exportProperty(
    ol.layer.Base.prototype,
    'getRevision',
    ol.layer.Base.prototype.getRevision);

goog.exportProperty(
    ol.layer.Base.prototype,
    'on',
    ol.layer.Base.prototype.on);

goog.exportProperty(
    ol.layer.Base.prototype,
    'once',
    ol.layer.Base.prototype.once);

goog.exportProperty(
    ol.layer.Base.prototype,
    'un',
    ol.layer.Base.prototype.un);

goog.exportProperty(
    ol.layer.Base.prototype,
    'unByKey',
    ol.layer.Base.prototype.unByKey);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'getExtent',
    ol.layer.Layer.prototype.getExtent);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'getMaxResolution',
    ol.layer.Layer.prototype.getMaxResolution);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'getMinResolution',
    ol.layer.Layer.prototype.getMinResolution);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'getOpacity',
    ol.layer.Layer.prototype.getOpacity);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'getVisible',
    ol.layer.Layer.prototype.getVisible);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'getZIndex',
    ol.layer.Layer.prototype.getZIndex);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'setExtent',
    ol.layer.Layer.prototype.setExtent);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'setMaxResolution',
    ol.layer.Layer.prototype.setMaxResolution);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'setMinResolution',
    ol.layer.Layer.prototype.setMinResolution);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'setOpacity',
    ol.layer.Layer.prototype.setOpacity);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'setVisible',
    ol.layer.Layer.prototype.setVisible);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'setZIndex',
    ol.layer.Layer.prototype.setZIndex);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'get',
    ol.layer.Layer.prototype.get);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'getKeys',
    ol.layer.Layer.prototype.getKeys);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'getProperties',
    ol.layer.Layer.prototype.getProperties);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'set',
    ol.layer.Layer.prototype.set);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'setProperties',
    ol.layer.Layer.prototype.setProperties);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'unset',
    ol.layer.Layer.prototype.unset);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'changed',
    ol.layer.Layer.prototype.changed);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'dispatchEvent',
    ol.layer.Layer.prototype.dispatchEvent);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'getRevision',
    ol.layer.Layer.prototype.getRevision);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'on',
    ol.layer.Layer.prototype.on);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'once',
    ol.layer.Layer.prototype.once);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'un',
    ol.layer.Layer.prototype.un);

goog.exportProperty(
    ol.layer.Layer.prototype,
    'unByKey',
    ol.layer.Layer.prototype.unByKey);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'setMap',
    ol.layer.Vector.prototype.setMap);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'setSource',
    ol.layer.Vector.prototype.setSource);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'getExtent',
    ol.layer.Vector.prototype.getExtent);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'getMaxResolution',
    ol.layer.Vector.prototype.getMaxResolution);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'getMinResolution',
    ol.layer.Vector.prototype.getMinResolution);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'getOpacity',
    ol.layer.Vector.prototype.getOpacity);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'getVisible',
    ol.layer.Vector.prototype.getVisible);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'getZIndex',
    ol.layer.Vector.prototype.getZIndex);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'setExtent',
    ol.layer.Vector.prototype.setExtent);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'setMaxResolution',
    ol.layer.Vector.prototype.setMaxResolution);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'setMinResolution',
    ol.layer.Vector.prototype.setMinResolution);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'setOpacity',
    ol.layer.Vector.prototype.setOpacity);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'setVisible',
    ol.layer.Vector.prototype.setVisible);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'setZIndex',
    ol.layer.Vector.prototype.setZIndex);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'get',
    ol.layer.Vector.prototype.get);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'getKeys',
    ol.layer.Vector.prototype.getKeys);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'getProperties',
    ol.layer.Vector.prototype.getProperties);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'set',
    ol.layer.Vector.prototype.set);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'setProperties',
    ol.layer.Vector.prototype.setProperties);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'unset',
    ol.layer.Vector.prototype.unset);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'changed',
    ol.layer.Vector.prototype.changed);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'dispatchEvent',
    ol.layer.Vector.prototype.dispatchEvent);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'getRevision',
    ol.layer.Vector.prototype.getRevision);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'on',
    ol.layer.Vector.prototype.on);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'once',
    ol.layer.Vector.prototype.once);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'un',
    ol.layer.Vector.prototype.un);

goog.exportProperty(
    ol.layer.Vector.prototype,
    'unByKey',
    ol.layer.Vector.prototype.unByKey);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'getSource',
    ol.layer.Heatmap.prototype.getSource);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'getStyle',
    ol.layer.Heatmap.prototype.getStyle);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'getStyleFunction',
    ol.layer.Heatmap.prototype.getStyleFunction);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'setStyle',
    ol.layer.Heatmap.prototype.setStyle);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'setMap',
    ol.layer.Heatmap.prototype.setMap);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'setSource',
    ol.layer.Heatmap.prototype.setSource);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'getExtent',
    ol.layer.Heatmap.prototype.getExtent);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'getMaxResolution',
    ol.layer.Heatmap.prototype.getMaxResolution);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'getMinResolution',
    ol.layer.Heatmap.prototype.getMinResolution);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'getOpacity',
    ol.layer.Heatmap.prototype.getOpacity);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'getVisible',
    ol.layer.Heatmap.prototype.getVisible);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'getZIndex',
    ol.layer.Heatmap.prototype.getZIndex);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'setExtent',
    ol.layer.Heatmap.prototype.setExtent);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'setMaxResolution',
    ol.layer.Heatmap.prototype.setMaxResolution);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'setMinResolution',
    ol.layer.Heatmap.prototype.setMinResolution);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'setOpacity',
    ol.layer.Heatmap.prototype.setOpacity);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'setVisible',
    ol.layer.Heatmap.prototype.setVisible);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'setZIndex',
    ol.layer.Heatmap.prototype.setZIndex);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'get',
    ol.layer.Heatmap.prototype.get);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'getKeys',
    ol.layer.Heatmap.prototype.getKeys);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'getProperties',
    ol.layer.Heatmap.prototype.getProperties);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'set',
    ol.layer.Heatmap.prototype.set);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'setProperties',
    ol.layer.Heatmap.prototype.setProperties);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'unset',
    ol.layer.Heatmap.prototype.unset);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'changed',
    ol.layer.Heatmap.prototype.changed);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'dispatchEvent',
    ol.layer.Heatmap.prototype.dispatchEvent);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'getRevision',
    ol.layer.Heatmap.prototype.getRevision);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'on',
    ol.layer.Heatmap.prototype.on);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'once',
    ol.layer.Heatmap.prototype.once);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'un',
    ol.layer.Heatmap.prototype.un);

goog.exportProperty(
    ol.layer.Heatmap.prototype,
    'unByKey',
    ol.layer.Heatmap.prototype.unByKey);

goog.exportProperty(
    ol.layer.Image.prototype,
    'setMap',
    ol.layer.Image.prototype.setMap);

goog.exportProperty(
    ol.layer.Image.prototype,
    'setSource',
    ol.layer.Image.prototype.setSource);

goog.exportProperty(
    ol.layer.Image.prototype,
    'getExtent',
    ol.layer.Image.prototype.getExtent);

goog.exportProperty(
    ol.layer.Image.prototype,
    'getMaxResolution',
    ol.layer.Image.prototype.getMaxResolution);

goog.exportProperty(
    ol.layer.Image.prototype,
    'getMinResolution',
    ol.layer.Image.prototype.getMinResolution);

goog.exportProperty(
    ol.layer.Image.prototype,
    'getOpacity',
    ol.layer.Image.prototype.getOpacity);

goog.exportProperty(
    ol.layer.Image.prototype,
    'getVisible',
    ol.layer.Image.prototype.getVisible);

goog.exportProperty(
    ol.layer.Image.prototype,
    'getZIndex',
    ol.layer.Image.prototype.getZIndex);

goog.exportProperty(
    ol.layer.Image.prototype,
    'setExtent',
    ol.layer.Image.prototype.setExtent);

goog.exportProperty(
    ol.layer.Image.prototype,
    'setMaxResolution',
    ol.layer.Image.prototype.setMaxResolution);

goog.exportProperty(
    ol.layer.Image.prototype,
    'setMinResolution',
    ol.layer.Image.prototype.setMinResolution);

goog.exportProperty(
    ol.layer.Image.prototype,
    'setOpacity',
    ol.layer.Image.prototype.setOpacity);

goog.exportProperty(
    ol.layer.Image.prototype,
    'setVisible',
    ol.layer.Image.prototype.setVisible);

goog.exportProperty(
    ol.layer.Image.prototype,
    'setZIndex',
    ol.layer.Image.prototype.setZIndex);

goog.exportProperty(
    ol.layer.Image.prototype,
    'get',
    ol.layer.Image.prototype.get);

goog.exportProperty(
    ol.layer.Image.prototype,
    'getKeys',
    ol.layer.Image.prototype.getKeys);

goog.exportProperty(
    ol.layer.Image.prototype,
    'getProperties',
    ol.layer.Image.prototype.getProperties);

goog.exportProperty(
    ol.layer.Image.prototype,
    'set',
    ol.layer.Image.prototype.set);

goog.exportProperty(
    ol.layer.Image.prototype,
    'setProperties',
    ol.layer.Image.prototype.setProperties);

goog.exportProperty(
    ol.layer.Image.prototype,
    'unset',
    ol.layer.Image.prototype.unset);

goog.exportProperty(
    ol.layer.Image.prototype,
    'changed',
    ol.layer.Image.prototype.changed);

goog.exportProperty(
    ol.layer.Image.prototype,
    'dispatchEvent',
    ol.layer.Image.prototype.dispatchEvent);

goog.exportProperty(
    ol.layer.Image.prototype,
    'getRevision',
    ol.layer.Image.prototype.getRevision);

goog.exportProperty(
    ol.layer.Image.prototype,
    'on',
    ol.layer.Image.prototype.on);

goog.exportProperty(
    ol.layer.Image.prototype,
    'once',
    ol.layer.Image.prototype.once);

goog.exportProperty(
    ol.layer.Image.prototype,
    'un',
    ol.layer.Image.prototype.un);

goog.exportProperty(
    ol.layer.Image.prototype,
    'unByKey',
    ol.layer.Image.prototype.unByKey);

goog.exportProperty(
    ol.layer.Group.prototype,
    'getExtent',
    ol.layer.Group.prototype.getExtent);

goog.exportProperty(
    ol.layer.Group.prototype,
    'getMaxResolution',
    ol.layer.Group.prototype.getMaxResolution);

goog.exportProperty(
    ol.layer.Group.prototype,
    'getMinResolution',
    ol.layer.Group.prototype.getMinResolution);

goog.exportProperty(
    ol.layer.Group.prototype,
    'getOpacity',
    ol.layer.Group.prototype.getOpacity);

goog.exportProperty(
    ol.layer.Group.prototype,
    'getVisible',
    ol.layer.Group.prototype.getVisible);

goog.exportProperty(
    ol.layer.Group.prototype,
    'getZIndex',
    ol.layer.Group.prototype.getZIndex);

goog.exportProperty(
    ol.layer.Group.prototype,
    'setExtent',
    ol.layer.Group.prototype.setExtent);

goog.exportProperty(
    ol.layer.Group.prototype,
    'setMaxResolution',
    ol.layer.Group.prototype.setMaxResolution);

goog.exportProperty(
    ol.layer.Group.prototype,
    'setMinResolution',
    ol.layer.Group.prototype.setMinResolution);

goog.exportProperty(
    ol.layer.Group.prototype,
    'setOpacity',
    ol.layer.Group.prototype.setOpacity);

goog.exportProperty(
    ol.layer.Group.prototype,
    'setVisible',
    ol.layer.Group.prototype.setVisible);

goog.exportProperty(
    ol.layer.Group.prototype,
    'setZIndex',
    ol.layer.Group.prototype.setZIndex);

goog.exportProperty(
    ol.layer.Group.prototype,
    'get',
    ol.layer.Group.prototype.get);

goog.exportProperty(
    ol.layer.Group.prototype,
    'getKeys',
    ol.layer.Group.prototype.getKeys);

goog.exportProperty(
    ol.layer.Group.prototype,
    'getProperties',
    ol.layer.Group.prototype.getProperties);

goog.exportProperty(
    ol.layer.Group.prototype,
    'set',
    ol.layer.Group.prototype.set);

goog.exportProperty(
    ol.layer.Group.prototype,
    'setProperties',
    ol.layer.Group.prototype.setProperties);

goog.exportProperty(
    ol.layer.Group.prototype,
    'unset',
    ol.layer.Group.prototype.unset);

goog.exportProperty(
    ol.layer.Group.prototype,
    'changed',
    ol.layer.Group.prototype.changed);

goog.exportProperty(
    ol.layer.Group.prototype,
    'dispatchEvent',
    ol.layer.Group.prototype.dispatchEvent);

goog.exportProperty(
    ol.layer.Group.prototype,
    'getRevision',
    ol.layer.Group.prototype.getRevision);

goog.exportProperty(
    ol.layer.Group.prototype,
    'on',
    ol.layer.Group.prototype.on);

goog.exportProperty(
    ol.layer.Group.prototype,
    'once',
    ol.layer.Group.prototype.once);

goog.exportProperty(
    ol.layer.Group.prototype,
    'un',
    ol.layer.Group.prototype.un);

goog.exportProperty(
    ol.layer.Group.prototype,
    'unByKey',
    ol.layer.Group.prototype.unByKey);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'setMap',
    ol.layer.Tile.prototype.setMap);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'setSource',
    ol.layer.Tile.prototype.setSource);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'getExtent',
    ol.layer.Tile.prototype.getExtent);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'getMaxResolution',
    ol.layer.Tile.prototype.getMaxResolution);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'getMinResolution',
    ol.layer.Tile.prototype.getMinResolution);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'getOpacity',
    ol.layer.Tile.prototype.getOpacity);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'getVisible',
    ol.layer.Tile.prototype.getVisible);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'getZIndex',
    ol.layer.Tile.prototype.getZIndex);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'setExtent',
    ol.layer.Tile.prototype.setExtent);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'setMaxResolution',
    ol.layer.Tile.prototype.setMaxResolution);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'setMinResolution',
    ol.layer.Tile.prototype.setMinResolution);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'setOpacity',
    ol.layer.Tile.prototype.setOpacity);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'setVisible',
    ol.layer.Tile.prototype.setVisible);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'setZIndex',
    ol.layer.Tile.prototype.setZIndex);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'get',
    ol.layer.Tile.prototype.get);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'getKeys',
    ol.layer.Tile.prototype.getKeys);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'getProperties',
    ol.layer.Tile.prototype.getProperties);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'set',
    ol.layer.Tile.prototype.set);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'setProperties',
    ol.layer.Tile.prototype.setProperties);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'unset',
    ol.layer.Tile.prototype.unset);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'changed',
    ol.layer.Tile.prototype.changed);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'dispatchEvent',
    ol.layer.Tile.prototype.dispatchEvent);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'getRevision',
    ol.layer.Tile.prototype.getRevision);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'on',
    ol.layer.Tile.prototype.on);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'once',
    ol.layer.Tile.prototype.once);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'un',
    ol.layer.Tile.prototype.un);

goog.exportProperty(
    ol.layer.Tile.prototype,
    'unByKey',
    ol.layer.Tile.prototype.unByKey);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'getStyle',
    ol.layer.VectorTile.prototype.getStyle);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'getStyleFunction',
    ol.layer.VectorTile.prototype.getStyleFunction);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'setStyle',
    ol.layer.VectorTile.prototype.setStyle);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'setMap',
    ol.layer.VectorTile.prototype.setMap);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'setSource',
    ol.layer.VectorTile.prototype.setSource);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'getExtent',
    ol.layer.VectorTile.prototype.getExtent);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'getMaxResolution',
    ol.layer.VectorTile.prototype.getMaxResolution);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'getMinResolution',
    ol.layer.VectorTile.prototype.getMinResolution);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'getOpacity',
    ol.layer.VectorTile.prototype.getOpacity);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'getVisible',
    ol.layer.VectorTile.prototype.getVisible);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'getZIndex',
    ol.layer.VectorTile.prototype.getZIndex);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'setExtent',
    ol.layer.VectorTile.prototype.setExtent);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'setMaxResolution',
    ol.layer.VectorTile.prototype.setMaxResolution);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'setMinResolution',
    ol.layer.VectorTile.prototype.setMinResolution);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'setOpacity',
    ol.layer.VectorTile.prototype.setOpacity);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'setVisible',
    ol.layer.VectorTile.prototype.setVisible);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'setZIndex',
    ol.layer.VectorTile.prototype.setZIndex);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'get',
    ol.layer.VectorTile.prototype.get);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'getKeys',
    ol.layer.VectorTile.prototype.getKeys);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'getProperties',
    ol.layer.VectorTile.prototype.getProperties);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'set',
    ol.layer.VectorTile.prototype.set);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'setProperties',
    ol.layer.VectorTile.prototype.setProperties);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'unset',
    ol.layer.VectorTile.prototype.unset);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'changed',
    ol.layer.VectorTile.prototype.changed);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'dispatchEvent',
    ol.layer.VectorTile.prototype.dispatchEvent);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'getRevision',
    ol.layer.VectorTile.prototype.getRevision);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'on',
    ol.layer.VectorTile.prototype.on);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'once',
    ol.layer.VectorTile.prototype.once);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'un',
    ol.layer.VectorTile.prototype.un);

goog.exportProperty(
    ol.layer.VectorTile.prototype,
    'unByKey',
    ol.layer.VectorTile.prototype.unByKey);

goog.exportProperty(
    ol.interaction.Interaction.prototype,
    'get',
    ol.interaction.Interaction.prototype.get);

goog.exportProperty(
    ol.interaction.Interaction.prototype,
    'getKeys',
    ol.interaction.Interaction.prototype.getKeys);

goog.exportProperty(
    ol.interaction.Interaction.prototype,
    'getProperties',
    ol.interaction.Interaction.prototype.getProperties);

goog.exportProperty(
    ol.interaction.Interaction.prototype,
    'set',
    ol.interaction.Interaction.prototype.set);

goog.exportProperty(
    ol.interaction.Interaction.prototype,
    'setProperties',
    ol.interaction.Interaction.prototype.setProperties);

goog.exportProperty(
    ol.interaction.Interaction.prototype,
    'unset',
    ol.interaction.Interaction.prototype.unset);

goog.exportProperty(
    ol.interaction.Interaction.prototype,
    'changed',
    ol.interaction.Interaction.prototype.changed);

goog.exportProperty(
    ol.interaction.Interaction.prototype,
    'dispatchEvent',
    ol.interaction.Interaction.prototype.dispatchEvent);

goog.exportProperty(
    ol.interaction.Interaction.prototype,
    'getRevision',
    ol.interaction.Interaction.prototype.getRevision);

goog.exportProperty(
    ol.interaction.Interaction.prototype,
    'on',
    ol.interaction.Interaction.prototype.on);

goog.exportProperty(
    ol.interaction.Interaction.prototype,
    'once',
    ol.interaction.Interaction.prototype.once);

goog.exportProperty(
    ol.interaction.Interaction.prototype,
    'un',
    ol.interaction.Interaction.prototype.un);

goog.exportProperty(
    ol.interaction.Interaction.prototype,
    'unByKey',
    ol.interaction.Interaction.prototype.unByKey);

goog.exportProperty(
    ol.interaction.DoubleClickZoom.prototype,
    'getActive',
    ol.interaction.DoubleClickZoom.prototype.getActive);

goog.exportProperty(
    ol.interaction.DoubleClickZoom.prototype,
    'getMap',
    ol.interaction.DoubleClickZoom.prototype.getMap);

goog.exportProperty(
    ol.interaction.DoubleClickZoom.prototype,
    'setActive',
    ol.interaction.DoubleClickZoom.prototype.setActive);

goog.exportProperty(
    ol.interaction.DoubleClickZoom.prototype,
    'get',
    ol.interaction.DoubleClickZoom.prototype.get);

goog.exportProperty(
    ol.interaction.DoubleClickZoom.prototype,
    'getKeys',
    ol.interaction.DoubleClickZoom.prototype.getKeys);

goog.exportProperty(
    ol.interaction.DoubleClickZoom.prototype,
    'getProperties',
    ol.interaction.DoubleClickZoom.prototype.getProperties);

goog.exportProperty(
    ol.interaction.DoubleClickZoom.prototype,
    'set',
    ol.interaction.DoubleClickZoom.prototype.set);

goog.exportProperty(
    ol.interaction.DoubleClickZoom.prototype,
    'setProperties',
    ol.interaction.DoubleClickZoom.prototype.setProperties);

goog.exportProperty(
    ol.interaction.DoubleClickZoom.prototype,
    'unset',
    ol.interaction.DoubleClickZoom.prototype.unset);

goog.exportProperty(
    ol.interaction.DoubleClickZoom.prototype,
    'changed',
    ol.interaction.DoubleClickZoom.prototype.changed);

goog.exportProperty(
    ol.interaction.DoubleClickZoom.prototype,
    'dispatchEvent',
    ol.interaction.DoubleClickZoom.prototype.dispatchEvent);

goog.exportProperty(
    ol.interaction.DoubleClickZoom.prototype,
    'getRevision',
    ol.interaction.DoubleClickZoom.prototype.getRevision);

goog.exportProperty(
    ol.interaction.DoubleClickZoom.prototype,
    'on',
    ol.interaction.DoubleClickZoom.prototype.on);

goog.exportProperty(
    ol.interaction.DoubleClickZoom.prototype,
    'once',
    ol.interaction.DoubleClickZoom.prototype.once);

goog.exportProperty(
    ol.interaction.DoubleClickZoom.prototype,
    'un',
    ol.interaction.DoubleClickZoom.prototype.un);

goog.exportProperty(
    ol.interaction.DoubleClickZoom.prototype,
    'unByKey',
    ol.interaction.DoubleClickZoom.prototype.unByKey);

goog.exportProperty(
    ol.interaction.DragAndDrop.prototype,
    'getActive',
    ol.interaction.DragAndDrop.prototype.getActive);

goog.exportProperty(
    ol.interaction.DragAndDrop.prototype,
    'getMap',
    ol.interaction.DragAndDrop.prototype.getMap);

goog.exportProperty(
    ol.interaction.DragAndDrop.prototype,
    'setActive',
    ol.interaction.DragAndDrop.prototype.setActive);

goog.exportProperty(
    ol.interaction.DragAndDrop.prototype,
    'get',
    ol.interaction.DragAndDrop.prototype.get);

goog.exportProperty(
    ol.interaction.DragAndDrop.prototype,
    'getKeys',
    ol.interaction.DragAndDrop.prototype.getKeys);

goog.exportProperty(
    ol.interaction.DragAndDrop.prototype,
    'getProperties',
    ol.interaction.DragAndDrop.prototype.getProperties);

goog.exportProperty(
    ol.interaction.DragAndDrop.prototype,
    'set',
    ol.interaction.DragAndDrop.prototype.set);

goog.exportProperty(
    ol.interaction.DragAndDrop.prototype,
    'setProperties',
    ol.interaction.DragAndDrop.prototype.setProperties);

goog.exportProperty(
    ol.interaction.DragAndDrop.prototype,
    'unset',
    ol.interaction.DragAndDrop.prototype.unset);

goog.exportProperty(
    ol.interaction.DragAndDrop.prototype,
    'changed',
    ol.interaction.DragAndDrop.prototype.changed);

goog.exportProperty(
    ol.interaction.DragAndDrop.prototype,
    'dispatchEvent',
    ol.interaction.DragAndDrop.prototype.dispatchEvent);

goog.exportProperty(
    ol.interaction.DragAndDrop.prototype,
    'getRevision',
    ol.interaction.DragAndDrop.prototype.getRevision);

goog.exportProperty(
    ol.interaction.DragAndDrop.prototype,
    'on',
    ol.interaction.DragAndDrop.prototype.on);

goog.exportProperty(
    ol.interaction.DragAndDrop.prototype,
    'once',
    ol.interaction.DragAndDrop.prototype.once);

goog.exportProperty(
    ol.interaction.DragAndDrop.prototype,
    'un',
    ol.interaction.DragAndDrop.prototype.un);

goog.exportProperty(
    ol.interaction.DragAndDrop.prototype,
    'unByKey',
    ol.interaction.DragAndDrop.prototype.unByKey);

goog.exportProperty(
    ol.interaction.Pointer.prototype,
    'getActive',
    ol.interaction.Pointer.prototype.getActive);

goog.exportProperty(
    ol.interaction.Pointer.prototype,
    'getMap',
    ol.interaction.Pointer.prototype.getMap);

goog.exportProperty(
    ol.interaction.Pointer.prototype,
    'setActive',
    ol.interaction.Pointer.prototype.setActive);

goog.exportProperty(
    ol.interaction.Pointer.prototype,
    'get',
    ol.interaction.Pointer.prototype.get);

goog.exportProperty(
    ol.interaction.Pointer.prototype,
    'getKeys',
    ol.interaction.Pointer.prototype.getKeys);

goog.exportProperty(
    ol.interaction.Pointer.prototype,
    'getProperties',
    ol.interaction.Pointer.prototype.getProperties);

goog.exportProperty(
    ol.interaction.Pointer.prototype,
    'set',
    ol.interaction.Pointer.prototype.set);

goog.exportProperty(
    ol.interaction.Pointer.prototype,
    'setProperties',
    ol.interaction.Pointer.prototype.setProperties);

goog.exportProperty(
    ol.interaction.Pointer.prototype,
    'unset',
    ol.interaction.Pointer.prototype.unset);

goog.exportProperty(
    ol.interaction.Pointer.prototype,
    'changed',
    ol.interaction.Pointer.prototype.changed);

goog.exportProperty(
    ol.interaction.Pointer.prototype,
    'dispatchEvent',
    ol.interaction.Pointer.prototype.dispatchEvent);

goog.exportProperty(
    ol.interaction.Pointer.prototype,
    'getRevision',
    ol.interaction.Pointer.prototype.getRevision);

goog.exportProperty(
    ol.interaction.Pointer.prototype,
    'on',
    ol.interaction.Pointer.prototype.on);

goog.exportProperty(
    ol.interaction.Pointer.prototype,
    'once',
    ol.interaction.Pointer.prototype.once);

goog.exportProperty(
    ol.interaction.Pointer.prototype,
    'un',
    ol.interaction.Pointer.prototype.un);

goog.exportProperty(
    ol.interaction.Pointer.prototype,
    'unByKey',
    ol.interaction.Pointer.prototype.unByKey);

goog.exportProperty(
    ol.interaction.DragBox.prototype,
    'getActive',
    ol.interaction.DragBox.prototype.getActive);

goog.exportProperty(
    ol.interaction.DragBox.prototype,
    'getMap',
    ol.interaction.DragBox.prototype.getMap);

goog.exportProperty(
    ol.interaction.DragBox.prototype,
    'setActive',
    ol.interaction.DragBox.prototype.setActive);

goog.exportProperty(
    ol.interaction.DragBox.prototype,
    'get',
    ol.interaction.DragBox.prototype.get);

goog.exportProperty(
    ol.interaction.DragBox.prototype,
    'getKeys',
    ol.interaction.DragBox.prototype.getKeys);

goog.exportProperty(
    ol.interaction.DragBox.prototype,
    'getProperties',
    ol.interaction.DragBox.prototype.getProperties);

goog.exportProperty(
    ol.interaction.DragBox.prototype,
    'set',
    ol.interaction.DragBox.prototype.set);

goog.exportProperty(
    ol.interaction.DragBox.prototype,
    'setProperties',
    ol.interaction.DragBox.prototype.setProperties);

goog.exportProperty(
    ol.interaction.DragBox.prototype,
    'unset',
    ol.interaction.DragBox.prototype.unset);

goog.exportProperty(
    ol.interaction.DragBox.prototype,
    'changed',
    ol.interaction.DragBox.prototype.changed);

goog.exportProperty(
    ol.interaction.DragBox.prototype,
    'dispatchEvent',
    ol.interaction.DragBox.prototype.dispatchEvent);

goog.exportProperty(
    ol.interaction.DragBox.prototype,
    'getRevision',
    ol.interaction.DragBox.prototype.getRevision);

goog.exportProperty(
    ol.interaction.DragBox.prototype,
    'on',
    ol.interaction.DragBox.prototype.on);

goog.exportProperty(
    ol.interaction.DragBox.prototype,
    'once',
    ol.interaction.DragBox.prototype.once);

goog.exportProperty(
    ol.interaction.DragBox.prototype,
    'un',
    ol.interaction.DragBox.prototype.un);

goog.exportProperty(
    ol.interaction.DragBox.prototype,
    'unByKey',
    ol.interaction.DragBox.prototype.unByKey);

goog.exportProperty(
    ol.interaction.DragPan.prototype,
    'getActive',
    ol.interaction.DragPan.prototype.getActive);

goog.exportProperty(
    ol.interaction.DragPan.prototype,
    'getMap',
    ol.interaction.DragPan.prototype.getMap);

goog.exportProperty(
    ol.interaction.DragPan.prototype,
    'setActive',
    ol.interaction.DragPan.prototype.setActive);

goog.exportProperty(
    ol.interaction.DragPan.prototype,
    'get',
    ol.interaction.DragPan.prototype.get);

goog.exportProperty(
    ol.interaction.DragPan.prototype,
    'getKeys',
    ol.interaction.DragPan.prototype.getKeys);

goog.exportProperty(
    ol.interaction.DragPan.prototype,
    'getProperties',
    ol.interaction.DragPan.prototype.getProperties);

goog.exportProperty(
    ol.interaction.DragPan.prototype,
    'set',
    ol.interaction.DragPan.prototype.set);

goog.exportProperty(
    ol.interaction.DragPan.prototype,
    'setProperties',
    ol.interaction.DragPan.prototype.setProperties);

goog.exportProperty(
    ol.interaction.DragPan.prototype,
    'unset',
    ol.interaction.DragPan.prototype.unset);

goog.exportProperty(
    ol.interaction.DragPan.prototype,
    'changed',
    ol.interaction.DragPan.prototype.changed);

goog.exportProperty(
    ol.interaction.DragPan.prototype,
    'dispatchEvent',
    ol.interaction.DragPan.prototype.dispatchEvent);

goog.exportProperty(
    ol.interaction.DragPan.prototype,
    'getRevision',
    ol.interaction.DragPan.prototype.getRevision);

goog.exportProperty(
    ol.interaction.DragPan.prototype,
    'on',
    ol.interaction.DragPan.prototype.on);

goog.exportProperty(
    ol.interaction.DragPan.prototype,
    'once',
    ol.interaction.DragPan.prototype.once);

goog.exportProperty(
    ol.interaction.DragPan.prototype,
    'un',
    ol.interaction.DragPan.prototype.un);

goog.exportProperty(
    ol.interaction.DragPan.prototype,
    'unByKey',
    ol.interaction.DragPan.prototype.unByKey);

goog.exportProperty(
    ol.interaction.DragRotateAndZoom.prototype,
    'getActive',
    ol.interaction.DragRotateAndZoom.prototype.getActive);

goog.exportProperty(
    ol.interaction.DragRotateAndZoom.prototype,
    'getMap',
    ol.interaction.DragRotateAndZoom.prototype.getMap);

goog.exportProperty(
    ol.interaction.DragRotateAndZoom.prototype,
    'setActive',
    ol.interaction.DragRotateAndZoom.prototype.setActive);

goog.exportProperty(
    ol.interaction.DragRotateAndZoom.prototype,
    'get',
    ol.interaction.DragRotateAndZoom.prototype.get);

goog.exportProperty(
    ol.interaction.DragRotateAndZoom.prototype,
    'getKeys',
    ol.interaction.DragRotateAndZoom.prototype.getKeys);

goog.exportProperty(
    ol.interaction.DragRotateAndZoom.prototype,
    'getProperties',
    ol.interaction.DragRotateAndZoom.prototype.getProperties);

goog.exportProperty(
    ol.interaction.DragRotateAndZoom.prototype,
    'set',
    ol.interaction.DragRotateAndZoom.prototype.set);

goog.exportProperty(
    ol.interaction.DragRotateAndZoom.prototype,
    'setProperties',
    ol.interaction.DragRotateAndZoom.prototype.setProperties);

goog.exportProperty(
    ol.interaction.DragRotateAndZoom.prototype,
    'unset',
    ol.interaction.DragRotateAndZoom.prototype.unset);

goog.exportProperty(
    ol.interaction.DragRotateAndZoom.prototype,
    'changed',
    ol.interaction.DragRotateAndZoom.prototype.changed);

goog.exportProperty(
    ol.interaction.DragRotateAndZoom.prototype,
    'dispatchEvent',
    ol.interaction.DragRotateAndZoom.prototype.dispatchEvent);

goog.exportProperty(
    ol.interaction.DragRotateAndZoom.prototype,
    'getRevision',
    ol.interaction.DragRotateAndZoom.prototype.getRevision);

goog.exportProperty(
    ol.interaction.DragRotateAndZoom.prototype,
    'on',
    ol.interaction.DragRotateAndZoom.prototype.on);

goog.exportProperty(
    ol.interaction.DragRotateAndZoom.prototype,
    'once',
    ol.interaction.DragRotateAndZoom.prototype.once);

goog.exportProperty(
    ol.interaction.DragRotateAndZoom.prototype,
    'un',
    ol.interaction.DragRotateAndZoom.prototype.un);

goog.exportProperty(
    ol.interaction.DragRotateAndZoom.prototype,
    'unByKey',
    ol.interaction.DragRotateAndZoom.prototype.unByKey);

goog.exportProperty(
    ol.interaction.DragRotate.prototype,
    'getActive',
    ol.interaction.DragRotate.prototype.getActive);

goog.exportProperty(
    ol.interaction.DragRotate.prototype,
    'getMap',
    ol.interaction.DragRotate.prototype.getMap);

goog.exportProperty(
    ol.interaction.DragRotate.prototype,
    'setActive',
    ol.interaction.DragRotate.prototype.setActive);

goog.exportProperty(
    ol.interaction.DragRotate.prototype,
    'get',
    ol.interaction.DragRotate.prototype.get);

goog.exportProperty(
    ol.interaction.DragRotate.prototype,
    'getKeys',
    ol.interaction.DragRotate.prototype.getKeys);

goog.exportProperty(
    ol.interaction.DragRotate.prototype,
    'getProperties',
    ol.interaction.DragRotate.prototype.getProperties);

goog.exportProperty(
    ol.interaction.DragRotate.prototype,
    'set',
    ol.interaction.DragRotate.prototype.set);

goog.exportProperty(
    ol.interaction.DragRotate.prototype,
    'setProperties',
    ol.interaction.DragRotate.prototype.setProperties);

goog.exportProperty(
    ol.interaction.DragRotate.prototype,
    'unset',
    ol.interaction.DragRotate.prototype.unset);

goog.exportProperty(
    ol.interaction.DragRotate.prototype,
    'changed',
    ol.interaction.DragRotate.prototype.changed);

goog.exportProperty(
    ol.interaction.DragRotate.prototype,
    'dispatchEvent',
    ol.interaction.DragRotate.prototype.dispatchEvent);

goog.exportProperty(
    ol.interaction.DragRotate.prototype,
    'getRevision',
    ol.interaction.DragRotate.prototype.getRevision);

goog.exportProperty(
    ol.interaction.DragRotate.prototype,
    'on',
    ol.interaction.DragRotate.prototype.on);

goog.exportProperty(
    ol.interaction.DragRotate.prototype,
    'once',
    ol.interaction.DragRotate.prototype.once);

goog.exportProperty(
    ol.interaction.DragRotate.prototype,
    'un',
    ol.interaction.DragRotate.prototype.un);

goog.exportProperty(
    ol.interaction.DragRotate.prototype,
    'unByKey',
    ol.interaction.DragRotate.prototype.unByKey);

goog.exportProperty(
    ol.interaction.DragZoom.prototype,
    'getGeometry',
    ol.interaction.DragZoom.prototype.getGeometry);

goog.exportProperty(
    ol.interaction.DragZoom.prototype,
    'getActive',
    ol.interaction.DragZoom.prototype.getActive);

goog.exportProperty(
    ol.interaction.DragZoom.prototype,
    'getMap',
    ol.interaction.DragZoom.prototype.getMap);

goog.exportProperty(
    ol.interaction.DragZoom.prototype,
    'setActive',
    ol.interaction.DragZoom.prototype.setActive);

goog.exportProperty(
    ol.interaction.DragZoom.prototype,
    'get',
    ol.interaction.DragZoom.prototype.get);

goog.exportProperty(
    ol.interaction.DragZoom.prototype,
    'getKeys',
    ol.interaction.DragZoom.prototype.getKeys);

goog.exportProperty(
    ol.interaction.DragZoom.prototype,
    'getProperties',
    ol.interaction.DragZoom.prototype.getProperties);

goog.exportProperty(
    ol.interaction.DragZoom.prototype,
    'set',
    ol.interaction.DragZoom.prototype.set);

goog.exportProperty(
    ol.interaction.DragZoom.prototype,
    'setProperties',
    ol.interaction.DragZoom.prototype.setProperties);

goog.exportProperty(
    ol.interaction.DragZoom.prototype,
    'unset',
    ol.interaction.DragZoom.prototype.unset);

goog.exportProperty(
    ol.interaction.DragZoom.prototype,
    'changed',
    ol.interaction.DragZoom.prototype.changed);

goog.exportProperty(
    ol.interaction.DragZoom.prototype,
    'dispatchEvent',
    ol.interaction.DragZoom.prototype.dispatchEvent);

goog.exportProperty(
    ol.interaction.DragZoom.prototype,
    'getRevision',
    ol.interaction.DragZoom.prototype.getRevision);

goog.exportProperty(
    ol.interaction.DragZoom.prototype,
    'on',
    ol.interaction.DragZoom.prototype.on);

goog.exportProperty(
    ol.interaction.DragZoom.prototype,
    'once',
    ol.interaction.DragZoom.prototype.once);

goog.exportProperty(
    ol.interaction.DragZoom.prototype,
    'un',
    ol.interaction.DragZoom.prototype.un);

goog.exportProperty(
    ol.interaction.DragZoom.prototype,
    'unByKey',
    ol.interaction.DragZoom.prototype.unByKey);

goog.exportProperty(
    ol.interaction.Draw.prototype,
    'getActive',
    ol.interaction.Draw.prototype.getActive);

goog.exportProperty(
    ol.interaction.Draw.prototype,
    'getMap',
    ol.interaction.Draw.prototype.getMap);

goog.exportProperty(
    ol.interaction.Draw.prototype,
    'setActive',
    ol.interaction.Draw.prototype.setActive);

goog.exportProperty(
    ol.interaction.Draw.prototype,
    'get',
    ol.interaction.Draw.prototype.get);

goog.exportProperty(
    ol.interaction.Draw.prototype,
    'getKeys',
    ol.interaction.Draw.prototype.getKeys);

goog.exportProperty(
    ol.interaction.Draw.prototype,
    'getProperties',
    ol.interaction.Draw.prototype.getProperties);

goog.exportProperty(
    ol.interaction.Draw.prototype,
    'set',
    ol.interaction.Draw.prototype.set);

goog.exportProperty(
    ol.interaction.Draw.prototype,
    'setProperties',
    ol.interaction.Draw.prototype.setProperties);

goog.exportProperty(
    ol.interaction.Draw.prototype,
    'unset',
    ol.interaction.Draw.prototype.unset);

goog.exportProperty(
    ol.interaction.Draw.prototype,
    'changed',
    ol.interaction.Draw.prototype.changed);

goog.exportProperty(
    ol.interaction.Draw.prototype,
    'dispatchEvent',
    ol.interaction.Draw.prototype.dispatchEvent);

goog.exportProperty(
    ol.interaction.Draw.prototype,
    'getRevision',
    ol.interaction.Draw.prototype.getRevision);

goog.exportProperty(
    ol.interaction.Draw.prototype,
    'on',
    ol.interaction.Draw.prototype.on);

goog.exportProperty(
    ol.interaction.Draw.prototype,
    'once',
    ol.interaction.Draw.prototype.once);

goog.exportProperty(
    ol.interaction.Draw.prototype,
    'un',
    ol.interaction.Draw.prototype.un);

goog.exportProperty(
    ol.interaction.Draw.prototype,
    'unByKey',
    ol.interaction.Draw.prototype.unByKey);

goog.exportProperty(
    ol.interaction.KeyboardPan.prototype,
    'getActive',
    ol.interaction.KeyboardPan.prototype.getActive);

goog.exportProperty(
    ol.interaction.KeyboardPan.prototype,
    'getMap',
    ol.interaction.KeyboardPan.prototype.getMap);

goog.exportProperty(
    ol.interaction.KeyboardPan.prototype,
    'setActive',
    ol.interaction.KeyboardPan.prototype.setActive);

goog.exportProperty(
    ol.interaction.KeyboardPan.prototype,
    'get',
    ol.interaction.KeyboardPan.prototype.get);

goog.exportProperty(
    ol.interaction.KeyboardPan.prototype,
    'getKeys',
    ol.interaction.KeyboardPan.prototype.getKeys);

goog.exportProperty(
    ol.interaction.KeyboardPan.prototype,
    'getProperties',
    ol.interaction.KeyboardPan.prototype.getProperties);

goog.exportProperty(
    ol.interaction.KeyboardPan.prototype,
    'set',
    ol.interaction.KeyboardPan.prototype.set);

goog.exportProperty(
    ol.interaction.KeyboardPan.prototype,
    'setProperties',
    ol.interaction.KeyboardPan.prototype.setProperties);

goog.exportProperty(
    ol.interaction.KeyboardPan.prototype,
    'unset',
    ol.interaction.KeyboardPan.prototype.unset);

goog.exportProperty(
    ol.interaction.KeyboardPan.prototype,
    'changed',
    ol.interaction.KeyboardPan.prototype.changed);

goog.exportProperty(
    ol.interaction.KeyboardPan.prototype,
    'dispatchEvent',
    ol.interaction.KeyboardPan.prototype.dispatchEvent);

goog.exportProperty(
    ol.interaction.KeyboardPan.prototype,
    'getRevision',
    ol.interaction.KeyboardPan.prototype.getRevision);

goog.exportProperty(
    ol.interaction.KeyboardPan.prototype,
    'on',
    ol.interaction.KeyboardPan.prototype.on);

goog.exportProperty(
    ol.interaction.KeyboardPan.prototype,
    'once',
    ol.interaction.KeyboardPan.prototype.once);

goog.exportProperty(
    ol.interaction.KeyboardPan.prototype,
    'un',
    ol.interaction.KeyboardPan.prototype.un);

goog.exportProperty(
    ol.interaction.KeyboardPan.prototype,
    'unByKey',
    ol.interaction.KeyboardPan.prototype.unByKey);

goog.exportProperty(
    ol.interaction.KeyboardZoom.prototype,
    'getActive',
    ol.interaction.KeyboardZoom.prototype.getActive);

goog.exportProperty(
    ol.interaction.KeyboardZoom.prototype,
    'getMap',
    ol.interaction.KeyboardZoom.prototype.getMap);

goog.exportProperty(
    ol.interaction.KeyboardZoom.prototype,
    'setActive',
    ol.interaction.KeyboardZoom.prototype.setActive);

goog.exportProperty(
    ol.interaction.KeyboardZoom.prototype,
    'get',
    ol.interaction.KeyboardZoom.prototype.get);

goog.exportProperty(
    ol.interaction.KeyboardZoom.prototype,
    'getKeys',
    ol.interaction.KeyboardZoom.prototype.getKeys);

goog.exportProperty(
    ol.interaction.KeyboardZoom.prototype,
    'getProperties',
    ol.interaction.KeyboardZoom.prototype.getProperties);

goog.exportProperty(
    ol.interaction.KeyboardZoom.prototype,
    'set',
    ol.interaction.KeyboardZoom.prototype.set);

goog.exportProperty(
    ol.interaction.KeyboardZoom.prototype,
    'setProperties',
    ol.interaction.KeyboardZoom.prototype.setProperties);

goog.exportProperty(
    ol.interaction.KeyboardZoom.prototype,
    'unset',
    ol.interaction.KeyboardZoom.prototype.unset);

goog.exportProperty(
    ol.interaction.KeyboardZoom.prototype,
    'changed',
    ol.interaction.KeyboardZoom.prototype.changed);

goog.exportProperty(
    ol.interaction.KeyboardZoom.prototype,
    'dispatchEvent',
    ol.interaction.KeyboardZoom.prototype.dispatchEvent);

goog.exportProperty(
    ol.interaction.KeyboardZoom.prototype,
    'getRevision',
    ol.interaction.KeyboardZoom.prototype.getRevision);

goog.exportProperty(
    ol.interaction.KeyboardZoom.prototype,
    'on',
    ol.interaction.KeyboardZoom.prototype.on);

goog.exportProperty(
    ol.interaction.KeyboardZoom.prototype,
    'once',
    ol.interaction.KeyboardZoom.prototype.once);

goog.exportProperty(
    ol.interaction.KeyboardZoom.prototype,
    'un',
    ol.interaction.KeyboardZoom.prototype.un);

goog.exportProperty(
    ol.interaction.KeyboardZoom.prototype,
    'unByKey',
    ol.interaction.KeyboardZoom.prototype.unByKey);

goog.exportProperty(
    ol.interaction.Modify.prototype,
    'getActive',
    ol.interaction.Modify.prototype.getActive);

goog.exportProperty(
    ol.interaction.Modify.prototype,
    'getMap',
    ol.interaction.Modify.prototype.getMap);

goog.exportProperty(
    ol.interaction.Modify.prototype,
    'setActive',
    ol.interaction.Modify.prototype.setActive);

goog.exportProperty(
    ol.interaction.Modify.prototype,
    'get',
    ol.interaction.Modify.prototype.get);

goog.exportProperty(
    ol.interaction.Modify.prototype,
    'getKeys',
    ol.interaction.Modify.prototype.getKeys);

goog.exportProperty(
    ol.interaction.Modify.prototype,
    'getProperties',
    ol.interaction.Modify.prototype.getProperties);

goog.exportProperty(
    ol.interaction.Modify.prototype,
    'set',
    ol.interaction.Modify.prototype.set);

goog.exportProperty(
    ol.interaction.Modify.prototype,
    'setProperties',
    ol.interaction.Modify.prototype.setProperties);

goog.exportProperty(
    ol.interaction.Modify.prototype,
    'unset',
    ol.interaction.Modify.prototype.unset);

goog.exportProperty(
    ol.interaction.Modify.prototype,
    'changed',
    ol.interaction.Modify.prototype.changed);

goog.exportProperty(
    ol.interaction.Modify.prototype,
    'dispatchEvent',
    ol.interaction.Modify.prototype.dispatchEvent);

goog.exportProperty(
    ol.interaction.Modify.prototype,
    'getRevision',
    ol.interaction.Modify.prototype.getRevision);

goog.exportProperty(
    ol.interaction.Modify.prototype,
    'on',
    ol.interaction.Modify.prototype.on);

goog.exportProperty(
    ol.interaction.Modify.prototype,
    'once',
    ol.interaction.Modify.prototype.once);

goog.exportProperty(
    ol.interaction.Modify.prototype,
    'un',
    ol.interaction.Modify.prototype.un);

goog.exportProperty(
    ol.interaction.Modify.prototype,
    'unByKey',
    ol.interaction.Modify.prototype.unByKey);

goog.exportProperty(
    ol.interaction.MouseWheelZoom.prototype,
    'getActive',
    ol.interaction.MouseWheelZoom.prototype.getActive);

goog.exportProperty(
    ol.interaction.MouseWheelZoom.prototype,
    'getMap',
    ol.interaction.MouseWheelZoom.prototype.getMap);

goog.exportProperty(
    ol.interaction.MouseWheelZoom.prototype,
    'setActive',
    ol.interaction.MouseWheelZoom.prototype.setActive);

goog.exportProperty(
    ol.interaction.MouseWheelZoom.prototype,
    'get',
    ol.interaction.MouseWheelZoom.prototype.get);

goog.exportProperty(
    ol.interaction.MouseWheelZoom.prototype,
    'getKeys',
    ol.interaction.MouseWheelZoom.prototype.getKeys);

goog.exportProperty(
    ol.interaction.MouseWheelZoom.prototype,
    'getProperties',
    ol.interaction.MouseWheelZoom.prototype.getProperties);

goog.exportProperty(
    ol.interaction.MouseWheelZoom.prototype,
    'set',
    ol.interaction.MouseWheelZoom.prototype.set);

goog.exportProperty(
    ol.interaction.MouseWheelZoom.prototype,
    'setProperties',
    ol.interaction.MouseWheelZoom.prototype.setProperties);

goog.exportProperty(
    ol.interaction.MouseWheelZoom.prototype,
    'unset',
    ol.interaction.MouseWheelZoom.prototype.unset);

goog.exportProperty(
    ol.interaction.MouseWheelZoom.prototype,
    'changed',
    ol.interaction.MouseWheelZoom.prototype.changed);

goog.exportProperty(
    ol.interaction.MouseWheelZoom.prototype,
    'dispatchEvent',
    ol.interaction.MouseWheelZoom.prototype.dispatchEvent);

goog.exportProperty(
    ol.interaction.MouseWheelZoom.prototype,
    'getRevision',
    ol.interaction.MouseWheelZoom.prototype.getRevision);

goog.exportProperty(
    ol.interaction.MouseWheelZoom.prototype,
    'on',
    ol.interaction.MouseWheelZoom.prototype.on);

goog.exportProperty(
    ol.interaction.MouseWheelZoom.prototype,
    'once',
    ol.interaction.MouseWheelZoom.prototype.once);

goog.exportProperty(
    ol.interaction.MouseWheelZoom.prototype,
    'un',
    ol.interaction.MouseWheelZoom.prototype.un);

goog.exportProperty(
    ol.interaction.MouseWheelZoom.prototype,
    'unByKey',
    ol.interaction.MouseWheelZoom.prototype.unByKey);

goog.exportProperty(
    ol.interaction.PinchRotate.prototype,
    'getActive',
    ol.interaction.PinchRotate.prototype.getActive);

goog.exportProperty(
    ol.interaction.PinchRotate.prototype,
    'getMap',
    ol.interaction.PinchRotate.prototype.getMap);

goog.exportProperty(
    ol.interaction.PinchRotate.prototype,
    'setActive',
    ol.interaction.PinchRotate.prototype.setActive);

goog.exportProperty(
    ol.interaction.PinchRotate.prototype,
    'get',
    ol.interaction.PinchRotate.prototype.get);

goog.exportProperty(
    ol.interaction.PinchRotate.prototype,
    'getKeys',
    ol.interaction.PinchRotate.prototype.getKeys);

goog.exportProperty(
    ol.interaction.PinchRotate.prototype,
    'getProperties',
    ol.interaction.PinchRotate.prototype.getProperties);

goog.exportProperty(
    ol.interaction.PinchRotate.prototype,
    'set',
    ol.interaction.PinchRotate.prototype.set);

goog.exportProperty(
    ol.interaction.PinchRotate.prototype,
    'setProperties',
    ol.interaction.PinchRotate.prototype.setProperties);

goog.exportProperty(
    ol.interaction.PinchRotate.prototype,
    'unset',
    ol.interaction.PinchRotate.prototype.unset);

goog.exportProperty(
    ol.interaction.PinchRotate.prototype,
    'changed',
    ol.interaction.PinchRotate.prototype.changed);

goog.exportProperty(
    ol.interaction.PinchRotate.prototype,
    'dispatchEvent',
    ol.interaction.PinchRotate.prototype.dispatchEvent);

goog.exportProperty(
    ol.interaction.PinchRotate.prototype,
    'getRevision',
    ol.interaction.PinchRotate.prototype.getRevision);

goog.exportProperty(
    ol.interaction.PinchRotate.prototype,
    'on',
    ol.interaction.PinchRotate.prototype.on);

goog.exportProperty(
    ol.interaction.PinchRotate.prototype,
    'once',
    ol.interaction.PinchRotate.prototype.once);

goog.exportProperty(
    ol.interaction.PinchRotate.prototype,
    'un',
    ol.interaction.PinchRotate.prototype.un);

goog.exportProperty(
    ol.interaction.PinchRotate.prototype,
    'unByKey',
    ol.interaction.PinchRotate.prototype.unByKey);

goog.exportProperty(
    ol.interaction.PinchZoom.prototype,
    'getActive',
    ol.interaction.PinchZoom.prototype.getActive);

goog.exportProperty(
    ol.interaction.PinchZoom.prototype,
    'getMap',
    ol.interaction.PinchZoom.prototype.getMap);

goog.exportProperty(
    ol.interaction.PinchZoom.prototype,
    'setActive',
    ol.interaction.PinchZoom.prototype.setActive);

goog.exportProperty(
    ol.interaction.PinchZoom.prototype,
    'get',
    ol.interaction.PinchZoom.prototype.get);

goog.exportProperty(
    ol.interaction.PinchZoom.prototype,
    'getKeys',
    ol.interaction.PinchZoom.prototype.getKeys);

goog.exportProperty(
    ol.interaction.PinchZoom.prototype,
    'getProperties',
    ol.interaction.PinchZoom.prototype.getProperties);

goog.exportProperty(
    ol.interaction.PinchZoom.prototype,
    'set',
    ol.interaction.PinchZoom.prototype.set);

goog.exportProperty(
    ol.interaction.PinchZoom.prototype,
    'setProperties',
    ol.interaction.PinchZoom.prototype.setProperties);

goog.exportProperty(
    ol.interaction.PinchZoom.prototype,
    'unset',
    ol.interaction.PinchZoom.prototype.unset);

goog.exportProperty(
    ol.interaction.PinchZoom.prototype,
    'changed',
    ol.interaction.PinchZoom.prototype.changed);

goog.exportProperty(
    ol.interaction.PinchZoom.prototype,
    'dispatchEvent',
    ol.interaction.PinchZoom.prototype.dispatchEvent);

goog.exportProperty(
    ol.interaction.PinchZoom.prototype,
    'getRevision',
    ol.interaction.PinchZoom.prototype.getRevision);

goog.exportProperty(
    ol.interaction.PinchZoom.prototype,
    'on',
    ol.interaction.PinchZoom.prototype.on);

goog.exportProperty(
    ol.interaction.PinchZoom.prototype,
    'once',
    ol.interaction.PinchZoom.prototype.once);

goog.exportProperty(
    ol.interaction.PinchZoom.prototype,
    'un',
    ol.interaction.PinchZoom.prototype.un);

goog.exportProperty(
    ol.interaction.PinchZoom.prototype,
    'unByKey',
    ol.interaction.PinchZoom.prototype.unByKey);

goog.exportProperty(
    ol.interaction.Select.prototype,
    'getActive',
    ol.interaction.Select.prototype.getActive);

goog.exportProperty(
    ol.interaction.Select.prototype,
    'getMap',
    ol.interaction.Select.prototype.getMap);

goog.exportProperty(
    ol.interaction.Select.prototype,
    'setActive',
    ol.interaction.Select.prototype.setActive);

goog.exportProperty(
    ol.interaction.Select.prototype,
    'get',
    ol.interaction.Select.prototype.get);

goog.exportProperty(
    ol.interaction.Select.prototype,
    'getKeys',
    ol.interaction.Select.prototype.getKeys);

goog.exportProperty(
    ol.interaction.Select.prototype,
    'getProperties',
    ol.interaction.Select.prototype.getProperties);

goog.exportProperty(
    ol.interaction.Select.prototype,
    'set',
    ol.interaction.Select.prototype.set);

goog.exportProperty(
    ol.interaction.Select.prototype,
    'setProperties',
    ol.interaction.Select.prototype.setProperties);

goog.exportProperty(
    ol.interaction.Select.prototype,
    'unset',
    ol.interaction.Select.prototype.unset);

goog.exportProperty(
    ol.interaction.Select.prototype,
    'changed',
    ol.interaction.Select.prototype.changed);

goog.exportProperty(
    ol.interaction.Select.prototype,
    'dispatchEvent',
    ol.interaction.Select.prototype.dispatchEvent);

goog.exportProperty(
    ol.interaction.Select.prototype,
    'getRevision',
    ol.interaction.Select.prototype.getRevision);

goog.exportProperty(
    ol.interaction.Select.prototype,
    'on',
    ol.interaction.Select.prototype.on);

goog.exportProperty(
    ol.interaction.Select.prototype,
    'once',
    ol.interaction.Select.prototype.once);

goog.exportProperty(
    ol.interaction.Select.prototype,
    'un',
    ol.interaction.Select.prototype.un);

goog.exportProperty(
    ol.interaction.Select.prototype,
    'unByKey',
    ol.interaction.Select.prototype.unByKey);

goog.exportProperty(
    ol.interaction.Snap.prototype,
    'getActive',
    ol.interaction.Snap.prototype.getActive);

goog.exportProperty(
    ol.interaction.Snap.prototype,
    'getMap',
    ol.interaction.Snap.prototype.getMap);

goog.exportProperty(
    ol.interaction.Snap.prototype,
    'setActive',
    ol.interaction.Snap.prototype.setActive);

goog.exportProperty(
    ol.interaction.Snap.prototype,
    'get',
    ol.interaction.Snap.prototype.get);

goog.exportProperty(
    ol.interaction.Snap.prototype,
    'getKeys',
    ol.interaction.Snap.prototype.getKeys);

goog.exportProperty(
    ol.interaction.Snap.prototype,
    'getProperties',
    ol.interaction.Snap.prototype.getProperties);

goog.exportProperty(
    ol.interaction.Snap.prototype,
    'set',
    ol.interaction.Snap.prototype.set);

goog.exportProperty(
    ol.interaction.Snap.prototype,
    'setProperties',
    ol.interaction.Snap.prototype.setProperties);

goog.exportProperty(
    ol.interaction.Snap.prototype,
    'unset',
    ol.interaction.Snap.prototype.unset);

goog.exportProperty(
    ol.interaction.Snap.prototype,
    'changed',
    ol.interaction.Snap.prototype.changed);

goog.exportProperty(
    ol.interaction.Snap.prototype,
    'dispatchEvent',
    ol.interaction.Snap.prototype.dispatchEvent);

goog.exportProperty(
    ol.interaction.Snap.prototype,
    'getRevision',
    ol.interaction.Snap.prototype.getRevision);

goog.exportProperty(
    ol.interaction.Snap.prototype,
    'on',
    ol.interaction.Snap.prototype.on);

goog.exportProperty(
    ol.interaction.Snap.prototype,
    'once',
    ol.interaction.Snap.prototype.once);

goog.exportProperty(
    ol.interaction.Snap.prototype,
    'un',
    ol.interaction.Snap.prototype.un);

goog.exportProperty(
    ol.interaction.Snap.prototype,
    'unByKey',
    ol.interaction.Snap.prototype.unByKey);

goog.exportProperty(
    ol.interaction.Translate.prototype,
    'getActive',
    ol.interaction.Translate.prototype.getActive);

goog.exportProperty(
    ol.interaction.Translate.prototype,
    'getMap',
    ol.interaction.Translate.prototype.getMap);

goog.exportProperty(
    ol.interaction.Translate.prototype,
    'setActive',
    ol.interaction.Translate.prototype.setActive);

goog.exportProperty(
    ol.interaction.Translate.prototype,
    'get',
    ol.interaction.Translate.prototype.get);

goog.exportProperty(
    ol.interaction.Translate.prototype,
    'getKeys',
    ol.interaction.Translate.prototype.getKeys);

goog.exportProperty(
    ol.interaction.Translate.prototype,
    'getProperties',
    ol.interaction.Translate.prototype.getProperties);

goog.exportProperty(
    ol.interaction.Translate.prototype,
    'set',
    ol.interaction.Translate.prototype.set);

goog.exportProperty(
    ol.interaction.Translate.prototype,
    'setProperties',
    ol.interaction.Translate.prototype.setProperties);

goog.exportProperty(
    ol.interaction.Translate.prototype,
    'unset',
    ol.interaction.Translate.prototype.unset);

goog.exportProperty(
    ol.interaction.Translate.prototype,
    'changed',
    ol.interaction.Translate.prototype.changed);

goog.exportProperty(
    ol.interaction.Translate.prototype,
    'dispatchEvent',
    ol.interaction.Translate.prototype.dispatchEvent);

goog.exportProperty(
    ol.interaction.Translate.prototype,
    'getRevision',
    ol.interaction.Translate.prototype.getRevision);

goog.exportProperty(
    ol.interaction.Translate.prototype,
    'on',
    ol.interaction.Translate.prototype.on);

goog.exportProperty(
    ol.interaction.Translate.prototype,
    'once',
    ol.interaction.Translate.prototype.once);

goog.exportProperty(
    ol.interaction.Translate.prototype,
    'un',
    ol.interaction.Translate.prototype.un);

goog.exportProperty(
    ol.interaction.Translate.prototype,
    'unByKey',
    ol.interaction.Translate.prototype.unByKey);

goog.exportProperty(
    ol.geom.Geometry.prototype,
    'get',
    ol.geom.Geometry.prototype.get);

goog.exportProperty(
    ol.geom.Geometry.prototype,
    'getKeys',
    ol.geom.Geometry.prototype.getKeys);

goog.exportProperty(
    ol.geom.Geometry.prototype,
    'getProperties',
    ol.geom.Geometry.prototype.getProperties);

goog.exportProperty(
    ol.geom.Geometry.prototype,
    'set',
    ol.geom.Geometry.prototype.set);

goog.exportProperty(
    ol.geom.Geometry.prototype,
    'setProperties',
    ol.geom.Geometry.prototype.setProperties);

goog.exportProperty(
    ol.geom.Geometry.prototype,
    'unset',
    ol.geom.Geometry.prototype.unset);

goog.exportProperty(
    ol.geom.Geometry.prototype,
    'changed',
    ol.geom.Geometry.prototype.changed);

goog.exportProperty(
    ol.geom.Geometry.prototype,
    'dispatchEvent',
    ol.geom.Geometry.prototype.dispatchEvent);

goog.exportProperty(
    ol.geom.Geometry.prototype,
    'getRevision',
    ol.geom.Geometry.prototype.getRevision);

goog.exportProperty(
    ol.geom.Geometry.prototype,
    'on',
    ol.geom.Geometry.prototype.on);

goog.exportProperty(
    ol.geom.Geometry.prototype,
    'once',
    ol.geom.Geometry.prototype.once);

goog.exportProperty(
    ol.geom.Geometry.prototype,
    'un',
    ol.geom.Geometry.prototype.un);

goog.exportProperty(
    ol.geom.Geometry.prototype,
    'unByKey',
    ol.geom.Geometry.prototype.unByKey);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'getClosestPoint',
    ol.geom.SimpleGeometry.prototype.getClosestPoint);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'getExtent',
    ol.geom.SimpleGeometry.prototype.getExtent);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'simplify',
    ol.geom.SimpleGeometry.prototype.simplify);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'transform',
    ol.geom.SimpleGeometry.prototype.transform);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'get',
    ol.geom.SimpleGeometry.prototype.get);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'getKeys',
    ol.geom.SimpleGeometry.prototype.getKeys);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'getProperties',
    ol.geom.SimpleGeometry.prototype.getProperties);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'set',
    ol.geom.SimpleGeometry.prototype.set);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'setProperties',
    ol.geom.SimpleGeometry.prototype.setProperties);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'unset',
    ol.geom.SimpleGeometry.prototype.unset);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'changed',
    ol.geom.SimpleGeometry.prototype.changed);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'dispatchEvent',
    ol.geom.SimpleGeometry.prototype.dispatchEvent);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'getRevision',
    ol.geom.SimpleGeometry.prototype.getRevision);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'on',
    ol.geom.SimpleGeometry.prototype.on);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'once',
    ol.geom.SimpleGeometry.prototype.once);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'un',
    ol.geom.SimpleGeometry.prototype.un);

goog.exportProperty(
    ol.geom.SimpleGeometry.prototype,
    'unByKey',
    ol.geom.SimpleGeometry.prototype.unByKey);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'getFirstCoordinate',
    ol.geom.Circle.prototype.getFirstCoordinate);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'getLastCoordinate',
    ol.geom.Circle.prototype.getLastCoordinate);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'getLayout',
    ol.geom.Circle.prototype.getLayout);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'getClosestPoint',
    ol.geom.Circle.prototype.getClosestPoint);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'getExtent',
    ol.geom.Circle.prototype.getExtent);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'simplify',
    ol.geom.Circle.prototype.simplify);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'get',
    ol.geom.Circle.prototype.get);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'getKeys',
    ol.geom.Circle.prototype.getKeys);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'getProperties',
    ol.geom.Circle.prototype.getProperties);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'set',
    ol.geom.Circle.prototype.set);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'setProperties',
    ol.geom.Circle.prototype.setProperties);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'unset',
    ol.geom.Circle.prototype.unset);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'changed',
    ol.geom.Circle.prototype.changed);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'dispatchEvent',
    ol.geom.Circle.prototype.dispatchEvent);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'getRevision',
    ol.geom.Circle.prototype.getRevision);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'on',
    ol.geom.Circle.prototype.on);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'once',
    ol.geom.Circle.prototype.once);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'un',
    ol.geom.Circle.prototype.un);

goog.exportProperty(
    ol.geom.Circle.prototype,
    'unByKey',
    ol.geom.Circle.prototype.unByKey);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'getClosestPoint',
    ol.geom.GeometryCollection.prototype.getClosestPoint);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'getExtent',
    ol.geom.GeometryCollection.prototype.getExtent);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'simplify',
    ol.geom.GeometryCollection.prototype.simplify);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'transform',
    ol.geom.GeometryCollection.prototype.transform);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'get',
    ol.geom.GeometryCollection.prototype.get);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'getKeys',
    ol.geom.GeometryCollection.prototype.getKeys);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'getProperties',
    ol.geom.GeometryCollection.prototype.getProperties);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'set',
    ol.geom.GeometryCollection.prototype.set);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'setProperties',
    ol.geom.GeometryCollection.prototype.setProperties);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'unset',
    ol.geom.GeometryCollection.prototype.unset);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'changed',
    ol.geom.GeometryCollection.prototype.changed);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'dispatchEvent',
    ol.geom.GeometryCollection.prototype.dispatchEvent);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'getRevision',
    ol.geom.GeometryCollection.prototype.getRevision);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'on',
    ol.geom.GeometryCollection.prototype.on);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'once',
    ol.geom.GeometryCollection.prototype.once);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'un',
    ol.geom.GeometryCollection.prototype.un);

goog.exportProperty(
    ol.geom.GeometryCollection.prototype,
    'unByKey',
    ol.geom.GeometryCollection.prototype.unByKey);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'getFirstCoordinate',
    ol.geom.LinearRing.prototype.getFirstCoordinate);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'getLastCoordinate',
    ol.geom.LinearRing.prototype.getLastCoordinate);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'getLayout',
    ol.geom.LinearRing.prototype.getLayout);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'getClosestPoint',
    ol.geom.LinearRing.prototype.getClosestPoint);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'getExtent',
    ol.geom.LinearRing.prototype.getExtent);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'simplify',
    ol.geom.LinearRing.prototype.simplify);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'transform',
    ol.geom.LinearRing.prototype.transform);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'get',
    ol.geom.LinearRing.prototype.get);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'getKeys',
    ol.geom.LinearRing.prototype.getKeys);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'getProperties',
    ol.geom.LinearRing.prototype.getProperties);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'set',
    ol.geom.LinearRing.prototype.set);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'setProperties',
    ol.geom.LinearRing.prototype.setProperties);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'unset',
    ol.geom.LinearRing.prototype.unset);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'changed',
    ol.geom.LinearRing.prototype.changed);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'dispatchEvent',
    ol.geom.LinearRing.prototype.dispatchEvent);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'getRevision',
    ol.geom.LinearRing.prototype.getRevision);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'on',
    ol.geom.LinearRing.prototype.on);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'once',
    ol.geom.LinearRing.prototype.once);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'un',
    ol.geom.LinearRing.prototype.un);

goog.exportProperty(
    ol.geom.LinearRing.prototype,
    'unByKey',
    ol.geom.LinearRing.prototype.unByKey);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'getFirstCoordinate',
    ol.geom.LineString.prototype.getFirstCoordinate);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'getLastCoordinate',
    ol.geom.LineString.prototype.getLastCoordinate);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'getLayout',
    ol.geom.LineString.prototype.getLayout);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'getClosestPoint',
    ol.geom.LineString.prototype.getClosestPoint);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'getExtent',
    ol.geom.LineString.prototype.getExtent);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'simplify',
    ol.geom.LineString.prototype.simplify);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'transform',
    ol.geom.LineString.prototype.transform);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'get',
    ol.geom.LineString.prototype.get);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'getKeys',
    ol.geom.LineString.prototype.getKeys);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'getProperties',
    ol.geom.LineString.prototype.getProperties);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'set',
    ol.geom.LineString.prototype.set);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'setProperties',
    ol.geom.LineString.prototype.setProperties);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'unset',
    ol.geom.LineString.prototype.unset);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'changed',
    ol.geom.LineString.prototype.changed);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'dispatchEvent',
    ol.geom.LineString.prototype.dispatchEvent);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'getRevision',
    ol.geom.LineString.prototype.getRevision);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'on',
    ol.geom.LineString.prototype.on);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'once',
    ol.geom.LineString.prototype.once);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'un',
    ol.geom.LineString.prototype.un);

goog.exportProperty(
    ol.geom.LineString.prototype,
    'unByKey',
    ol.geom.LineString.prototype.unByKey);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'getFirstCoordinate',
    ol.geom.MultiLineString.prototype.getFirstCoordinate);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'getLastCoordinate',
    ol.geom.MultiLineString.prototype.getLastCoordinate);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'getLayout',
    ol.geom.MultiLineString.prototype.getLayout);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'getClosestPoint',
    ol.geom.MultiLineString.prototype.getClosestPoint);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'getExtent',
    ol.geom.MultiLineString.prototype.getExtent);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'simplify',
    ol.geom.MultiLineString.prototype.simplify);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'transform',
    ol.geom.MultiLineString.prototype.transform);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'get',
    ol.geom.MultiLineString.prototype.get);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'getKeys',
    ol.geom.MultiLineString.prototype.getKeys);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'getProperties',
    ol.geom.MultiLineString.prototype.getProperties);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'set',
    ol.geom.MultiLineString.prototype.set);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'setProperties',
    ol.geom.MultiLineString.prototype.setProperties);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'unset',
    ol.geom.MultiLineString.prototype.unset);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'changed',
    ol.geom.MultiLineString.prototype.changed);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'dispatchEvent',
    ol.geom.MultiLineString.prototype.dispatchEvent);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'getRevision',
    ol.geom.MultiLineString.prototype.getRevision);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'on',
    ol.geom.MultiLineString.prototype.on);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'once',
    ol.geom.MultiLineString.prototype.once);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'un',
    ol.geom.MultiLineString.prototype.un);

goog.exportProperty(
    ol.geom.MultiLineString.prototype,
    'unByKey',
    ol.geom.MultiLineString.prototype.unByKey);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'getFirstCoordinate',
    ol.geom.MultiPoint.prototype.getFirstCoordinate);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'getLastCoordinate',
    ol.geom.MultiPoint.prototype.getLastCoordinate);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'getLayout',
    ol.geom.MultiPoint.prototype.getLayout);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'getClosestPoint',
    ol.geom.MultiPoint.prototype.getClosestPoint);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'getExtent',
    ol.geom.MultiPoint.prototype.getExtent);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'simplify',
    ol.geom.MultiPoint.prototype.simplify);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'transform',
    ol.geom.MultiPoint.prototype.transform);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'get',
    ol.geom.MultiPoint.prototype.get);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'getKeys',
    ol.geom.MultiPoint.prototype.getKeys);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'getProperties',
    ol.geom.MultiPoint.prototype.getProperties);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'set',
    ol.geom.MultiPoint.prototype.set);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'setProperties',
    ol.geom.MultiPoint.prototype.setProperties);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'unset',
    ol.geom.MultiPoint.prototype.unset);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'changed',
    ol.geom.MultiPoint.prototype.changed);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'dispatchEvent',
    ol.geom.MultiPoint.prototype.dispatchEvent);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'getRevision',
    ol.geom.MultiPoint.prototype.getRevision);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'on',
    ol.geom.MultiPoint.prototype.on);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'once',
    ol.geom.MultiPoint.prototype.once);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'un',
    ol.geom.MultiPoint.prototype.un);

goog.exportProperty(
    ol.geom.MultiPoint.prototype,
    'unByKey',
    ol.geom.MultiPoint.prototype.unByKey);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'getFirstCoordinate',
    ol.geom.MultiPolygon.prototype.getFirstCoordinate);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'getLastCoordinate',
    ol.geom.MultiPolygon.prototype.getLastCoordinate);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'getLayout',
    ol.geom.MultiPolygon.prototype.getLayout);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'getClosestPoint',
    ol.geom.MultiPolygon.prototype.getClosestPoint);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'getExtent',
    ol.geom.MultiPolygon.prototype.getExtent);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'simplify',
    ol.geom.MultiPolygon.prototype.simplify);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'transform',
    ol.geom.MultiPolygon.prototype.transform);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'get',
    ol.geom.MultiPolygon.prototype.get);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'getKeys',
    ol.geom.MultiPolygon.prototype.getKeys);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'getProperties',
    ol.geom.MultiPolygon.prototype.getProperties);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'set',
    ol.geom.MultiPolygon.prototype.set);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'setProperties',
    ol.geom.MultiPolygon.prototype.setProperties);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'unset',
    ol.geom.MultiPolygon.prototype.unset);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'changed',
    ol.geom.MultiPolygon.prototype.changed);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'dispatchEvent',
    ol.geom.MultiPolygon.prototype.dispatchEvent);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'getRevision',
    ol.geom.MultiPolygon.prototype.getRevision);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'on',
    ol.geom.MultiPolygon.prototype.on);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'once',
    ol.geom.MultiPolygon.prototype.once);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'un',
    ol.geom.MultiPolygon.prototype.un);

goog.exportProperty(
    ol.geom.MultiPolygon.prototype,
    'unByKey',
    ol.geom.MultiPolygon.prototype.unByKey);

goog.exportProperty(
    ol.geom.Point.prototype,
    'getFirstCoordinate',
    ol.geom.Point.prototype.getFirstCoordinate);

goog.exportProperty(
    ol.geom.Point.prototype,
    'getLastCoordinate',
    ol.geom.Point.prototype.getLastCoordinate);

goog.exportProperty(
    ol.geom.Point.prototype,
    'getLayout',
    ol.geom.Point.prototype.getLayout);

goog.exportProperty(
    ol.geom.Point.prototype,
    'getClosestPoint',
    ol.geom.Point.prototype.getClosestPoint);

goog.exportProperty(
    ol.geom.Point.prototype,
    'getExtent',
    ol.geom.Point.prototype.getExtent);

goog.exportProperty(
    ol.geom.Point.prototype,
    'simplify',
    ol.geom.Point.prototype.simplify);

goog.exportProperty(
    ol.geom.Point.prototype,
    'transform',
    ol.geom.Point.prototype.transform);

goog.exportProperty(
    ol.geom.Point.prototype,
    'get',
    ol.geom.Point.prototype.get);

goog.exportProperty(
    ol.geom.Point.prototype,
    'getKeys',
    ol.geom.Point.prototype.getKeys);

goog.exportProperty(
    ol.geom.Point.prototype,
    'getProperties',
    ol.geom.Point.prototype.getProperties);

goog.exportProperty(
    ol.geom.Point.prototype,
    'set',
    ol.geom.Point.prototype.set);

goog.exportProperty(
    ol.geom.Point.prototype,
    'setProperties',
    ol.geom.Point.prototype.setProperties);

goog.exportProperty(
    ol.geom.Point.prototype,
    'unset',
    ol.geom.Point.prototype.unset);

goog.exportProperty(
    ol.geom.Point.prototype,
    'changed',
    ol.geom.Point.prototype.changed);

goog.exportProperty(
    ol.geom.Point.prototype,
    'dispatchEvent',
    ol.geom.Point.prototype.dispatchEvent);

goog.exportProperty(
    ol.geom.Point.prototype,
    'getRevision',
    ol.geom.Point.prototype.getRevision);

goog.exportProperty(
    ol.geom.Point.prototype,
    'on',
    ol.geom.Point.prototype.on);

goog.exportProperty(
    ol.geom.Point.prototype,
    'once',
    ol.geom.Point.prototype.once);

goog.exportProperty(
    ol.geom.Point.prototype,
    'un',
    ol.geom.Point.prototype.un);

goog.exportProperty(
    ol.geom.Point.prototype,
    'unByKey',
    ol.geom.Point.prototype.unByKey);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'getFirstCoordinate',
    ol.geom.Polygon.prototype.getFirstCoordinate);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'getLastCoordinate',
    ol.geom.Polygon.prototype.getLastCoordinate);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'getLayout',
    ol.geom.Polygon.prototype.getLayout);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'getClosestPoint',
    ol.geom.Polygon.prototype.getClosestPoint);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'getExtent',
    ol.geom.Polygon.prototype.getExtent);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'simplify',
    ol.geom.Polygon.prototype.simplify);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'transform',
    ol.geom.Polygon.prototype.transform);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'get',
    ol.geom.Polygon.prototype.get);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'getKeys',
    ol.geom.Polygon.prototype.getKeys);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'getProperties',
    ol.geom.Polygon.prototype.getProperties);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'set',
    ol.geom.Polygon.prototype.set);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'setProperties',
    ol.geom.Polygon.prototype.setProperties);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'unset',
    ol.geom.Polygon.prototype.unset);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'changed',
    ol.geom.Polygon.prototype.changed);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'dispatchEvent',
    ol.geom.Polygon.prototype.dispatchEvent);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'getRevision',
    ol.geom.Polygon.prototype.getRevision);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'on',
    ol.geom.Polygon.prototype.on);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'once',
    ol.geom.Polygon.prototype.once);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'un',
    ol.geom.Polygon.prototype.un);

goog.exportProperty(
    ol.geom.Polygon.prototype,
    'unByKey',
    ol.geom.Polygon.prototype.unByKey);

goog.exportProperty(
    ol.format.GML2.prototype,
    'readFeatures',
    ol.format.GML2.prototype.readFeatures);

goog.exportProperty(
    ol.format.GML3.prototype,
    'readFeatures',
    ol.format.GML3.prototype.readFeatures);

goog.exportProperty(
    ol.format.GML.prototype,
    'readFeatures',
    ol.format.GML.prototype.readFeatures);

goog.exportProperty(
    ol.control.Control.prototype,
    'get',
    ol.control.Control.prototype.get);

goog.exportProperty(
    ol.control.Control.prototype,
    'getKeys',
    ol.control.Control.prototype.getKeys);

goog.exportProperty(
    ol.control.Control.prototype,
    'getProperties',
    ol.control.Control.prototype.getProperties);

goog.exportProperty(
    ol.control.Control.prototype,
    'set',
    ol.control.Control.prototype.set);

goog.exportProperty(
    ol.control.Control.prototype,
    'setProperties',
    ol.control.Control.prototype.setProperties);

goog.exportProperty(
    ol.control.Control.prototype,
    'unset',
    ol.control.Control.prototype.unset);

goog.exportProperty(
    ol.control.Control.prototype,
    'changed',
    ol.control.Control.prototype.changed);

goog.exportProperty(
    ol.control.Control.prototype,
    'dispatchEvent',
    ol.control.Control.prototype.dispatchEvent);

goog.exportProperty(
    ol.control.Control.prototype,
    'getRevision',
    ol.control.Control.prototype.getRevision);

goog.exportProperty(
    ol.control.Control.prototype,
    'on',
    ol.control.Control.prototype.on);

goog.exportProperty(
    ol.control.Control.prototype,
    'once',
    ol.control.Control.prototype.once);

goog.exportProperty(
    ol.control.Control.prototype,
    'un',
    ol.control.Control.prototype.un);

goog.exportProperty(
    ol.control.Control.prototype,
    'unByKey',
    ol.control.Control.prototype.unByKey);

goog.exportProperty(
    ol.control.Attribution.prototype,
    'getMap',
    ol.control.Attribution.prototype.getMap);

goog.exportProperty(
    ol.control.Attribution.prototype,
    'setMap',
    ol.control.Attribution.prototype.setMap);

goog.exportProperty(
    ol.control.Attribution.prototype,
    'setTarget',
    ol.control.Attribution.prototype.setTarget);

goog.exportProperty(
    ol.control.Attribution.prototype,
    'get',
    ol.control.Attribution.prototype.get);

goog.exportProperty(
    ol.control.Attribution.prototype,
    'getKeys',
    ol.control.Attribution.prototype.getKeys);

goog.exportProperty(
    ol.control.Attribution.prototype,
    'getProperties',
    ol.control.Attribution.prototype.getProperties);

goog.exportProperty(
    ol.control.Attribution.prototype,
    'set',
    ol.control.Attribution.prototype.set);

goog.exportProperty(
    ol.control.Attribution.prototype,
    'setProperties',
    ol.control.Attribution.prototype.setProperties);

goog.exportProperty(
    ol.control.Attribution.prototype,
    'unset',
    ol.control.Attribution.prototype.unset);

goog.exportProperty(
    ol.control.Attribution.prototype,
    'changed',
    ol.control.Attribution.prototype.changed);

goog.exportProperty(
    ol.control.Attribution.prototype,
    'dispatchEvent',
    ol.control.Attribution.prototype.dispatchEvent);

goog.exportProperty(
    ol.control.Attribution.prototype,
    'getRevision',
    ol.control.Attribution.prototype.getRevision);

goog.exportProperty(
    ol.control.Attribution.prototype,
    'on',
    ol.control.Attribution.prototype.on);

goog.exportProperty(
    ol.control.Attribution.prototype,
    'once',
    ol.control.Attribution.prototype.once);

goog.exportProperty(
    ol.control.Attribution.prototype,
    'un',
    ol.control.Attribution.prototype.un);

goog.exportProperty(
    ol.control.Attribution.prototype,
    'unByKey',
    ol.control.Attribution.prototype.unByKey);

goog.exportProperty(
    ol.control.FullScreen.prototype,
    'getMap',
    ol.control.FullScreen.prototype.getMap);

goog.exportProperty(
    ol.control.FullScreen.prototype,
    'setMap',
    ol.control.FullScreen.prototype.setMap);

goog.exportProperty(
    ol.control.FullScreen.prototype,
    'setTarget',
    ol.control.FullScreen.prototype.setTarget);

goog.exportProperty(
    ol.control.FullScreen.prototype,
    'get',
    ol.control.FullScreen.prototype.get);

goog.exportProperty(
    ol.control.FullScreen.prototype,
    'getKeys',
    ol.control.FullScreen.prototype.getKeys);

goog.exportProperty(
    ol.control.FullScreen.prototype,
    'getProperties',
    ol.control.FullScreen.prototype.getProperties);

goog.exportProperty(
    ol.control.FullScreen.prototype,
    'set',
    ol.control.FullScreen.prototype.set);

goog.exportProperty(
    ol.control.FullScreen.prototype,
    'setProperties',
    ol.control.FullScreen.prototype.setProperties);

goog.exportProperty(
    ol.control.FullScreen.prototype,
    'unset',
    ol.control.FullScreen.prototype.unset);

goog.exportProperty(
    ol.control.FullScreen.prototype,
    'changed',
    ol.control.FullScreen.prototype.changed);

goog.exportProperty(
    ol.control.FullScreen.prototype,
    'dispatchEvent',
    ol.control.FullScreen.prototype.dispatchEvent);

goog.exportProperty(
    ol.control.FullScreen.prototype,
    'getRevision',
    ol.control.FullScreen.prototype.getRevision);

goog.exportProperty(
    ol.control.FullScreen.prototype,
    'on',
    ol.control.FullScreen.prototype.on);

goog.exportProperty(
    ol.control.FullScreen.prototype,
    'once',
    ol.control.FullScreen.prototype.once);

goog.exportProperty(
    ol.control.FullScreen.prototype,
    'un',
    ol.control.FullScreen.prototype.un);

goog.exportProperty(
    ol.control.FullScreen.prototype,
    'unByKey',
    ol.control.FullScreen.prototype.unByKey);

goog.exportProperty(
    ol.control.MousePosition.prototype,
    'getMap',
    ol.control.MousePosition.prototype.getMap);

goog.exportProperty(
    ol.control.MousePosition.prototype,
    'setMap',
    ol.control.MousePosition.prototype.setMap);

goog.exportProperty(
    ol.control.MousePosition.prototype,
    'setTarget',
    ol.control.MousePosition.prototype.setTarget);

goog.exportProperty(
    ol.control.MousePosition.prototype,
    'get',
    ol.control.MousePosition.prototype.get);

goog.exportProperty(
    ol.control.MousePosition.prototype,
    'getKeys',
    ol.control.MousePosition.prototype.getKeys);

goog.exportProperty(
    ol.control.MousePosition.prototype,
    'getProperties',
    ol.control.MousePosition.prototype.getProperties);

goog.exportProperty(
    ol.control.MousePosition.prototype,
    'set',
    ol.control.MousePosition.prototype.set);

goog.exportProperty(
    ol.control.MousePosition.prototype,
    'setProperties',
    ol.control.MousePosition.prototype.setProperties);

goog.exportProperty(
    ol.control.MousePosition.prototype,
    'unset',
    ol.control.MousePosition.prototype.unset);

goog.exportProperty(
    ol.control.MousePosition.prototype,
    'changed',
    ol.control.MousePosition.prototype.changed);

goog.exportProperty(
    ol.control.MousePosition.prototype,
    'dispatchEvent',
    ol.control.MousePosition.prototype.dispatchEvent);

goog.exportProperty(
    ol.control.MousePosition.prototype,
    'getRevision',
    ol.control.MousePosition.prototype.getRevision);

goog.exportProperty(
    ol.control.MousePosition.prototype,
    'on',
    ol.control.MousePosition.prototype.on);

goog.exportProperty(
    ol.control.MousePosition.prototype,
    'once',
    ol.control.MousePosition.prototype.once);

goog.exportProperty(
    ol.control.MousePosition.prototype,
    'un',
    ol.control.MousePosition.prototype.un);

goog.exportProperty(
    ol.control.MousePosition.prototype,
    'unByKey',
    ol.control.MousePosition.prototype.unByKey);

goog.exportProperty(
    ol.control.OverviewMap.prototype,
    'getMap',
    ol.control.OverviewMap.prototype.getMap);

goog.exportProperty(
    ol.control.OverviewMap.prototype,
    'setMap',
    ol.control.OverviewMap.prototype.setMap);

goog.exportProperty(
    ol.control.OverviewMap.prototype,
    'setTarget',
    ol.control.OverviewMap.prototype.setTarget);

goog.exportProperty(
    ol.control.OverviewMap.prototype,
    'get',
    ol.control.OverviewMap.prototype.get);

goog.exportProperty(
    ol.control.OverviewMap.prototype,
    'getKeys',
    ol.control.OverviewMap.prototype.getKeys);

goog.exportProperty(
    ol.control.OverviewMap.prototype,
    'getProperties',
    ol.control.OverviewMap.prototype.getProperties);

goog.exportProperty(
    ol.control.OverviewMap.prototype,
    'set',
    ol.control.OverviewMap.prototype.set);

goog.exportProperty(
    ol.control.OverviewMap.prototype,
    'setProperties',
    ol.control.OverviewMap.prototype.setProperties);

goog.exportProperty(
    ol.control.OverviewMap.prototype,
    'unset',
    ol.control.OverviewMap.prototype.unset);

goog.exportProperty(
    ol.control.OverviewMap.prototype,
    'changed',
    ol.control.OverviewMap.prototype.changed);

goog.exportProperty(
    ol.control.OverviewMap.prototype,
    'dispatchEvent',
    ol.control.OverviewMap.prototype.dispatchEvent);

goog.exportProperty(
    ol.control.OverviewMap.prototype,
    'getRevision',
    ol.control.OverviewMap.prototype.getRevision);

goog.exportProperty(
    ol.control.OverviewMap.prototype,
    'on',
    ol.control.OverviewMap.prototype.on);

goog.exportProperty(
    ol.control.OverviewMap.prototype,
    'once',
    ol.control.OverviewMap.prototype.once);

goog.exportProperty(
    ol.control.OverviewMap.prototype,
    'un',
    ol.control.OverviewMap.prototype.un);

goog.exportProperty(
    ol.control.OverviewMap.prototype,
    'unByKey',
    ol.control.OverviewMap.prototype.unByKey);

goog.exportProperty(
    ol.control.Rotate.prototype,
    'getMap',
    ol.control.Rotate.prototype.getMap);

goog.exportProperty(
    ol.control.Rotate.prototype,
    'setMap',
    ol.control.Rotate.prototype.setMap);

goog.exportProperty(
    ol.control.Rotate.prototype,
    'setTarget',
    ol.control.Rotate.prototype.setTarget);

goog.exportProperty(
    ol.control.Rotate.prototype,
    'get',
    ol.control.Rotate.prototype.get);

goog.exportProperty(
    ol.control.Rotate.prototype,
    'getKeys',
    ol.control.Rotate.prototype.getKeys);

goog.exportProperty(
    ol.control.Rotate.prototype,
    'getProperties',
    ol.control.Rotate.prototype.getProperties);

goog.exportProperty(
    ol.control.Rotate.prototype,
    'set',
    ol.control.Rotate.prototype.set);

goog.exportProperty(
    ol.control.Rotate.prototype,
    'setProperties',
    ol.control.Rotate.prototype.setProperties);

goog.exportProperty(
    ol.control.Rotate.prototype,
    'unset',
    ol.control.Rotate.prototype.unset);

goog.exportProperty(
    ol.control.Rotate.prototype,
    'changed',
    ol.control.Rotate.prototype.changed);

goog.exportProperty(
    ol.control.Rotate.prototype,
    'dispatchEvent',
    ol.control.Rotate.prototype.dispatchEvent);

goog.exportProperty(
    ol.control.Rotate.prototype,
    'getRevision',
    ol.control.Rotate.prototype.getRevision);

goog.exportProperty(
    ol.control.Rotate.prototype,
    'on',
    ol.control.Rotate.prototype.on);

goog.exportProperty(
    ol.control.Rotate.prototype,
    'once',
    ol.control.Rotate.prototype.once);

goog.exportProperty(
    ol.control.Rotate.prototype,
    'un',
    ol.control.Rotate.prototype.un);

goog.exportProperty(
    ol.control.Rotate.prototype,
    'unByKey',
    ol.control.Rotate.prototype.unByKey);

goog.exportProperty(
    ol.control.ScaleLine.prototype,
    'getMap',
    ol.control.ScaleLine.prototype.getMap);

goog.exportProperty(
    ol.control.ScaleLine.prototype,
    'setMap',
    ol.control.ScaleLine.prototype.setMap);

goog.exportProperty(
    ol.control.ScaleLine.prototype,
    'setTarget',
    ol.control.ScaleLine.prototype.setTarget);

goog.exportProperty(
    ol.control.ScaleLine.prototype,
    'get',
    ol.control.ScaleLine.prototype.get);

goog.exportProperty(
    ol.control.ScaleLine.prototype,
    'getKeys',
    ol.control.ScaleLine.prototype.getKeys);

goog.exportProperty(
    ol.control.ScaleLine.prototype,
    'getProperties',
    ol.control.ScaleLine.prototype.getProperties);

goog.exportProperty(
    ol.control.ScaleLine.prototype,
    'set',
    ol.control.ScaleLine.prototype.set);

goog.exportProperty(
    ol.control.ScaleLine.prototype,
    'setProperties',
    ol.control.ScaleLine.prototype.setProperties);

goog.exportProperty(
    ol.control.ScaleLine.prototype,
    'unset',
    ol.control.ScaleLine.prototype.unset);

goog.exportProperty(
    ol.control.ScaleLine.prototype,
    'changed',
    ol.control.ScaleLine.prototype.changed);

goog.exportProperty(
    ol.control.ScaleLine.prototype,
    'dispatchEvent',
    ol.control.ScaleLine.prototype.dispatchEvent);

goog.exportProperty(
    ol.control.ScaleLine.prototype,
    'getRevision',
    ol.control.ScaleLine.prototype.getRevision);

goog.exportProperty(
    ol.control.ScaleLine.prototype,
    'on',
    ol.control.ScaleLine.prototype.on);

goog.exportProperty(
    ol.control.ScaleLine.prototype,
    'once',
    ol.control.ScaleLine.prototype.once);

goog.exportProperty(
    ol.control.ScaleLine.prototype,
    'un',
    ol.control.ScaleLine.prototype.un);

goog.exportProperty(
    ol.control.ScaleLine.prototype,
    'unByKey',
    ol.control.ScaleLine.prototype.unByKey);

goog.exportProperty(
    ol.control.Zoom.prototype,
    'getMap',
    ol.control.Zoom.prototype.getMap);

goog.exportProperty(
    ol.control.Zoom.prototype,
    'setMap',
    ol.control.Zoom.prototype.setMap);

goog.exportProperty(
    ol.control.Zoom.prototype,
    'setTarget',
    ol.control.Zoom.prototype.setTarget);

goog.exportProperty(
    ol.control.Zoom.prototype,
    'get',
    ol.control.Zoom.prototype.get);

goog.exportProperty(
    ol.control.Zoom.prototype,
    'getKeys',
    ol.control.Zoom.prototype.getKeys);

goog.exportProperty(
    ol.control.Zoom.prototype,
    'getProperties',
    ol.control.Zoom.prototype.getProperties);

goog.exportProperty(
    ol.control.Zoom.prototype,
    'set',
    ol.control.Zoom.prototype.set);

goog.exportProperty(
    ol.control.Zoom.prototype,
    'setProperties',
    ol.control.Zoom.prototype.setProperties);

goog.exportProperty(
    ol.control.Zoom.prototype,
    'unset',
    ol.control.Zoom.prototype.unset);

goog.exportProperty(
    ol.control.Zoom.prototype,
    'changed',
    ol.control.Zoom.prototype.changed);

goog.exportProperty(
    ol.control.Zoom.prototype,
    'dispatchEvent',
    ol.control.Zoom.prototype.dispatchEvent);

goog.exportProperty(
    ol.control.Zoom.prototype,
    'getRevision',
    ol.control.Zoom.prototype.getRevision);

goog.exportProperty(
    ol.control.Zoom.prototype,
    'on',
    ol.control.Zoom.prototype.on);

goog.exportProperty(
    ol.control.Zoom.prototype,
    'once',
    ol.control.Zoom.prototype.once);

goog.exportProperty(
    ol.control.Zoom.prototype,
    'un',
    ol.control.Zoom.prototype.un);

goog.exportProperty(
    ol.control.Zoom.prototype,
    'unByKey',
    ol.control.Zoom.prototype.unByKey);

goog.exportProperty(
    ol.control.ZoomSlider.prototype,
    'getMap',
    ol.control.ZoomSlider.prototype.getMap);

goog.exportProperty(
    ol.control.ZoomSlider.prototype,
    'setMap',
    ol.control.ZoomSlider.prototype.setMap);

goog.exportProperty(
    ol.control.ZoomSlider.prototype,
    'setTarget',
    ol.control.ZoomSlider.prototype.setTarget);

goog.exportProperty(
    ol.control.ZoomSlider.prototype,
    'get',
    ol.control.ZoomSlider.prototype.get);

goog.exportProperty(
    ol.control.ZoomSlider.prototype,
    'getKeys',
    ol.control.ZoomSlider.prototype.getKeys);

goog.exportProperty(
    ol.control.ZoomSlider.prototype,
    'getProperties',
    ol.control.ZoomSlider.prototype.getProperties);

goog.exportProperty(
    ol.control.ZoomSlider.prototype,
    'set',
    ol.control.ZoomSlider.prototype.set);

goog.exportProperty(
    ol.control.ZoomSlider.prototype,
    'setProperties',
    ol.control.ZoomSlider.prototype.setProperties);

goog.exportProperty(
    ol.control.ZoomSlider.prototype,
    'unset',
    ol.control.ZoomSlider.prototype.unset);

goog.exportProperty(
    ol.control.ZoomSlider.prototype,
    'changed',
    ol.control.ZoomSlider.prototype.changed);

goog.exportProperty(
    ol.control.ZoomSlider.prototype,
    'dispatchEvent',
    ol.control.ZoomSlider.prototype.dispatchEvent);

goog.exportProperty(
    ol.control.ZoomSlider.prototype,
    'getRevision',
    ol.control.ZoomSlider.prototype.getRevision);

goog.exportProperty(
    ol.control.ZoomSlider.prototype,
    'on',
    ol.control.ZoomSlider.prototype.on);

goog.exportProperty(
    ol.control.ZoomSlider.prototype,
    'once',
    ol.control.ZoomSlider.prototype.once);

goog.exportProperty(
    ol.control.ZoomSlider.prototype,
    'un',
    ol.control.ZoomSlider.prototype.un);

goog.exportProperty(
    ol.control.ZoomSlider.prototype,
    'unByKey',
    ol.control.ZoomSlider.prototype.unByKey);

goog.exportProperty(
    ol.control.ZoomToExtent.prototype,
    'getMap',
    ol.control.ZoomToExtent.prototype.getMap);

goog.exportProperty(
    ol.control.ZoomToExtent.prototype,
    'setMap',
    ol.control.ZoomToExtent.prototype.setMap);

goog.exportProperty(
    ol.control.ZoomToExtent.prototype,
    'setTarget',
    ol.control.ZoomToExtent.prototype.setTarget);

goog.exportProperty(
    ol.control.ZoomToExtent.prototype,
    'get',
    ol.control.ZoomToExtent.prototype.get);

goog.exportProperty(
    ol.control.ZoomToExtent.prototype,
    'getKeys',
    ol.control.ZoomToExtent.prototype.getKeys);

goog.exportProperty(
    ol.control.ZoomToExtent.prototype,
    'getProperties',
    ol.control.ZoomToExtent.prototype.getProperties);

goog.exportProperty(
    ol.control.ZoomToExtent.prototype,
    'set',
    ol.control.ZoomToExtent.prototype.set);

goog.exportProperty(
    ol.control.ZoomToExtent.prototype,
    'setProperties',
    ol.control.ZoomToExtent.prototype.setProperties);

goog.exportProperty(
    ol.control.ZoomToExtent.prototype,
    'unset',
    ol.control.ZoomToExtent.prototype.unset);

goog.exportProperty(
    ol.control.ZoomToExtent.prototype,
    'changed',
    ol.control.ZoomToExtent.prototype.changed);

goog.exportProperty(
    ol.control.ZoomToExtent.prototype,
    'dispatchEvent',
    ol.control.ZoomToExtent.prototype.dispatchEvent);

goog.exportProperty(
    ol.control.ZoomToExtent.prototype,
    'getRevision',
    ol.control.ZoomToExtent.prototype.getRevision);

goog.exportProperty(
    ol.control.ZoomToExtent.prototype,
    'on',
    ol.control.ZoomToExtent.prototype.on);

goog.exportProperty(
    ol.control.ZoomToExtent.prototype,
    'once',
    ol.control.ZoomToExtent.prototype.once);

goog.exportProperty(
    ol.control.ZoomToExtent.prototype,
    'un',
    ol.control.ZoomToExtent.prototype.un);

goog.exportProperty(
    ol.control.ZoomToExtent.prototype,
    'unByKey',
    ol.control.ZoomToExtent.prototype.unByKey);
OPENLAYERS.ol = ol;

  return OPENLAYERS.ol;
}));
