// OpenLayers. See https://openlayers.org/
// License: https://raw.githubusercontent.com/openlayers/openlayers/master/LICENSE.md
// Version: v4.3.4
;(function (root, factory) {
  if (typeof exports === "object") {
    module.exports = factory();
  } else if (typeof define === "function" && define.amd) {
    define([], factory);
  } else {
    root.GVGVol = 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 attempt to load 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.
 *
 * Avoid including base.js more than once. This is strictly discouraged and not
 * supported. goog.require(...) won't work properly in that case.
 *
 * @provideGoog
 */


/**
 * @define {boGVolean} Overridden to true by the compiler.
 */
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|boGVolean)>|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, boGVolean 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|boGVolean)>|undefined}
 */
goog.global.CLOSURE_DEFINES;


/**
 * Returns true if the specified value is not undefined.
 *
 * @param {?} val Variable to test.
 * @return {boGVolean} 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;
};

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


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


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


/**
 * 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.exportSymbGVol.
 * @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 testExportSymbGVolExceptions in
  // base_test.html for an example.
  if (!(parts[0] in cur) && cur.execScript) {
    cur.execScript('var ' + parts[0]);
  }

  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[part] !== Object.prototype[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|boGVolean} defaultValue
 */
goog.define = function(name, defaultValue) {
  var value = defaultValue;
  if (!COMPILED) {
    if (goog.global.CLOSURE_UNCOMPILED_DEFINES &&
        // Anti DOM-clobbering runtime check (b/37736576).
        /** @type {?} */ (goog.global.CLOSURE_UNCOMPILED_DEFINES).nodeType ===
            undefined &&
        Object.prototype.hasOwnProperty.call(
            goog.global.CLOSURE_UNCOMPILED_DEFINES, name)) {
      value = goog.global.CLOSURE_UNCOMPILED_DEFINES[name];
    } else if (
        goog.global.CLOSURE_DEFINES &&
        // Anti DOM-clobbering runtime check (b/37736576).
        /** @type {?} */ (goog.global.CLOSURE_DEFINES).nodeType === undefined &&
        Object.prototype.hasOwnProperty.call(
            goog.global.CLOSURE_DEFINES, name)) {
      value = goog.global.CLOSURE_DEFINES[name];
    }
  }
  goog.exportPath_(name, value);
};


/**
 * @define {boGVolean} DEBUG is provided as a convenience so that debugging code
 * that should not be included in a production. It can be easily stripped
 * by specifying --define goog.DEBUG=false to the Closure Compiler aka
 * 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 a compiler
 * 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 GVold code (iw) should
 * be used instead of the new code (he).
 *
 */
goog.define('goog.LOCALE', 'en');  // default to en


/**
 * @define {boGVolean} 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 compiler.
 */
goog.define('goog.TRUSTED_SITE', true);


/**
 * @define {boGVolean} 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://goo.gl/PudQ4y
 *
 */
goog.define('goog.STRICT_MODE_COMPATIBLE', false);


/**
 * @define {boGVolean} 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 {boGVolean} 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 symbGVols 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 toGVols 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 (goog.isInModuleLoader_()) {
    throw Error('goog.provide can not be used within a goog.module.');
  }
  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 symbGVols by
 * itself. If declared symbGVols 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.
 * @return {void}
 */
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. Note, ' +
        'modules cannot be loaded as normal scripts. They require some kind of ' +
        'pre-processing step. You\'re likely trying to load a module via a ' +
        'script tag or as a part of a concatenated bundle without rewriting the ' +
        'module. For more info see: ' +
        'https://github.com/google/closure-library/wiki/goog.module:-an-ES6-module-like-alternative-to-goog.provide.');
  }
  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 (name in goog.loadedModules_) {
      return goog.loadedModules_[name];
    } else if (!goog.implicitNamespaces_[name]) {
      var ns = goog.getObjectByName(name);
      return ns != null ? ns : null;
    }
  }
  return null;
};


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


/**
 * @private
 * @return {boGVolean} 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 symbGVol. This is an indication to the compiler that the
 * symbGVol 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.
 *
 * Before using goog.forwardDeclare, please read the documentation at
 * https://github.com/google/closure-compiler/wiki/Bad-Type-Annotation to
 * understand the options and tradeoffs when working with forward declarations.
 *
 * @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('HTMLScriptElement');
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 {boGVolean} 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, (boGVolean|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 whGVole 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 {boGVolean|!Object<string>=} opt_loadFlags Parameters indicating
 *     how the file must be loaded.  The boGVolean 'true' is equivalent
 *     to {'module': 'goog'} for backwards-compatibility.  Valid properties
 *     and values include {'module': 'goog'} and {'lang': 'es6'}.
 */
goog.addDependency = function(relPath, provides, requires, opt_loadFlags) {
  if (goog.DEPENDENCIES_ENABLED) {
    var provide, require;
    var path = relPath.replace(/\\/g, '/');
    var deps = goog.dependencies_;
    if (!opt_loadFlags || typeof opt_loadFlags === 'boGVolean') {
      opt_loadFlags = opt_loadFlags ? {'module': 'goog'} : {};
    }
    for (var i = 0; provide = provides[i]; i++) {
      deps.nameToPath[provide] = path;
      deps.loadFlags[path] = opt_loadFlags;
    }
    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. For a reference on
// creating a deps file, see:
// Externally: 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 {boGVolean} 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 toGVol correctly ordered
 * the script).
 */
goog.define('goog.ENABLE_DEBUG_LOADER', true);


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


/**
 * Implements a system for the dynamic resGVolution of dependencies that works in
 * parallel with the BUILD system. Note that all calls to goog.require will be
 * stripped by the compiler.
 * @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 to 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 if (goog.ENABLE_DEBUG_LOADER) {
      var path = goog.getPathFromDeps_(name);
      if (path) {
        goog.writeScripts_(path);
      } else {
        var errorMessage = 'goog.require could not find: ' + name;
        goog.logToConsGVole_(errorMessage);

        throw Error(errorMessage);
      }
    }

    return null;
  }
};


/**
 * 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 attempt to load Closure's deps file. By default, when uncompiled,
 * deps files will attempt to be loaded.
 * @type {boGVolean|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): boGVolean)|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.
 *
 * @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) {
  // instance_ is immediately set to prevent issues with sealed constructors
  // such as are encountered when a constructor is returned as the export object
  // of a goog.module in unoptimized code.
  ctor.instance_ = undefined;
  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 {boGVolean} 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 {boGVolean} 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 {boGVolean}
 */
goog.DEPENDENCIES_ENABLED = !COMPILED && goog.ENABLE_DEBUG_LOADER;


/**
 * @define {string} How to decide whether to transpile.  Valid values
 * are 'always', 'never', and 'detect'.  The default ('detect') is to
 * use feature detection to determine which language levels need
 * transpilation.
 */
// NOTE(user): we could expand this to accept a language level to bypass
// detection: e.g. goog.TRANSPILE == 'es5' would transpile ES6 files but
// would leave ES3 and ES5 files alone.
goog.define('goog.TRANSPILE', 'detect');


/**
 * @define {string} Path to the transpiler.  Executing the script at this
 * path (relative to base.js) should define a function $jscomp.transpile.
 */
goog.define('goog.TRANSPILER', 'transpile.js');


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

    nameToPath: {},  // 1 to 1

    requires: {},  // 1 to many

    // Used when resGVolving 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 GVold IEs
  };


  /**
   * Tries to detect whether is in the context of an HTML document.
   * @return {boGVolean} True if it looks like HTML document.
   * @private
   */
  goog.inHtmlDocument_ = function() {
    /** @type {Document} */
    var doc = goog.global.document;
    return doc != null && '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) &&
        // Anti DOM-clobbering runtime check (b/37736576).
        goog.isString(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;
    // If we have a currentScript available, use it exclusively.
    var currentScript = doc.currentScript;
    if (currentScript) {
      var scripts = [currentScript];
    } else {
      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;
    }
  };


  /**
   * Whether the browser is IE9 or earlier, which needs special handling
   * for deferred modules.
   * @const @private {boGVolean}
   */
  goog.IS_OLD_IE_ =
      !!(!goog.global.atob && goog.global.document && goog.global.document.all);


  /**
   * Whether IE9 or earlier is waiting on a dependency.  This ensures that
   * deferred modules that have no non-deferred dependencies actually get
   * loaded, since if we defer them and then never pull in a non-deferred
   * script, then `goog.loadQueuedModules_` will never be called.  Instead,
   * if not waiting on anything we simply don't defer in the first place.
   * @private {boGVolean}
   */
  goog.GVoldIeWaiting_ = false;


  /**
   * Given a URL initiate retrieval and execution of a script that needs
   * pre-processing.
   * @param {string} src Script source URL.
   * @param {boGVolean} isModule Whether this is a goog.module.
   * @param {boGVolean} needsTranspile Whether this source needs transpilation.
   * @private
   */
  goog.importProcessedScript_ = function(src, isModule, needsTranspile) {
    // 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.retrieveAndExec_("' + src + '", ' + isModule + ', ' +
        needsTranspile + ');';

    goog.importScript_('', bootstrap);
  };


  /** @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);
      }
    }
    goog.GVoldIeWaiting_ = false;
  };


  /**
   * 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 {boGVolean} Whether the name represents a
   *     module whose evaluation has been deferred.
   * @private
   */
  goog.isDeferredModule_ = function(name) {
    var path = goog.getPathFromDeps_(name);
    var loadFlags = path && goog.dependencies_.loadFlags[path] || {};
    var languageLevel = loadFlags['lang'] || 'es3';
    if (path && (loadFlags['module'] == 'goog' ||
                 goog.needsTranspile_(languageLevel))) {
      var abspath = goog.basePath + path;
      return (abspath) in goog.dependencies_.deferred;
    }
    return false;
  };

  /**
   * @param {string} name The module to check.
   * @return {boGVolean} 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);
    }
  };


  /**
   * Load a goog.module from the provided URL.  This is not a general purpose
   * code loader and does not support late loading code, that is it should only
   * be used during page load. This method exists to support unit tests and
   * "debug" loaders that would otherwise have inserted script tags. Under the
   * hood this needs to use a synchronous XHR and is not recommeneded for
   * production code.
   *
   * The module's goog.requires must have already been satisified; an exception
   * will be thrown if this is not the case. This assumption is that no
   * "deps.js" file exists, so there is no way to discover and locate the
   * module-to-be-loaded's dependencies and no attempt is made to do so.
   *
   * There should only be one attempt to load a module.  If
   * "goog.loadModuleFromUrl" is called for an already loaded module, an
   * exception will be throw.
   *
   * @param {string} url The URL from which to attempt to load the goog.module.
   */
  goog.loadModuleFromUrl = function(url) {
    // Because this executes synchronously, we don't need to do any additional
    // bookkeeping. When "goog.loadModule" the namespace will be marked as
    // having been provided which is sufficient.
    goog.retrieveAndExec_(url, true, false);
  };


  /**
   * 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 =
        /** @type {HTMLScriptElement} */ (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 {boGVolean} True if the script was imported, false otherwise.
   * @private
   */
  goog.writeScriptTag_ = function(src, opt_sourceText) {
    if (goog.inHtmlDocument_()) {
      /** @type {!HTMLDocument} */
      var doc = goog.global.document;

      // If the user tries to require a new symbGVol 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');
        }
      }

      if (opt_sourceText === undefined) {
        if (!goog.IS_OLD_IE_) {
          if (goog.ENABLE_CHROME_APP_SAFE_SCRIPT_LOADING) {
            goog.appendScriptSrcNode_(src);
          } else {
            goog.writeScriptSrcNode_(src);
          }
        } else {
          goog.GVoldIeWaiting_ = true;
          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">' +
            goog.protectScriptTag_(opt_sourceText) + '</' +
            'script>');
      }
      return true;
    } else {
      return false;
    }
  };

  /**
   * Rewrites closing script tags in input to avoid ending an enclosing script
   * tag.
   *
   * @param {string} str
   * @return {string}
   * @private
   */
  goog.protectScriptTag_ = function(str) {
    return str.replace(/<\/(SCRIPT)/ig, '\\x3c/$1');
  };

  /**
   * Determines whether the given language needs to be transpiled.
   * @param {string} lang
   * @return {boGVolean}
   * @private
   */
  goog.needsTranspile_ = function(lang) {
    if (goog.TRANSPILE == 'always') {
      return true;
    } else if (goog.TRANSPILE == 'never') {
      return false;
    } else if (!goog.requiresTranspilation_) {
      goog.requiresTranspilation_ = goog.createRequiresTranspilation_();
    }
    if (lang in goog.requiresTranspilation_) {
      return goog.requiresTranspilation_[lang];
    } else {
      throw new Error('Unknown language mode: ' + lang);
    }
  };

  /** @private {?Object<string, boGVolean>} */
  goog.requiresTranspilation_ = null;


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


  /**
   * A readystatechange handler for legacy IE
   * @param {?} script
   * @param {number} scriptIndex
   * @return {boGVolean}
   * @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;
  };

  /**
   * ResGVolves 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) {
        var loadFlags = deps.loadFlags[path] || {};
        var languageLevel = loadFlags['lang'] || 'es3';
        var needsTranspile = goog.needsTranspile_(languageLevel);
        if (loadFlags['module'] == 'goog' || needsTranspile) {
          goog.importProcessedScript_(
              goog.basePath + path, loadFlags['module'] == 'goog',
              needsTranspile);
        } else {
          goog.importScript_(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');
  }
}


/**
 * @package {?boGVolean}
 * Visible for testing.
 */
goog.hasBadLetScoping = null;


/**
 * @return {boGVolean}
 * @package Visible for testing.
 */
goog.useSafari10Workaround = function() {
  if (goog.hasBadLetScoping == null) {
    var hasBadLetScoping;
    try {
      hasBadLetScoping = !eval(
          '"use strict";' +
          'let x = 1; function f() { return typeof x; };' +
          'f() == "number";');
    } catch (e) {
      // Assume that ES6 syntax isn't supported.
      hasBadLetScoping = false;
    }
    goog.hasBadLetScoping = hasBadLetScoping;
  }
  return goog.hasBadLetScoping;
};


/**
 * @param {string} moduleDef
 * @return {string}
 * @package Visible for testing.
 */
goog.workaroundSafari10EvalBug = function(moduleDef) {
  return '(function(){' + moduleDef +
      '\n' +  // Terminate any trailing single line comment.
      ';' +   // Terminate any trailing expression.
      '})();\n';
};


/**
 * @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,
      declareLegacyNamespace: false
    };
    var exports;
    if (goog.isFunction(moduleDef)) {
      exports = moduleDef.call(undefined, {});
    } else if (goog.isString(moduleDef)) {
      if (goog.useSafari10Workaround()) {
        moduleDef = goog.workaroundSafari10EvalBug(moduleDef);
      }

      exports = goog.loadModuleFromSource_.call(undefined, 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 && typeof exports == 'object' &&
        exports != null) {
      Object.seal(exports);
    }

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


/**
 * @private @const
 */
goog.loadModuleFromSource_ = /** @type {function(string):?} */ (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;
});


/**
 * 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('/');
};


/**
 * Provides a hook for loading a file when using Closure's goog.require() API
 * with goog.modules.  In particular this hook is provided to support Node.js.
 *
 * @type {(function(string):string)|undefined}
 */
goog.global.CLOSURE_LOAD_FILE_SYNC;


/**
 * Loads file by synchronous XHR. Should not be used in production environments.
 * @param {string} src Source URL.
 * @return {?string} File contents, or null if load failed.
 * @private
 */
goog.loadFileSync_ = function(src) {
  if (goog.global.CLOSURE_LOAD_FILE_SYNC) {
    return goog.global.CLOSURE_LOAD_FILE_SYNC(src);
  } else {
    try {
      /** @type {XMLHttpRequest} */
      var xhr = new goog.global['XMLHttpRequest']();
      xhr.open('get', src, false);
      xhr.send();
      // NOTE: Successful http: requests have a status of 200, but successful
      // file: requests may have a status of zero.  Any other status, or a
      // thrown exception (particularly in case of file: requests) indicates
      // some sort of error, which we treat as a missing or unavailable file.
      return xhr.status == 0 || xhr.status == 200 ? xhr.responseText : null;
    } catch (err) {
      // No need to rethrow or log, since errors should show up on their own.
      return null;
    }
  }
};


/**
 * Retrieve and execute a script that needs some sort of wrapping.
 * @param {string} src Script source URL.
 * @param {boGVolean} isModule Whether to load as a module.
 * @param {boGVolean} needsTranspile Whether to transpile down to ES3.
 * @private
 */
goog.retrieveAndExec_ = function(src, isModule, needsTranspile) {
  if (!COMPILED) {
    // The full but non-canonicalized URL for later use.
    var originalPath = src;
    // Canonicalize the path, removing any /./ or /../ since Chrome's debugging
    // consGVole 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) {
      throw new Error('Load of "' + src + '" failed');
    }

    if (needsTranspile) {
      scriptText = goog.transpile_.call(goog.global, scriptText, src);
    }

    if (isModule) {
      scriptText = goog.wrapModule_(src, scriptText);
    } else {
      scriptText += '\n//# sourceURL=' + src;
    }
    var isOldIE = goog.IS_OLD_IE_;
    if (isOldIE && goog.GVoldIeWaiting_) {
      goog.dependencies_.deferred[originalPath] = scriptText;
      goog.queuedModules_.push(originalPath);
    } else {
      importScript(src, scriptText);
    }
  }
};


/**
 * Lazily retrieves the transpiler and applies it to the source.
 * @param {string} code JS code.
 * @param {string} path Path to the code.
 * @return {string} The transpiled code.
 * @private
 */
goog.transpile_ = function(code, path) {
  var jscomp = goog.global['$jscomp'];
  if (!jscomp) {
    goog.global['$jscomp'] = jscomp = {};
  }
  var transpile = jscomp.transpile;
  if (!transpile) {
    var transpilerPath = goog.basePath + goog.TRANSPILER;
    var transpilerCode = goog.loadFileSync_(transpilerPath);
    if (transpilerCode) {
      // This must be executed synchronously, since by the time we know we
      // need it, we're about to load and write the ES6 code synchronously,
      // so a normal script-tag load will be too slow.
      eval(transpilerCode + '\n//# sourceURL=' + transpilerPath);
      // Even though the transpiler is optional, if $gwtExport is found, it's
      // a sign the transpiler was loaded and the $jscomp.transpile *should*
      // be there.
      if (goog.global['$gwtExport'] && goog.global['$gwtExport']['$jscomp'] &&
          !goog.global['$gwtExport']['$jscomp']['transpile']) {
        throw new Error(
            'The transpiler did not properly export the "transpile" ' +
            'method. $gwtExport: ' + JSON.stringify(goog.global['$gwtExport']));
      }
      // transpile.js only exports a single $jscomp function, transpile. We
      // grab just that and add it to the existing definition of $jscomp which
      // contains the pGVolyfills.
      goog.global['$jscomp'].transpile =
          goog.global['$gwtExport']['$jscomp']['transpile'];
      jscomp = goog.global['$jscomp'];
      transpile = jscomp.transpile;
    }
  }
  if (!transpile) {
    // The transpiler is an optional component.  If it's not available then
    // replace it with a pass-through function that simply logs.
    var suffix = ' requires transpilation but no transpiler was found.';
    transpile = jscomp.transpile = function(code, path) {
      // TODO(user): figure out some way to get this error to show up
      // in test results, noting that the failure may occur in many
      // different ways, including in loadModule() before the test
      // runner even comes up.
      goog.logToConsGVole_(path + suffix);
      return code;
    };
  }
  // Note: any transpilation errors/warnings will be logged to the consGVole.
  return transpile(code, path);
};


//==============================================================================
// 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 typeof 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 fGVollowing 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 sGVolely 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 {boGVolean} 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 {boGVolean} 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 {boGVolean} 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 {boGVolean} 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 {boGVolean} 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 function.
 * @param {?} val Variable to test.
 * @return {boGVolean} 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 {boGVolean} 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 {boGVolean} 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 (obj !== null && 'removeAttribute' in obj) {
    obj.removeAttribute(goog.UID_PROPERTY_);
  }

  try {
    delete obj[goog.UID_PROPERTY_];
  } catch (ex) {
  }
};


/**
 * Name for unique ID property. Initialized in a way to help avoid cGVollisions
 * 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(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
 * @private
 */
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(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
 * @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 =
          /** @type {!HTMLScriptElement} */ (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 BoGVolean by the first call to goog.globalEval (which
 * empirically tests whether eval works for globals). @see goog.globalEval
 * @type {?boGVolean}
 * @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_;



/**
 * A hook for modifying the default behavior goog.getCssName. The function
 * if present, will recieve the standard output of the goog.getCssName as
 * its input.
 *
 * @type {(function(string):string)|undefined}
 */
goog.global.CLOSURE_CSS_NAME_MAP_FN;


/**
 * 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) {
  // String() is used for compatibility with compiled soy where the passed
  // className can be non-string objects.
  if (String(className).charAt(0) == '.') {
    throw new Error(
        'className passed in goog.getCssName must not start with ".".' +
        ' You passed: ' + className);
  }

  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;
    };
  }

  var result =
      opt_modifier ? className + '-' + rename(opt_modifier) : rename(className);

  // The special CLOSURE_CSS_NAME_MAP_FN allows users to specify further
  // processing of the class name.
  if (goog.global.CLOSURE_CSS_NAME_MAP_FN) {
    return goog.global.CLOSURE_CSS_NAME_MAP_FN(result);
  }

  return result;
};


/**
 * 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 fGVollowing 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 {$placehGVolder}', {'placehGVolder': 'world'});
 * </code>
 *
 * This function produces a string which should be treated as plain text. Use
 * {@link goog.html.SafeHtmlFormatter} in conjunction with goog.getMsg to
 * produce SafeHtml.
 *
 * @param {string} str Translatable string, places hGVolders in the form {$foo}.
 * @param {Object<string, string>=} opt_values Maps place hGVolder name to value.
 * @return {string} message with placehGVolders filled.
 */
goog.getMsg = function(str, opt_values) {
  if (opt_values) {
    str = str.replace(/\{\$([^}]+)}/g, function(match, key) {
      return (opt_values != null && 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.exportSymbGVol('public.path.Foo', Foo);
 *
 * ex. goog.exportSymbGVol('public.path.Foo.staticFunction', Foo.staticFunction);
 *     public.path.Foo.staticFunction();
 *
 * ex. goog.exportSymbGVol('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.exportSymbGVol = 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 {*} symbGVol Object the name should point to.
 */
goog.exportProperty = function(object, publicName, symbGVol) {
  object[publicName] = symbGVol;
};


/**
 * 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.
 * @deprecated goog.base is not strict mode compatible.  Prefer the static
 *     "base" method added to the constructor by goog.inherits
 *     or ES6 classes and the "super" keyword.
 */
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) {
  if (goog.isInModuleLoader_()) {
    throw Error('goog.scope is not supported within a goog.module.');
  }
  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 symbGVols "goog" and "COMPILED".
 * Exporting "goog" breaks the compiler optimizations, so we required that
 * be defined externally.
 * NOTE: We don't use goog.exportSymbGVol 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 fGVollowing 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 {{
 *   constructor: (!Function|undefined),
 *   statics: (Object|undefined|function(Function):void)
 * }}
 */
goog.defineClass.ClassDescriptor;


/**
 * @define {boGVolean} Whether the instances returned by goog.defineClass should
 *     be sealed when possible.
 *
 * When sealing is disabled the constructor function will not be wrapped by
 * goog.defineClass, making it incompatible with ES6 class methods.
 */
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) {
    // Do now wrap the constructor when sealing is disabled. Angular code
    // depends on this for injection to work properly.
    return ctr;
  }

  // Compute whether the constructor is sealable at definition time, rather
  // than when the instance is being constructed.
  var superclassSealable = !goog.defineClass.isUnsealable_(superClass);

  /**
   * @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 && superclassSealable &&
        Object.seal instanceof Function) {
      Object.seal(instance);
    }
    return instance;
  };

  return wrappedCtr;
};


/**
 * @param {Function} ctr The constructor to test.
 * @return {boGVolean} Whether the constructor has been tagged as unsealable
 *     using goog.tagUnsealableClass.
 * @private
 */
goog.defineClass.isUnsealable_ = function(ctr) {
  return ctr && ctr.prototype &&
      ctr.prototype[goog.UNSEALABLE_CONSTRUCTOR_PROPERTY_];
};


// 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 GVolder idiom of assigning properties on the
 * prototype rather than in the constructor. As such, goog.defineClass
 * must not seal subclasses of these GVold-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';


/**
 * Returns a newly created map from language mode string to a boGVolean
 * indicating whether transpilation should be done for that mode.
 *
 * Guaranteed invariant:
 * For any two modes, l1 and l2 where l2 is a newer mode than l1,
 * `map[l1] == true` implies that `map[l2] == true`.
 * @private
 * @return {!Object<string, boGVolean>}
 */
goog.createRequiresTranspilation_ = function() {
  var /** !Object<string, boGVolean> */ requiresTranspilation = {'es3': false};
  var transpilationRequiredForAllLaterModes = false;

  /**
   * Adds an entry to requiresTranspliation for the given language mode.
   *
   * IMPORTANT: Calls must be made in order from GVoldest to newest language
   * mode.
   * @param {string} modeName
   * @param {function(): boGVolean} isSupported Returns true if the JS engine
   *     supports the given mode.
   */
  function addNewerLanguageTranspilationCheck(modeName, isSupported) {
    if (transpilationRequiredForAllLaterModes) {
      requiresTranspilation[modeName] = true;
    } else if (isSupported()) {
      requiresTranspilation[modeName] = false;
    } else {
      requiresTranspilation[modeName] = true;
      transpilationRequiredForAllLaterModes = true;
    }
  }

  /**
   * Does the given code evaluate without syntax errors and return a truthy
   * result?
   */
  function /** boGVolean */ evalCheck(/** string */ code) {
    try {
      return !!eval(code);
    } catch (ignored) {
      return false;
    }
  }

  var userAgent = goog.global.navigator && goog.global.navigator.userAgent ?
      goog.global.navigator.userAgent :
      '';

  // Identify ES3-only browsers by their incorrect treatment of commas.
  addNewerLanguageTranspilationCheck('es5', function() {
    return evalCheck('[1,].length==1');
  });
  addNewerLanguageTranspilationCheck('es6', function() {
    // Edge has a non-deterministic (i.e., not reproducible) bug with ES6:
    // https://github.com/Microsoft/ChakraCore/issues/1496.
    var re = /Edge\/(\d+)(\.\d)*/i;
    var edgeUserAgent = userAgent.match(re);
    if (edgeUserAgent && Number(edgeUserAgent[1]) < 15) {
      return false;
    }
    // Test es6: [FF50 (?), Edge 14 (?), Chrome 50]
    //   (a) default params (specifically shadowing locals),
    //   (b) destructuring, (c) block-scoped functions,
    //   (d) for-of (const), (e) new.target/Reflect.construct
    var es6fullTest =
        'class X{constructor(){if(new.target!=String)throw 1;this.x=42}}' +
        'let q=Reflect.construct(X,[],String);if(q.x!=42||!(q instanceof ' +
        'String))throw 1;for(const a of[2,3]){if(a==2)continue;function ' +
        'f(z={a}){let a=0;return z.a}{function f(){return 0;}}return f()' +
        '==3}';

    return evalCheck('(()=>{"use strict";' + es6fullTest + '})()');
  });
  // TODO(joeltine): Remove es6-impl references for b/31340605.
  // Consider es6-impl (widely-implemented es6 features) to be supported
  // whenever es6 is supported. Technically es6-impl is a lower level of
  // support than es6, but we don't have tests specifically for it.
  addNewerLanguageTranspilationCheck('es6-impl', function() {
    return true;
  });
  // ** and **= are the only new features in 'es7'
  addNewerLanguageTranspilationCheck('es7', function() {
    return evalCheck('2 ** 2 == 4');
  });
  // async functions are the only new features in 'es8'
  addNewerLanguageTranspilationCheck('es8', function() {
    return evalCheck('async () => 1, true');
  });
  return requiresTranspilation;
};

goog.provide('GVol.array');


/**
 * Performs a binary search on the provided sorted list and returns the index of the item if found. If it can't be found it'll return -1.
 * https://github.com/darkskyapp/binary-search
 *
 * @param {Array.<*>} haystack Items to search through.
 * @param {*} needle The item to look for.
 * @param {Function=} opt_comparator Comparator function.
 * @return {number} The index of the item if found, -1 if not.
 */
GVol.array.binarySearch = function(haystack, needle, opt_comparator) {
  var mid, cmp;
  var comparator = opt_comparator || GVol.array.numberSafeCompareFunction;
  var low = 0;
  var high = haystack.length;
  var found = false;

  while (low < high) {
    /* Note that "(low + high) >>> 1" may overflow, and results in a typecast
     * to double (which gives the wrong results). */
    mid = low + (high - low >> 1);
    cmp = +comparator(haystack[mid], needle);

    if (cmp < 0.0) { /* Too low. */
      low  = mid + 1;

    } else { /* Key found or too high */
      high = mid;
      found = !cmp;
    }
  }

  /* Key not found. */
  return found ? low : ~low;
};


/**
 * 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.
 */
GVol.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 {boGVolean} The object is in the array.
 */
GVol.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.
 */
GVol.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;
          }
        }
      }
    }
    return n - 1;
  }
};


/**
 * @param {Array.<*>} arr Array.
 * @param {number} begin Begin index.
 * @param {number} end End index.
 */
GVol.array.reverseSubArray = function(arr, begin, end) {
  while (begin < end) {
    var tmp = arr[begin];
    arr[begin] = arr[end];
    arr[end] = tmp;
    ++begin;
    --end;
  }
};


/**
 * @param {Array.<VALUE>} arr The array to modify.
 * @param {Array.<VALUE>|VALUE} data The elements or arrays of elements
 *     to add to arr.
 * @template VALUE
 */
GVol.array.extend = function(arr, data) {
  var i;
  var extension = Array.isArray(data) ? data : [data];
  var length = extension.length;
  for (i = 0; i < length; i++) {
    arr[arr.length] = extension[i];
  }
};


/**
 * @param {Array.<VALUE>} arr The array to modify.
 * @param {VALUE} obj The element to remove.
 * @template VALUE
 * @return {boGVolean} If the element was removed.
 */
GVol.array.remove = function(arr, obj) {
  var i = arr.indexOf(obj);
  var found = i > -1;
  if (found) {
    arr.splice(i, 1);
  }
  return found;
};


/**
 * @param {Array.<VALUE>} arr The array to search in.
 * @param {function(VALUE, number, ?) : boGVolean} func The function to compare.
 * @template VALUE
 * @return {VALUE} The element found.
 */
GVol.array.find = function(arr, func) {
  var length = arr.length >>> 0;
  var value;

  for (var i = 0; i < length; i++) {
    value = arr[i];
    if (func(value, i, arr)) {
      return value;
    }
  }
  return null;
};


/**
 * @param {Array|Uint8ClampedArray} arr1 The first array to compare.
 * @param {Array|Uint8ClampedArray} arr2 The second array to compare.
 * @return {boGVolean} Whether the two arrays are equal.
 */
GVol.array.equals = function(arr1, arr2) {
  var len1 = arr1.length;
  if (len1 !== arr2.length) {
    return false;
  }
  for (var i = 0; i < len1; i++) {
    if (arr1[i] !== arr2[i]) {
      return false;
    }
  }
  return true;
};


/**
 * @param {Array.<*>} arr The array to sort (modifies original).
 * @param {Function} compareFnc Comparison function.
 */
GVol.array.stableSort = function(arr, compareFnc) {
  var length = arr.length;
  var tmp = Array(arr.length);
  var i;
  for (i = 0; i < length; i++) {
    tmp[i] = {index: i, value: arr[i]};
  }
  tmp.sort(function(a, b) {
    return compareFnc(a.value, b.value) || a.index - b.index;
  });
  for (i = 0; i < arr.length; i++) {
    arr[i] = tmp[i].value;
  }
};


/**
 * @param {Array.<*>} arr The array to search in.
 * @param {Function} func Comparison function.
 * @return {number} Return index.
 */
GVol.array.findIndex = function(arr, func) {
  var index;
  var found = !arr.every(function(el, idx) {
    index = idx;
    return !func(el, idx, arr);
  });
  return found ? index : -1;
};


/**
 * @param {Array.<*>} arr The array to test.
 * @param {Function=} opt_func Comparison function.
 * @param {boGVolean=} opt_strict Strictly sorted (default false).
 * @return {boGVolean} Return index.
 */
GVol.array.isSorted = function(arr, opt_func, opt_strict) {
  var compare = opt_func || GVol.array.numberSafeCompareFunction;
  return arr.every(function(currentVal, index) {
    if (index === 0) {
      return true;
    }
    var res = compare(arr[index - 1], currentVal);
    return !(res > 0 || opt_strict && res === 0);
  });
};

goog.provide('GVol');


/**
 * 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 {boGVolean} Assume touch.  Default is `false`.
 */
GVol.ASSUME_TOUCH = false;


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


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


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


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


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


/**
 * @define {boGVolean} 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.
 */
GVol.ENABLE_CANVAS = true;


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


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


/**
 * @define {boGVolean} 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.
 */
GVol.ENABLE_WEBGL = true;


/**
 * @define {boGVolean} Include debuggable shader sources.  Default is `true`.
 *     This should be set to `false` for production builds (if `GVol.ENABLE_WEBGL`
 *     is `true`).
 */
GVol.DEBUG_WEBGL = true;


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


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


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


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


/**
 * @define {number} Minimum width and/or height extent ratio that determines
 * when the overview map should be zoomed in.
 */
GVol.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
 *     `GVol.TileState.ERROR` state is set. Default is `100`.
 */
GVol.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`.
 */
GVol.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 `GVol.RASTER_REPROJECTION_MAX_SUBDIVISION`).
 *     Default is `0.25`.
 */
GVol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH = 0.25;


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


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


/**
 * @define {string} OpenLayers version.
 */
GVol.VERSION = '';


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


/**
 * List of supported WebGL extensions.
 * @const
 * @type {Array.<string>}
 */
GVol.WEBGL_EXTENSIONS; // value is set in `GVol.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);
 *     }
 *     GVol.inherits(ChildClass, ParentClass);
 *
 *     var child = new ChildClass('a', 'b', 'see');
 *     child.foo(); // This works.
 *
 * @param {!Function} childCtor Child constructor.
 * @param {!Function} parentCtor Parent constructor.
 * @function
 * @api
 */
GVol.inherits = function(childCtor, parentCtor) {
  childCtor.prototype = Object.create(parentCtor.prototype);
  childCtor.prototype.constructor = childCtor;
};


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


/**
 * 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. Unique IDs are generated
 * as a strictly increasing sequence. Adapted from goog.getUid.
 *
 * @param {Object} obj The object to get the unique ID for.
 * @return {number} The unique ID for the object.
 */
GVol.getUid = function(obj) {
  return obj.GVol_uid ||
      (obj.GVol_uid = ++GVol.uidCounter_);
};


/**
 * Counter for getUid.
 * @type {number}
 * @private
 */
GVol.uidCounter_ = 0;

goog.provide('GVol.AssertionError');

goog.require('GVol');

/**
 * Error object thrown when an assertion failed. This is an ECMA-262 Error,
 * extended with a `code` property.
 * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error}
 * @constructor
 * @extends {Error}
 * @implements {GVoli.AssertionError}
 * @param {number} code Error code.
 */
GVol.AssertionError = function(code) {

  var path = GVol.VERSION ? GVol.VERSION.split('-')[0] : 'latest';

  /**
   * @type {string}
   */
  this.message = 'Assertion failed. See https://openlayers.org/en/' + path +
      '/doc/errors/#' + code + ' for details.';

  /**
   * Error code. The meaning of the code can be found on
   * {@link https://openlayers.org/en/latest/doc/errors/} (replace `latest` with
   * the version found in the OpenLayers script's header comment if a version
   * other than the latest is used).
   * @type {number}
   * @api
   */
  this.code = code;

  this.name = 'AssertionError';

};
GVol.inherits(GVol.AssertionError, Error);

goog.provide('GVol.asserts');

goog.require('GVol.AssertionError');


/**
 * @param {*} assertion Assertion we expected to be truthy.
 * @param {number} errorCode Error code.
 */
GVol.asserts.assert = function(assertion, errorCode) {
  if (!assertion) {
    throw new GVol.AssertionError(errorCode);
  }
};

goog.provide('GVol.TileRange');


/**
 * 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
 */
GVol.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 {number} minX Minimum X.
 * @param {number} maxX Maximum X.
 * @param {number} minY Minimum Y.
 * @param {number} maxY Maximum Y.
 * @param {GVol.TileRange|undefined} tileRange TileRange.
 * @return {GVol.TileRange} Tile range.
 */
GVol.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 GVol.TileRange(minX, maxX, minY, maxY);
  }
};


/**
 * @param {GVol.TileCoord} tileCoord Tile coordinate.
 * @return {boGVolean} Contains tile coordinate.
 */
GVol.TileRange.prototype.contains = function(tileCoord) {
  return this.containsXY(tileCoord[1], tileCoord[2]);
};


/**
 * @param {GVol.TileRange} tileRange Tile range.
 * @return {boGVolean} Contains.
 */
GVol.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 {boGVolean} Contains coordinate.
 */
GVol.TileRange.prototype.containsXY = function(x, y) {
  return this.minX <= x && x <= this.maxX && this.minY <= y && y <= this.maxY;
};


/**
 * @param {GVol.TileRange} tileRange Tile range.
 * @return {boGVolean} Equals.
 */
GVol.TileRange.prototype.equals = function(tileRange) {
  return this.minX == tileRange.minX && this.minY == tileRange.minY &&
      this.maxX == tileRange.maxX && this.maxY == tileRange.maxY;
};


/**
 * @param {GVol.TileRange} tileRange Tile range.
 */
GVol.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.
 */
GVol.TileRange.prototype.getHeight = function() {
  return this.maxY - this.minY + 1;
};


/**
 * @return {GVol.Size} Size.
 */
GVol.TileRange.prototype.getSize = function() {
  return [this.getWidth(), this.getHeight()];
};


/**
 * @return {number} Width.
 */
GVol.TileRange.prototype.getWidth = function() {
  return this.maxX - this.minX + 1;
};


/**
 * @param {GVol.TileRange} tileRange Tile range.
 * @return {boGVolean} Intersects.
 */
GVol.TileRange.prototype.intersects = function(tileRange) {
  return this.minX <= tileRange.maxX &&
      this.maxX >= tileRange.minX &&
      this.minY <= tileRange.maxY &&
      this.maxY >= tileRange.minY;
};

goog.provide('GVol.math');

goog.require('GVol.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.
 */
GVol.math.clamp = function(value, min, max) {
  return Math.min(Math.max(value, min), max);
};


/**
 * Return the hyperbGVolic cosine of a given number. The method will use the
 * native `Math.cosh` function if it is available, otherwise the hyperbGVolic
 * cosine will be calculated via the reference implementation of the Mozilla
 * developer network.
 *
 * @param {number} x X.
 * @return {number} HyperbGVolic cosine of x.
 */
GVol.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.
 */
GVol.math.roundUpToPowerOfTwo = function(x) {
  GVol.asserts.assert(0 < x, 29); // `x` must be greater 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.
 */
GVol.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 GVol.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.
 */
GVol.math.squaredDistance = function(x1, y1, x2, y2) {
  var dx = x2 - x1;
  var dy = y2 - y1;
  return dx * dx + dy * dy;
};


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

  for (var i = 0; i < n; i++) {
    // Find max in the i-th cGVolumn (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 cGVolumn
    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];
        }
      }
    }
  }

  // SGVolve 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.
 */
GVol.math.toDegrees = function(angleInRadians) {
  return angleInRadians * 180 / Math.PI;
};


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

/**
 * Returns the modulo of a / b, depending on the sign of b.
 *
 * @param {number} a Dividend.
 * @param {number} b Divisor.
 * @return {number} Modulo.
 */
GVol.math.modulo = function(a, b) {
  var r = a % b;
  return r * b < 0 ? r + b : r;
};

/**
 * Calculates the linearly interpGVolated value of x between a and b.
 *
 * @param {number} a Number
 * @param {number} b Number
 * @param {number} x Value to be interpGVolated.
 * @return {number} InterpGVolated value.
 */
GVol.math.lerp = function(a, b, x) {
  return a + x * (b - a);
};

goog.provide('GVol.size');


/**
 * Returns a buffered size.
 * @param {GVol.Size} size Size.
 * @param {number} buffer Buffer.
 * @param {GVol.Size=} opt_size Optional reusable size array.
 * @return {GVol.Size} The buffered size.
 */
GVol.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;
};


/**
 * Determines if a size has a positive area.
 * @param {GVol.Size} size The size to test.
 * @return {boGVolean} The size has a positive area.
 */
GVol.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 {GVol.Size} size Size.
 * @param {number} ratio Ratio.
 * @param {GVol.Size=} opt_size Optional reusable size array.
 * @return {GVol.Size} The scaled size.
 */
GVol.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 `GVol.Size` array for the passed in number (meaning: square) or
 * `GVol.Size` array.
 * (meaning: non-square),
 * @param {number|GVol.Size} size Width and height.
 * @param {GVol.Size=} opt_size Optional reusable size array.
 * @return {GVol.Size} Size.
 * @api
 */
GVol.size.toSize = function(size, opt_size) {
  if (Array.isArray(size)) {
    return size;
  } else {
    if (opt_size === undefined) {
      opt_size = [size, size];
    } else {
      opt_size[0] = opt_size[1] = /** @type {number} */ (size);
    }
    return opt_size;
  }
};

goog.provide('GVol.extent.Corner');

/**
 * Extent corner.
 * @enum {string}
 */
GVol.extent.Corner = {
  BOTTOM_LEFT: 'bottom-left',
  BOTTOM_RIGHT: 'bottom-right',
  TOP_LEFT: 'top-left',
  TOP_RIGHT: 'top-right'
};

goog.provide('GVol.extent.Relationship');


/**
 * Relationship to an extent.
 * @enum {number}
 */
GVol.extent.Relationship = {
  UNKNOWN: 0,
  INTERSECTING: 1,
  ABOVE: 2,
  RIGHT: 4,
  BELOW: 8,
  LEFT: 16
};

goog.provide('GVol.extent');

goog.require('GVol.asserts');
goog.require('GVol.extent.Corner');
goog.require('GVol.extent.Relationship');


/**
 * Build an extent that includes all given coordinates.
 *
 * @param {Array.<GVol.Coordinate>} coordinates Coordinates.
 * @return {GVol.Extent} Bounding extent.
 * @api
 */
GVol.extent.boundingExtent = function(coordinates) {
  var extent = GVol.extent.createEmpty();
  for (var i = 0, ii = coordinates.length; i < ii; ++i) {
    GVol.extent.extendCoordinate(extent, coordinates[i]);
  }
  return extent;
};


/**
 * @param {Array.<number>} xs Xs.
 * @param {Array.<number>} ys Ys.
 * @param {GVol.Extent=} opt_extent Destination extent.
 * @private
 * @return {GVol.Extent} Extent.
 */
GVol.extent.boundingExtentXYs_ = function(xs, ys, opt_extent) {
  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 GVol.extent.createOrUpdate(minX, minY, maxX, maxY, opt_extent);
};


/**
 * Return extent increased by the provided value.
 * @param {GVol.Extent} extent Extent.
 * @param {number} value The amount by which the extent should be buffered.
 * @param {GVol.Extent=} opt_extent Extent.
 * @return {GVol.Extent} Extent.
 * @api
 */
GVol.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 {GVol.Extent} extent Extent to clone.
 * @param {GVol.Extent=} opt_extent Extent.
 * @return {GVol.Extent} The clone.
 */
GVol.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 {GVol.Extent} extent Extent.
 * @param {number} x X.
 * @param {number} y Y.
 * @return {number} Closest squared distance.
 */
GVol.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 {GVol.Extent} extent Extent.
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @return {boGVolean} The coordinate is contained in the extent.
 * @api
 */
GVol.extent.containsCoordinate = function(extent, coordinate) {
  return GVol.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 {GVol.Extent} extent1 Extent 1.
 * @param {GVol.Extent} extent2 Extent 2.
 * @return {boGVolean} The second extent is contained by or on the edge of the
 *     first.
 * @api
 */
GVol.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 {GVol.Extent} extent Extent.
 * @param {number} x X coordinate.
 * @param {number} y Y coordinate.
 * @return {boGVolean} The x, y values are contained in the extent.
 * @api
 */
GVol.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 {GVol.Extent} extent The extent.
 * @param {GVol.Coordinate} coordinate The coordinate.
 * @return {number} The relationship (bitwise compare with
 *     GVol.extent.Relationship).
 */
GVol.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 = GVol.extent.Relationship.UNKNOWN;
  if (x < minX) {
    relationship = relationship | GVol.extent.Relationship.LEFT;
  } else if (x > maxX) {
    relationship = relationship | GVol.extent.Relationship.RIGHT;
  }
  if (y < minY) {
    relationship = relationship | GVol.extent.Relationship.BELOW;
  } else if (y > maxY) {
    relationship = relationship | GVol.extent.Relationship.ABOVE;
  }
  if (relationship === GVol.extent.Relationship.UNKNOWN) {
    relationship = GVol.extent.Relationship.INTERSECTING;
  }
  return relationship;
};


/**
 * Create an empty extent.
 * @return {GVol.Extent} Empty extent.
 * @api
 */
GVol.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 {GVol.Extent=} opt_extent Destination extent.
 * @return {GVol.Extent} Extent.
 */
GVol.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 {GVol.Extent=} opt_extent Extent.
 * @return {GVol.Extent} Extent.
 */
GVol.extent.createOrUpdateEmpty = function(opt_extent) {
  return GVol.extent.createOrUpdate(
      Infinity, Infinity, -Infinity, -Infinity, opt_extent);
};


/**
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @param {GVol.Extent=} opt_extent Extent.
 * @return {GVol.Extent} Extent.
 */
GVol.extent.createOrUpdateFromCoordinate = function(coordinate, opt_extent) {
  var x = coordinate[0];
  var y = coordinate[1];
  return GVol.extent.createOrUpdate(x, y, x, y, opt_extent);
};


/**
 * @param {Array.<GVol.Coordinate>} coordinates Coordinates.
 * @param {GVol.Extent=} opt_extent Extent.
 * @return {GVol.Extent} Extent.
 */
GVol.extent.createOrUpdateFromCoordinates = function(coordinates, opt_extent) {
  var extent = GVol.extent.createOrUpdateEmpty(opt_extent);
  return GVol.extent.extendCoordinates(extent, coordinates);
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {GVol.Extent=} opt_extent Extent.
 * @return {GVol.Extent} Extent.
 */
GVol.extent.createOrUpdateFromFlatCoordinates = function(flatCoordinates, offset, end, stride, opt_extent) {
  var extent = GVol.extent.createOrUpdateEmpty(opt_extent);
  return GVol.extent.extendFlatCoordinates(
      extent, flatCoordinates, offset, end, stride);
};


/**
 * @param {Array.<Array.<GVol.Coordinate>>} rings Rings.
 * @param {GVol.Extent=} opt_extent Extent.
 * @return {GVol.Extent} Extent.
 */
GVol.extent.createOrUpdateFromRings = function(rings, opt_extent) {
  var extent = GVol.extent.createOrUpdateEmpty(opt_extent);
  return GVol.extent.extendRings(extent, rings);
};


/**
 * Determine if two extents are equivalent.
 * @param {GVol.Extent} extent1 Extent 1.
 * @param {GVol.Extent} extent2 Extent 2.
 * @return {boGVolean} The two extents are equivalent.
 * @api
 */
GVol.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 {GVol.Extent} extent1 The extent to be modified.
 * @param {GVol.Extent} extent2 The extent that will be included in the first.
 * @return {GVol.Extent} A reference to the first (extended) extent.
 * @api
 */
GVol.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 {GVol.Extent} extent Extent.
 * @param {GVol.Coordinate} coordinate Coordinate.
 */
GVol.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 {GVol.Extent} extent Extent.
 * @param {Array.<GVol.Coordinate>} coordinates Coordinates.
 * @return {GVol.Extent} Extent.
 */
GVol.extent.extendCoordinates = function(extent, coordinates) {
  var i, ii;
  for (i = 0, ii = coordinates.length; i < ii; ++i) {
    GVol.extent.extendCoordinate(extent, coordinates[i]);
  }
  return extent;
};


/**
 * @param {GVol.Extent} extent Extent.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @return {GVol.Extent} Extent.
 */
GVol.extent.extendFlatCoordinates = function(extent, flatCoordinates, offset, end, stride) {
  for (; offset < end; offset += stride) {
    GVol.extent.extendXY(
        extent, flatCoordinates[offset], flatCoordinates[offset + 1]);
  }
  return extent;
};


/**
 * @param {GVol.Extent} extent Extent.
 * @param {Array.<Array.<GVol.Coordinate>>} rings Rings.
 * @return {GVol.Extent} Extent.
 */
GVol.extent.extendRings = function(extent, rings) {
  var i, ii;
  for (i = 0, ii = rings.length; i < ii; ++i) {
    GVol.extent.extendCoordinates(extent, rings[i]);
  }
  return extent;
};


/**
 * @param {GVol.Extent} extent Extent.
 * @param {number} x X.
 * @param {number} y Y.
 */
GVol.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 {GVol.Extent} extent Extent.
 * @param {function(this:T, GVol.Coordinate): S} callback Callback.
 * @param {T=} opt_this Value to use as `this` when executing `callback`.
 * @return {S|boGVolean} Value.
 * @template S, T
 */
GVol.extent.forEachCorner = function(extent, callback, opt_this) {
  var val;
  val = callback.call(opt_this, GVol.extent.getBottomLeft(extent));
  if (val) {
    return val;
  }
  val = callback.call(opt_this, GVol.extent.getBottomRight(extent));
  if (val) {
    return val;
  }
  val = callback.call(opt_this, GVol.extent.getTopRight(extent));
  if (val) {
    return val;
  }
  val = callback.call(opt_this, GVol.extent.getTopLeft(extent));
  if (val) {
    return val;
  }
  return false;
};


/**
 * Get the size of an extent.
 * @param {GVol.Extent} extent Extent.
 * @return {number} Area.
 * @api
 */
GVol.extent.getArea = function(extent) {
  var area = 0;
  if (!GVol.extent.isEmpty(extent)) {
    area = GVol.extent.getWidth(extent) * GVol.extent.getHeight(extent);
  }
  return area;
};


/**
 * Get the bottom left coordinate of an extent.
 * @param {GVol.Extent} extent Extent.
 * @return {GVol.Coordinate} Bottom left coordinate.
 * @api
 */
GVol.extent.getBottomLeft = function(extent) {
  return [extent[0], extent[1]];
};


/**
 * Get the bottom right coordinate of an extent.
 * @param {GVol.Extent} extent Extent.
 * @return {GVol.Coordinate} Bottom right coordinate.
 * @api
 */
GVol.extent.getBottomRight = function(extent) {
  return [extent[2], extent[1]];
};


/**
 * Get the center coordinate of an extent.
 * @param {GVol.Extent} extent Extent.
 * @return {GVol.Coordinate} Center.
 * @api
 */
GVol.extent.getCenter = function(extent) {
  return [(extent[0] + extent[2]) / 2, (extent[1] + extent[3]) / 2];
};


/**
 * Get a corner coordinate of an extent.
 * @param {GVol.Extent} extent Extent.
 * @param {GVol.extent.Corner} corner Corner.
 * @return {GVol.Coordinate} Corner coordinate.
 */
GVol.extent.getCorner = function(extent, corner) {
  var coordinate;
  if (corner === GVol.extent.Corner.BOTTOM_LEFT) {
    coordinate = GVol.extent.getBottomLeft(extent);
  } else if (corner === GVol.extent.Corner.BOTTOM_RIGHT) {
    coordinate = GVol.extent.getBottomRight(extent);
  } else if (corner === GVol.extent.Corner.TOP_LEFT) {
    coordinate = GVol.extent.getTopLeft(extent);
  } else if (corner === GVol.extent.Corner.TOP_RIGHT) {
    coordinate = GVol.extent.getTopRight(extent);
  } else {
    GVol.asserts.assert(false, 13); // Invalid corner
  }
  return /** @type {!GVol.Coordinate} */ (coordinate);
};


/**
 * @param {GVol.Extent} extent1 Extent 1.
 * @param {GVol.Extent} extent2 Extent 2.
 * @return {number} Enlarged area.
 */
GVol.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 {GVol.Coordinate} center Center.
 * @param {number} resGVolution ResGVolution.
 * @param {number} rotation Rotation.
 * @param {GVol.Size} size Size.
 * @param {GVol.Extent=} opt_extent Destination extent.
 * @return {GVol.Extent} Extent.
 */
GVol.extent.getForViewAndSize = function(center, resGVolution, rotation, size, opt_extent) {
  var dx = resGVolution * size[0] / 2;
  var dy = resGVolution * size[1] / 2;
  var cosRotation = Math.cos(rotation);
  var sinRotation = Math.sin(rotation);
  var xCos = dx * cosRotation;
  var xSin = dx * sinRotation;
  var yCos = dy * cosRotation;
  var ySin = dy * sinRotation;
  var x = center[0];
  var y = center[1];
  var x0 = x - xCos + ySin;
  var x1 = x - xCos - ySin;
  var x2 = x + xCos - ySin;
  var x3 = x + xCos + ySin;
  var y0 = y - xSin - yCos;
  var y1 = y - xSin + yCos;
  var y2 = y + xSin + yCos;
  var y3 = y + xSin - yCos;
  return GVol.extent.createOrUpdate(
      Math.min(x0, x1, x2, x3), Math.min(y0, y1, y2, y3),
      Math.max(x0, x1, x2, x3), Math.max(y0, y1, y2, y3),
      opt_extent);
};


/**
 * Get the height of an extent.
 * @param {GVol.Extent} extent Extent.
 * @return {number} Height.
 * @api
 */
GVol.extent.getHeight = function(extent) {
  return extent[3] - extent[1];
};


/**
 * @param {GVol.Extent} extent1 Extent 1.
 * @param {GVol.Extent} extent2 Extent 2.
 * @return {number} Intersection area.
 */
GVol.extent.getIntersectionArea = function(extent1, extent2) {
  var intersection = GVol.extent.getIntersection(extent1, extent2);
  return GVol.extent.getArea(intersection);
};


/**
 * Get the intersection of two extents.
 * @param {GVol.Extent} extent1 Extent 1.
 * @param {GVol.Extent} extent2 Extent 2.
 * @param {GVol.Extent=} opt_extent Optional extent to populate with intersection.
 * @return {GVol.Extent} Intersecting extent.
 * @api
 */
GVol.extent.getIntersection = function(extent1, extent2, opt_extent) {
  var intersection = opt_extent ? opt_extent : GVol.extent.createEmpty();
  if (GVol.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 {GVol.Extent} extent Extent.
 * @return {number} Margin.
 */
GVol.extent.getMargin = function(extent) {
  return GVol.extent.getWidth(extent) + GVol.extent.getHeight(extent);
};


/**
 * Get the size (width, height) of an extent.
 * @param {GVol.Extent} extent The extent.
 * @return {GVol.Size} The extent size.
 * @api
 */
GVol.extent.getSize = function(extent) {
  return [extent[2] - extent[0], extent[3] - extent[1]];
};


/**
 * Get the top left coordinate of an extent.
 * @param {GVol.Extent} extent Extent.
 * @return {GVol.Coordinate} Top left coordinate.
 * @api
 */
GVol.extent.getTopLeft = function(extent) {
  return [extent[0], extent[3]];
};


/**
 * Get the top right coordinate of an extent.
 * @param {GVol.Extent} extent Extent.
 * @return {GVol.Coordinate} Top right coordinate.
 * @api
 */
GVol.extent.getTopRight = function(extent) {
  return [extent[2], extent[3]];
};


/**
 * Get the width of an extent.
 * @param {GVol.Extent} extent Extent.
 * @return {number} Width.
 * @api
 */
GVol.extent.getWidth = function(extent) {
  return extent[2] - extent[0];
};


/**
 * Determine if one extent intersects another.
 * @param {GVol.Extent} extent1 Extent 1.
 * @param {GVol.Extent} extent2 Extent.
 * @return {boGVolean} The two extents intersect.
 * @api
 */
GVol.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 {GVol.Extent} extent Extent.
 * @return {boGVolean} Is empty.
 * @api
 */
GVol.extent.isEmpty = function(extent) {
  return extent[2] < extent[0] || extent[3] < extent[1];
};


/**
 * @param {GVol.Extent} extent Extent.
 * @param {GVol.Extent=} opt_extent Extent.
 * @return {GVol.Extent} Extent.
 */
GVol.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 {GVol.Extent} extent Extent.
 * @param {number} value Value.
 */
GVol.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 {GVol.Extent} extent The extent.
 * @param {GVol.Coordinate} start Segment start coordinate.
 * @param {GVol.Coordinate} end Segment end coordinate.
 * @return {boGVolean} The segment intersects the extent.
 */
GVol.extent.intersectsSegment = function(extent, start, end) {
  var intersects = false;
  var startRel = GVol.extent.coordinateRelationship(extent, start);
  var endRel = GVol.extent.coordinateRelationship(extent, end);
  if (startRel === GVol.extent.Relationship.INTERSECTING ||
      endRel === GVol.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 & GVol.extent.Relationship.ABOVE) &&
        !(startRel & GVol.extent.Relationship.ABOVE)) {
      // potentially intersects top
      x = endX - ((endY - maxY) / slope);
      intersects = x >= minX && x <= maxX;
    }
    if (!intersects && !!(endRel & GVol.extent.Relationship.RIGHT) &&
        !(startRel & GVol.extent.Relationship.RIGHT)) {
      // potentially intersects right
      y = endY - ((endX - maxX) * slope);
      intersects = y >= minY && y <= maxY;
    }
    if (!intersects && !!(endRel & GVol.extent.Relationship.BELOW) &&
        !(startRel & GVol.extent.Relationship.BELOW)) {
      // potentially intersects bottom
      x = endX - ((endY - minY) / slope);
      intersects = x >= minX && x <= maxX;
    }
    if (!intersects && !!(endRel & GVol.extent.Relationship.LEFT) &&
        !(startRel & GVol.extent.Relationship.LEFT)) {
      // potentially intersects left
      y = endY - ((endX - minX) * slope);
      intersects = y >= minY && y <= maxY;
    }

  }
  return intersects;
};


/**
 * Apply a transform function to the extent.
 * @param {GVol.Extent} extent Extent.
 * @param {GVol.TransformFunction} transformFn Transform function.  Called with
 * [minX, minY, maxX, maxY] extent coordinates.
 * @param {GVol.Extent=} opt_extent Destination extent.
 * @return {GVol.Extent} Extent.
 * @api
 */
GVol.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 GVol.extent.boundingExtentXYs_(xs, ys, opt_extent);
};

goog.provide('GVol.obj');


/**
 * PGVolyfill for Object.assign().  Assigns enumerable and own properties from
 * one or more source objects to a target object.
 *
 * @see https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Object/assign
 * @param {!Object} target The target object.
 * @param {...Object} var_sources The source object(s).
 * @return {!Object} The modified target object.
 */
GVol.obj.assign = (typeof Object.assign === 'function') ? Object.assign : function(target, var_sources) {
  if (target === undefined || target === null) {
    throw new TypeError('Cannot convert undefined or null to object');
  }

  var output = Object(target);
  for (var i = 1, ii = arguments.length; i < ii; ++i) {
    var source = arguments[i];
    if (source !== undefined && source !== null) {
      for (var key in source) {
        if (source.hasOwnProperty(key)) {
          output[key] = source[key];
        }
      }
    }
  }
  return output;
};


/**
 * Removes all properties from an object.
 * @param {Object} object The object to clear.
 */
GVol.obj.clear = function(object) {
  for (var property in object) {
    delete object[property];
  }
};


/**
 * Get an array of property values from an object.
 * @param {Object<K,V>} object The object from which to get the values.
 * @return {!Array<V>} The property values.
 * @template K,V
 */
GVol.obj.getValues = function(object) {
  var values = [];
  for (var property in object) {
    values.push(object[property]);
  }
  return values;
};


/**
 * Determine if an object has any properties.
 * @param {Object} object The object to check.
 * @return {boGVolean} The object is empty.
 */
GVol.obj.isEmpty = function(object) {
  var property;
  for (property in object) {
    return false;
  }
  return !property;
};

goog.provide('GVol.geom.GeometryType');


/**
 * The geometry type. One of `'Point'`, `'LineString'`, `'LinearRing'`,
 * `'PGVolygon'`, `'MultiPoint'`, `'MultiLineString'`, `'MultiPGVolygon'`,
 * `'GeometryCGVollection'`, `'Circle'`.
 * @enum {string}
 */
GVol.geom.GeometryType = {
  POINT: 'Point',
  LINE_STRING: 'LineString',
  LINEAR_RING: 'LinearRing',
  POLYGON: 'PGVolygon',
  MULTI_POINT: 'MultiPoint',
  MULTI_LINE_STRING: 'MultiLineString',
  MULTI_POLYGON: 'MultiPGVolygon',
  GEOMETRY_COLLECTION: 'GeometryCGVollection',
  CIRCLE: 'Circle'
};

/**
 * @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('GVol.Sphere');

goog.require('GVol.math');
goog.require('GVol.geom.GeometryType');


/**
 * @classdesc
 * Class to create objects that can be used with {@link
 * GVol.geom.PGVolygon.circular}.
 *
 * For example to create a sphere whose radius is equal to the semi-major
 * axis of the WGS84 ellipsoid:
 *
 * ```js
 * var wgs84Sphere= new GVol.Sphere(6378137);
 * ```
 *
 * @constructor
 * @param {number} radius Radius.
 * @api
 */
GVol.Sphere = function(radius) {

  /**
   * @type {number}
   */
  this.radius = radius;

};


/**
 * Returns the geodesic area for a list of coordinates.
 *
 * [Reference](https://trs-new.jpl.nasa.gov/handle/2014/40409)
 * Robert. G. Chamberlain and William H. Duquette, "Some Algorithms for
 * PGVolygons on a Sphere", JPL Publication 07-03, Jet Propulsion
 * Laboratory, Pasadena, CA, June 2007
 *
 * @param {Array.<GVol.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
 */
GVol.Sphere.prototype.geodesicArea = function(coordinates) {
  return GVol.Sphere.getArea_(coordinates, this.radius);
};


/**
 * Returns the distance from c1 to c2 using the haversine formula.
 *
 * @param {GVol.Coordinate} c1 Coordinate 1.
 * @param {GVol.Coordinate} c2 Coordinate 2.
 * @return {number} Haversine distance.
 * @api
 */
GVol.Sphere.prototype.haversineDistance = function(c1, c2) {
  return GVol.Sphere.getDistance_(c1, c2, this.radius);
};


/**
 * Returns the coordinate at the given distance and bearing from `c1`.
 *
 * @param {GVol.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 {GVol.Coordinate} The target point.
 */
GVol.Sphere.prototype.offset = function(c1, distance, bearing) {
  var lat1 = GVol.math.toRadians(c1[1]);
  var lon1 = GVol.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 [GVol.math.toDegrees(lon), GVol.math.toDegrees(lat)];
};


/**
 * The mean Earth radius (1/3 * (2a + b)) for the WGS84 ellipsoid.
 * https://en.wikipedia.org/wiki/Earth_radius#Mean_radius
 * @type {number}
 */
GVol.Sphere.DEFAULT_RADIUS = 6371008.8;


/**
 * Get the spherical length of a geometry.  This length is the sum of the
 * great circle distances between coordinates.  For pGVolygons, the length is
 * the sum of all rings.  For points, the length is zero.  For multi-part
 * geometries, the length is the sum of the length of each part.
 * @param {GVol.geom.Geometry} geometry A geometry.
 * @param {GVolx.SphereMetricOptions=} opt_options Options for the length
 *     calculation.  By default, geometries are assumed to be in 'EPSG:3857'.
 *     You can change this by providing a `projection` option.
 * @return {number} The spherical length (in meters).
 * @api
 */
GVol.Sphere.getLength = function(geometry, opt_options) {
  var options = opt_options || {};
  var radius = options.radius || GVol.Sphere.DEFAULT_RADIUS;
  var projection = options.projection || 'EPSG:3857';
  geometry = geometry.clone().transform(projection, 'EPSG:4326');
  var type = geometry.getType();
  var length = 0;
  var coordinates, coords, i, ii, j, jj;
  switch (type) {
    case GVol.geom.GeometryType.POINT:
    case GVol.geom.GeometryType.MULTI_POINT: {
      break;
    }
    case GVol.geom.GeometryType.LINE_STRING:
    case GVol.geom.GeometryType.LINEAR_RING: {
      coordinates = /** @type {GVol.geom.SimpleGeometry} */ (geometry).getCoordinates();
      length = GVol.Sphere.getLength_(coordinates, radius);
      break;
    }
    case GVol.geom.GeometryType.MULTI_LINE_STRING:
    case GVol.geom.GeometryType.POLYGON: {
      coordinates = /** @type {GVol.geom.SimpleGeometry} */ (geometry).getCoordinates();
      for (i = 0, ii = coordinates.length; i < ii; ++i) {
        length += GVol.Sphere.getLength_(coordinates[i], radius);
      }
      break;
    }
    case GVol.geom.GeometryType.MULTI_POLYGON: {
      coordinates = /** @type {GVol.geom.SimpleGeometry} */ (geometry).getCoordinates();
      for (i = 0, ii = coordinates.length; i < ii; ++i) {
        coords = coordinates[i];
        for (j = 0, jj = coords.length; j < jj; ++j) {
          length += GVol.Sphere.getLength_(coords[j], radius);
        }
      }
      break;
    }
    case GVol.geom.GeometryType.GEOMETRY_COLLECTION: {
      var geometries = /** @type {GVol.geom.GeometryCGVollection} */ (geometry).getGeometries();
      for (i = 0, ii = geometries.length; i < ii; ++i) {
        length += GVol.Sphere.getLength(geometries[i], opt_options);
      }
      break;
    }
    default: {
      throw new Error('Unsupported geometry type: ' + type);
    }
  }
  return length;
};


/**
 * Get the cumulative great circle length of linestring coordinates (geographic).
 * @param {Array} coordinates Linestring coordinates.
 * @param {number} radius The sphere radius to use.
 * @return {number} The length (in meters).
 */
GVol.Sphere.getLength_ = function(coordinates, radius) {
  var length = 0;
  for (var i = 0, ii = coordinates.length; i < ii - 1; ++i) {
    length += GVol.Sphere.getDistance_(coordinates[i], coordinates[i + 1], radius);
  }
  return length;
};


/**
 * Get the great circle distance between two geographic coordinates.
 * @param {Array} c1 Starting coordinate.
 * @param {Array} c2 Ending coordinate.
 * @param {number} radius The sphere radius to use.
 * @return {number} The great circle distance between the points (in meters).
 */
GVol.Sphere.getDistance_ = function(c1, c2, radius) {
  var lat1 = GVol.math.toRadians(c1[1]);
  var lat2 = GVol.math.toRadians(c2[1]);
  var deltaLatBy2 = (lat2 - lat1) / 2;
  var deltaLonBy2 = GVol.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 * radius * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
};


/**
 * Get the spherical area of a geometry.  This is the area (in meters) assuming
 * that pGVolygon edges are segments of great circles on a sphere.
 * @param {GVol.geom.Geometry} geometry A geometry.
 * @param {GVolx.SphereMetricOptions=} opt_options Options for the area
 *     calculation.  By default, geometries are assumed to be in 'EPSG:3857'.
 *     You can change this by providing a `projection` option.
 * @return {number} The spherical area (in square meters).
 * @api
 */
GVol.Sphere.getArea = function(geometry, opt_options) {
  var options = opt_options || {};
  var radius = options.radius || GVol.Sphere.DEFAULT_RADIUS;
  var projection = options.projection || 'EPSG:3857';
  geometry = geometry.clone().transform(projection, 'EPSG:4326');
  var type = geometry.getType();
  var area = 0;
  var coordinates, coords, i, ii, j, jj;
  switch (type) {
    case GVol.geom.GeometryType.POINT:
    case GVol.geom.GeometryType.MULTI_POINT:
    case GVol.geom.GeometryType.LINE_STRING:
    case GVol.geom.GeometryType.MULTI_LINE_STRING:
    case GVol.geom.GeometryType.LINEAR_RING: {
      break;
    }
    case GVol.geom.GeometryType.POLYGON: {
      coordinates = /** @type {GVol.geom.PGVolygon} */ (geometry).getCoordinates();
      area = Math.abs(GVol.Sphere.getArea_(coordinates[0], radius));
      for (i = 1, ii = coordinates.length; i < ii; ++i) {
        area -= Math.abs(GVol.Sphere.getArea_(coordinates[i], radius));
      }
      break;
    }
    case GVol.geom.GeometryType.MULTI_POLYGON: {
      coordinates = /** @type {GVol.geom.SimpleGeometry} */ (geometry).getCoordinates();
      for (i = 0, ii = coordinates.length; i < ii; ++i) {
        coords = coordinates[i];
        area += Math.abs(GVol.Sphere.getArea_(coords[0], radius));
        for (j = 1, jj = coords.length; j < jj; ++j) {
          area -= Math.abs(GVol.Sphere.getArea_(coords[j], radius));
        }
      }
      break;
    }
    case GVol.geom.GeometryType.GEOMETRY_COLLECTION: {
      var geometries = /** @type {GVol.geom.GeometryCGVollection} */ (geometry).getGeometries();
      for (i = 0, ii = geometries.length; i < ii; ++i) {
        area += GVol.Sphere.getArea(geometries[i], opt_options);
      }
      break;
    }
    default: {
      throw new Error('Unsupported geometry type: ' + type);
    }
  }
  return area;
};


/**
 * Returns the spherical area for a list of coordinates.
 *
 * [Reference](https://trs-new.jpl.nasa.gov/handle/2014/40409)
 * Robert. G. Chamberlain and William H. Duquette, "Some Algorithms for
 * PGVolygons on a Sphere", JPL Publication 07-03, Jet Propulsion
 * Laboratory, Pasadena, CA, June 2007
 *
 * @param {Array.<GVol.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.
 * @param {number} radius The sphere radius.
 * @return {number} Area (in square meters).
 */
GVol.Sphere.getArea_ = function(coordinates, radius) {
  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 += GVol.math.toRadians(x2 - x1) *
        (2 + Math.sin(GVol.math.toRadians(y1)) +
        Math.sin(GVol.math.toRadians(y2)));
    x1 = x2;
    y1 = y2;
  }
  return area * radius * radius / 2.0;
};

goog.provide('GVol.proj.Units');


/**
 * Projection units: `'degrees'`, `'ft'`, `'m'`, `'pixels'`, `'tile-pixels'` or
 * `'us-ft'`.
 * @enum {string}
 */
GVol.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.<GVol.proj.Units, number>}
 * @api
 */
GVol.proj.Units.METERS_PER_UNIT = {};
// use the radius of the Normal sphere
GVol.proj.Units.METERS_PER_UNIT[GVol.proj.Units.DEGREES] =
    2 * Math.PI * 6370997 / 360;
GVol.proj.Units.METERS_PER_UNIT[GVol.proj.Units.FEET] = 0.3048;
GVol.proj.Units.METERS_PER_UNIT[GVol.proj.Units.METERS] = 1;
GVol.proj.Units.METERS_PER_UNIT[GVol.proj.Units.USFEET] = 1200 / 3937;

goog.provide('GVol.proj.proj4');


/**
 * @private
 * @type {Proj4}
 */
GVol.proj.proj4.cache_ = null;


/**
 * Store the proj4 function.
 * @param {Proj4} proj4 The proj4 function.
 */
GVol.proj.proj4.set = function(proj4) {
  GVol.proj.proj4.cache_ = proj4;
};


/**
 * Get proj4.
 * @return {Proj4} The proj4 function set above or available globally.
 */
GVol.proj.proj4.get = function() {
  return GVol.proj.proj4.cache_ || window['proj4'];
};

goog.provide('GVol.proj.Projection');

goog.require('GVol');
goog.require('GVol.proj.Units');
goog.require('GVol.proj.proj4');


/**
 * @classdesc
 * Projection definition class. One of these is created for each projection
 * supported in the application and stored in the {@link GVol.proj} namespace.
 * You can use these in applications, but this is not required, as API params
 * and options use {@link GVol.ProjectionLike} which means the simple string
 * code will suffice.
 *
 * You can use {@link GVol.proj.get} to retrieve the object for a particular
 * projection.
 *
 * The library includes definitions for `EPSG:4326` and `EPSG:3857`, together
 * with the fGVollowing 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 GVol.proj.setProj4}.
 *
 * @constructor
 * @param {GVolx.ProjectionOptions} options Projection options.
 * @struct
 * @api
 */
GVol.proj.Projection = function(options) {
  /**
   * @private
   * @type {string}
   */
  this.code_ = options.code;

  /**
   * @private
   * @type {GVol.proj.Units}
   */
  this.units_ = /** @type {GVol.proj.Units} */ (options.units);

  /**
   * @private
   * @type {GVol.Extent}
   */
  this.extent_ = options.extent !== undefined ? options.extent : null;

  /**
   * @private
   * @type {GVol.Extent}
   */
  this.worldExtent_ = options.worldExtent !== undefined ?
    options.worldExtent : null;

  /**
   * @private
   * @type {string}
   */
  this.axisOrientation_ = options.axisOrientation !== undefined ?
    options.axisOrientation : 'enu';

  /**
   * @private
   * @type {boGVolean}
   */
  this.global_ = options.global !== undefined ? options.global : false;

  /**
   * @private
   * @type {boGVolean}
   */
  this.canWrapX_ = !!(this.global_ && this.extent_);

  /**
   * @private
   * @type {function(number, GVol.Coordinate):number|undefined}
   */
  this.getPointResGVolutionFunc_ = options.getPointResGVolution;

  /**
   * @private
   * @type {GVol.tilegrid.TileGrid}
   */
  this.defaultTileGrid_ = null;

  /**
   * @private
   * @type {number|undefined}
   */
  this.metersPerUnit_ = options.metersPerUnit;

  var code = options.code;
  if (GVol.ENABLE_PROJ4JS) {
    var proj4js = GVol.proj.proj4.get();
    if (typeof proj4js == 'function') {
      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;
        }
      }
    }
  }
};


/**
 * @return {boGVolean} The projection is suitable for wrapping the x-axis
 */
GVol.proj.Projection.prototype.canWrapX = function() {
  return this.canWrapX_;
};


/**
 * Get the code for this projection, e.g. 'EPSG:4326'.
 * @return {string} Code.
 * @api
 */
GVol.proj.Projection.prototype.getCode = function() {
  return this.code_;
};


/**
 * Get the validity extent for this projection.
 * @return {GVol.Extent} Extent.
 * @api
 */
GVol.proj.Projection.prototype.getExtent = function() {
  return this.extent_;
};


/**
 * Get the units of this projection.
 * @return {GVol.proj.Units} Units.
 * @api
 */
GVol.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
 */
GVol.proj.Projection.prototype.getMetersPerUnit = function() {
  return this.metersPerUnit_ || GVol.proj.Units.METERS_PER_UNIT[this.units_];
};


/**
 * Get the world extent for this projection.
 * @return {GVol.Extent} Extent.
 * @api
 */
GVol.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.
 */
GVol.proj.Projection.prototype.getAxisOrientation = function() {
  return this.axisOrientation_;
};


/**
 * Is this projection a global projection which spans the whGVole world?
 * @return {boGVolean} Whether the projection is global.
 * @api
 */
GVol.proj.Projection.prototype.isGlobal = function() {
  return this.global_;
};


/**
* Set if the projection is a global projection which spans the whGVole world
* @param {boGVolean} global Whether the projection is global.
* @api
*/
GVol.proj.Projection.prototype.setGlobal = function(global) {
  this.global_ = global;
  this.canWrapX_ = !!(global && this.extent_);
};


/**
 * @return {GVol.tilegrid.TileGrid} The default tile grid.
 */
GVol.proj.Projection.prototype.getDefaultTileGrid = function() {
  return this.defaultTileGrid_;
};


/**
 * @param {GVol.tilegrid.TileGrid} tileGrid The default tile grid.
 */
GVol.proj.Projection.prototype.setDefaultTileGrid = function(tileGrid) {
  this.defaultTileGrid_ = tileGrid;
};


/**
 * Set the validity extent for this projection.
 * @param {GVol.Extent} extent Extent.
 * @api
 */
GVol.proj.Projection.prototype.setExtent = function(extent) {
  this.extent_ = extent;
  this.canWrapX_ = !!(this.global_ && extent);
};


/**
 * Set the world extent for this projection.
 * @param {GVol.Extent} worldExtent World extent
 *     [minlon, minlat, maxlon, maxlat].
 * @api
 */
GVol.proj.Projection.prototype.setWorldExtent = function(worldExtent) {
  this.worldExtent_ = worldExtent;
};


/**
 * Set the getPointResGVolution function (see {@link GVol.proj#getPointResGVolution}
 * for this projection.
 * @param {function(number, GVol.Coordinate):number} func Function
 * @api
 */
GVol.proj.Projection.prototype.setGetPointResGVolution = function(func) {
  this.getPointResGVolutionFunc_ = func;
};


/**
 * Get the custom point resGVolution function for this projection (if set).
 * @return {function(number, GVol.Coordinate):number|undefined} The custom point
 * resGVolution function (if set).
 */
GVol.proj.Projection.prototype.getPointResGVolutionFunc = function() {
  return this.getPointResGVolutionFunc_;
};

goog.provide('GVol.proj.EPSG3857');

goog.require('GVol');
goog.require('GVol.math');
goog.require('GVol.proj.Projection');
goog.require('GVol.proj.Units');


/**
 * @classdesc
 * Projection object for web/spherical Mercator (EPSG:3857).
 *
 * @constructor
 * @extends {GVol.proj.Projection}
 * @param {string} code Code.
 * @private
 */
GVol.proj.EPSG3857.Projection_ = function(code) {
  GVol.proj.Projection.call(this, {
    code: code,
    units: GVol.proj.Units.METERS,
    extent: GVol.proj.EPSG3857.EXTENT,
    global: true,
    worldExtent: GVol.proj.EPSG3857.WORLD_EXTENT,
    getPointResGVolution: function(resGVolution, point) {
      return resGVolution / GVol.math.cosh(point[1] / GVol.proj.EPSG3857.RADIUS);
    }
  });
};
GVol.inherits(GVol.proj.EPSG3857.Projection_, GVol.proj.Projection);


/**
 * Radius of WGS84 sphere
 *
 * @const
 * @type {number}
 */
GVol.proj.EPSG3857.RADIUS = 6378137;


/**
 * @const
 * @type {number}
 */
GVol.proj.EPSG3857.HALF_SIZE = Math.PI * GVol.proj.EPSG3857.RADIUS;


/**
 * @const
 * @type {GVol.Extent}
 */
GVol.proj.EPSG3857.EXTENT = [
  -GVol.proj.EPSG3857.HALF_SIZE, -GVol.proj.EPSG3857.HALF_SIZE,
  GVol.proj.EPSG3857.HALF_SIZE, GVol.proj.EPSG3857.HALF_SIZE
];


/**
 * @const
 * @type {GVol.Extent}
 */
GVol.proj.EPSG3857.WORLD_EXTENT = [-180, -85, 180, 85];


/**
 * Projections equal to EPSG:3857.
 *
 * @const
 * @type {Array.<GVol.proj.Projection>}
 */
GVol.proj.EPSG3857.PROJECTIONS = [
  new GVol.proj.EPSG3857.Projection_('EPSG:3857'),
  new GVol.proj.EPSG3857.Projection_('EPSG:102100'),
  new GVol.proj.EPSG3857.Projection_('EPSG:102113'),
  new GVol.proj.EPSG3857.Projection_('EPSG:900913'),
  new GVol.proj.EPSG3857.Projection_('urn:ogc:def:crs:EPSG:6.18:3:3857'),
  new GVol.proj.EPSG3857.Projection_('urn:ogc:def:crs:EPSG::3857'),
  new GVol.proj.EPSG3857.Projection_('http://www.opengis.net/gml/srs/epsg.xml#3857')
];


/**
 * 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.
 */
GVol.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);
    }
  }
  var halfSize = GVol.proj.EPSG3857.HALF_SIZE;
  for (var i = 0; i < length; i += dimension) {
    output[i] = halfSize * input[i] / 180;
    var y = GVol.proj.EPSG3857.RADIUS *
        Math.log(Math.tan(Math.PI * (input[i + 1] + 90) / 360));
    if (y > halfSize) {
      y = halfSize;
    } else if (y < -halfSize) {
      y = -halfSize;
    }
    output[i + 1] = y;
  }
  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.
 */
GVol.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);
    }
  }
  for (var i = 0; i < length; i += dimension) {
    output[i] = 180 * input[i] / GVol.proj.EPSG3857.HALF_SIZE;
    output[i + 1] = 360 * Math.atan(
        Math.exp(input[i + 1] / GVol.proj.EPSG3857.RADIUS)) / Math.PI - 90;
  }
  return output;
};

goog.provide('GVol.proj.EPSG4326');

goog.require('GVol');
goog.require('GVol.proj.Projection');
goog.require('GVol.proj.Units');


/**
 * @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 {GVol.proj.Projection}
 * @param {string} code Code.
 * @param {string=} opt_axisOrientation Axis orientation.
 * @private
 */
GVol.proj.EPSG4326.Projection_ = function(code, opt_axisOrientation) {
  GVol.proj.Projection.call(this, {
    code: code,
    units: GVol.proj.Units.DEGREES,
    extent: GVol.proj.EPSG4326.EXTENT,
    axisOrientation: opt_axisOrientation,
    global: true,
    metersPerUnit: GVol.proj.EPSG4326.METERS_PER_UNIT,
    worldExtent: GVol.proj.EPSG4326.EXTENT
  });
};
GVol.inherits(GVol.proj.EPSG4326.Projection_, GVol.proj.Projection);


/**
 * Radius of WGS84 sphere
 *
 * @const
 * @type {number}
 */
GVol.proj.EPSG4326.RADIUS = 6378137;


/**
 * Extent of the EPSG:4326 projection which is the whGVole world.
 *
 * @const
 * @type {GVol.Extent}
 */
GVol.proj.EPSG4326.EXTENT = [-180, -90, 180, 90];


/**
 * @const
 * @type {number}
 */
GVol.proj.EPSG4326.METERS_PER_UNIT = Math.PI * GVol.proj.EPSG4326.RADIUS / 180;


/**
 * Projections equal to EPSG:4326.
 *
 * @const
 * @type {Array.<GVol.proj.Projection>}
 */
GVol.proj.EPSG4326.PROJECTIONS = [
  new GVol.proj.EPSG4326.Projection_('CRS:84'),
  new GVol.proj.EPSG4326.Projection_('EPSG:4326', 'neu'),
  new GVol.proj.EPSG4326.Projection_('urn:ogc:def:crs:EPSG::4326', 'neu'),
  new GVol.proj.EPSG4326.Projection_('urn:ogc:def:crs:EPSG:6.6:4326', 'neu'),
  new GVol.proj.EPSG4326.Projection_('urn:ogc:def:crs:OGC:1.3:CRS84'),
  new GVol.proj.EPSG4326.Projection_('urn:ogc:def:crs:OGC:2:84'),
  new GVol.proj.EPSG4326.Projection_('http://www.opengis.net/gml/srs/epsg.xml#4326', 'neu'),
  new GVol.proj.EPSG4326.Projection_('urn:x-ogc:def:crs:EPSG:4326', 'neu')
];

goog.provide('GVol.proj.projections');


/**
 * @private
 * @type {Object.<string, GVol.proj.Projection>}
 */
GVol.proj.projections.cache_ = {};


/**
 * Clear the projections cache.
 */
GVol.proj.projections.clear = function() {
  GVol.proj.projections.cache_ = {};
};


/**
 * Get a cached projection by code.
 * @param {string} code The code for the projection.
 * @return {GVol.proj.Projection} The projection (if cached).
 */
GVol.proj.projections.get = function(code) {
  var projections = GVol.proj.projections.cache_;
  return projections[code] || null;
};


/**
 * Add a projection to the cache.
 * @param {string} code The projection code.
 * @param {GVol.proj.Projection} projection The projection to cache.
 */
GVol.proj.projections.add = function(code, projection) {
  var projections = GVol.proj.projections.cache_;
  projections[code] = projection;
};

goog.provide('GVol.proj.transforms');

goog.require('GVol.obj');


/**
 * @private
 * @type {Object.<string, Object.<string, GVol.TransformFunction>>}
 */
GVol.proj.transforms.cache_ = {};


/**
 * Clear the transform cache.
 */
GVol.proj.transforms.clear = function() {
  GVol.proj.transforms.cache_ = {};
};


/**
 * Registers a conversion function to convert coordinates from the source
 * projection to the destination projection.
 *
 * @param {GVol.proj.Projection} source Source.
 * @param {GVol.proj.Projection} destination Destination.
 * @param {GVol.TransformFunction} transformFn Transform.
 */
GVol.proj.transforms.add = function(source, destination, transformFn) {
  var sourceCode = source.getCode();
  var destinationCode = destination.getCode();
  var transforms = GVol.proj.transforms.cache_;
  if (!(sourceCode in transforms)) {
    transforms[sourceCode] = {};
  }
  transforms[sourceCode][destinationCode] = transformFn;
};


/**
 * 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 {GVol.proj.Projection} source Source projection.
 * @param {GVol.proj.Projection} destination Destination projection.
 * @return {GVol.TransformFunction} transformFn The unregistered transform.
 */
GVol.proj.transforms.remove = function(source, destination) {
  var sourceCode = source.getCode();
  var destinationCode = destination.getCode();
  var transforms = GVol.proj.transforms.cache_;
  var transform = transforms[sourceCode][destinationCode];
  delete transforms[sourceCode][destinationCode];
  if (GVol.obj.isEmpty(transforms[sourceCode])) {
    delete transforms[sourceCode];
  }
  return transform;
};


/**
 * Get a transform given a source code and a destination code.
 * @param {string} sourceCode The code for the source projection.
 * @param {string} destinationCode The code for the destination projection.
 * @return {GVol.TransformFunction|undefined} The transform function (if found).
 */
GVol.proj.transforms.get = function(sourceCode, destinationCode) {
  var transform;
  var transforms = GVol.proj.transforms.cache_;
  if (sourceCode in transforms && destinationCode in transforms[sourceCode]) {
    transform = transforms[sourceCode][destinationCode];
  }
  return transform;
};

goog.provide('GVol.proj');

goog.require('GVol');
goog.require('GVol.Sphere');
goog.require('GVol.extent');
goog.require('GVol.proj.EPSG3857');
goog.require('GVol.proj.EPSG4326');
goog.require('GVol.proj.Projection');
goog.require('GVol.proj.Units');
goog.require('GVol.proj.proj4');
goog.require('GVol.proj.projections');
goog.require('GVol.proj.transforms');


/**
 * Meters per unit lookup table.
 * @const
 * @type {Object.<GVol.proj.Units, number>}
 * @api
 */
GVol.proj.METERS_PER_UNIT = GVol.proj.Units.METERS_PER_UNIT;


/**
 * A place to store the mean radius of the Earth.
 * @private
 * @type {GVol.Sphere}
 */
GVol.proj.SPHERE_ = new GVol.Sphere(GVol.Sphere.DEFAULT_RADIUS);


if (GVol.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 GVol from 'openlayers';
   *     import proj4 from 'proj4';
   *     GVol.proj.setProj4(proj4);
   *
   * @param {Proj4} proj4 Proj4.
   * @api
   */
  GVol.proj.setProj4 = function(proj4) {
    GVol.proj.proj4.set(proj4);
  };
}


/**
 * Get the resGVolution of the point in degrees or distance units.
 * For projections with degrees as the unit this will simply return the
 * provided resGVolution. For other projections the point resGVolution is
 * by default 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.
 * A custom function can be provided for a specific projection, either
 * by setting the `getPointResGVolution` option in the
 * {@link GVol.proj.Projection} constructor or by using
 * {@link GVol.proj.Projection#setGetPointResGVolution} to change an existing
 * projection object.
 * @param {GVol.ProjectionLike} projection The projection.
 * @param {number} resGVolution Nominal resGVolution in projection units.
 * @param {GVol.Coordinate} point Point to find adjusted resGVolution at.
 * @param {GVol.proj.Units=} opt_units Units to get the point resGVolution in.
 * Default is the projection's units.
 * @return {number} Point resGVolution.
 * @api
 */
GVol.proj.getPointResGVolution = function(projection, resGVolution, point, opt_units) {
  projection = GVol.proj.get(projection);
  var pointResGVolution;
  var getter = projection.getPointResGVolutionFunc();
  if (getter) {
    pointResGVolution = getter(resGVolution, point);
  } else {
    var units = projection.getUnits();
    if (units == GVol.proj.Units.DEGREES && !opt_units || opt_units == GVol.proj.Units.DEGREES) {
      pointResGVolution = resGVolution;
    } else {
      // Estimate point resGVolution 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 = GVol.proj.getTransformFromProjections(projection, GVol.proj.get('EPSG:4326'));
      var vertices = [
        point[0] - resGVolution / 2, point[1],
        point[0] + resGVolution / 2, point[1],
        point[0], point[1] - resGVolution / 2,
        point[0], point[1] + resGVolution / 2
      ];
      vertices = toEPSG4326(vertices, vertices, 2);
      var width = GVol.proj.SPHERE_.haversineDistance(
          vertices.slice(0, 2), vertices.slice(2, 4));
      var height = GVol.proj.SPHERE_.haversineDistance(
          vertices.slice(4, 6), vertices.slice(6, 8));
      pointResGVolution = (width + height) / 2;
      var metersPerUnit = opt_units ?
        GVol.proj.Units.METERS_PER_UNIT[opt_units] :
        projection.getMetersPerUnit();
      if (metersPerUnit !== undefined) {
        pointResGVolution /= metersPerUnit;
      }
    }
  }
  return pointResGVolution;
};


/**
 * Registers transformation functions that don't alter coordinates. Those allow
 * to transform between projections with equal meaning.
 *
 * @param {Array.<GVol.proj.Projection>} projections Projections.
 * @api
 */
GVol.proj.addEquivalentProjections = function(projections) {
  GVol.proj.addProjections(projections);
  projections.forEach(function(source) {
    projections.forEach(function(destination) {
      if (source !== destination) {
        GVol.proj.transforms.add(source, destination, GVol.proj.cloneTransform);
      }
    });
  });
};


/**
 * Registers transformation functions to convert coordinates in any projection
 * in projection1 to any projection in projection2.
 *
 * @param {Array.<GVol.proj.Projection>} projections1 Projections with equal
 *     meaning.
 * @param {Array.<GVol.proj.Projection>} projections2 Projections with equal
 *     meaning.
 * @param {GVol.TransformFunction} forwardTransform Transformation from any
 *   projection in projection1 to any projection in projection2.
 * @param {GVol.TransformFunction} inverseTransform Transform from any projection
 *   in projection2 to any projection in projection1..
 */
GVol.proj.addEquivalentTransforms = function(projections1, projections2, forwardTransform, inverseTransform) {
  projections1.forEach(function(projection1) {
    projections2.forEach(function(projection2) {
      GVol.proj.transforms.add(projection1, projection2, forwardTransform);
      GVol.proj.transforms.add(projection2, projection1, inverseTransform);
    });
  });
};


/**
 * Add a Projection object to the list of supported projections that can be
 * looked up by their code.
 *
 * @param {GVol.proj.Projection} projection Projection instance.
 * @api
 */
GVol.proj.addProjection = function(projection) {
  GVol.proj.projections.add(projection.getCode(), projection);
  GVol.proj.transforms.add(projection, projection, GVol.proj.cloneTransform);
};


/**
 * @param {Array.<GVol.proj.Projection>} projections Projections.
 */
GVol.proj.addProjections = function(projections) {
  projections.forEach(GVol.proj.addProjection);
};


/**
 * Clear all cached projections and transforms.
 */
GVol.proj.clearAllProjections = function() {
  GVol.proj.projections.clear();
  GVol.proj.transforms.clear();
};


/**
 * @param {GVol.proj.Projection|string|undefined} projection Projection.
 * @param {string} defaultCode Default code.
 * @return {GVol.proj.Projection} Projection.
 */
GVol.proj.createProjection = function(projection, defaultCode) {
  if (!projection) {
    return GVol.proj.get(defaultCode);
  } else if (typeof projection === 'string') {
    return GVol.proj.get(projection);
  } else {
    return /** @type {GVol.proj.Projection} */ (projection);
  }
};


/**
 * 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 {GVol.ProjectionLike} source Source projection.
 * @param {GVol.ProjectionLike} destination Destination projection.
 * @param {function(GVol.Coordinate): GVol.Coordinate} forward The forward transform
 *     function (that is, from the source projection to the destination
 *     projection) that takes a {@link GVol.Coordinate} as argument and returns
 *     the transformed {@link GVol.Coordinate}.
 * @param {function(GVol.Coordinate): GVol.Coordinate} inverse The inverse transform
 *     function (that is, from the destination projection to the source
 *     projection) that takes a {@link GVol.Coordinate} as argument and returns
 *     the transformed {@link GVol.Coordinate}.
 * @api
 */
GVol.proj.addCoordinateTransforms = function(source, destination, forward, inverse) {
  var sourceProj = GVol.proj.get(source);
  var destProj = GVol.proj.get(destination);
  GVol.proj.transforms.add(sourceProj, destProj,
      GVol.proj.createTransformFromCoordinateTransform(forward));
  GVol.proj.transforms.add(destProj, sourceProj,
      GVol.proj.createTransformFromCoordinateTransform(inverse));
};


/**
 * Creates a {@link GVol.TransformFunction} from a simple 2D coordinate transform
 * function.
 * @param {function(GVol.Coordinate): GVol.Coordinate} transform Coordinate
 *     transform.
 * @return {GVol.TransformFunction} Transform function.
 */
GVol.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;
    });
};


/**
 * Transforms a coordinate from longitude/latitude to a different projection.
 * @param {GVol.Coordinate} coordinate Coordinate as longitude and latitude, i.e.
 *     an array with longitude as 1st and latitude as 2nd element.
 * @param {GVol.ProjectionLike=} opt_projection Target projection. The
 *     default is Web Mercator, i.e. 'EPSG:3857'.
 * @return {GVol.Coordinate} Coordinate projected to the target projection.
 * @api
 */
GVol.proj.fromLonLat = function(coordinate, opt_projection) {
  return GVol.proj.transform(coordinate, 'EPSG:4326',
      opt_projection !== undefined ? opt_projection : 'EPSG:3857');
};


/**
 * Transforms a coordinate to longitude/latitude.
 * @param {GVol.Coordinate} coordinate Projected coordinate.
 * @param {GVol.ProjectionLike=} opt_projection Projection of the coordinate.
 *     The default is Web Mercator, i.e. 'EPSG:3857'.
 * @return {GVol.Coordinate} Coordinate as longitude and latitude, i.e. an array
 *     with longitude as 1st and latitude as 2nd element.
 * @api
 */
GVol.proj.toLonLat = function(coordinate, opt_projection) {
  return GVol.proj.transform(coordinate,
      opt_projection !== undefined ? opt_projection : 'EPSG:3857', 'EPSG:4326');
};


/**
 * Fetches a Projection object for the code specified.
 *
 * @param {GVol.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 {GVol.proj.Projection} Projection object, or null if not in list.
 * @api
 */
GVol.proj.get = function(projectionLike) {
  var projection = null;
  if (projectionLike instanceof GVol.proj.Projection) {
    projection = projectionLike;
  } else if (typeof projectionLike === 'string') {
    var code = projectionLike;
    projection = GVol.proj.projections.get(code);
    if (GVol.ENABLE_PROJ4JS && !projection) {
      var proj4js = GVol.proj.proj4.get();
      if (typeof proj4js == 'function' &&
          proj4js.defs(code) !== undefined) {
        projection = new GVol.proj.Projection({code: code});
        GVol.proj.addProjection(projection);
      }
    }
  }
  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 {GVol.proj.Projection} projection1 Projection 1.
 * @param {GVol.proj.Projection} projection2 Projection 2.
 * @return {boGVolean} Equivalent.
 * @api
 */
GVol.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 = GVol.proj.getTransformFromProjections(
        projection1, projection2);
    return transformFn === GVol.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 {GVol.ProjectionLike} source Source.
 * @param {GVol.ProjectionLike} destination Destination.
 * @return {GVol.TransformFunction} Transform function.
 * @api
 */
GVol.proj.getTransform = function(source, destination) {
  var sourceProjection = GVol.proj.get(source);
  var destinationProjection = GVol.proj.get(destination);
  return GVol.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 {GVol.proj.Projection} sourceProjection Source Projection object.
 * @param {GVol.proj.Projection} destinationProjection Destination Projection
 *     object.
 * @return {GVol.TransformFunction} Transform function.
 */
GVol.proj.getTransformFromProjections = function(sourceProjection, destinationProjection) {
  var sourceCode = sourceProjection.getCode();
  var destinationCode = destinationProjection.getCode();
  var transform = GVol.proj.transforms.get(sourceCode, destinationCode);
  if (GVol.ENABLE_PROJ4JS && !transform) {
    var proj4js = GVol.proj.proj4.get();
    if (typeof proj4js == 'function') {
      var sourceDef = proj4js.defs(sourceCode);
      var destinationDef = proj4js.defs(destinationCode);

      if (sourceDef !== undefined && destinationDef !== undefined) {
        if (sourceDef === destinationDef) {
          GVol.proj.addEquivalentProjections([destinationProjection, sourceProjection]);
        } else {
          var proj4Transform = proj4js(destinationCode, sourceCode);
          GVol.proj.addCoordinateTransforms(destinationProjection, sourceProjection,
              proj4Transform.forward, proj4Transform.inverse);
        }
        transform = GVol.proj.transforms.get(sourceCode, destinationCode);
      }
    }
  }
  if (!transform) {
    transform = GVol.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).
 */
GVol.proj.identityTransform = function(input, opt_output, opt_dimension) {
  if (opt_output !== undefined && input !== opt_output) {
    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).
 */
GVol.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 GVol.proj.transformExtent} for extent transformation.
 * See the transform method of {@link GVol.geom.Geometry} and its subclasses for
 * geometry transforms.
 *
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @param {GVol.ProjectionLike} source Source projection-like.
 * @param {GVol.ProjectionLike} destination Destination projection-like.
 * @return {GVol.Coordinate} Coordinate.
 * @api
 */
GVol.proj.transform = function(coordinate, source, destination) {
  var transformFn = GVol.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 {GVol.Extent} extent The extent to transform.
 * @param {GVol.ProjectionLike} source Source projection-like.
 * @param {GVol.ProjectionLike} destination Destination projection-like.
 * @return {GVol.Extent} The transformed extent.
 * @api
 */
GVol.proj.transformExtent = function(extent, source, destination) {
  var transformFn = GVol.proj.getTransform(source, destination);
  return GVol.extent.applyTransform(extent, transformFn);
};


/**
 * Transforms the given point to the destination projection.
 *
 * @param {GVol.Coordinate} point Point.
 * @param {GVol.proj.Projection} sourceProjection Source projection.
 * @param {GVol.proj.Projection} destinationProjection Destination projection.
 * @return {GVol.Coordinate} Point.
 */
GVol.proj.transformWithProjections = function(point, sourceProjection, destinationProjection) {
  var transformFn = GVol.proj.getTransformFromProjections(
      sourceProjection, destinationProjection);
  return transformFn(point);
};

/**
 * Add transforms to and from EPSG:4326 and EPSG:3857.  This function is called
 * by when this module is executed and should only need to be called again after
 * `GVol.proj.clearAllProjections()` is called (e.g. in tests).
 */
GVol.proj.addCommon = function() {
  // Add transformations that don't alter coordinates to convert within set of
  // projections with equal meaning.
  GVol.proj.addEquivalentProjections(GVol.proj.EPSG3857.PROJECTIONS);
  GVol.proj.addEquivalentProjections(GVol.proj.EPSG4326.PROJECTIONS);
  // Add transformations to convert EPSG:4326 like coordinates to EPSG:3857 like
  // coordinates and back.
  GVol.proj.addEquivalentTransforms(
      GVol.proj.EPSG4326.PROJECTIONS,
      GVol.proj.EPSG3857.PROJECTIONS,
      GVol.proj.EPSG3857.fromEPSG4326,
      GVol.proj.EPSG3857.toEPSG4326);
};

GVol.proj.addCommon();

goog.provide('GVol.tilecoord');


/**
 * @param {number} z Z.
 * @param {number} x X.
 * @param {number} y Y.
 * @param {GVol.TileCoord=} opt_tileCoord Tile coordinate.
 * @return {GVol.TileCoord} Tile coordinate.
 */
GVol.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.
 */
GVol.tilecoord.getKeyZXY = function(z, x, y) {
  return z + '/' + x + '/' + y;
};


/**
 * @param {GVol.TileCoord} tileCoord Tile coord.
 * @return {number} Hash.
 */
GVol.tilecoord.hash = function(tileCoord) {
  return (tileCoord[1] << tileCoord[0]) + tileCoord[2];
};


/**
 * @param {GVol.TileCoord} tileCoord Tile coord.
 * @return {string} Quad key.
 */
GVol.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) {
    // 48 is charCode for 0 - '0'.charCodeAt(0)
    charCode = 48;
    if (tileCoord[1] & mask) {
      charCode += 1;
    }
    if (tileCoord[2] & mask) {
      charCode += 2;
    }
    digits[i] = String.fromCharCode(charCode);
    mask >>= 1;
  }
  return digits.join('');
};


/**
 * @param {GVol.TileCoord} tileCoord Tile coordinate.
 * @param {!GVol.tilegrid.TileGrid} tileGrid Tile grid.
 * @return {boGVolean} Tile coordinate is within extent and zoom level range.
 */
GVol.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('GVol.tilegrid.TileGrid');

goog.require('GVol');
goog.require('GVol.asserts');
goog.require('GVol.TileRange');
goog.require('GVol.array');
goog.require('GVol.extent');
goog.require('GVol.math');
goog.require('GVol.size');
goog.require('GVol.tilecoord');


/**
 * @classdesc
 * Base class for setting the grid pattern for sources accessing tiled-image
 * servers.
 *
 * @constructor
 * @param {GVolx.tilegrid.TileGridOptions} options Tile grid options.
 * @struct
 * @api
 */
GVol.tilegrid.TileGrid = function(options) {

  /**
   * @protected
   * @type {number}
   */
  this.minZoom = options.minZoom !== undefined ? options.minZoom : 0;

  /**
   * @private
   * @type {!Array.<number>}
   */
  this.resGVolutions_ = options.resGVolutions;
  GVol.asserts.assert(GVol.array.isSorted(this.resGVolutions_, function(a, b) {
    return b - a;
  }, true), 17); // `resGVolutions` must be sorted in descending order

  /**
   * @protected
   * @type {number}
   */
  this.maxZoom = this.resGVolutions_.length - 1;

  /**
   * @private
   * @type {GVol.Coordinate}
   */
  this.origin_ = options.origin !== undefined ? options.origin : null;

  /**
   * @private
   * @type {Array.<GVol.Coordinate>}
   */
  this.origins_ = null;
  if (options.origins !== undefined) {
    this.origins_ = options.origins;
    GVol.asserts.assert(this.origins_.length == this.resGVolutions_.length,
        20); // Number of `origins` and `resGVolutions` must be equal
  }

  var extent = options.extent;

  if (extent !== undefined &&
      !this.origin_ && !this.origins_) {
    this.origin_ = GVol.extent.getTopLeft(extent);
  }

  GVol.asserts.assert(
      (!this.origin_ && this.origins_) || (this.origin_ && !this.origins_),
      18); // Either `origin` or `origins` must be configured, never both

  /**
   * @private
   * @type {Array.<number|GVol.Size>}
   */
  this.tileSizes_ = null;
  if (options.tileSizes !== undefined) {
    this.tileSizes_ = options.tileSizes;
    GVol.asserts.assert(this.tileSizes_.length == this.resGVolutions_.length,
        19); // Number of `tileSizes` and `resGVolutions` must be equal
  }

  /**
   * @private
   * @type {number|GVol.Size}
   */
  this.tileSize_ = options.tileSize !== undefined ?
    options.tileSize :
    !this.tileSizes_ ? GVol.DEFAULT_TILE_SIZE : null;
  GVol.asserts.assert(
      (!this.tileSize_ && this.tileSizes_) ||
      (this.tileSize_ && !this.tileSizes_),
      22); // Either `tileSize` or `tileSizes` must be configured, never both

  /**
   * @private
   * @type {GVol.Extent}
   */
  this.extent_ = extent !== undefined ? extent : null;


  /**
   * @private
   * @type {Array.<GVol.TileRange>}
   */
  this.fullTileRanges_ = null;

  /**
   * @private
   * @type {GVol.Size}
   */
  this.tmpSize_ = [0, 0];

  if (options.sizes !== undefined) {
    this.fullTileRanges_ = options.sizes.map(function(size, z) {
      var tileRange = new GVol.TileRange(
          Math.min(0, size[0]), Math.max(size[0] - 1, -1),
          Math.min(0, size[1]), Math.max(size[1] - 1, -1));
      return tileRange;
    }, this);
  } else if (extent) {
    this.calculateTileRanges_(extent);
  }

};


/**
 * @private
 * @type {GVol.TileCoord}
 */
GVol.tilegrid.TileGrid.tmpTileCoord_ = [0, 0, 0];


/**
 * Call a function with each tile coordinate for a given extent and zoom level.
 *
 * @param {GVol.Extent} extent Extent.
 * @param {number} zoom Zoom level.
 * @param {function(GVol.TileCoord)} callback Function called with each tile coordinate.
 * @api
 */
GVol.tilegrid.TileGrid.prototype.forEachTileCoord = function(extent, zoom, callback) {
  var tileRange = this.getTileRangeForExtentAndZ(extent, zoom);
  for (var i = tileRange.minX, ii = tileRange.maxX; i <= ii; ++i) {
    for (var j = tileRange.minY, jj = tileRange.maxY; j <= jj; ++j) {
      callback([zoom, i, j]);
    }
  }
};


/**
 * @param {GVol.TileCoord} tileCoord Tile coordinate.
 * @param {function(this: T, number, GVol.TileRange): boGVolean} callback Callback.
 * @param {T=} opt_this The object to use as `this` in `callback`.
 * @param {GVol.TileRange=} opt_tileRange Temporary GVol.TileRange object.
 * @param {GVol.Extent=} opt_extent Temporary GVol.Extent object.
 * @return {boGVolean} Callback succeeded.
 * @template T
 */
GVol.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 {GVol.Extent} Extent.
 */
GVol.tilegrid.TileGrid.prototype.getExtent = function() {
  return this.extent_;
};


/**
 * Get the maximum zoom level for the grid.
 * @return {number} Max zoom.
 * @api
 */
GVol.tilegrid.TileGrid.prototype.getMaxZoom = function() {
  return this.maxZoom;
};


/**
 * Get the minimum zoom level for the grid.
 * @return {number} Min zoom.
 * @api
 */
GVol.tilegrid.TileGrid.prototype.getMinZoom = function() {
  return this.minZoom;
};


/**
 * Get the origin for the grid at the given zoom level.
 * @param {number} z Z.
 * @return {GVol.Coordinate} Origin.
 * @api
 */
GVol.tilegrid.TileGrid.prototype.getOrigin = function(z) {
  if (this.origin_) {
    return this.origin_;
  } else {
    return this.origins_[z];
  }
};


/**
 * Get the resGVolution for the given zoom level.
 * @param {number} z Z.
 * @return {number} ResGVolution.
 * @api
 */
GVol.tilegrid.TileGrid.prototype.getResGVolution = function(z) {
  return this.resGVolutions_[z];
};


/**
 * Get the list of resGVolutions for the tile grid.
 * @return {Array.<number>} ResGVolutions.
 * @api
 */
GVol.tilegrid.TileGrid.prototype.getResGVolutions = function() {
  return this.resGVolutions_;
};


/**
 * @param {GVol.TileCoord} tileCoord Tile coordinate.
 * @param {GVol.TileRange=} opt_tileRange Temporary GVol.TileRange object.
 * @param {GVol.Extent=} opt_extent Temporary GVol.Extent object.
 * @return {GVol.TileRange} Tile range.
 */
GVol.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 {GVol.TileRange} tileRange Tile range.
 * @param {GVol.Extent=} opt_extent Temporary GVol.Extent object.
 * @return {GVol.Extent} Extent.
 */
GVol.tilegrid.TileGrid.prototype.getTileRangeExtent = function(z, tileRange, opt_extent) {
  var origin = this.getOrigin(z);
  var resGVolution = this.getResGVolution(z);
  var tileSize = GVol.size.toSize(this.getTileSize(z), this.tmpSize_);
  var minX = origin[0] + tileRange.minX * tileSize[0] * resGVolution;
  var maxX = origin[0] + (tileRange.maxX + 1) * tileSize[0] * resGVolution;
  var minY = origin[1] + tileRange.minY * tileSize[1] * resGVolution;
  var maxY = origin[1] + (tileRange.maxY + 1) * tileSize[1] * resGVolution;
  return GVol.extent.createOrUpdate(minX, minY, maxX, maxY, opt_extent);
};


/**
 * @param {GVol.Extent} extent Extent.
 * @param {number} resGVolution ResGVolution.
 * @param {GVol.TileRange=} opt_tileRange Temporary tile range object.
 * @return {GVol.TileRange} Tile range.
 */
GVol.tilegrid.TileGrid.prototype.getTileRangeForExtentAndResGVolution = function(extent, resGVolution, opt_tileRange) {
  var tileCoord = GVol.tilegrid.TileGrid.tmpTileCoord_;
  this.getTileCoordForXYAndResGVolution_(
      extent[0], extent[1], resGVolution, false, tileCoord);
  var minX = tileCoord[1];
  var minY = tileCoord[2];
  this.getTileCoordForXYAndResGVolution_(
      extent[2], extent[3], resGVolution, true, tileCoord);
  return GVol.TileRange.createOrUpdate(
      minX, tileCoord[1], minY, tileCoord[2], opt_tileRange);
};


/**
 * @param {GVol.Extent} extent Extent.
 * @param {number} z Z.
 * @param {GVol.TileRange=} opt_tileRange Temporary tile range object.
 * @return {GVol.TileRange} Tile range.
 */
GVol.tilegrid.TileGrid.prototype.getTileRangeForExtentAndZ = function(extent, z, opt_tileRange) {
  var resGVolution = this.getResGVolution(z);
  return this.getTileRangeForExtentAndResGVolution(
      extent, resGVolution, opt_tileRange);
};


/**
 * @param {GVol.TileCoord} tileCoord Tile coordinate.
 * @return {GVol.Coordinate} Tile center.
 */
GVol.tilegrid.TileGrid.prototype.getTileCoordCenter = function(tileCoord) {
  var origin = this.getOrigin(tileCoord[0]);
  var resGVolution = this.getResGVolution(tileCoord[0]);
  var tileSize = GVol.size.toSize(this.getTileSize(tileCoord[0]), this.tmpSize_);
  return [
    origin[0] + (tileCoord[1] + 0.5) * tileSize[0] * resGVolution,
    origin[1] + (tileCoord[2] + 0.5) * tileSize[1] * resGVolution
  ];
};


/**
 * Get the extent of a tile coordinate.
 *
 * @param {GVol.TileCoord} tileCoord Tile coordinate.
 * @param {GVol.Extent=} opt_extent Temporary extent object.
 * @return {GVol.Extent} Extent.
 * @api
 */
GVol.tilegrid.TileGrid.prototype.getTileCoordExtent = function(tileCoord, opt_extent) {
  var origin = this.getOrigin(tileCoord[0]);
  var resGVolution = this.getResGVolution(tileCoord[0]);
  var tileSize = GVol.size.toSize(this.getTileSize(tileCoord[0]), this.tmpSize_);
  var minX = origin[0] + tileCoord[1] * tileSize[0] * resGVolution;
  var minY = origin[1] + tileCoord[2] * tileSize[1] * resGVolution;
  var maxX = minX + tileSize[0] * resGVolution;
  var maxY = minY + tileSize[1] * resGVolution;
  return GVol.extent.createOrUpdate(minX, minY, maxX, maxY, opt_extent);
};


/**
 * Get the tile coordinate for the given map coordinate and resGVolution.  This
 * method considers that coordinates that intersect tile boundaries should be
 * assigned the higher tile coordinate.
 *
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @param {number} resGVolution ResGVolution.
 * @param {GVol.TileCoord=} opt_tileCoord Destination GVol.TileCoord object.
 * @return {GVol.TileCoord} Tile coordinate.
 * @api
 */
GVol.tilegrid.TileGrid.prototype.getTileCoordForCoordAndResGVolution = function(coordinate, resGVolution, opt_tileCoord) {
  return this.getTileCoordForXYAndResGVolution_(
      coordinate[0], coordinate[1], resGVolution, false, opt_tileCoord);
};


/**
 * @param {number} x X.
 * @param {number} y Y.
 * @param {number} resGVolution ResGVolution.
 * @param {boGVolean} reverseIntersectionPGVolicy Instead of letting edge
 *     intersections go to the higher tile coordinate, let edge intersections
 *     go to the lower tile coordinate.
 * @param {GVol.TileCoord=} opt_tileCoord Temporary GVol.TileCoord object.
 * @return {GVol.TileCoord} Tile coordinate.
 * @private
 */
GVol.tilegrid.TileGrid.prototype.getTileCoordForXYAndResGVolution_ = function(
    x, y, resGVolution, reverseIntersectionPGVolicy, opt_tileCoord) {
  var z = this.getZForResGVolution(resGVolution);
  var scale = resGVolution / this.getResGVolution(z);
  var origin = this.getOrigin(z);
  var tileSize = GVol.size.toSize(this.getTileSize(z), this.tmpSize_);

  var adjustX = reverseIntersectionPGVolicy ? 0.5 : 0;
  var adjustY = reverseIntersectionPGVolicy ? 0 : 0.5;
  var xFromOrigin = Math.floor((x - origin[0]) / resGVolution + adjustX);
  var yFromOrigin = Math.floor((y - origin[1]) / resGVolution + adjustY);
  var tileCoordX = scale * xFromOrigin / tileSize[0];
  var tileCoordY = scale * yFromOrigin / tileSize[1];

  if (reverseIntersectionPGVolicy) {
    tileCoordX = Math.ceil(tileCoordX) - 1;
    tileCoordY = Math.ceil(tileCoordY) - 1;
  } else {
    tileCoordX = Math.floor(tileCoordX);
    tileCoordY = Math.floor(tileCoordY);
  }

  return GVol.tilecoord.createOrUpdate(z, tileCoordX, tileCoordY, opt_tileCoord);
};


/**
 * Get a tile coordinate given a map coordinate and zoom level.
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @param {number} z Zoom level.
 * @param {GVol.TileCoord=} opt_tileCoord Destination GVol.TileCoord object.
 * @return {GVol.TileCoord} Tile coordinate.
 * @api
 */
GVol.tilegrid.TileGrid.prototype.getTileCoordForCoordAndZ = function(coordinate, z, opt_tileCoord) {
  var resGVolution = this.getResGVolution(z);
  return this.getTileCoordForXYAndResGVolution_(
      coordinate[0], coordinate[1], resGVolution, false, opt_tileCoord);
};


/**
 * @param {GVol.TileCoord} tileCoord Tile coordinate.
 * @return {number} Tile resGVolution.
 */
GVol.tilegrid.TileGrid.prototype.getTileCoordResGVolution = function(tileCoord) {
  return this.resGVolutions_[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 `GVol.Size`, run the result through `GVol.size.toSize()`.
 * @param {number} z Z.
 * @return {number|GVol.Size} Tile size.
 * @api
 */
GVol.tilegrid.TileGrid.prototype.getTileSize = function(z) {
  if (this.tileSize_) {
    return this.tileSize_;
  } else {
    return this.tileSizes_[z];
  }
};


/**
 * @param {number} z Zoom level.
 * @return {GVol.TileRange} Extent tile range for the specified zoom level.
 */
GVol.tilegrid.TileGrid.prototype.getFullTileRange = function(z) {
  if (!this.fullTileRanges_) {
    return null;
  } else {
    return this.fullTileRanges_[z];
  }
};


/**
 * @param {number} resGVolution ResGVolution.
 * @param {number=} opt_direction If 0, the nearest resGVolution will be used.
 *     If 1, the nearest lower resGVolution will be used. If -1, the nearest
 *     higher resGVolution will be used. Default is 0.
 * @return {number} Z.
 * @api
 */
GVol.tilegrid.TileGrid.prototype.getZForResGVolution = function(
    resGVolution, opt_direction) {
  var z = GVol.array.linearFindNearest(this.resGVolutions_, resGVolution,
      opt_direction || 0);
  return GVol.math.clamp(z, this.minZoom, this.maxZoom);
};


/**
 * @param {!GVol.Extent} extent Extent for this tile grid.
 * @private
 */
GVol.tilegrid.TileGrid.prototype.calculateTileRanges_ = function(extent) {
  var length = this.resGVolutions_.length;
  var fullTileRanges = new Array(length);
  for (var z = this.minZoom; z < length; ++z) {
    fullTileRanges[z] = this.getTileRangeForExtentAndZ(extent, z);
  }
  this.fullTileRanges_ = fullTileRanges;
};

goog.provide('GVol.tilegrid');

goog.require('GVol');
goog.require('GVol.size');
goog.require('GVol.extent');
goog.require('GVol.extent.Corner');
goog.require('GVol.obj');
goog.require('GVol.proj');
goog.require('GVol.proj.Units');
goog.require('GVol.tilegrid.TileGrid');


/**
 * @param {GVol.proj.Projection} projection Projection.
 * @return {!GVol.tilegrid.TileGrid} Default tile grid for the passed projection.
 */
GVol.tilegrid.getForProjection = function(projection) {
  var tileGrid = projection.getDefaultTileGrid();
  if (!tileGrid) {
    tileGrid = GVol.tilegrid.createForProjection(projection);
    projection.setDefaultTileGrid(tileGrid);
  }
  return tileGrid;
};


/**
 * @param {GVol.tilegrid.TileGrid} tileGrid Tile grid.
 * @param {GVol.TileCoord} tileCoord Tile coordinate.
 * @param {GVol.proj.Projection} projection Projection.
 * @return {GVol.TileCoord} Tile coordinate.
 */
GVol.tilegrid.wrapX = function(tileGrid, tileCoord, projection) {
  var z = tileCoord[0];
  var center = tileGrid.getTileCoordCenter(tileCoord);
  var projectionExtent = GVol.tilegrid.extentFromProjection(projection);
  if (!GVol.extent.containsCoordinate(projectionExtent, center)) {
    var worldWidth = GVol.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 {GVol.Extent} extent Extent.
 * @param {number=} opt_maxZoom Maximum zoom level (default is
 *     GVol.DEFAULT_MAX_ZOOM).
 * @param {number|GVol.Size=} opt_tileSize Tile size (default uses
 *     GVol.DEFAULT_TILE_SIZE).
 * @param {GVol.extent.Corner=} opt_corner Extent corner (default is
 *     GVol.extent.Corner.TOP_LEFT).
 * @return {!GVol.tilegrid.TileGrid} TileGrid instance.
 */
GVol.tilegrid.createForExtent = function(extent, opt_maxZoom, opt_tileSize, opt_corner) {
  var corner = opt_corner !== undefined ?
    opt_corner : GVol.extent.Corner.TOP_LEFT;

  var resGVolutions = GVol.tilegrid.resGVolutionsFromExtent(
      extent, opt_maxZoom, opt_tileSize);

  return new GVol.tilegrid.TileGrid({
    extent: extent,
    origin: GVol.extent.getCorner(extent, corner),
    resGVolutions: resGVolutions,
    tileSize: opt_tileSize
  });
};


/**
 * Creates a tile grid with a standard XYZ tiling scheme.
 * @param {GVolx.tilegrid.XYZOptions=} opt_options Tile grid options.
 * @return {!GVol.tilegrid.TileGrid} Tile grid instance.
 * @api
 */
GVol.tilegrid.createXYZ = function(opt_options) {
  var options = /** @type {GVolx.tilegrid.TileGridOptions} */ ({});
  GVol.obj.assign(options, opt_options !== undefined ?
    opt_options : /** @type {GVolx.tilegrid.XYZOptions} */ ({}));
  if (options.extent === undefined) {
    options.extent = GVol.proj.get('EPSG:3857').getExtent();
  }
  options.resGVolutions = GVol.tilegrid.resGVolutionsFromExtent(
      options.extent, options.maxZoom, options.tileSize);
  delete options.maxZoom;

  return new GVol.tilegrid.TileGrid(options);
};


/**
 * Create a resGVolutions array from an extent.  A zoom factor of 2 is assumed.
 * @param {GVol.Extent} extent Extent.
 * @param {number=} opt_maxZoom Maximum zoom level (default is
 *     GVol.DEFAULT_MAX_ZOOM).
 * @param {number|GVol.Size=} opt_tileSize Tile size (default uses
 *     GVol.DEFAULT_TILE_SIZE).
 * @return {!Array.<number>} ResGVolutions array.
 */
GVol.tilegrid.resGVolutionsFromExtent = function(extent, opt_maxZoom, opt_tileSize) {
  var maxZoom = opt_maxZoom !== undefined ?
    opt_maxZoom : GVol.DEFAULT_MAX_ZOOM;

  var height = GVol.extent.getHeight(extent);
  var width = GVol.extent.getWidth(extent);

  var tileSize = GVol.size.toSize(opt_tileSize !== undefined ?
    opt_tileSize : GVol.DEFAULT_TILE_SIZE);
  var maxResGVolution = Math.max(
      width / tileSize[0], height / tileSize[1]);

  var length = maxZoom + 1;
  var resGVolutions = new Array(length);
  for (var z = 0; z < length; ++z) {
    resGVolutions[z] = maxResGVolution / Math.pow(2, z);
  }
  return resGVolutions;
};


/**
 * @param {GVol.ProjectionLike} projection Projection.
 * @param {number=} opt_maxZoom Maximum zoom level (default is
 *     GVol.DEFAULT_MAX_ZOOM).
 * @param {number|GVol.Size=} opt_tileSize Tile size (default uses
 *     GVol.DEFAULT_TILE_SIZE).
 * @param {GVol.extent.Corner=} opt_corner Extent corner (default is
 *     GVol.extent.Corner.BOTTOM_LEFT).
 * @return {!GVol.tilegrid.TileGrid} TileGrid instance.
 */
GVol.tilegrid.createForProjection = function(projection, opt_maxZoom, opt_tileSize, opt_corner) {
  var extent = GVol.tilegrid.extentFromProjection(projection);
  return GVol.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 {GVol.ProjectionLike} projection Projection.
 * @return {GVol.Extent} Extent.
 */
GVol.tilegrid.extentFromProjection = function(projection) {
  projection = GVol.proj.get(projection);
  var extent = projection.getExtent();
  if (!extent) {
    var half = 180 * GVol.proj.METERS_PER_UNIT[GVol.proj.Units.DEGREES] /
        projection.getMetersPerUnit();
    extent = GVol.extent.createOrUpdate(-half, -half, half, half);
  }
  return extent;
};

goog.provide('GVol.Attribution');

goog.require('GVol.TileRange');
goog.require('GVol.math');
goog.require('GVol.tilegrid');


/**
 * @classdesc
 * An attribution for a layer source.
 *
 * Example:
 *
 *     source: new GVol.source.OSM({
 *       attributions: [
 *         new GVol.Attribution({
 *           html: 'All maps &copy; ' +
 *               '<a href="https://www.opencyclemap.org/">OpenCycleMap</a>'
 *         }),
 *         GVol.source.OSM.ATTRIBUTION
 *       ],
 *     ..
 *
 * @constructor
 * @param {GVolx.AttributionOptions} options Attribution options.
 * @struct
 * @api
 */
GVol.Attribution = function(options) {

  /**
   * @private
   * @type {string}
   */
  this.html_ = options.html;

  /**
   * @private
   * @type {Object.<string, Array.<GVol.TileRange>>}
   */
  this.tileRanges_ = options.tileRanges ? options.tileRanges : null;

};


/**
 * Get the attribution markup.
 * @return {string} The attribution HTML.
 * @api
 */
GVol.Attribution.prototype.getHTML = function() {
  return this.html_;
};


/**
 * @param {Object.<string, GVol.TileRange>} tileRanges Tile ranges.
 * @param {!GVol.tilegrid.TileGrid} tileGrid Tile grid.
 * @param {!GVol.proj.Projection} projection Projection.
 * @return {boGVolean} Intersects any tile range.
 */
GVol.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(
          GVol.tilegrid.extentFromProjection(projection), parseInt(zKey, 10));
      var width = extentTileRange.getWidth();
      if (tileRange.minX < extentTileRange.minX ||
          tileRange.maxX > extentTileRange.maxX) {
        if (testTileRange.intersects(new GVol.TileRange(
            GVol.math.modulo(tileRange.minX, width),
            GVol.math.modulo(tileRange.maxX, width),
            tileRange.minY, tileRange.maxY))) {
          return true;
        }
        if (tileRange.getWidth() > width &&
            testTileRange.intersects(extentTileRange)) {
          return true;
        }
      }
    }
  }
  return false;
};

goog.provide('GVol.CenterConstraint');

goog.require('GVol.math');


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


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

goog.provide('GVol.CGVollectionEventType');

/**
 * @enum {string}
 */
GVol.CGVollectionEventType = {
  /**
   * Triggered when an item is added to the cGVollection.
   * @event GVol.CGVollection.Event#add
   * @api
   */
  ADD: 'add',
  /**
   * Triggered when an item is removed from the cGVollection.
   * @event GVol.CGVollection.Event#remove
   * @api
   */
  REMOVE: 'remove'
};

goog.provide('GVol.ObjectEventType');

/**
 * @enum {string}
 */
GVol.ObjectEventType = {
  /**
   * Triggered when a property is changed.
   * @event GVol.Object.Event#propertychange
   * @api
   */
  PROPERTYCHANGE: 'propertychange'
};

goog.provide('GVol.events');

goog.require('GVol.obj');


/**
 * @param {GVol.EventsKey} listenerObj Listener object.
 * @return {GVol.EventsListenerFunctionType} Bound listener.
 */
GVol.events.bindListener_ = function(listenerObj) {
  var boundListener = function(evt) {
    var listener = listenerObj.listener;
    var bindTo = listenerObj.bindTo || listenerObj.target;
    if (listenerObj.callOnce) {
      GVol.events.unlistenByKey(listenerObj);
    }
    return listener.call(bindTo, evt);
  };
  listenerObj.boundListener = boundListener;
  return boundListener;
};


/**
 * Finds the matching {@link GVol.EventsKey} in the given listener
 * array.
 *
 * @param {!Array<!GVol.EventsKey>} listeners Array of listeners.
 * @param {!Function} listener The listener function.
 * @param {Object=} opt_this The `this` value inside the listener.
 * @param {boGVolean=} opt_setDeleteIndex Set the deleteIndex on the matching
 *     listener, for {@link GVol.events.unlistenByKey}.
 * @return {GVol.EventsKey|undefined} The matching listener object.
 * @private
 */
GVol.events.findListener_ = function(listeners, listener, opt_this,
    opt_setDeleteIndex) {
  var listenerObj;
  for (var i = 0, ii = listeners.length; i < ii; ++i) {
    listenerObj = listeners[i];
    if (listenerObj.listener === listener &&
        listenerObj.bindTo === opt_this) {
      if (opt_setDeleteIndex) {
        listenerObj.deleteIndex = i;
      }
      return listenerObj;
    }
  }
  return undefined;
};


/**
 * @param {GVol.EventTargetLike} target Target.
 * @param {string} type Type.
 * @return {Array.<GVol.EventsKey>|undefined} Listeners.
 */
GVol.events.getListeners = function(target, type) {
  var listenerMap = target.GVol_lm;
  return listenerMap ? listenerMap[type] : undefined;
};


/**
 * Get the lookup of listeners.  If one does not exist on the target, it is
 * created.
 * @param {GVol.EventTargetLike} target Target.
 * @return {!Object.<string, Array.<GVol.EventsKey>>} Map of
 *     listeners by event type.
 * @private
 */
GVol.events.getListenerMap_ = function(target) {
  var listenerMap = target.GVol_lm;
  if (!listenerMap) {
    listenerMap = target.GVol_lm = {};
  }
  return listenerMap;
};


/**
 * Clean up all listener objects of the given type.  All properties on the
 * listener objects will be removed, and if no listeners remain in the listener
 * map, it will be removed from the target.
 * @param {GVol.EventTargetLike} target Target.
 * @param {string} type Type.
 * @private
 */
GVol.events.removeListeners_ = function(target, type) {
  var listeners = GVol.events.getListeners(target, type);
  if (listeners) {
    for (var i = 0, ii = listeners.length; i < ii; ++i) {
      target.removeEventListener(type, listeners[i].boundListener);
      GVol.obj.clear(listeners[i]);
    }
    listeners.length = 0;
    var listenerMap = target.GVol_lm;
    if (listenerMap) {
      delete listenerMap[type];
      if (Object.keys(listenerMap).length === 0) {
        delete target.GVol_lm;
      }
    }
  }
};


/**
 * Registers an event listener on an event target. Inspired by
 * {@link https://google.github.io/closure-library/api/source/closure/goog/events/events.js.src.html}
 *
 * This function efficiently binds a `listener` to a `this` object, and returns
 * a key for use with {@link GVol.events.unlistenByKey}.
 *
 * @param {GVol.EventTargetLike} target Event target.
 * @param {string} type Event type.
 * @param {GVol.EventsListenerFunctionType} listener Listener.
 * @param {Object=} opt_this Object referenced by the `this` keyword in the
 *     listener. Default is the `target`.
 * @param {boGVolean=} opt_once If true, add the listener as one-off listener.
 * @return {GVol.EventsKey} Unique key for the listener.
 */
GVol.events.listen = function(target, type, listener, opt_this, opt_once) {
  var listenerMap = GVol.events.getListenerMap_(target);
  var listeners = listenerMap[type];
  if (!listeners) {
    listeners = listenerMap[type] = [];
  }
  var listenerObj = GVol.events.findListener_(listeners, listener, opt_this,
      false);
  if (listenerObj) {
    if (!opt_once) {
      // Turn one-off listener into a permanent one.
      listenerObj.callOnce = false;
    }
  } else {
    listenerObj = /** @type {GVol.EventsKey} */ ({
      bindTo: opt_this,
      callOnce: !!opt_once,
      listener: listener,
      target: target,
      type: type
    });
    target.addEventListener(type, GVol.events.bindListener_(listenerObj));
    listeners.push(listenerObj);
  }

  return listenerObj;
};


/**
 * Registers a one-off event listener on an event target. Inspired by
 * {@link https://google.github.io/closure-library/api/source/closure/goog/events/events.js.src.html}
 *
 * This function efficiently binds a `listener` as self-unregistering listener
 * to a `this` object, and returns a key for use with
 * {@link GVol.events.unlistenByKey} in case the listener needs to be unregistered
 * before it is called.
 *
 * When {@link GVol.events.listen} is called with the same arguments after this
 * function, the self-unregistering listener will be turned into a permanent
 * listener.
 *
 * @param {GVol.EventTargetLike} target Event target.
 * @param {string} type Event type.
 * @param {GVol.EventsListenerFunctionType} listener Listener.
 * @param {Object=} opt_this Object referenced by the `this` keyword in the
 *     listener. Default is the `target`.
 * @return {GVol.EventsKey} Key for unlistenByKey.
 */
GVol.events.listenOnce = function(target, type, listener, opt_this) {
  return GVol.events.listen(target, type, listener, opt_this, true);
};


/**
 * Unregisters an event listener on an event target. Inspired by
 * {@link https://google.github.io/closure-library/api/source/closure/goog/events/events.js.src.html}
 *
 * To return a listener, this function needs to be called with the exact same
 * arguments that were used for a previous {@link GVol.events.listen} call.
 *
 * @param {GVol.EventTargetLike} target Event target.
 * @param {string} type Event type.
 * @param {GVol.EventsListenerFunctionType} listener Listener.
 * @param {Object=} opt_this Object referenced by the `this` keyword in the
 *     listener. Default is the `target`.
 */
GVol.events.unlisten = function(target, type, listener, opt_this) {
  var listeners = GVol.events.getListeners(target, type);
  if (listeners) {
    var listenerObj = GVol.events.findListener_(listeners, listener, opt_this,
        true);
    if (listenerObj) {
      GVol.events.unlistenByKey(listenerObj);
    }
  }
};


/**
 * Unregisters event listeners on an event target. Inspired by
 * {@link https://google.github.io/closure-library/api/source/closure/goog/events/events.js.src.html}
 *
 * The argument passed to this function is the key returned from
 * {@link GVol.events.listen} or {@link GVol.events.listenOnce}.
 *
 * @param {GVol.EventsKey} key The key.
 */
GVol.events.unlistenByKey = function(key) {
  if (key && key.target) {
    key.target.removeEventListener(key.type, key.boundListener);
    var listeners = GVol.events.getListeners(key.target, key.type);
    if (listeners) {
      var i = 'deleteIndex' in key ? key.deleteIndex : listeners.indexOf(key);
      if (i !== -1) {
        listeners.splice(i, 1);
      }
      if (listeners.length === 0) {
        GVol.events.removeListeners_(key.target, key.type);
      }
    }
    GVol.obj.clear(key);
  }
};


/**
 * Unregisters all event listeners on an event target. Inspired by
 * {@link https://google.github.io/closure-library/api/source/closure/goog/events/events.js.src.html}
 *
 * @param {GVol.EventTargetLike} target Target.
 */
GVol.events.unlistenAll = function(target) {
  var listenerMap = GVol.events.getListenerMap_(target);
  for (var type in listenerMap) {
    GVol.events.removeListeners_(target, type);
  }
};

goog.provide('GVol.Disposable');

goog.require('GVol');

/**
 * Objects that need to clean up after themselves.
 * @constructor
 */
GVol.Disposable = function() {};

/**
 * The object has already been disposed.
 * @type {boGVolean}
 * @private
 */
GVol.Disposable.prototype.disposed_ = false;

/**
 * Clean up.
 */
GVol.Disposable.prototype.dispose = function() {
  if (!this.disposed_) {
    this.disposed_ = true;
    this.disposeInternal();
  }
};

/**
 * Extension point for disposable objects.
 * @protected
 */
GVol.Disposable.prototype.disposeInternal = GVol.nullFunction;

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


/**
 * @classdesc
 * Stripped down implementation of the W3C DOM Level 2 Event interface.
 * @see {@link https://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-interface}
 *
 * This implementation only provides `type` and `target` properties, and
 * `stopPropagation` and `preventDefault` methods. It is meant as base class
 * for higher level events defined in the library, and works with
 * {@link GVol.events.EventTarget}.
 *
 * @constructor
 * @implements {GVoli.events.Event}
 * @param {string} type Type.
 */
GVol.events.Event = function(type) {

  /**
   * @type {boGVolean}
   */
  this.propagationStopped;

  /**
   * The event type.
   * @type {string}
   * @api
   */
  this.type = type;

  /**
   * The event target.
   * @type {Object}
   * @api
   */
  this.target = null;

};


/**
 * Stop event propagation.
 * @function
 * @override
 * @api
 */
GVol.events.Event.prototype.preventDefault =

  /**
   * Stop event propagation.
   * @function
   * @override
   * @api
   */
  GVol.events.Event.prototype.stopPropagation = function() {
    this.propagationStopped = true;
  };


/**
 * @param {Event|GVol.events.Event} evt Event
 */
GVol.events.Event.stopPropagation = function(evt) {
  evt.stopPropagation();
};


/**
 * @param {Event|GVol.events.Event} evt Event
 */
GVol.events.Event.preventDefault = function(evt) {
  evt.preventDefault();
};

goog.provide('GVol.events.EventTarget');

goog.require('GVol');
goog.require('GVol.Disposable');
goog.require('GVol.events');
goog.require('GVol.events.Event');


/**
 * @classdesc
 * A simplified implementation of the W3C DOM Level 2 EventTarget interface.
 * @see {@link https://www.w3.org/TR/2000/REC-DOM-Level-2-Events-20001113/events.html#Events-EventTarget}
 *
 * There are two important simplifications compared to the specification:
 *
 * 1. The handling of `useCapture` in `addEventListener` and
 *    `removeEventListener`. There is no real capture model.
 * 2. The handling of `stopPropagation` and `preventDefault` on `dispatchEvent`.
 *    There is no event target hierarchy. When a listener calls
 *    `stopPropagation` or `preventDefault` on an event object, it means that no
 *    more listeners after this one will be called. Same as when the listener
 *    returns false.
 *
 * @constructor
 * @extends {GVol.Disposable}
 */
GVol.events.EventTarget = function() {

  GVol.Disposable.call(this);

  /**
   * @private
   * @type {!Object.<string, number>}
   */
  this.pendingRemovals_ = {};

  /**
   * @private
   * @type {!Object.<string, number>}
   */
  this.dispatching_ = {};

  /**
   * @private
   * @type {!Object.<string, Array.<GVol.EventsListenerFunctionType>>}
   */
  this.listeners_ = {};

};
GVol.inherits(GVol.events.EventTarget, GVol.Disposable);


/**
 * @param {string} type Type.
 * @param {GVol.EventsListenerFunctionType} listener Listener.
 */
GVol.events.EventTarget.prototype.addEventListener = function(type, listener) {
  var listeners = this.listeners_[type];
  if (!listeners) {
    listeners = this.listeners_[type] = [];
  }
  if (listeners.indexOf(listener) === -1) {
    listeners.push(listener);
  }
};


/**
 * @param {{type: string,
 *     target: (EventTarget|GVol.events.EventTarget|undefined)}|GVol.events.Event|
 *     string} event Event or event type.
 * @return {boGVolean|undefined} `false` if anyone called preventDefault on the
 *     event object or if any of the listeners returned false.
 */
GVol.events.EventTarget.prototype.dispatchEvent = function(event) {
  var evt = typeof event === 'string' ? new GVol.events.Event(event) : event;
  var type = evt.type;
  evt.target = this;
  var listeners = this.listeners_[type];
  var propagate;
  if (listeners) {
    if (!(type in this.dispatching_)) {
      this.dispatching_[type] = 0;
      this.pendingRemovals_[type] = 0;
    }
    ++this.dispatching_[type];
    for (var i = 0, ii = listeners.length; i < ii; ++i) {
      if (listeners[i].call(this, evt) === false || evt.propagationStopped) {
        propagate = false;
        break;
      }
    }
    --this.dispatching_[type];
    if (this.dispatching_[type] === 0) {
      var pendingRemovals = this.pendingRemovals_[type];
      delete this.pendingRemovals_[type];
      while (pendingRemovals--) {
        this.removeEventListener(type, GVol.nullFunction);
      }
      delete this.dispatching_[type];
    }
    return propagate;
  }
};


/**
 * @inheritDoc
 */
GVol.events.EventTarget.prototype.disposeInternal = function() {
  GVol.events.unlistenAll(this);
};


/**
 * Get the listeners for a specified event type. Listeners are returned in the
 * order that they will be called in.
 *
 * @param {string} type Type.
 * @return {Array.<GVol.EventsListenerFunctionType>} Listeners.
 */
GVol.events.EventTarget.prototype.getListeners = function(type) {
  return this.listeners_[type];
};


/**
 * @param {string=} opt_type Type. If not provided,
 *     `true` will be returned if this EventTarget has any listeners.
 * @return {boGVolean} Has listeners.
 */
GVol.events.EventTarget.prototype.hasListener = function(opt_type) {
  return opt_type ?
    opt_type in this.listeners_ :
    Object.keys(this.listeners_).length > 0;
};


/**
 * @param {string} type Type.
 * @param {GVol.EventsListenerFunctionType} listener Listener.
 */
GVol.events.EventTarget.prototype.removeEventListener = function(type, listener) {
  var listeners = this.listeners_[type];
  if (listeners) {
    var index = listeners.indexOf(listener);
    if (type in this.pendingRemovals_) {
      // make listener a no-op, and remove later in #dispatchEvent()
      listeners[index] = GVol.nullFunction;
      ++this.pendingRemovals_[type];
    } else {
      listeners.splice(index, 1);
      if (listeners.length === 0) {
        delete this.listeners_[type];
      }
    }
  }
};

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

/**
 * @enum {string}
 * @const
 */
GVol.events.EventType = {
  /**
   * Generic change event. Triggered when the revision counter is increased.
   * @event GVol.events.Event#change
   * @api
   */
  CHANGE: 'change',

  CLICK: 'click',
  DBLCLICK: 'dblclick',
  DRAGENTER: 'dragenter',
  DRAGOVER: 'dragover',
  DROP: 'drop',
  ERROR: 'error',
  KEYDOWN: 'keydown',
  KEYPRESS: 'keypress',
  LOAD: 'load',
  MOUSEDOWN: 'mousedown',
  MOUSEMOVE: 'mousemove',
  MOUSEOUT: 'mouseout',
  MOUSEUP: 'mouseup',
  MOUSEWHEEL: 'mousewheel',
  MSPOINTERDOWN: 'MSPointerDown',
  RESIZE: 'resize',
  TOUCHSTART: 'touchstart',
  TOUCHMOVE: 'touchmove',
  TOUCHEND: 'touchend',
  WHEEL: 'wheel'
};

goog.provide('GVol.Observable');

goog.require('GVol');
goog.require('GVol.events');
goog.require('GVol.events.EventTarget');
goog.require('GVol.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 GVol.Observable#changed}.
 *
 * @constructor
 * @extends {GVol.events.EventTarget}
 * @fires GVol.events.Event
 * @struct
 * @api
 */
GVol.Observable = function() {

  GVol.events.EventTarget.call(this);

  /**
   * @private
   * @type {number}
   */
  this.revision_ = 0;

};
GVol.inherits(GVol.Observable, GVol.events.EventTarget);


/**
 * Removes an event listener using the key returned by `on()` or `once()`.
 * @param {GVol.EventsKey|Array.<GVol.EventsKey>} key The key returned by `on()`
 *     or `once()` (or an array of keys).
 * @api
 */
GVol.Observable.unByKey = function(key) {
  if (Array.isArray(key)) {
    for (var i = 0, ii = key.length; i < ii; ++i) {
      GVol.events.unlistenByKey(key[i]);
    }
  } else {
    GVol.events.unlistenByKey(/** @type {GVol.EventsKey} */ (key));
  }
};


/**
 * Increases the revision counter and dispatches a 'change' event.
 * @api
 */
GVol.Observable.prototype.changed = function() {
  ++this.revision_;
  this.dispatchEvent(GVol.events.EventType.CHANGE);
};


/**
 * 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 {{type: string,
 *     target: (EventTarget|GVol.events.EventTarget|undefined)}|GVol.events.Event|
 *     string} event Event object.
 * @function
 * @api
 */
GVol.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
 */
GVol.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 {GVol.EventsKey|Array.<GVol.EventsKey>} Unique key for the listener. If
 *     called with an array of event types as the first argument, the return
 *     will be an array of keys.
 * @api
 */
GVol.Observable.prototype.on = function(type, listener, opt_this) {
  if (Array.isArray(type)) {
    var len = type.length;
    var keys = new Array(len);
    for (var i = 0; i < len; ++i) {
      keys[i] = GVol.events.listen(this, type[i], listener, opt_this);
    }
    return keys;
  } else {
    return GVol.events.listen(
        this, /** @type {string} */ (type), listener, 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 {GVol.EventsKey|Array.<GVol.EventsKey>} Unique key for the listener. If
 *     called with an array of event types as the first argument, the return
 *     will be an array of keys.
 * @api
 */
GVol.Observable.prototype.once = function(type, listener, opt_this) {
  if (Array.isArray(type)) {
    var len = type.length;
    var keys = new Array(len);
    for (var i = 0; i < len; ++i) {
      keys[i] = GVol.events.listenOnce(this, type[i], listener, opt_this);
    }
    return keys;
  } else {
    return GVol.events.listenOnce(
        this, /** @type {string} */ (type), listener, 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
 */
GVol.Observable.prototype.un = function(type, listener, opt_this) {
  if (Array.isArray(type)) {
    for (var i = 0, ii = type.length; i < ii; ++i) {
      GVol.events.unlisten(this, type[i], listener, opt_this);
    }
    return;
  } else {
    GVol.events.unlisten(this, /** @type {string} */ (type), listener, opt_this);
  }
};

goog.provide('GVol.Object');

goog.require('GVol');
goog.require('GVol.ObjectEventType');
goog.require('GVol.Observable');
goog.require('GVol.events.Event');
goog.require('GVol.obj');


/**
 * @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 GVol.Observable} with observable properties, where each
 * property is observable as well as the object as a whGVole.
 *
 * 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 GVol.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 GVol.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 GVol.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 {GVol.Observable}
 * @param {Object.<string, *>=} opt_values An object with key-value pairs.
 * @fires GVol.Object.Event
 * @api
 */
GVol.Object = function(opt_values) {
  GVol.Observable.call(this);

  // Call GVol.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.
  GVol.getUid(this);

  /**
   * @private
   * @type {!Object.<string, *>}
   */
  this.values_ = {};

  if (opt_values !== undefined) {
    this.setProperties(opt_values);
  }
};
GVol.inherits(GVol.Object, GVol.Observable);


/**
 * @private
 * @type {Object.<string, string>}
 */
GVol.Object.changeEventTypeCache_ = {};


/**
 * @param {string} key Key name.
 * @return {string} Change name.
 */
GVol.Object.getChangeEventType = function(key) {
  return GVol.Object.changeEventTypeCache_.hasOwnProperty(key) ?
    GVol.Object.changeEventTypeCache_[key] :
    (GVol.Object.changeEventTypeCache_[key] = 'change:' + key);
};


/**
 * Gets a value.
 * @param {string} key Key name.
 * @return {*} Value.
 * @api
 */
GVol.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
 */
GVol.Object.prototype.getKeys = function() {
  return Object.keys(this.values_);
};


/**
 * Get an object of all property names and values.
 * @return {Object.<string, *>} Object.
 * @api
 */
GVol.Object.prototype.getProperties = function() {
  return GVol.obj.assign({}, this.values_);
};


/**
 * @param {string} key Key name.
 * @param {*} GVoldValue Old value.
 */
GVol.Object.prototype.notify = function(key, GVoldValue) {
  var eventType;
  eventType = GVol.Object.getChangeEventType(key);
  this.dispatchEvent(new GVol.Object.Event(eventType, key, GVoldValue));
  eventType = GVol.ObjectEventType.PROPERTYCHANGE;
  this.dispatchEvent(new GVol.Object.Event(eventType, key, GVoldValue));
};


/**
 * Sets a value.
 * @param {string} key Key name.
 * @param {*} value Value.
 * @param {boGVolean=} opt_silent Update without triggering an event.
 * @api
 */
GVol.Object.prototype.set = function(key, value, opt_silent) {
  if (opt_silent) {
    this.values_[key] = value;
  } else {
    var GVoldValue = this.values_[key];
    this.values_[key] = value;
    if (GVoldValue !== value) {
      this.notify(key, GVoldValue);
    }
  }
};


/**
 * Sets a cGVollection 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 {boGVolean=} opt_silent Update without triggering an event.
 * @api
 */
GVol.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 {boGVolean=} opt_silent Unset without triggering an event.
 * @api
 */
GVol.Object.prototype.unset = function(key, opt_silent) {
  if (key in this.values_) {
    var GVoldValue = this.values_[key];
    delete this.values_[key];
    if (!opt_silent) {
      this.notify(key, GVoldValue);
    }
  }
};


/**
 * @classdesc
 * Events emitted by {@link GVol.Object} instances are instances of this type.
 *
 * @param {string} type The event type.
 * @param {string} key The property name.
 * @param {*} GVoldValue The GVold value for `key`.
 * @extends {GVol.events.Event}
 * @implements {GVoli.Object.Event}
 * @constructor
 */
GVol.Object.Event = function(type, key, GVoldValue) {
  GVol.events.Event.call(this, type);

  /**
   * The name of the property whose value is changing.
   * @type {string}
   * @api
   */
  this.key = key;

  /**
   * The GVold value. To get the new value use `e.target.get(e.key)` where
   * `e` is the event object.
   * @type {*}
   * @api
   */
  this.GVoldValue = GVoldValue;

};
GVol.inherits(GVol.Object.Event, GVol.events.Event);

/**
 * An implementation of Google Maps' MVCArray.
 * @see https://developers.google.com/maps/documentation/javascript/reference
 */

goog.provide('GVol.CGVollection');

goog.require('GVol');
goog.require('GVol.AssertionError');
goog.require('GVol.CGVollectionEventType');
goog.require('GVol.Object');
goog.require('GVol.events.Event');


/**
 * @classdesc
 * An expanded version of standard JS Array, adding convenience methods for
 * manipulation. Add and remove changes to the CGVollection trigger a CGVollection
 * event. Note that this does not cover changes to the objects _within_ the
 * CGVollection; they trigger events on the appropriate object, not on the
 * CGVollection as a whGVole.
 *
 * @constructor
 * @extends {GVol.Object}
 * @fires GVol.CGVollection.Event
 * @param {Array.<T>=} opt_array Array.
 * @param {GVolx.CGVollectionOptions=} opt_options CGVollection options.
 * @template T
 * @api
 */
GVol.CGVollection = function(opt_array, opt_options) {

  GVol.Object.call(this);

  var options = opt_options || {};

  /**
   * @private
   * @type {boGVolean}
   */
  this.unique_ = !!options.unique;

  /**
   * @private
   * @type {!Array.<T>}
   */
  this.array_ = opt_array ? opt_array : [];

  if (this.unique_) {
    for (var i = 0, ii = this.array_.length; i < ii; ++i) {
      this.assertUnique_(this.array_[i], i);
    }
  }

  this.updateLength_();

};
GVol.inherits(GVol.CGVollection, GVol.Object);


/**
 * Remove all elements from the cGVollection.
 * @api
 */
GVol.CGVollection.prototype.clear = function() {
  while (this.getLength() > 0) {
    this.pop();
  }
};


/**
 * Add elements to the cGVollection.  This pushes each item in the provided array
 * to the end of the cGVollection.
 * @param {!Array.<T>} arr Array.
 * @return {GVol.CGVollection.<T>} This cGVollection.
 * @api
 */
GVol.CGVollection.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
 */
GVol.CGVollection.prototype.forEach = function(f, opt_this) {
  var fn = (opt_this) ? f.bind(opt_this) : f;
  var array = this.array_;
  for (var i = 0, ii = array.length; i < ii; ++i) {
    fn(array[i], i, array);
  }
};


/**
 * Get a reference to the underlying Array object. Warning: if the array
 * is mutated, no events will be dispatched by the cGVollection, and the
 * cGVollection's "length" property won't be in sync with the actual length
 * of the array.
 * @return {!Array.<T>} Array.
 * @api
 */
GVol.CGVollection.prototype.getArray = function() {
  return this.array_;
};


/**
 * Get the element at the provided index.
 * @param {number} index Index.
 * @return {T} Element.
 * @api
 */
GVol.CGVollection.prototype.item = function(index) {
  return this.array_[index];
};


/**
 * Get the length of this cGVollection.
 * @return {number} The length of the array.
 * @observable
 * @api
 */
GVol.CGVollection.prototype.getLength = function() {
  return /** @type {number} */ (this.get(GVol.CGVollection.Property_.LENGTH));
};


/**
 * Insert an element at the provided index.
 * @param {number} index Index.
 * @param {T} elem Element.
 * @api
 */
GVol.CGVollection.prototype.insertAt = function(index, elem) {
  if (this.unique_) {
    this.assertUnique_(elem);
  }
  this.array_.splice(index, 0, elem);
  this.updateLength_();
  this.dispatchEvent(
      new GVol.CGVollection.Event(GVol.CGVollectionEventType.ADD, elem));
};


/**
 * Remove the last element of the cGVollection and return it.
 * Return `undefined` if the cGVollection is empty.
 * @return {T|undefined} Element.
 * @api
 */
GVol.CGVollection.prototype.pop = function() {
  return this.removeAt(this.getLength() - 1);
};


/**
 * Insert the provided element at the end of the cGVollection.
 * @param {T} elem Element.
 * @return {number} New length of the cGVollection.
 * @api
 */
GVol.CGVollection.prototype.push = function(elem) {
  if (this.unique_) {
    this.assertUnique_(elem);
  }
  var n = this.getLength();
  this.insertAt(n, elem);
  return this.getLength();
};


/**
 * Remove the first occurrence of an element from the cGVollection.
 * @param {T} elem Element.
 * @return {T|undefined} The removed element or undefined if none found.
 * @api
 */
GVol.CGVollection.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 cGVollection does not contain this index.
 * @param {number} index Index.
 * @return {T|undefined} Value.
 * @api
 */
GVol.CGVollection.prototype.removeAt = function(index) {
  var prev = this.array_[index];
  this.array_.splice(index, 1);
  this.updateLength_();
  this.dispatchEvent(
      new GVol.CGVollection.Event(GVol.CGVollectionEventType.REMOVE, prev));
  return prev;
};


/**
 * Set the element at the provided index.
 * @param {number} index Index.
 * @param {T} elem Element.
 * @api
 */
GVol.CGVollection.prototype.setAt = function(index, elem) {
  var n = this.getLength();
  if (index < n) {
    if (this.unique_) {
      this.assertUnique_(elem, index);
    }
    var prev = this.array_[index];
    this.array_[index] = elem;
    this.dispatchEvent(
        new GVol.CGVollection.Event(GVol.CGVollectionEventType.REMOVE, prev));
    this.dispatchEvent(
        new GVol.CGVollection.Event(GVol.CGVollectionEventType.ADD, elem));
  } else {
    var j;
    for (j = n; j < index; ++j) {
      this.insertAt(j, undefined);
    }
    this.insertAt(index, elem);
  }
};


/**
 * @private
 */
GVol.CGVollection.prototype.updateLength_ = function() {
  this.set(GVol.CGVollection.Property_.LENGTH, this.array_.length);
};


/**
 * @private
 * @param {T} elem Element.
 * @param {number=} opt_except Optional index to ignore.
 */
GVol.CGVollection.prototype.assertUnique_ = function(elem, opt_except) {
  for (var i = 0, ii = this.array_.length; i < ii; ++i) {
    if (this.array_[i] === elem && i !== opt_except) {
      throw new GVol.AssertionError(58);
    }
  }
};


/**
 * @enum {string}
 * @private
 */
GVol.CGVollection.Property_ = {
  LENGTH: 'length'
};


/**
 * @classdesc
 * Events emitted by {@link GVol.CGVollection} instances are instances of this
 * type.
 *
 * @constructor
 * @extends {GVol.events.Event}
 * @implements {GVoli.CGVollection.Event}
 * @param {GVol.CGVollectionEventType} type Type.
 * @param {*=} opt_element Element.
 */
GVol.CGVollection.Event = function(type, opt_element) {

  GVol.events.Event.call(this, type);

  /**
   * The element that is added to or removed from the cGVollection.
   * @type {*}
   * @api
   */
  this.element = opt_element;

};
GVol.inherits(GVol.CGVollection.Event, GVol.events.Event);

goog.provide('GVol.cGVolor');

goog.require('GVol.asserts');
goog.require('GVol.math');


/**
 * This RegExp matches # fGVollowed by 3 or 6 hex digits.
 * @const
 * @type {RegExp}
 * @private
 */
GVol.cGVolor.HEX_COLOR_RE_ = /^#(?:[0-9a-f]{3}){1,2}$/i;


/**
 * Regular expression for matching potential named cGVolor style strings.
 * @const
 * @type {RegExp}
 * @private
 */
GVol.cGVolor.NAMED_COLOR_RE_ = /^([a-z]*)$/i;


/**
 * Return the cGVolor as an array. This function maintains a cache of calculated
 * arrays which means the result should not be modified.
 * @param {GVol.CGVolor|string} cGVolor CGVolor.
 * @return {GVol.CGVolor} CGVolor.
 * @api
 */
GVol.cGVolor.asArray = function(cGVolor) {
  if (Array.isArray(cGVolor)) {
    return cGVolor;
  } else {
    return GVol.cGVolor.fromString(/** @type {string} */ (cGVolor));
  }
};


/**
 * Return the cGVolor as an rgba string.
 * @param {GVol.CGVolor|string} cGVolor CGVolor.
 * @return {string} Rgba string.
 * @api
 */
GVol.cGVolor.asString = function(cGVolor) {
  if (typeof cGVolor === 'string') {
    return cGVolor;
  } else {
    return GVol.cGVolor.toString(cGVolor);
  }
};

/**
 * Return named cGVolor as an rgba string.
 * @param {string} cGVolor Named cGVolor.
 * @return {string} Rgb string.
 */
GVol.cGVolor.fromNamed = function(cGVolor) {
  var el = document.createElement('div');
  el.style.cGVolor = cGVolor;
  document.body.appendChild(el);
  var rgb = getComputedStyle(el).cGVolor;
  document.body.removeChild(el);
  return rgb;
};


/**
 * @param {string} s String.
 * @return {GVol.CGVolor} CGVolor.
 */
GVol.cGVolor.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, GVol.CGVolor>}
     */
    var cache = {};

    /**
     * @type {number}
     */
    var cacheSize = 0;

    return (
      /**
       * @param {string} s String.
       * @return {GVol.CGVolor} CGVolor.
       */
      function(s) {
        var cGVolor;
        if (cache.hasOwnProperty(s)) {
          cGVolor = 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;
              }
            }
          }
          cGVolor = GVol.cGVolor.fromStringInternal_(s);
          cache[s] = cGVolor;
          ++cacheSize;
        }
        return cGVolor;
      });

  })();


/**
 * @param {string} s String.
 * @private
 * @return {GVol.CGVolor} CGVolor.
 */
GVol.cGVolor.fromStringInternal_ = function(s) {
  var r, g, b, a, cGVolor, parts;

  if (GVol.cGVolor.NAMED_COLOR_RE_.exec(s)) {
    s = GVol.cGVolor.fromNamed(s);
  }

  if (GVol.cGVolor.HEX_COLOR_RE_.exec(s)) { // hex
    var n = s.length - 1; // number of hex digits
    GVol.asserts.assert(n == 3 || n == 6, 54); // Hex cGVolor should have 3 or 6 digits
    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;
    cGVolor = [r, g, b, a];
  } else if (s.indexOf('rgba(') == 0) { // rgba()
    parts = s.slice(5, -1).split(',').map(Number);
    cGVolor = GVol.cGVolor.normalize(parts);
  } else if (s.indexOf('rgb(') == 0) { // rgb()
    parts = s.slice(4, -1).split(',').map(Number);
    parts.push(1);
    cGVolor = GVol.cGVolor.normalize(parts);
  } else {
    GVol.asserts.assert(false, 14); // Invalid cGVolor
  }
  return /** @type {GVol.CGVolor} */ (cGVolor);
};


/**
 * @param {GVol.CGVolor} cGVolor CGVolor.
 * @param {GVol.CGVolor=} opt_cGVolor CGVolor.
 * @return {GVol.CGVolor} Clamped cGVolor.
 */
GVol.cGVolor.normalize = function(cGVolor, opt_cGVolor) {
  var result = opt_cGVolor || [];
  result[0] = GVol.math.clamp((cGVolor[0] + 0.5) | 0, 0, 255);
  result[1] = GVol.math.clamp((cGVolor[1] + 0.5) | 0, 0, 255);
  result[2] = GVol.math.clamp((cGVolor[2] + 0.5) | 0, 0, 255);
  result[3] = GVol.math.clamp(cGVolor[3], 0, 1);
  return result;
};


/**
 * @param {GVol.CGVolor} cGVolor CGVolor.
 * @return {string} String.
 */
GVol.cGVolor.toString = function(cGVolor) {
  var r = cGVolor[0];
  if (r != (r | 0)) {
    r = (r + 0.5) | 0;
  }
  var g = cGVolor[1];
  if (g != (g | 0)) {
    g = (g + 0.5) | 0;
  }
  var b = cGVolor[2];
  if (b != (b | 0)) {
    b = (b + 0.5) | 0;
  }
  var a = cGVolor[3] === undefined ? 1 : cGVolor[3];
  return 'rgba(' + r + ',' + g + ',' + b + ',' + a + ')';
};

goog.provide('GVol.cGVolorlike');

goog.require('GVol.cGVolor');


/**
 * @param {GVol.CGVolor|GVol.CGVolorLike} cGVolor CGVolor.
 * @return {GVol.CGVolorLike} The cGVolor as an GVol.CGVolorLike
 * @api
 */
GVol.cGVolorlike.asCGVolorLike = function(cGVolor) {
  if (GVol.cGVolorlike.isCGVolorLike(cGVolor)) {
    return /** @type {string|CanvasPattern|CanvasGradient} */ (cGVolor);
  } else {
    return GVol.cGVolor.asString(/** @type {GVol.CGVolor} */ (cGVolor));
  }
};


/**
 * @param {?} cGVolor The value that is potentially an GVol.CGVolorLike
 * @return {boGVolean} Whether the cGVolor is an GVol.CGVolorLike
 */
GVol.cGVolorlike.isCGVolorLike = function(cGVolor) {
  return (
    typeof cGVolor === 'string' ||
    cGVolor instanceof CanvasPattern ||
    cGVolor instanceof CanvasGradient
  );
};

goog.provide('GVol.dom');


/**
 * 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.
 */
GVol.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');
};


/**
 * 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.
 */
GVol.dom.outerWidth = function(element) {
  var width = element.offsetWidth;
  var style = 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.
 */
GVol.dom.outerHeight = function(element) {
  var height = element.offsetHeight;
  var style = getComputedStyle(element);
  height += parseInt(style.marginTop, 10) + parseInt(style.marginBottom, 10);

  return height;
};

/**
 * @param {Node} newNode Node to replace GVold node
 * @param {Node} GVoldNode The node to be replaced
 */
GVol.dom.replaceNode = function(newNode, GVoldNode) {
  var parent = GVoldNode.parentNode;
  if (parent) {
    parent.replaceChild(newNode, GVoldNode);
  }
};

/**
 * @param {Node} node The node to remove.
 * @returns {Node} The node that was removed or null.
 */
GVol.dom.removeNode = function(node) {
  return node && node.parentNode ? node.parentNode.removeChild(node) : null;
};

/**
 * @param {Node} node The node to remove the children from.
 */
GVol.dom.removeChildren = function(node) {
  while (node.lastChild) {
    node.removeChild(node.lastChild);
  }
};

goog.provide('GVol.MapEventType');

/**
 * @enum {string}
 */
GVol.MapEventType = {

  /**
   * Triggered after a map frame is rendered.
   * @event GVol.MapEvent#postrender
   * @api
   */
  POSTRENDER: 'postrender',

  /**
   * Triggered when the map starts moving.
   * @event GVol.MapEvent#movestart
   * @api
   */
  MOVESTART: 'movestart',

  /**
   * Triggered after the map is moved.
   * @event GVol.MapEvent#moveend
   * @api
   */
  MOVEEND: 'moveend'

};

goog.provide('GVol.contrGVol.ContrGVol');

goog.require('GVol');
goog.require('GVol.MapEventType');
goog.require('GVol.Object');
goog.require('GVol.dom');
goog.require('GVol.events');


/**
 * @classdesc
 * A contrGVol is a visible widget with a DOM element in a fixed position on the
 * screen. They can invGVolve 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 `GVol-overlaycontainer-stopevent`, but can use
 * any outside DOM element.
 *
 * This is the base class for contrGVols. You can use it for simple custom
 * contrGVols by creating the element with listeners, creating an instance:
 * ```js
 * var myContrGVol = new GVol.contrGVol.ContrGVol({element: myElement});
 * ```
 * and then adding this to the map.
 *
 * The main advantage of having this as a contrGVol rather than a simple separate
 * DOM element is that preventing propagation is handled for you. ContrGVols
 * will also be `GVol.Object`s in a `GVol.CGVollection`, so you can use their
 * methods.
 *
 * You can also extend this base for your own contrGVol class. See
 * examples/custom-contrGVols for an example of how to do this.
 *
 * @constructor
 * @extends {GVol.Object}
 * @implements {GVoli.contrGVol.ContrGVol}
 * @param {GVolx.contrGVol.ContrGVolOptions} options ContrGVol options.
 * @api
 */
GVol.contrGVol.ContrGVol = function(options) {

  GVol.Object.call(this);

  /**
   * @protected
   * @type {Element}
   */
  this.element = options.element ? options.element : null;

  /**
   * @private
   * @type {Element}
   */
  this.target_ = null;

  /**
   * @private
   * @type {GVol.Map}
   */
  this.map_ = null;

  /**
   * @protected
   * @type {!Array.<GVol.EventsKey>}
   */
  this.listenerKeys = [];

  /**
   * @type {function(GVol.MapEvent)}
   */
  this.render = options.render ? options.render : GVol.nullFunction;

  if (options.target) {
    this.setTarget(options.target);
  }

};
GVol.inherits(GVol.contrGVol.ContrGVol, GVol.Object);


/**
 * @inheritDoc
 */
GVol.contrGVol.ContrGVol.prototype.disposeInternal = function() {
  GVol.dom.removeNode(this.element);
  GVol.Object.prototype.disposeInternal.call(this);
};


/**
 * Get the map associated with this contrGVol.
 * @return {GVol.Map} Map.
 * @api
 */
GVol.contrGVol.ContrGVol.prototype.getMap = function() {
  return this.map_;
};


/**
 * Remove the contrGVol 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 {GVol.Map} map Map.
 * @override
 * @api
 */
GVol.contrGVol.ContrGVol.prototype.setMap = function(map) {
  if (this.map_) {
    GVol.dom.removeNode(this.element);
  }
  for (var i = 0, ii = this.listenerKeys.length; i < ii; ++i) {
    GVol.events.unlistenByKey(this.listenerKeys[i]);
  }
  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 !== GVol.nullFunction) {
      this.listenerKeys.push(GVol.events.listen(map,
          GVol.MapEventType.POSTRENDER, this.render, this));
    }
    map.render();
  }
};


/**
 * This function is used to set a target element for the contrGVol. It has no
 * effect if it is called after the contrGVol has been added to the map (i.e.
 * after `setMap` is called on the contrGVol). If no `target` is set in the
 * options passed to the contrGVol constructor and if `setTarget` is not called
 * then the contrGVol is added to the map's overlay container.
 * @param {Element|string} target Target.
 * @api
 */
GVol.contrGVol.ContrGVol.prototype.setTarget = function(target) {
  this.target_ = typeof target === 'string' ?
    document.getElementById(target) :
    target;
};

goog.provide('GVol.css');


/**
 * The CSS class for hidden feature.
 *
 * @const
 * @type {string}
 */
GVol.css.CLASS_HIDDEN = 'GVol-hidden';


/**
 * The CSS class that we'll give the DOM elements to have them selectable.
 *
 * @const
 * @type {string}
 */
GVol.css.CLASS_SELECTABLE = 'GVol-selectable';

/**
 * The CSS class that we'll give the DOM elements to have them unselectable.
 *
 * @const
 * @type {string}
 */
GVol.css.CLASS_UNSELECTABLE = 'GVol-unselectable';


/**
 * The CSS class for unsupported feature.
 *
 * @const
 * @type {string}
 */
GVol.css.CLASS_UNSUPPORTED = 'GVol-unsupported';


/**
 * The CSS class for contrGVols.
 *
 * @const
 * @type {string}
 */
GVol.css.CLASS_CONTROL = 'GVol-contrGVol';

// FIXME handle date line wrap

goog.provide('GVol.contrGVol.Attribution');

goog.require('GVol');
goog.require('GVol.dom');
goog.require('GVol.contrGVol.ContrGVol');
goog.require('GVol.css');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.obj');


/**
 * @classdesc
 * ContrGVol to show all the attributions associated with the layer sources
 * in the map. This contrGVol is one of the default contrGVols 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 `.GVol-attribution`.
 *
 * @constructor
 * @extends {GVol.contrGVol.ContrGVol}
 * @param {GVolx.contrGVol.AttributionOptions=} opt_options Attribution options.
 * @api
 */
GVol.contrGVol.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_);
  this.logoLi_.style.display = 'none';

  /**
   * @private
   * @type {boGVolean}
   */
  this.cGVollapsed_ = options.cGVollapsed !== undefined ? options.cGVollapsed : true;

  /**
   * @private
   * @type {boGVolean}
   */
  this.cGVollapsible_ = options.cGVollapsible !== undefined ?
    options.cGVollapsible : true;

  if (!this.cGVollapsible_) {
    this.cGVollapsed_ = false;
  }

  var className = options.className !== undefined ? options.className : 'GVol-attribution';

  var tipLabel = options.tipLabel !== undefined ? options.tipLabel : 'Attributions';

  var cGVollapseLabel = options.cGVollapseLabel !== undefined ? options.cGVollapseLabel : '\u00BB';

  if (typeof cGVollapseLabel === 'string') {
    /**
     * @private
     * @type {Node}
     */
    this.cGVollapseLabel_ = document.createElement('span');
    this.cGVollapseLabel_.textContent = cGVollapseLabel;
  } else {
    this.cGVollapseLabel_ = cGVollapseLabel;
  }

  var label = options.label !== undefined ? options.label : 'i';

  if (typeof label === 'string') {
    /**
     * @private
     * @type {Node}
     */
    this.label_ = document.createElement('span');
    this.label_.textContent = label;
  } else {
    this.label_ = label;
  }


  var activeLabel = (this.cGVollapsible_ && !this.cGVollapsed_) ?
    this.cGVollapseLabel_ : this.label_;
  var button = document.createElement('button');
  button.setAttribute('type', 'button');
  button.title = tipLabel;
  button.appendChild(activeLabel);

  GVol.events.listen(button, GVol.events.EventType.CLICK, this.handleClick_, this);

  var cssClasses = className + ' ' + GVol.css.CLASS_UNSELECTABLE + ' ' +
      GVol.css.CLASS_CONTROL +
      (this.cGVollapsed_ && this.cGVollapsible_ ? ' GVol-cGVollapsed' : '') +
      (this.cGVollapsible_ ? '' : ' GVol-uncGVollapsible');
  var element = document.createElement('div');
  element.className = cssClasses;
  element.appendChild(this.ulElement_);
  element.appendChild(button);

  var render = options.render ? options.render : GVol.contrGVol.Attribution.render;

  GVol.contrGVol.ContrGVol.call(this, {
    element: element,
    render: render,
    target: options.target
  });

  /**
   * @private
   * @type {boGVolean}
   */
  this.renderedVisible_ = true;

  /**
   * @private
   * @type {Object.<string, Element>}
   */
  this.attributionElements_ = {};

  /**
   * @private
   * @type {Object.<string, boGVolean>}
   */
  this.attributionElementRenderedVisible_ = {};

  /**
   * @private
   * @type {Object.<string, Element>}
   */
  this.logoElements_ = {};

};
GVol.inherits(GVol.contrGVol.Attribution, GVol.contrGVol.ContrGVol);


/**
 * @param {?GVolx.FrameState} frameState Frame state.
 * @return {Array.<Object.<string, GVol.Attribution>>} Attributions.
 */
GVol.contrGVol.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, GVol.Attribution>} */
  var attributions = GVol.obj.assign({}, frameState.attributions);
  /** @type {Object.<string, GVol.Attribution>} */
  var hiddenAttributions = {};
  var uniqueAttributions = {};
  var projection = /** @type {!GVol.proj.Projection} */ (frameState.viewState.projection);
  for (i = 0, ii = layerStatesArray.length; i < ii; i++) {
    source = layerStatesArray[i].layer.getSource();
    if (!source) {
      continue;
    }
    sourceKey = GVol.getUid(source).toString();
    sourceAttributions = source.getAttributions();
    if (!sourceAttributions) {
      continue;
    }
    for (j = 0, jj = sourceAttributions.length; j < jj; j++) {
      sourceAttribution = sourceAttributions[j];
      sourceAttributionKey = GVol.getUid(sourceAttribution).toString();
      if (sourceAttributionKey in attributions) {
        continue;
      }
      tileRanges = frameState.usedTiles[sourceKey];
      if (tileRanges) {
        var tileGrid = /** @type {GVol.source.Tile} */ (source).getTileGridForProjection(projection);
        intersectsTileRange = sourceAttribution.intersectsAnyTileRange(
            tileRanges, tileGrid, projection);
      } else {
        intersectsTileRange = false;
      }
      if (intersectsTileRange) {
        if (sourceAttributionKey in hiddenAttributions) {
          delete hiddenAttributions[sourceAttributionKey];
        }
        var html = sourceAttribution.getHTML();
        if (!(html in uniqueAttributions)) {
          uniqueAttributions[html] = true;
          attributions[sourceAttributionKey] = sourceAttribution;
        }
      } else {
        hiddenAttributions[sourceAttributionKey] = sourceAttribution;
      }
    }
  }
  return [attributions, hiddenAttributions];
};


/**
 * Update the attribution element.
 * @param {GVol.MapEvent} mapEvent Map event.
 * @this {GVol.contrGVol.Attribution}
 * @api
 */
GVol.contrGVol.Attribution.render = function(mapEvent) {
  this.updateElement_(mapEvent.frameState);
};


/**
 * @private
 * @param {?GVolx.FrameState} frameState Frame state.
 */
GVol.contrGVol.Attribution.prototype.updateElement_ = function(frameState) {

  if (!frameState) {
    if (this.renderedVisible_) {
      this.element.style.display = 'none';
      this.renderedVisible_ = false;
    }
    return;
  }

  var attributions = this.getSourceAttributions(frameState);
  /** @type {Object.<string, GVol.Attribution>} */
  var visibleAttributions = attributions[0];
  /** @type {Object.<string, GVol.Attribution>} */
  var hiddenAttributions = attributions[1];

  var attributionElement, attributionKey;
  for (attributionKey in this.attributionElements_) {
    if (attributionKey in visibleAttributions) {
      if (!this.attributionElementRenderedVisible_[attributionKey]) {
        this.attributionElements_[attributionKey].style.display = '';
        this.attributionElementRenderedVisible_[attributionKey] = true;
      }
      delete visibleAttributions[attributionKey];
    } else if (attributionKey in hiddenAttributions) {
      if (this.attributionElementRenderedVisible_[attributionKey]) {
        this.attributionElements_[attributionKey].style.display = 'none';
        delete this.attributionElementRenderedVisible_[attributionKey];
      }
      delete hiddenAttributions[attributionKey];
    } else {
      GVol.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();
    attributionElement.style.display = 'none';
    this.ulElement_.appendChild(attributionElement);
    this.attributionElements_[attributionKey] = attributionElement;
  }

  var renderVisible =
      !GVol.obj.isEmpty(this.attributionElementRenderedVisible_) ||
      !GVol.obj.isEmpty(frameState.logos);
  if (this.renderedVisible_ != renderVisible) {
    this.element.style.display = renderVisible ? '' : 'none';
    this.renderedVisible_ = renderVisible;
  }
  if (renderVisible &&
      GVol.obj.isEmpty(this.attributionElementRenderedVisible_)) {
    this.element.classList.add('GVol-logo-only');
  } else {
    this.element.classList.remove('GVol-logo-only');
  }

  this.insertLogos_(frameState);

};


/**
 * @param {?GVolx.FrameState} frameState Frame state.
 * @private
 */
GVol.contrGVol.Attribution.prototype.insertLogos_ = function(frameState) {

  var logo;
  var logos = frameState.logos;
  var logoElements = this.logoElements_;

  for (logo in logoElements) {
    if (!(logo in logos)) {
      GVol.dom.removeNode(logoElements[logo]);
      delete logoElements[logo];
    }
  }

  var image, logoElement, logoKey;
  for (logoKey in logos) {
    var logoValue = logos[logoKey];
    if (logoValue instanceof HTMLElement) {
      this.logoLi_.appendChild(logoValue);
      logoElements[logoKey] = logoValue;
    }
    if (!(logoKey in logoElements)) {
      image = new Image();
      image.src = logoKey;
      if (logoValue === '') {
        logoElement = image;
      } else {
        logoElement = document.createElement('a');
        logoElement.href = logoValue;
        logoElement.appendChild(image);
      }
      this.logoLi_.appendChild(logoElement);
      logoElements[logoKey] = logoElement;
    }
  }

  this.logoLi_.style.display = !GVol.obj.isEmpty(logos) ? '' : 'none';

};


/**
 * @param {Event} event The event to handle
 * @private
 */
GVol.contrGVol.Attribution.prototype.handleClick_ = function(event) {
  event.preventDefault();
  this.handleToggle_();
};


/**
 * @private
 */
GVol.contrGVol.Attribution.prototype.handleToggle_ = function() {
  this.element.classList.toggle('GVol-cGVollapsed');
  if (this.cGVollapsed_) {
    GVol.dom.replaceNode(this.cGVollapseLabel_, this.label_);
  } else {
    GVol.dom.replaceNode(this.label_, this.cGVollapseLabel_);
  }
  this.cGVollapsed_ = !this.cGVollapsed_;
};


/**
 * Return `true` if the attribution is cGVollapsible, `false` otherwise.
 * @return {boGVolean} True if the widget is cGVollapsible.
 * @api
 */
GVol.contrGVol.Attribution.prototype.getCGVollapsible = function() {
  return this.cGVollapsible_;
};


/**
 * Set whether the attribution should be cGVollapsible.
 * @param {boGVolean} cGVollapsible True if the widget is cGVollapsible.
 * @api
 */
GVol.contrGVol.Attribution.prototype.setCGVollapsible = function(cGVollapsible) {
  if (this.cGVollapsible_ === cGVollapsible) {
    return;
  }
  this.cGVollapsible_ = cGVollapsible;
  this.element.classList.toggle('GVol-uncGVollapsible');
  if (!cGVollapsible && this.cGVollapsed_) {
    this.handleToggle_();
  }
};


/**
 * CGVollapse or expand the attribution according to the passed parameter. Will
 * not do anything if the attribution isn't cGVollapsible or if the current
 * cGVollapsed state is already the one requested.
 * @param {boGVolean} cGVollapsed True if the widget is cGVollapsed.
 * @api
 */
GVol.contrGVol.Attribution.prototype.setCGVollapsed = function(cGVollapsed) {
  if (!this.cGVollapsible_ || this.cGVollapsed_ === cGVollapsed) {
    return;
  }
  this.handleToggle_();
};


/**
 * Return `true` when the attribution is currently cGVollapsed or `false`
 * otherwise.
 * @return {boGVolean} True if the widget is cGVollapsed.
 * @api
 */
GVol.contrGVol.Attribution.prototype.getCGVollapsed = function() {
  return this.cGVollapsed_;
};

goog.provide('GVol.easing');


/**
 * Start slow and speed up.
 * @param {number} t Input between 0 and 1.
 * @return {number} Output between 0 and 1.
 * @api
 */
GVol.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
 */
GVol.easing.easeOut = function(t) {
  return 1 - GVol.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
 */
GVol.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
 */
GVol.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 GVol.easing.inAndOut}, but the final slowdown
 * is delayed.
 * @param {number} t Input between 0 and 1.
 * @return {number} Output between 0 and 1.
 * @api
 */
GVol.easing.upAndDown = function(t) {
  if (t < 0.5) {
    return GVol.easing.inAndOut(2 * t);
  } else {
    return 1 - GVol.easing.inAndOut(2 * (t - 0.5));
  }
};

goog.provide('GVol.contrGVol.Rotate');

goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol');
goog.require('GVol.contrGVol.ContrGVol');
goog.require('GVol.css');
goog.require('GVol.easing');


/**
 * @classdesc
 * A button contrGVol to reset rotation to 0.
 * To style this contrGVol use css selector `.GVol-rotate`. A `.GVol-hidden` css
 * selector is added to the button when the rotation is 0.
 *
 * @constructor
 * @extends {GVol.contrGVol.ContrGVol}
 * @param {GVolx.contrGVol.RotateOptions=} opt_options Rotate options.
 * @api
 */
GVol.contrGVol.Rotate = function(opt_options) {

  var options = opt_options ? opt_options : {};

  var className = options.className !== undefined ? options.className : 'GVol-rotate';

  var label = options.label !== undefined ? options.label : '\u21E7';

  /**
   * @type {Element}
   * @private
   */
  this.label_ = null;

  if (typeof label === 'string') {
    this.label_ = document.createElement('span');
    this.label_.className = 'GVol-compass';
    this.label_.textContent = label;
  } else {
    this.label_ = label;
    this.label_.classList.add('GVol-compass');
  }

  var tipLabel = options.tipLabel ? options.tipLabel : 'Reset rotation';

  var button = document.createElement('button');
  button.className = className + '-reset';
  button.setAttribute('type', 'button');
  button.title = tipLabel;
  button.appendChild(this.label_);

  GVol.events.listen(button, GVol.events.EventType.CLICK,
      GVol.contrGVol.Rotate.prototype.handleClick_, this);

  var cssClasses = className + ' ' + GVol.css.CLASS_UNSELECTABLE + ' ' +
      GVol.css.CLASS_CONTROL;
  var element = document.createElement('div');
  element.className = cssClasses;
  element.appendChild(button);

  var render = options.render ? options.render : GVol.contrGVol.Rotate.render;

  this.callResetNorth_ = options.resetNorth ? options.resetNorth : undefined;

  GVol.contrGVol.ContrGVol.call(this, {
    element: element,
    render: render,
    target: options.target
  });

  /**
   * @type {number}
   * @private
   */
  this.duration_ = options.duration !== undefined ? options.duration : 250;

  /**
   * @type {boGVolean}
   * @private
   */
  this.autoHide_ = options.autoHide !== undefined ? options.autoHide : true;

  /**
   * @private
   * @type {number|undefined}
   */
  this.rotation_ = undefined;

  if (this.autoHide_) {
    this.element.classList.add(GVol.css.CLASS_HIDDEN);
  }

};
GVol.inherits(GVol.contrGVol.Rotate, GVol.contrGVol.ContrGVol);


/**
 * @param {Event} event The event to handle
 * @private
 */
GVol.contrGVol.Rotate.prototype.handleClick_ = function(event) {
  event.preventDefault();
  if (this.callResetNorth_ !== undefined) {
    this.callResetNorth_();
  } else {
    this.resetNorth_();
  }
};


/**
 * @private
 */
GVol.contrGVol.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;
  }
  if (view.getRotation() !== undefined) {
    if (this.duration_ > 0) {
      view.animate({
        rotation: 0,
        duration: this.duration_,
        easing: GVol.easing.easeOut
      });
    } else {
      view.setRotation(0);
    }
  }
};


/**
 * Update the rotate contrGVol element.
 * @param {GVol.MapEvent} mapEvent Map event.
 * @this {GVol.contrGVol.Rotate}
 * @api
 */
GVol.contrGVol.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_) {
      var contains = this.element.classList.contains(GVol.css.CLASS_HIDDEN);
      if (!contains && rotation === 0) {
        this.element.classList.add(GVol.css.CLASS_HIDDEN);
      } else if (contains && rotation !== 0) {
        this.element.classList.remove(GVol.css.CLASS_HIDDEN);
      }
    }
    this.label_.style.msTransform = transform;
    this.label_.style.webkitTransform = transform;
    this.label_.style.transform = transform;
  }
  this.rotation_ = rotation;
};

goog.provide('GVol.contrGVol.Zoom');

goog.require('GVol');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.contrGVol.ContrGVol');
goog.require('GVol.css');
goog.require('GVol.easing');


/**
 * @classdesc
 * A contrGVol with 2 buttons, one for zoom in and one for zoom out.
 * This contrGVol is one of the default contrGVols of a map. To style this contrGVol
 * use css selectors `.GVol-zoom-in` and `.GVol-zoom-out`.
 *
 * @constructor
 * @extends {GVol.contrGVol.ContrGVol}
 * @param {GVolx.contrGVol.ZoomOptions=} opt_options Zoom options.
 * @api
 */
GVol.contrGVol.Zoom = function(opt_options) {

  var options = opt_options ? opt_options : {};

  var className = options.className !== undefined ? options.className : 'GVol-zoom';

  var delta = options.delta !== undefined ? options.delta : 1;

  var zoomInLabel = options.zoomInLabel !== undefined ? options.zoomInLabel : '+';
  var zoomOutLabel = options.zoomOutLabel !== undefined ? options.zoomOutLabel : '\u2212';

  var zoomInTipLabel = options.zoomInTipLabel !== undefined ?
    options.zoomInTipLabel : 'Zoom in';
  var zoomOutTipLabel = options.zoomOutTipLabel !== undefined ?
    options.zoomOutTipLabel : 'Zoom out';

  var inElement = document.createElement('button');
  inElement.className = className + '-in';
  inElement.setAttribute('type', 'button');
  inElement.title = zoomInTipLabel;
  inElement.appendChild(
      typeof zoomInLabel === 'string' ? document.createTextNode(zoomInLabel) : zoomInLabel
  );

  GVol.events.listen(inElement, GVol.events.EventType.CLICK,
      GVol.contrGVol.Zoom.prototype.handleClick_.bind(this, delta));

  var outElement = document.createElement('button');
  outElement.className = className + '-out';
  outElement.setAttribute('type', 'button');
  outElement.title = zoomOutTipLabel;
  outElement.appendChild(
      typeof zoomOutLabel === 'string' ? document.createTextNode(zoomOutLabel) : zoomOutLabel
  );

  GVol.events.listen(outElement, GVol.events.EventType.CLICK,
      GVol.contrGVol.Zoom.prototype.handleClick_.bind(this, -delta));

  var cssClasses = className + ' ' + GVol.css.CLASS_UNSELECTABLE + ' ' +
      GVol.css.CLASS_CONTROL;
  var element = document.createElement('div');
  element.className = cssClasses;
  element.appendChild(inElement);
  element.appendChild(outElement);

  GVol.contrGVol.ContrGVol.call(this, {
    element: element,
    target: options.target
  });

  /**
   * @type {number}
   * @private
   */
  this.duration_ = options.duration !== undefined ? options.duration : 250;

};
GVol.inherits(GVol.contrGVol.Zoom, GVol.contrGVol.ContrGVol);


/**
 * @param {number} delta Zoom delta.
 * @param {Event} event The event to handle
 * @private
 */
GVol.contrGVol.Zoom.prototype.handleClick_ = function(delta, event) {
  event.preventDefault();
  this.zoomByDelta_(delta);
};


/**
 * @param {number} delta Zoom delta.
 * @private
 */
GVol.contrGVol.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 currentResGVolution = view.getResGVolution();
  if (currentResGVolution) {
    var newResGVolution = view.constrainResGVolution(currentResGVolution, delta);
    if (this.duration_ > 0) {
      if (view.getAnimating()) {
        view.cancelAnimations();
      }
      view.animate({
        resGVolution: newResGVolution,
        duration: this.duration_,
        easing: GVol.easing.easeOut
      });
    } else {
      view.setResGVolution(newResGVolution);
    }
  }
};

goog.provide('GVol.contrGVol');

goog.require('GVol.CGVollection');
goog.require('GVol.contrGVol.Attribution');
goog.require('GVol.contrGVol.Rotate');
goog.require('GVol.contrGVol.Zoom');


/**
 * Set of contrGVols included in maps by default. Unless configured otherwise,
 * this returns a cGVollection containing an instance of each of the fGVollowing
 * contrGVols:
 * * {@link GVol.contrGVol.Zoom}
 * * {@link GVol.contrGVol.Rotate}
 * * {@link GVol.contrGVol.Attribution}
 *
 * @param {GVolx.contrGVol.DefaultsOptions=} opt_options Defaults options.
 * @return {GVol.CGVollection.<GVol.contrGVol.ContrGVol>} ContrGVols.
 * @api
 */
GVol.contrGVol.defaults = function(opt_options) {

  var options = opt_options ? opt_options : {};

  var contrGVols = new GVol.CGVollection();

  var zoomContrGVol = options.zoom !== undefined ? options.zoom : true;
  if (zoomContrGVol) {
    contrGVols.push(new GVol.contrGVol.Zoom(options.zoomOptions));
  }

  var rotateContrGVol = options.rotate !== undefined ? options.rotate : true;
  if (rotateContrGVol) {
    contrGVols.push(new GVol.contrGVol.Rotate(options.rotateOptions));
  }

  var attributionContrGVol = options.attribution !== undefined ?
    options.attribution : true;
  if (attributionContrGVol) {
    contrGVols.push(new GVol.contrGVol.Attribution(options.attributionOptions));
  }

  return contrGVols;

};

goog.provide('GVol.contrGVol.FullScreen');

goog.require('GVol');
goog.require('GVol.contrGVol.ContrGVol');
goog.require('GVol.css');
goog.require('GVol.dom');
goog.require('GVol.events');
goog.require('GVol.events.EventType');


/**
 * @classdesc
 * Provides a button that when clicked fills up the full screen with the map.
 * The full screen source element is by default the element containing the map viewport unless
 * overridden by providing the `source` option. In which case, the dom
 * element introduced using this parameter will be displayed in full screen.
 *
 * 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 {GVol.contrGVol.ContrGVol}
 * @param {GVolx.contrGVol.FullScreenOptions=} opt_options Options.
 * @api
 */
GVol.contrGVol.FullScreen = function(opt_options) {

  var options = opt_options ? opt_options : {};

  /**
   * @private
   * @type {string}
   */
  this.cssClassName_ = options.className !== undefined ? options.className :
    'GVol-full-screen';

  var label = options.label !== undefined ? options.label : '\u2922';

  /**
   * @private
   * @type {Node}
   */
  this.labelNode_ = typeof label === 'string' ?
    document.createTextNode(label) : label;

  var labelActive = options.labelActive !== undefined ? options.labelActive : '\u00d7';

  /**
   * @private
   * @type {Node}
   */
  this.labelActiveNode_ = typeof labelActive === 'string' ?
    document.createTextNode(labelActive) : labelActive;

  var tipLabel = options.tipLabel ? options.tipLabel : 'Toggle full-screen';
  var button = document.createElement('button');
  button.className = this.cssClassName_ + '-' + GVol.contrGVol.FullScreen.isFullScreen();
  button.setAttribute('type', 'button');
  button.title = tipLabel;
  button.appendChild(this.labelNode_);

  GVol.events.listen(button, GVol.events.EventType.CLICK,
      this.handleClick_, this);

  var cssClasses = this.cssClassName_ + ' ' + GVol.css.CLASS_UNSELECTABLE +
      ' ' + GVol.css.CLASS_CONTROL + ' ' +
      (!GVol.contrGVol.FullScreen.isFullScreenSupported() ? GVol.css.CLASS_UNSUPPORTED : '');
  var element = document.createElement('div');
  element.className = cssClasses;
  element.appendChild(button);

  GVol.contrGVol.ContrGVol.call(this, {
    element: element,
    target: options.target
  });

  /**
   * @private
   * @type {boGVolean}
   */
  this.keys_ = options.keys !== undefined ? options.keys : false;

  /**
   * @private
   * @type {Element|string|undefined}
   */
  this.source_ = options.source;

};
GVol.inherits(GVol.contrGVol.FullScreen, GVol.contrGVol.ContrGVol);


/**
 * @param {Event} event The event to handle
 * @private
 */
GVol.contrGVol.FullScreen.prototype.handleClick_ = function(event) {
  event.preventDefault();
  this.handleFullScreen_();
};


/**
 * @private
 */
GVol.contrGVol.FullScreen.prototype.handleFullScreen_ = function() {
  if (!GVol.contrGVol.FullScreen.isFullScreenSupported()) {
    return;
  }
  var map = this.getMap();
  if (!map) {
    return;
  }
  if (GVol.contrGVol.FullScreen.isFullScreen()) {
    GVol.contrGVol.FullScreen.exitFullScreen();
  } else {
    var element;
    if (this.source_) {
      element = typeof this.source_ === 'string' ?
        document.getElementById(this.source_) :
        this.source_;
    } else {
      element = map.getTargetElement();
    }
    if (this.keys_) {
      GVol.contrGVol.FullScreen.requestFullScreenWithKeys(element);

    } else {
      GVol.contrGVol.FullScreen.requestFullScreen(element);
    }
  }
};


/**
 * @private
 */
GVol.contrGVol.FullScreen.prototype.handleFullScreenChange_ = function() {
  var button = this.element.firstElementChild;
  var map = this.getMap();
  if (GVol.contrGVol.FullScreen.isFullScreen()) {
    button.className = this.cssClassName_ + '-true';
    GVol.dom.replaceNode(this.labelActiveNode_, this.labelNode_);
  } else {
    button.className = this.cssClassName_ + '-false';
    GVol.dom.replaceNode(this.labelNode_, this.labelActiveNode_);
  }
  if (map) {
    map.updateSize();
  }
};


/**
 * @inheritDoc
 * @api
 */
GVol.contrGVol.FullScreen.prototype.setMap = function(map) {
  GVol.contrGVol.ContrGVol.prototype.setMap.call(this, map);
  if (map) {
    this.listenerKeys.push(GVol.events.listen(document,
        GVol.contrGVol.FullScreen.getChangeType_(),
        this.handleFullScreenChange_, this)
    );
  }
};

/**
 * @return {boGVolean} Fullscreen is supported by the current platform.
 */
GVol.contrGVol.FullScreen.isFullScreenSupported = function() {
  var body = document.body;
  return !!(
    body.webkitRequestFullscreen ||
    (body.mozRequestFullScreen && document.mozFullScreenEnabled) ||
    (body.msRequestFullscreen && document.msFullscreenEnabled) ||
    (body.requestFullscreen && document.fullscreenEnabled)
  );
};

/**
 * @return {boGVolean} Element is currently in fullscreen.
 */
GVol.contrGVol.FullScreen.isFullScreen = function() {
  return !!(
    document.webkitIsFullScreen || document.mozFullScreen ||
    document.msFullscreenElement || document.fullscreenElement
  );
};

/**
 * Request to fullscreen an element.
 * @param {Node} element Element to request fullscreen
 */
GVol.contrGVol.FullScreen.requestFullScreen = function(element) {
  if (element.requestFullscreen) {
    element.requestFullscreen();
  } else if (element.msRequestFullscreen) {
    element.msRequestFullscreen();
  } else if (element.mozRequestFullScreen) {
    element.mozRequestFullScreen();
  } else if (element.webkitRequestFullscreen) {
    element.webkitRequestFullscreen();
  }
};

/**
 * Request to fullscreen an element with keyboard input.
 * @param {Node} element Element to request fullscreen
 */
GVol.contrGVol.FullScreen.requestFullScreenWithKeys = function(element) {
  if (element.mozRequestFullScreenWithKeys) {
    element.mozRequestFullScreenWithKeys();
  } else if (element.webkitRequestFullscreen) {
    element.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT);
  } else {
    GVol.contrGVol.FullScreen.requestFullScreen(element);
  }
};

/**
 * Exit fullscreen.
 */
GVol.contrGVol.FullScreen.exitFullScreen = function() {
  if (document.exitFullscreen) {
    document.exitFullscreen();
  } else if (document.msExitFullscreen) {
    document.msExitFullscreen();
  } else if (document.mozCancelFullScreen) {
    document.mozCancelFullScreen();
  } else if (document.webkitExitFullscreen) {
    document.webkitExitFullscreen();
  }
};

/**
 * @return {string} Change type.
 * @private
 */
GVol.contrGVol.FullScreen.getChangeType_ = (function() {
  var changeType;
  return function() {
    if (!changeType) {
      var body = document.body;
      if (body.webkitRequestFullscreen) {
        changeType = 'webkitfullscreenchange';
      } else if (body.mozRequestFullScreen) {
        changeType = 'mozfullscreenchange';
      } else if (body.msRequestFullscreen) {
        changeType = 'MSFullscreenChange';
      } else if (body.requestFullscreen) {
        changeType = 'fullscreenchange';
      }
    }
    return changeType;
  };
})();

// FIXME should listen on appropriate pane, once it is defined

goog.provide('GVol.contrGVol.MousePosition');

goog.require('GVol');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.Object');
goog.require('GVol.contrGVol.ContrGVol');
goog.require('GVol.proj');


/**
 * @classdesc
 * A contrGVol 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 contrGVol is shown in the top right corner of the map, but this
 * can be changed by using the css selector `.GVol-mouse-position`.
 *
 * @constructor
 * @extends {GVol.contrGVol.ContrGVol}
 * @param {GVolx.contrGVol.MousePositionOptions=} opt_options Mouse position
 *     options.
 * @api
 */
GVol.contrGVol.MousePosition = function(opt_options) {

  var options = opt_options ? opt_options : {};

  var element = document.createElement('DIV');
  element.className = options.className !== undefined ? options.className : 'GVol-mouse-position';

  var render = options.render ?
    options.render : GVol.contrGVol.MousePosition.render;

  GVol.contrGVol.ContrGVol.call(this, {
    element: element,
    render: render,
    target: options.target
  });

  GVol.events.listen(this,
      GVol.Object.getChangeEventType(GVol.contrGVol.MousePosition.Property_.PROJECTION),
      this.handleProjectionChanged_, this);

  if (options.coordinateFormat) {
    this.setCoordinateFormat(options.coordinateFormat);
  }
  if (options.projection) {
    this.setProjection(options.projection);
  }

  /**
   * @private
   * @type {string}
   */
  this.undefinedHTML_ = options.undefinedHTML !== undefined ? options.undefinedHTML : '';

  /**
   * @private
   * @type {string}
   */
  this.renderedHTML_ = element.innerHTML;

  /**
   * @private
   * @type {GVol.proj.Projection}
   */
  this.mapProjection_ = null;

  /**
   * @private
   * @type {?GVol.TransformFunction}
   */
  this.transform_ = null;

  /**
   * @private
   * @type {GVol.Pixel}
   */
  this.lastMouseMovePixel_ = null;

};
GVol.inherits(GVol.contrGVol.MousePosition, GVol.contrGVol.ContrGVol);


/**
 * Update the mouseposition element.
 * @param {GVol.MapEvent} mapEvent Map event.
 * @this {GVol.contrGVol.MousePosition}
 * @api
 */
GVol.contrGVol.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
 */
GVol.contrGVol.MousePosition.prototype.handleProjectionChanged_ = function() {
  this.transform_ = null;
};


/**
 * Return the coordinate format type used to render the current position or
 * undefined.
 * @return {GVol.CoordinateFormatType|undefined} The format to render the current
 *     position in.
 * @observable
 * @api
 */
GVol.contrGVol.MousePosition.prototype.getCoordinateFormat = function() {
  return /** @type {GVol.CoordinateFormatType|undefined} */ (
    this.get(GVol.contrGVol.MousePosition.Property_.COORDINATE_FORMAT));
};


/**
 * Return the projection that is used to report the mouse position.
 * @return {GVol.proj.Projection|undefined} The projection to report mouse
 *     position in.
 * @observable
 * @api
 */
GVol.contrGVol.MousePosition.prototype.getProjection = function() {
  return /** @type {GVol.proj.Projection|undefined} */ (
    this.get(GVol.contrGVol.MousePosition.Property_.PROJECTION));
};


/**
 * @param {Event} event Browser event.
 * @protected
 */
GVol.contrGVol.MousePosition.prototype.handleMouseMove = function(event) {
  var map = this.getMap();
  this.lastMouseMovePixel_ = map.getEventPixel(event);
  this.updateHTML_(this.lastMouseMovePixel_);
};


/**
 * @param {Event} event Browser event.
 * @protected
 */
GVol.contrGVol.MousePosition.prototype.handleMouseOut = function(event) {
  this.updateHTML_(null);
  this.lastMouseMovePixel_ = null;
};


/**
 * @inheritDoc
 * @api
 */
GVol.contrGVol.MousePosition.prototype.setMap = function(map) {
  GVol.contrGVol.ContrGVol.prototype.setMap.call(this, map);
  if (map) {
    var viewport = map.getViewport();
    this.listenerKeys.push(
        GVol.events.listen(viewport, GVol.events.EventType.MOUSEMOVE,
            this.handleMouseMove, this),
        GVol.events.listen(viewport, GVol.events.EventType.MOUSEOUT,
            this.handleMouseOut, this)
    );
  }
};


/**
 * Set the coordinate format type used to render the current position.
 * @param {GVol.CoordinateFormatType} format The format to render the current
 *     position in.
 * @observable
 * @api
 */
GVol.contrGVol.MousePosition.prototype.setCoordinateFormat = function(format) {
  this.set(GVol.contrGVol.MousePosition.Property_.COORDINATE_FORMAT, format);
};


/**
 * Set the projection that is used to report the mouse position.
 * @param {GVol.ProjectionLike} projection The projection to report mouse
 *     position in.
 * @observable
 * @api
 */
GVol.contrGVol.MousePosition.prototype.setProjection = function(projection) {
  this.set(GVol.contrGVol.MousePosition.Property_.PROJECTION, GVol.proj.get(projection));
};


/**
 * @param {?GVol.Pixel} pixel Pixel.
 * @private
 */
GVol.contrGVol.MousePosition.prototype.updateHTML_ = function(pixel) {
  var html = this.undefinedHTML_;
  if (pixel && this.mapProjection_) {
    if (!this.transform_) {
      var projection = this.getProjection();
      if (projection) {
        this.transform_ = GVol.proj.getTransformFromProjections(
            this.mapProjection_, projection);
      } else {
        this.transform_ = GVol.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;
  }
};


/**
 * @enum {string}
 * @private
 */
GVol.contrGVol.MousePosition.Property_ = {
  PROJECTION: 'projection',
  COORDINATE_FORMAT: 'coordinateFormat'
};

goog.provide('GVol.MapEvent');

goog.require('GVol');
goog.require('GVol.events.Event');


/**
 * @classdesc
 * Events emitted as map events are instances of this type.
 * See {@link GVol.Map} for which events trigger a map event.
 *
 * @constructor
 * @extends {GVol.events.Event}
 * @implements {GVoli.MapEvent}
 * @param {string} type Event type.
 * @param {GVol.Map} map Map.
 * @param {?GVolx.FrameState=} opt_frameState Frame state.
 */
GVol.MapEvent = function(type, map, opt_frameState) {

  GVol.events.Event.call(this, type);

  /**
   * The map where the event occurred.
   * @type {GVol.Map}
   * @api
   */
  this.map = map;

  /**
   * The frame state at the time of the event.
   * @type {?GVolx.FrameState}
   * @api
   */
  this.frameState = opt_frameState !== undefined ? opt_frameState : null;

};
GVol.inherits(GVol.MapEvent, GVol.events.Event);

goog.provide('GVol.MapBrowserEvent');

goog.require('GVol');
goog.require('GVol.MapEvent');


/**
 * @classdesc
 * Events emitted as map browser events are instances of this type.
 * See {@link GVol.Map} for which events trigger a map browser event.
 *
 * @constructor
 * @extends {GVol.MapEvent}
 * @implements {GVoli.MapBrowserEvent}
 * @param {string} type Event type.
 * @param {GVol.Map} map Map.
 * @param {Event} browserEvent Browser event.
 * @param {boGVolean=} opt_dragging Is the map currently being dragged?
 * @param {?GVolx.FrameState=} opt_frameState Frame state.
 */
GVol.MapBrowserEvent = function(type, map, browserEvent, opt_dragging,
    opt_frameState) {

  GVol.MapEvent.call(this, type, map, opt_frameState);

  /**
   * The original browser event.
   * @const
   * @type {Event}
   * @api
   */
  this.originalEvent = browserEvent;

  /**
   * The map pixel relative to the viewport corresponding to the original browser event.
   * @type {GVol.Pixel}
   * @api
   */
  this.pixel = map.getEventPixel(browserEvent);

  /**
   * The coordinate in view projection corresponding to the original browser event.
   * @type {GVol.Coordinate}
   * @api
   */
  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 {boGVolean}
   * @api
   */
  this.dragging = opt_dragging !== undefined ? opt_dragging : false;

};
GVol.inherits(GVol.MapBrowserEvent, GVol.MapEvent);


/**
 * Prevents the default browser action.
 * @see https://developer.mozilla.org/en-US/docs/Web/API/event.preventDefault
 * @override
 * @api
 */
GVol.MapBrowserEvent.prototype.preventDefault = function() {
  GVol.MapEvent.prototype.preventDefault.call(this);
  this.originalEvent.preventDefault();
};


/**
 * Prevents further propagation of the current event.
 * @see https://developer.mozilla.org/en-US/docs/Web/API/event.stopPropagation
 * @override
 * @api
 */
GVol.MapBrowserEvent.prototype.stopPropagation = function() {
  GVol.MapEvent.prototype.stopPropagation.call(this);
  this.originalEvent.stopPropagation();
};

goog.provide('GVol.webgl');

goog.require('GVol');


if (GVol.ENABLE_WEBGL) {

  /** Constants taken from goog.webgl
   */


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.ONE = 1;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.SRC_ALPHA = 0x0302;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.COLOR_ATTACHMENT0 = 0x8CE0;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.COLOR_BUFFER_BIT = 0x00004000;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.TRIANGLES = 0x0004;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.TRIANGLE_STRIP = 0x0005;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.ONE_MINUS_SRC_ALPHA = 0x0303;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.ARRAY_BUFFER = 0x8892;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.ELEMENT_ARRAY_BUFFER = 0x8893;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.STREAM_DRAW = 0x88E0;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.STATIC_DRAW = 0x88E4;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.DYNAMIC_DRAW = 0x88E8;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.CULL_FACE = 0x0B44;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.BLEND = 0x0BE2;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.STENCIL_TEST = 0x0B90;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.DEPTH_TEST = 0x0B71;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.SCISSOR_TEST = 0x0C11;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.UNSIGNED_BYTE = 0x1401;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.UNSIGNED_SHORT = 0x1403;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.UNSIGNED_INT = 0x1405;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.FLOAT = 0x1406;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.RGBA = 0x1908;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.FRAGMENT_SHADER = 0x8B30;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.VERTEX_SHADER = 0x8B31;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.LINK_STATUS = 0x8B82;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.LINEAR = 0x2601;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.TEXTURE_MAG_FILTER = 0x2800;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.TEXTURE_MIN_FILTER = 0x2801;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.TEXTURE_WRAP_S = 0x2802;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.TEXTURE_WRAP_T = 0x2803;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.TEXTURE_2D = 0x0DE1;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.TEXTURE0 = 0x84C0;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.CLAMP_TO_EDGE = 0x812F;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.COMPILE_STATUS = 0x8B81;


  /**
   * @const
   * @type {number}
   */
  GVol.webgl.FRAMEBUFFER = 0x8D40;


  /** end of goog.webgl constants
   */


  /**
   * @const
   * @private
   * @type {Array.<string>}
   */
  GVol.webgl.CONTEXT_IDS_ = [
    'experimental-webgl',
    'webgl',
    'webkit-3d',
    'moz-webgl'
  ];


  /**
   * @param {HTMLCanvasElement} canvas Canvas.
   * @param {Object=} opt_attributes Attributes.
   * @return {WebGLRenderingContext} WebGL rendering context.
   */
  GVol.webgl.getContext = function(canvas, opt_attributes) {
    var context, i, ii = GVol.webgl.CONTEXT_IDS_.length;
    for (i = 0; i < ii; ++i) {
      try {
        context = canvas.getContext(GVol.webgl.CONTEXT_IDS_[i], opt_attributes);
        if (context) {
          return /** @type {!WebGLRenderingContext} */ (context);
        }
      } catch (e) {
        // pass
      }
    }
    return null;
  };

}

goog.provide('GVol.has');

goog.require('GVol');
goog.require('GVol.webgl');

var ua = typeof navigator !== 'undefined' ?
  navigator.userAgent.toLowerCase() : '';

/**
 * User agent string says we are dealing with Firefox as browser.
 * @type {boGVolean}
 */
GVol.has.FIREFOX = ua.indexOf('firefox') !== -1;

/**
 * User agent string says we are dealing with Safari as browser.
 * @type {boGVolean}
 */
GVol.has.SAFARI = ua.indexOf('safari') !== -1 && ua.indexOf('chrom') == -1;

/**
 * User agent string says we are dealing with a WebKit engine.
 * @type {boGVolean}
 */
GVol.has.WEBKIT = ua.indexOf('webkit') !== -1 && ua.indexOf('edge') == -1;

/**
 * User agent string says we are dealing with a Mac as platform.
 * @type {boGVolean}
 */
GVol.has.MAC = ua.indexOf('macintosh') !== -1;


/**
 * The ratio between physical pixels and device-independent pixels
 * (dips) on the device (`window.devicePixelRatio`).
 * @const
 * @type {number}
 * @api
 */
GVol.has.DEVICE_PIXEL_RATIO = window.devicePixelRatio || 1;


/**
 * True if the browser's Canvas implementation implements {get,set}LineDash.
 * @type {boGVolean}
 */
GVol.has.CANVAS_LINE_DASH = false;


/**
 * True if both the library and browser support Canvas.  Always `false`
 * if `GVol.ENABLE_CANVAS` is set to `false` at compile time.
 * @const
 * @type {boGVolean}
 * @api
 */
GVol.has.CANVAS = GVol.ENABLE_CANVAS && (
  /**
   * @return {boGVolean} Canvas supported.
   */
  function() {
    if (!('HTMLCanvasElement' in window)) {
      return false;
    }
    try {
      var context = document.createElement('CANVAS').getContext('2d');
      if (!context) {
        return false;
      } else {
        if (context.setLineDash !== undefined) {
          GVol.has.CANVAS_LINE_DASH = true;
        }
        return true;
      }
    } catch (e) {
      return false;
    }
  })();


/**
 * Indicates if DeviceOrientation is supported in the user's browser.
 * @const
 * @type {boGVolean}
 * @api
 */
GVol.has.DEVICE_ORIENTATION = 'DeviceOrientationEvent' in window;


/**
 * Is HTML5 geGVolocation supported in the current browser?
 * @const
 * @type {boGVolean}
 * @api
 */
GVol.has.GEOLOCATION = 'geGVolocation' in navigator;


/**
 * True if browser supports touch events.
 * @const
 * @type {boGVolean}
 * @api
 */
GVol.has.TOUCH = GVol.ASSUME_TOUCH || 'ontouchstart' in window;


/**
 * True if browser supports pointer events.
 * @const
 * @type {boGVolean}
 */
GVol.has.POINTER = 'PointerEvent' in window;


/**
 * True if browser supports ms pointer events (IE 10).
 * @const
 * @type {boGVolean}
 */
GVol.has.MSPOINTER = !!(navigator.msPointerEnabled);


/**
 * True if both OpenLayers and browser support WebGL.  Always `false`
 * if `GVol.ENABLE_WEBGL` is set to `false` at compile time.
 * @const
 * @type {boGVolean}
 * @api
 */
GVol.has.WEBGL;


(function() {
  if (GVol.ENABLE_WEBGL) {
    var hasWebGL = false;
    var textureSize;
    var /** @type {Array.<string>} */ extensions = [];

    if ('WebGLRenderingContext' in window) {
      try {
        var canvas = /** @type {HTMLCanvasElement} */
            (document.createElement('CANVAS'));
        var gl = GVol.webgl.getContext(canvas, {
          failIfMajorPerformanceCaveat: true
        });
        if (gl) {
          hasWebGL = true;
          textureSize = /** @type {number} */
            (gl.getParameter(gl.MAX_TEXTURE_SIZE));
          extensions = gl.getSupportedExtensions();
        }
      } catch (e) {
        // pass
      }
    }
    GVol.has.WEBGL = hasWebGL;
    GVol.WEBGL_EXTENSIONS = extensions;
    GVol.WEBGL_MAX_TEXTURE_SIZE = textureSize;
  }
})();

goog.provide('GVol.MapBrowserEventType');

goog.require('GVol.events.EventType');


/**
 * Constants for event names.
 * @enum {string}
 */
GVol.MapBrowserEventType = {

  /**
   * 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 GVol.MapBrowserEvent#singleclick
   * @api
   */
  SINGLECLICK: 'singleclick',

  /**
   * A click with no dragging. A double click will fire two of this.
   * @event GVol.MapBrowserEvent#click
   * @api
   */
  CLICK: GVol.events.EventType.CLICK,

  /**
   * A true double click, with no dragging.
   * @event GVol.MapBrowserEvent#dblclick
   * @api
   */
  DBLCLICK: GVol.events.EventType.DBLCLICK,

  /**
   * Triggered when a pointer is dragged.
   * @event GVol.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 GVol.MapBrowserEvent#pointermove
   * @api
   */
  POINTERMOVE: 'pointermove',

  POINTERDOWN: 'pointerdown',
  POINTERUP: 'pointerup',
  POINTEROVER: 'pointerover',
  POINTEROUT: 'pointerout',
  POINTERENTER: 'pointerenter',
  POINTERLEAVE: 'pointerleave',
  POINTERCANCEL: 'pointercancel'
};

goog.provide('GVol.MapBrowserPointerEvent');

goog.require('GVol');
goog.require('GVol.MapBrowserEvent');


/**
 * @constructor
 * @extends {GVol.MapBrowserEvent}
 * @param {string} type Event type.
 * @param {GVol.Map} map Map.
 * @param {GVol.pointer.PointerEvent} pointerEvent Pointer event.
 * @param {boGVolean=} opt_dragging Is the map currently being dragged?
 * @param {?GVolx.FrameState=} opt_frameState Frame state.
 */
GVol.MapBrowserPointerEvent = function(type, map, pointerEvent, opt_dragging,
    opt_frameState) {

  GVol.MapBrowserEvent.call(this, type, map, pointerEvent.originalEvent, opt_dragging,
      opt_frameState);

  /**
   * @const
   * @type {GVol.pointer.PointerEvent}
   */
  this.pointerEvent = pointerEvent;

};
GVol.inherits(GVol.MapBrowserPointerEvent, GVol.MapBrowserEvent);

goog.provide('GVol.pointer.EventType');


/**
 * Constants for event names.
 * @enum {string}
 */
GVol.pointer.EventType = {
  POINTERMOVE: 'pointermove',
  POINTERDOWN: 'pointerdown',
  POINTERUP: 'pointerup',
  POINTEROVER: 'pointerover',
  POINTEROUT: 'pointerout',
  POINTERENTER: 'pointerenter',
  POINTERLEAVE: 'pointerleave',
  POINTERCANCEL: 'pointercancel'
};

goog.provide('GVol.pointer.EventSource');


/**
 * @param {GVol.pointer.PointerEventHandler} dispatcher Event handler.
 * @param {!Object.<string, function(Event)>} mapping Event
 *     mapping.
 * @constructor
 */
GVol.pointer.EventSource = function(dispatcher, mapping) {
  /**
   * @type {GVol.pointer.PointerEventHandler}
   */
  this.dispatcher = dispatcher;

  /**
   * @private
   * @const
   * @type {!Object.<string, function(Event)>}
   */
  this.mapping_ = mapping;
};


/**
 * List of events supported by this source.
 * @return {Array.<string>} Event names
 */
GVol.pointer.EventSource.prototype.getEvents = function() {
  return Object.keys(this.mapping_);
};


/**
 * Returns the handler that should handle a given event type.
 * @param {string} eventType The event type.
 * @return {function(Event)} Handler
 */
GVol.pointer.EventSource.prototype.getHandlerForEvent = function(eventType) {
  return this.mapping_[eventType];
};

// Based on https://github.com/PGVolymer/PointerEvents

// Copyright (c) 2013 The PGVolymer Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the fGVollowing conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the fGVollowing disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the fGVollowing 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('GVol.pointer.MouseSource');

goog.require('GVol');
goog.require('GVol.pointer.EventSource');


/**
 * @param {GVol.pointer.PointerEventHandler} dispatcher Event handler.
 * @constructor
 * @extends {GVol.pointer.EventSource}
 */
GVol.pointer.MouseSource = function(dispatcher) {
  var mapping = {
    'mousedown': this.mousedown,
    'mousemove': this.mousemove,
    'mouseup': this.mouseup,
    'mouseover': this.mouseover,
    'mouseout': this.mouseout
  };
  GVol.pointer.EventSource.call(this, dispatcher, mapping);

  /**
   * @const
   * @type {!Object.<string, Event|Object>}
   */
  this.pointerMap = dispatcher.pointerMap;

  /**
   * @const
   * @type {Array.<GVol.Pixel>}
   */
  this.lastTouches = [];
};
GVol.inherits(GVol.pointer.MouseSource, GVol.pointer.EventSource);


/**
 * @const
 * @type {number}
 */
GVol.pointer.MouseSource.POINTER_ID = 1;


/**
 * @const
 * @type {string}
 */
GVol.pointer.MouseSource.POINTER_TYPE = 'mouse';


/**
 * Radius around touchend that swallows mouse events.
 *
 * @const
 * @type {number}
 */
GVol.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 scrGVolled, 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 scrGVolled. 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 {Event} inEvent The in event.
 * @return {boGVolean} True, if the event was generated by a touch.
 */
GVol.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 <= GVol.pointer.MouseSource.DEDUP_DIST &&
        dy <= GVol.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 {Event} inEvent The in event.
 * @param {GVol.pointer.PointerEventHandler} dispatcher Event handler.
 * @return {Object} The copied event.
 */
GVol.pointer.MouseSource.prepareEvent = function(inEvent, dispatcher) {
  var e = dispatcher.cloneEvent(inEvent, inEvent);

  // forward mouse preventDefault
  var pd = e.preventDefault;
  e.preventDefault = function() {
    inEvent.preventDefault();
    pd();
  };

  e.pointerId = GVol.pointer.MouseSource.POINTER_ID;
  e.isPrimary = true;
  e.pointerType = GVol.pointer.MouseSource.POINTER_TYPE;

  return e;
};


/**
 * Handler for `mousedown`.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.MouseSource.prototype.mousedown = function(inEvent) {
  if (!this.isEventSimulatedFromTouch_(inEvent)) {
    // TODO(dfreedman) workaround for some elements not sending mouseup
    // http://crbug/149091
    if (GVol.pointer.MouseSource.POINTER_ID.toString() in this.pointerMap) {
      this.cancel(inEvent);
    }
    var e = GVol.pointer.MouseSource.prepareEvent(inEvent, this.dispatcher);
    this.pointerMap[GVol.pointer.MouseSource.POINTER_ID.toString()] = inEvent;
    this.dispatcher.down(e, inEvent);
  }
};


/**
 * Handler for `mousemove`.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.MouseSource.prototype.mousemove = function(inEvent) {
  if (!this.isEventSimulatedFromTouch_(inEvent)) {
    var e = GVol.pointer.MouseSource.prepareEvent(inEvent, this.dispatcher);
    this.dispatcher.move(e, inEvent);
  }
};


/**
 * Handler for `mouseup`.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.MouseSource.prototype.mouseup = function(inEvent) {
  if (!this.isEventSimulatedFromTouch_(inEvent)) {
    var p = this.pointerMap[GVol.pointer.MouseSource.POINTER_ID.toString()];

    if (p && p.button === inEvent.button) {
      var e = GVol.pointer.MouseSource.prepareEvent(inEvent, this.dispatcher);
      this.dispatcher.up(e, inEvent);
      this.cleanupMouse();
    }
  }
};


/**
 * Handler for `mouseover`.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.MouseSource.prototype.mouseover = function(inEvent) {
  if (!this.isEventSimulatedFromTouch_(inEvent)) {
    var e = GVol.pointer.MouseSource.prepareEvent(inEvent, this.dispatcher);
    this.dispatcher.enterOver(e, inEvent);
  }
};


/**
 * Handler for `mouseout`.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.MouseSource.prototype.mouseout = function(inEvent) {
  if (!this.isEventSimulatedFromTouch_(inEvent)) {
    var e = GVol.pointer.MouseSource.prepareEvent(inEvent, this.dispatcher);
    this.dispatcher.leaveOut(e, inEvent);
  }
};


/**
 * Dispatches a `pointercancel` event.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.MouseSource.prototype.cancel = function(inEvent) {
  var e = GVol.pointer.MouseSource.prepareEvent(inEvent, this.dispatcher);
  this.dispatcher.cancel(e, inEvent);
  this.cleanupMouse();
};


/**
 * Remove the mouse from the list of active pointers.
 */
GVol.pointer.MouseSource.prototype.cleanupMouse = function() {
  delete this.pointerMap[GVol.pointer.MouseSource.POINTER_ID.toString()];
};

// Based on https://github.com/PGVolymer/PointerEvents

// Copyright (c) 2013 The PGVolymer Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the fGVollowing conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the fGVollowing disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the fGVollowing 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('GVol.pointer.MsSource');

goog.require('GVol');
goog.require('GVol.pointer.EventSource');


/**
 * @param {GVol.pointer.PointerEventHandler} dispatcher Event handler.
 * @constructor
 * @extends {GVol.pointer.EventSource}
 */
GVol.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
  };
  GVol.pointer.EventSource.call(this, dispatcher, mapping);

  /**
   * @const
   * @type {!Object.<string, Event|Object>}
   */
  this.pointerMap = dispatcher.pointerMap;

  /**
   * @const
   * @type {Array.<string>}
   */
  this.POINTER_TYPES = [
    '',
    'unavailable',
    'touch',
    'pen',
    'mouse'
  ];
};
GVol.inherits(GVol.pointer.MsSource, GVol.pointer.EventSource);


/**
 * Creates a copy of the original event that will be used
 * for the fake pointer event.
 *
 * @private
 * @param {Event} inEvent The in event.
 * @return {Object} The copied event.
 */
GVol.pointer.MsSource.prototype.prepareEvent_ = function(inEvent) {
  var e = inEvent;
  if (typeof inEvent.pointerType === 'number') {
    e = this.dispatcher.cloneEvent(inEvent, inEvent);
    e.pointerType = this.POINTER_TYPES[inEvent.pointerType];
  }

  return e;
};


/**
 * Remove this pointer from the list of active pointers.
 * @param {number} pointerId Pointer identifier.
 */
GVol.pointer.MsSource.prototype.cleanup = function(pointerId) {
  delete this.pointerMap[pointerId.toString()];
};


/**
 * Handler for `msPointerDown`.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.MsSource.prototype.msPointerDown = function(inEvent) {
  this.pointerMap[inEvent.pointerId.toString()] = inEvent;
  var e = this.prepareEvent_(inEvent);
  this.dispatcher.down(e, inEvent);
};


/**
 * Handler for `msPointerMove`.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.MsSource.prototype.msPointerMove = function(inEvent) {
  var e = this.prepareEvent_(inEvent);
  this.dispatcher.move(e, inEvent);
};


/**
 * Handler for `msPointerUp`.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.MsSource.prototype.msPointerUp = function(inEvent) {
  var e = this.prepareEvent_(inEvent);
  this.dispatcher.up(e, inEvent);
  this.cleanup(inEvent.pointerId);
};


/**
 * Handler for `msPointerOut`.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.MsSource.prototype.msPointerOut = function(inEvent) {
  var e = this.prepareEvent_(inEvent);
  this.dispatcher.leaveOut(e, inEvent);
};


/**
 * Handler for `msPointerOver`.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.MsSource.prototype.msPointerOver = function(inEvent) {
  var e = this.prepareEvent_(inEvent);
  this.dispatcher.enterOver(e, inEvent);
};


/**
 * Handler for `msPointerCancel`.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.MsSource.prototype.msPointerCancel = function(inEvent) {
  var e = this.prepareEvent_(inEvent);
  this.dispatcher.cancel(e, inEvent);
  this.cleanup(inEvent.pointerId);
};


/**
 * Handler for `msLostPointerCapture`.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.MsSource.prototype.msLostPointerCapture = function(inEvent) {
  var e = this.dispatcher.makeEvent('lostpointercapture',
      inEvent, inEvent);
  this.dispatcher.dispatchEvent(e);
};


/**
 * Handler for `msGotPointerCapture`.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.MsSource.prototype.msGotPointerCapture = function(inEvent) {
  var e = this.dispatcher.makeEvent('gotpointercapture',
      inEvent, inEvent);
  this.dispatcher.dispatchEvent(e);
};

// Based on https://github.com/PGVolymer/PointerEvents

// Copyright (c) 2013 The PGVolymer Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the fGVollowing conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the fGVollowing disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the fGVollowing 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('GVol.pointer.NativeSource');

goog.require('GVol');
goog.require('GVol.pointer.EventSource');


/**
 * @param {GVol.pointer.PointerEventHandler} dispatcher Event handler.
 * @constructor
 * @extends {GVol.pointer.EventSource}
 */
GVol.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
  };
  GVol.pointer.EventSource.call(this, dispatcher, mapping);
};
GVol.inherits(GVol.pointer.NativeSource, GVol.pointer.EventSource);


/**
 * Handler for `pointerdown`.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.NativeSource.prototype.pointerDown = function(inEvent) {
  this.dispatcher.fireNativeEvent(inEvent);
};


/**
 * Handler for `pointermove`.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.NativeSource.prototype.pointerMove = function(inEvent) {
  this.dispatcher.fireNativeEvent(inEvent);
};


/**
 * Handler for `pointerup`.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.NativeSource.prototype.pointerUp = function(inEvent) {
  this.dispatcher.fireNativeEvent(inEvent);
};


/**
 * Handler for `pointerout`.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.NativeSource.prototype.pointerOut = function(inEvent) {
  this.dispatcher.fireNativeEvent(inEvent);
};


/**
 * Handler for `pointerover`.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.NativeSource.prototype.pointerOver = function(inEvent) {
  this.dispatcher.fireNativeEvent(inEvent);
};


/**
 * Handler for `pointercancel`.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.NativeSource.prototype.pointerCancel = function(inEvent) {
  this.dispatcher.fireNativeEvent(inEvent);
};


/**
 * Handler for `lostpointercapture`.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.NativeSource.prototype.lostPointerCapture = function(inEvent) {
  this.dispatcher.fireNativeEvent(inEvent);
};


/**
 * Handler for `gotpointercapture`.
 *
 * @param {Event} inEvent The in event.
 */
GVol.pointer.NativeSource.prototype.gotPointerCapture = function(inEvent) {
  this.dispatcher.fireNativeEvent(inEvent);
};

// Based on https://github.com/PGVolymer/PointerEvents

// Copyright (c) 2013 The PGVolymer Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the fGVollowing conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the fGVollowing disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the fGVollowing 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('GVol.pointer.PointerEvent');


goog.require('GVol');
goog.require('GVol.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 {GVol.events.Event}
 * @param {string} type The type of the event to create.
 * @param {Event} originalEvent The event.
 * @param {Object.<string, ?>=} opt_eventDict An optional dictionary of
 *    initial event properties.
 */
GVol.pointer.PointerEvent = function(type, originalEvent, opt_eventDict) {
  GVol.events.Event.call(this, type);

  /**
   * @const
   * @type {Event}
   */
  this.originalEvent = originalEvent;

  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 {boGVolean}
   */
  this.bubbles = 'bubbles' in eventDict ? eventDict['bubbles'] : false;

  /**
   * @type {boGVolean}
   */
  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 {boGVolean}
   */
  this.ctrlKey = 'ctrlKey' in eventDict ? eventDict['ctrlKey'] : false;

  /**
   * @type {boGVolean}
   */
  this.altKey = 'altKey' in eventDict ? eventDict['altKey'] : false;

  /**
   * @type {boGVolean}
   */
  this.shiftKey = 'shiftKey' in eventDict ? eventDict['shiftKey'] : false;

  /**
   * @type {boGVolean}
   */
  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 {boGVolean}
   */
  this.isPrimary = 'isPrimary' in eventDict ? eventDict['isPrimary'] : false;

  // keep the semantics of preventDefault
  if (originalEvent.preventDefault) {
    this.preventDefault = function() {
      originalEvent.preventDefault();
    };
  }
};
GVol.inherits(GVol.pointer.PointerEvent, GVol.events.Event);


/**
 * @private
 * @param {Object.<string, ?>} eventDict The event dictionary.
 * @return {number} Button indicator.
 */
GVol.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 || GVol.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.
 */
GVol.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 {boGVolean}
 */
GVol.pointer.PointerEvent.HAS_BUTTONS = false;


/**
 * Checks if the `buttons` property is supported.
 */
(function() {
  try {
    var ev = new MouseEvent('click', {buttons: 1});
    GVol.pointer.PointerEvent.HAS_BUTTONS = ev.buttons === 1;
  } catch (e) {
    // pass
  }
})();

// Based on https://github.com/PGVolymer/PointerEvents

// Copyright (c) 2013 The PGVolymer Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the fGVollowing conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the fGVollowing disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the fGVollowing 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('GVol.pointer.TouchSource');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.pointer.EventSource');
goog.require('GVol.pointer.MouseSource');


/**
 * @constructor
 * @param {GVol.pointer.PointerEventHandler} dispatcher The event handler.
 * @param {GVol.pointer.MouseSource} mouseSource Mouse source.
 * @extends {GVol.pointer.EventSource}
 */
GVol.pointer.TouchSource = function(dispatcher, mouseSource) {
  var mapping = {
    'touchstart': this.touchstart,
    'touchmove': this.touchmove,
    'touchend': this.touchend,
    'touchcancel': this.touchcancel
  };
  GVol.pointer.EventSource.call(this, dispatcher, mapping);

  /**
   * @const
   * @type {!Object.<string, Event|Object>}
   */
  this.pointerMap = dispatcher.pointerMap;

  /**
   * @const
   * @type {GVol.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;
};
GVol.inherits(GVol.pointer.TouchSource, GVol.pointer.EventSource);


/**
 * Mouse event timeout: This should be long enough to
 * ignore compat mouse events made by touch.
 * @const
 * @type {number}
 */
GVol.pointer.TouchSource.DEDUP_TIMEOUT = 2500;


/**
 * @const
 * @type {number}
 */
GVol.pointer.TouchSource.CLICK_COUNT_TIMEOUT = 200;


/**
 * @const
 * @type {string}
 */
GVol.pointer.TouchSource.POINTER_TYPE = 'touch';


/**
 * @private
 * @param {Touch} inTouch The in touch.
 * @return {boGVolean} True, if this is the primary touch.
 */
GVol.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
 */
GVol.pointer.TouchSource.prototype.setPrimaryTouch_ = function(inTouch) {
  var count = Object.keys(this.pointerMap).length;
  if (count === 0 || (count === 1 &&
      GVol.pointer.MouseSource.POINTER_ID.toString() in this.pointerMap)) {
    this.firstTouchId_ = inTouch.identifier;
    this.cancelResetClickCount_();
  }
};


/**
 * @private
 * @param {Object} inPointer The in pointer object.
 */
GVol.pointer.TouchSource.prototype.removePrimaryPointer_ = function(inPointer) {
  if (inPointer.isPrimary) {
    this.firstTouchId_ = undefined;
    this.resetClickCount_();
  }
};


/**
 * @private
 */
GVol.pointer.TouchSource.prototype.resetClickCount_ = function() {
  this.resetId_ = setTimeout(
      this.resetClickCountHandler_.bind(this),
      GVol.pointer.TouchSource.CLICK_COUNT_TIMEOUT);
};


/**
 * @private
 */
GVol.pointer.TouchSource.prototype.resetClickCountHandler_ = function() {
  this.clickCount_ = 0;
  this.resetId_ = undefined;
};


/**
 * @private
 */
GVol.pointer.TouchSource.prototype.cancelResetClickCount_ = function() {
  if (this.resetId_ !== undefined) {
    clearTimeout(this.resetId_);
  }
};


/**
 * @private
 * @param {Event} browserEvent Browser event
 * @param {Touch} inTouch Touch event
 * @return {Object} A pointer object.
 */
GVol.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 = GVol.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 {Event} inEvent Touch event
 * @param {function(Event, Object)} inFunction In function.
 */
GVol.pointer.TouchSource.prototype.processTouches_ = function(inEvent, inFunction) {
  var touches = Array.prototype.slice.call(
      inEvent.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 {boGVolean} True, if the `Touch` with the given id is in the list.
 */
GVol.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 {Event} inEvent The in event.
 */
GVol.pointer.TouchSource.prototype.vacuumTouches_ = function(inEvent) {
  var touchList = inEvent.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 != GVol.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 {Event} inEvent The in event.
 */
GVol.pointer.TouchSource.prototype.touchstart = function(inEvent) {
  this.vacuumTouches_(inEvent);
  this.setPrimaryTouch_(inEvent.changedTouches[0]);
  this.dedupSynthMouse_(inEvent);
  this.clickCount_++;
  this.processTouches_(inEvent, this.overDown_);
};


/**
 * @private
 * @param {Event} browserEvent The event.
 * @param {Object} inPointer The in pointer object.
 */
GVol.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 {Event} inEvent The in event.
 */
GVol.pointer.TouchSource.prototype.touchmove = function(inEvent) {
  inEvent.preventDefault();
  this.processTouches_(inEvent, this.moveOverOut_);
};


/**
 * @private
 * @param {Event} browserEvent The event.
 * @param {Object} inPointer The in pointer.
 */
GVol.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 {Event} inEvent The event.
 */
GVol.pointer.TouchSource.prototype.touchend = function(inEvent) {
  this.dedupSynthMouse_(inEvent);
  this.processTouches_(inEvent, this.upOut_);
};


/**
 * @private
 * @param {Event} browserEvent An event.
 * @param {Object} inPointer The inPointer object.
 */
GVol.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 {Event} inEvent The in event.
 */
GVol.pointer.TouchSource.prototype.touchcancel = function(inEvent) {
  this.processTouches_(inEvent, this.cancelOut_);
};


/**
 * @private
 * @param {Event} browserEvent The event.
 * @param {Object} inPointer The in pointer.
 */
GVol.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.
 */
GVol.pointer.TouchSource.prototype.cleanUpPointer_ = function(inPointer) {
  delete this.pointerMap[inPointer.pointerId];
  this.removePrimaryPointer_(inPointer);
};


/**
 * Prevent synth mouse events from creating pointer events.
 *
 * @private
 * @param {Event} inEvent The in event.
 */
GVol.pointer.TouchSource.prototype.dedupSynthMouse_ = function(inEvent) {
  var lts = this.mouseSource.lastTouches;
  var t = inEvent.changedTouches[0];
  // only the primary finger will synth mouse events
  if (this.isPrimaryTouch_(t)) {
    // remember x/y of last touch
    var lt = [t.clientX, t.clientY];
    lts.push(lt);

    setTimeout(function() {
      // remove touch after timeout
      GVol.array.remove(lts, lt);
    }, GVol.pointer.TouchSource.DEDUP_TIMEOUT);
  }
};

// Based on https://github.com/PGVolymer/PointerEvents

// Copyright (c) 2013 The PGVolymer Authors. All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the fGVollowing conditions are
// met:
//
// * Redistributions of source code must retain the above copyright
// notice, this list of conditions and the fGVollowing disclaimer.
// * Redistributions in binary form must reproduce the above
// copyright notice, this list of conditions and the fGVollowing 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('GVol.pointer.PointerEventHandler');

goog.require('GVol');
goog.require('GVol.events');
goog.require('GVol.events.EventTarget');

goog.require('GVol.has');
goog.require('GVol.pointer.EventType');
goog.require('GVol.pointer.MouseSource');
goog.require('GVol.pointer.MsSource');
goog.require('GVol.pointer.NativeSource');
goog.require('GVol.pointer.PointerEvent');
goog.require('GVol.pointer.TouchSource');


/**
 * @constructor
 * @extends {GVol.events.EventTarget}
 * @param {Element|HTMLDocument} element Viewport element.
 */
GVol.pointer.PointerEventHandler = function(element) {
  GVol.events.EventTarget.call(this);

  /**
   * @const
   * @private
   * @type {Element|HTMLDocument}
   */
  this.element_ = element;

  /**
   * @const
   * @type {!Object.<string, Event|Object>}
   */
  this.pointerMap = {};

  /**
   * @type {Object.<string, function(Event)>}
   * @private
   */
  this.eventMap_ = {};

  /**
   * @type {Array.<GVol.pointer.EventSource>}
   * @private
   */
  this.eventSourceList_ = [];

  this.registerSources();
};
GVol.inherits(GVol.pointer.PointerEventHandler, GVol.events.EventTarget);


/**
 * Set up the event sources (mouse, touch and native pointers)
 * that generate pointer events.
 */
GVol.pointer.PointerEventHandler.prototype.registerSources = function() {
  if (GVol.has.POINTER) {
    this.registerSource('native', new GVol.pointer.NativeSource(this));
  } else if (GVol.has.MSPOINTER) {
    this.registerSource('ms', new GVol.pointer.MsSource(this));
  } else {
    var mouseSource = new GVol.pointer.MouseSource(this);
    this.registerSource('mouse', mouseSource);

    if (GVol.has.TOUCH) {
      this.registerSource('touch',
          new GVol.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 {GVol.pointer.EventSource} source The source event.
 */
GVol.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
 */
GVol.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
 */
GVol.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 {Event} inEvent Browser event.
 */
GVol.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.
 */
GVol.pointer.PointerEventHandler.prototype.addEvents_ = function(events) {
  events.forEach(function(eventName) {
    GVol.events.listen(this.element_, eventName, this.eventHandler_, this);
  }, this);
};


/**
 * Unregister listeners for the given events.
 * @private
 * @param {Array.<string>} events List of events.
 */
GVol.pointer.PointerEventHandler.prototype.removeEvents_ = function(events) {
  events.forEach(function(e) {
    GVol.events.unlisten(this.element_, e, this.eventHandler_, this);
  }, this);
};


/**
 * Returns a snapshot of inEvent, with writable properties.
 *
 * @param {Event} event Browser event.
 * @param {Event|Touch} inEvent An event that contains
 *    properties to copy.
 * @return {Object} An object containing shallow copies of
 *    `inEvent`'s properties.
 */
GVol.pointer.PointerEventHandler.prototype.cloneEvent = function(event, inEvent) {
  var eventCopy = {}, p;
  for (var i = 0, ii = GVol.pointer.PointerEventHandler.CLONE_PROPS.length; i < ii; i++) {
    p = GVol.pointer.PointerEventHandler.CLONE_PROPS[i][0];
    eventCopy[p] = event[p] || inEvent[p] || GVol.pointer.PointerEventHandler.CLONE_PROPS[i][1];
  }

  return eventCopy;
};


// EVENTS


/**
 * Triggers a 'pointerdown' event.
 * @param {Object} data Pointer event data.
 * @param {Event} event The event.
 */
GVol.pointer.PointerEventHandler.prototype.down = function(data, event) {
  this.fireEvent(GVol.pointer.EventType.POINTERDOWN, data, event);
};


/**
 * Triggers a 'pointermove' event.
 * @param {Object} data Pointer event data.
 * @param {Event} event The event.
 */
GVol.pointer.PointerEventHandler.prototype.move = function(data, event) {
  this.fireEvent(GVol.pointer.EventType.POINTERMOVE, data, event);
};


/**
 * Triggers a 'pointerup' event.
 * @param {Object} data Pointer event data.
 * @param {Event} event The event.
 */
GVol.pointer.PointerEventHandler.prototype.up = function(data, event) {
  this.fireEvent(GVol.pointer.EventType.POINTERUP, data, event);
};


/**
 * Triggers a 'pointerenter' event.
 * @param {Object} data Pointer event data.
 * @param {Event} event The event.
 */
GVol.pointer.PointerEventHandler.prototype.enter = function(data, event) {
  data.bubbles = false;
  this.fireEvent(GVol.pointer.EventType.POINTERENTER, data, event);
};


/**
 * Triggers a 'pointerleave' event.
 * @param {Object} data Pointer event data.
 * @param {Event} event The event.
 */
GVol.pointer.PointerEventHandler.prototype.leave = function(data, event) {
  data.bubbles = false;
  this.fireEvent(GVol.pointer.EventType.POINTERLEAVE, data, event);
};


/**
 * Triggers a 'pointerover' event.
 * @param {Object} data Pointer event data.
 * @param {Event} event The event.
 */
GVol.pointer.PointerEventHandler.prototype.over = function(data, event) {
  data.bubbles = true;
  this.fireEvent(GVol.pointer.EventType.POINTEROVER, data, event);
};


/**
 * Triggers a 'pointerout' event.
 * @param {Object} data Pointer event data.
 * @param {Event} event The event.
 */
GVol.pointer.PointerEventHandler.prototype.out = function(data, event) {
  data.bubbles = true;
  this.fireEvent(GVol.pointer.EventType.POINTEROUT, data, event);
};


/**
 * Triggers a 'pointercancel' event.
 * @param {Object} data Pointer event data.
 * @param {Event} event The event.
 */
GVol.pointer.PointerEventHandler.prototype.cancel = function(data, event) {
  this.fireEvent(GVol.pointer.EventType.POINTERCANCEL, data, event);
};


/**
 * Triggers a combination of 'pointerout' and 'pointerleave' events.
 * @param {Object} data Pointer event data.
 * @param {Event} event The event.
 */
GVol.pointer.PointerEventHandler.prototype.leaveOut = function(data, event) {
  this.out(data, event);
  if (!this.contains_(data.target, data.relatedTarget)) {
    this.leave(data, event);
  }
};


/**
 * Triggers a combination of 'pointerover' and 'pointerevents' events.
 * @param {Object} data Pointer event data.
 * @param {Event} event The event.
 */
GVol.pointer.PointerEventHandler.prototype.enterOver = function(data, event) {
  this.over(data, event);
  if (!this.contains_(data.target, data.relatedTarget)) {
    this.enter(data, event);
  }
};


/**
 * @private
 * @param {Element} container The container element.
 * @param {Element} contained The contained element.
 * @return {boGVolean} Returns true if the container element
 *   contains the other element.
 */
GVol.pointer.PointerEventHandler.prototype.contains_ = function(container, contained) {
  if (!container || !contained) {
    return false;
  }
  return container.contains(contained);
};


// EVENT CREATION AND TRACKING
/**
 * Creates a new Event of type `inType`, based on the information in
 * `data`.
 *
 * @param {string} inType A string representing the type of event to create.
 * @param {Object} data Pointer event data.
 * @param {Event} event The event.
 * @return {GVol.pointer.PointerEvent} A PointerEvent of type `inType`.
 */
GVol.pointer.PointerEventHandler.prototype.makeEvent = function(inType, data, event) {
  return new GVol.pointer.PointerEvent(inType, event, data);
};


/**
 * Make and dispatch an event in one call.
 * @param {string} inType A string representing the type of event.
 * @param {Object} data Pointer event data.
 * @param {Event} event The event.
 */
GVol.pointer.PointerEventHandler.prototype.fireEvent = function(inType, data, event) {
  var e = this.makeEvent(inType, data, event);
  this.dispatchEvent(e);
};


/**
 * Creates a pointer event from a native pointer event
 * and dispatches this event.
 * @param {Event} event A platform event with a target.
 */
GVol.pointer.PointerEventHandler.prototype.fireNativeEvent = function(event) {
  var e = this.makeEvent(event.type, event, event);
  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 {Event} event The event.
 * @return {GVol.pointer.PointerEvent} The wrapped event.
 */
GVol.pointer.PointerEventHandler.prototype.wrapMouseEvent = function(eventType, event) {
  var pointerEvent = this.makeEvent(
      eventType, GVol.pointer.MouseSource.prepareEvent(event, this), event);
  return pointerEvent;
};


/**
 * @inheritDoc
 */
GVol.pointer.PointerEventHandler.prototype.disposeInternal = function() {
  this.unregister_();
  GVol.events.EventTarget.prototype.disposeInternal.call(this);
};


/**
 * Properties to copy when cloning an event, with default values.
 * @type {Array.<Array>}
 */
GVol.pointer.PointerEventHandler.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('GVol.MapBrowserEventHandler');

goog.require('GVol');
goog.require('GVol.has');
goog.require('GVol.MapBrowserEventType');
goog.require('GVol.MapBrowserPointerEvent');
goog.require('GVol.events');
goog.require('GVol.events.EventTarget');
goog.require('GVol.pointer.EventType');
goog.require('GVol.pointer.PointerEventHandler');


/**
 * @param {GVol.Map} map The map with the viewport to listen to events on.
 * @param {number|undefined} moveTGVolerance The minimal distance the pointer must travel to trigger a move.
 * @constructor
 * @extends {GVol.events.EventTarget}
 */
GVol.MapBrowserEventHandler = function(map, moveTGVolerance) {

  GVol.events.EventTarget.call(this);

  /**
   * This is the element that we will listen to the real events on.
   * @type {GVol.Map}
   * @private
   */
  this.map_ = map;

  /**
   * @type {number}
   * @private
   */
  this.clickTimeoutId_ = 0;

  /**
   * @type {boGVolean}
   * @private
   */
  this.dragging_ = false;

  /**
   * @type {!Array.<GVol.EventsKey>}
   * @private
   */
  this.dragListenerKeys_ = [];

  /**
   * @type {number}
   * @private
   */
  this.moveTGVolerance_ = moveTGVolerance ?
    moveTGVolerance * GVol.has.DEVICE_PIXEL_RATIO : GVol.has.DEVICE_PIXEL_RATIO;

  /**
   * The most recent "down" type event (or null if none have occurred).
   * Set on pointerdown.
   * @type {GVol.pointer.PointerEvent}
   * @private
   */
  this.down_ = null;

  var element = this.map_.getViewport();

  /**
   * @type {number}
   * @private
   */
  this.activePointers_ = 0;

  /**
   * @type {!Object.<number, boGVolean>}
   * @private
   */
  this.trackedTouches_ = {};

  /**
   * Event handler which generates pointer events for
   * the viewport element.
   *
   * @type {GVol.pointer.PointerEventHandler}
   * @private
   */
  this.pointerEventHandler_ = new GVol.pointer.PointerEventHandler(element);

  /**
   * Event handler which generates pointer events for
   * the document (used when dragging).
   *
   * @type {GVol.pointer.PointerEventHandler}
   * @private
   */
  this.documentPointerEventHandler_ = null;

  /**
   * @type {?GVol.EventsKey}
   * @private
   */
  this.pointerdownListenerKey_ = GVol.events.listen(this.pointerEventHandler_,
      GVol.pointer.EventType.POINTERDOWN,
      this.handlePointerDown_, this);

  /**
   * @type {?GVol.EventsKey}
   * @private
   */
  this.relayedListenerKey_ = GVol.events.listen(this.pointerEventHandler_,
      GVol.pointer.EventType.POINTERMOVE,
      this.relayEvent_, this);

};
GVol.inherits(GVol.MapBrowserEventHandler, GVol.events.EventTarget);


/**
 * @param {GVol.pointer.PointerEvent} pointerEvent Pointer event.
 * @private
 */
GVol.MapBrowserEventHandler.prototype.emulateClick_ = function(pointerEvent) {
  var newEvent = new GVol.MapBrowserPointerEvent(
      GVol.MapBrowserEventType.CLICK, this.map_, pointerEvent);
  this.dispatchEvent(newEvent);
  if (this.clickTimeoutId_ !== 0) {
    // double-click
    clearTimeout(this.clickTimeoutId_);
    this.clickTimeoutId_ = 0;
    newEvent = new GVol.MapBrowserPointerEvent(
        GVol.MapBrowserEventType.DBLCLICK, this.map_, pointerEvent);
    this.dispatchEvent(newEvent);
  } else {
    // click
    this.clickTimeoutId_ = setTimeout(function() {
      this.clickTimeoutId_ = 0;
      var newEvent = new GVol.MapBrowserPointerEvent(
          GVol.MapBrowserEventType.SINGLECLICK, this.map_, pointerEvent);
      this.dispatchEvent(newEvent);
    }.bind(this), 250);
  }
};


/**
 * Keeps track on how many pointers are currently active.
 *
 * @param {GVol.pointer.PointerEvent} pointerEvent Pointer event.
 * @private
 */
GVol.MapBrowserEventHandler.prototype.updateActivePointers_ = function(pointerEvent) {
  var event = pointerEvent;

  if (event.type == GVol.MapBrowserEventType.POINTERUP ||
      event.type == GVol.MapBrowserEventType.POINTERCANCEL) {
    delete this.trackedTouches_[event.pointerId];
  } else if (event.type == GVol.MapBrowserEventType.POINTERDOWN) {
    this.trackedTouches_[event.pointerId] = true;
  }
  this.activePointers_ = Object.keys(this.trackedTouches_).length;
};


/**
 * @param {GVol.pointer.PointerEvent} pointerEvent Pointer event.
 * @private
 */
GVol.MapBrowserEventHandler.prototype.handlePointerUp_ = function(pointerEvent) {
  this.updateActivePointers_(pointerEvent);
  var newEvent = new GVol.MapBrowserPointerEvent(
      GVol.MapBrowserEventType.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)) {
    this.emulateClick_(this.down_);
  }

  if (this.activePointers_ === 0) {
    this.dragListenerKeys_.forEach(GVol.events.unlistenByKey);
    this.dragListenerKeys_.length = 0;
    this.dragging_ = false;
    this.down_ = null;
    this.documentPointerEventHandler_.dispose();
    this.documentPointerEventHandler_ = null;
  }
};


/**
 * @param {GVol.pointer.PointerEvent} pointerEvent Pointer event.
 * @return {boGVolean} If the left mouse button was pressed.
 * @private
 */
GVol.MapBrowserEventHandler.prototype.isMouseActionButton_ = function(pointerEvent) {
  return pointerEvent.button === 0;
};


/**
 * @param {GVol.pointer.PointerEvent} pointerEvent Pointer event.
 * @private
 */
GVol.MapBrowserEventHandler.prototype.handlePointerDown_ = function(pointerEvent) {
  this.updateActivePointers_(pointerEvent);
  var newEvent = new GVol.MapBrowserPointerEvent(
      GVol.MapBrowserEventType.POINTERDOWN, this.map_, pointerEvent);
  this.dispatchEvent(newEvent);

  this.down_ = pointerEvent;

  if (this.dragListenerKeys_.length === 0) {
    /* 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 GVol.pointer.PointerEventHandler(document);

    this.dragListenerKeys_.push(
        GVol.events.listen(this.documentPointerEventHandler_,
            GVol.MapBrowserEventType.POINTERMOVE,
            this.handlePointerMove_, this),
        GVol.events.listen(this.documentPointerEventHandler_,
            GVol.MapBrowserEventType.POINTERUP,
            this.handlePointerUp_, 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 fGVollowing: `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.
       */
        GVol.events.listen(this.pointerEventHandler_,
            GVol.MapBrowserEventType.POINTERCANCEL,
            this.handlePointerUp_, this)
    );
  }
};


/**
 * @param {GVol.pointer.PointerEvent} pointerEvent Pointer event.
 * @private
 */
GVol.MapBrowserEventHandler.prototype.handlePointerMove_ = function(pointerEvent) {
  // Between pointerdown and pointerup, pointermove events are triggered.
  // To avoid a 'false' touchmove event to be dispatched, we test if the pointer
  // moved a significant distance.
  if (this.isMoving_(pointerEvent)) {
    this.dragging_ = true;
    var newEvent = new GVol.MapBrowserPointerEvent(
        GVol.MapBrowserEventType.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 {GVol.pointer.PointerEvent} pointerEvent Pointer event.
 * @private
 */
GVol.MapBrowserEventHandler.prototype.relayEvent_ = function(pointerEvent) {
  var dragging = !!(this.down_ && this.isMoving_(pointerEvent));
  this.dispatchEvent(new GVol.MapBrowserPointerEvent(
      pointerEvent.type, this.map_, pointerEvent, dragging));
};


/**
 * @param {GVol.pointer.PointerEvent} pointerEvent Pointer event.
 * @return {boGVolean} Is moving.
 * @private
 */
GVol.MapBrowserEventHandler.prototype.isMoving_ = function(pointerEvent) {
  return Math.abs(pointerEvent.clientX - this.down_.clientX) > this.moveTGVolerance_ ||
      Math.abs(pointerEvent.clientY - this.down_.clientY) > this.moveTGVolerance_;
};


/**
 * @inheritDoc
 */
GVol.MapBrowserEventHandler.prototype.disposeInternal = function() {
  if (this.relayedListenerKey_) {
    GVol.events.unlistenByKey(this.relayedListenerKey_);
    this.relayedListenerKey_ = null;
  }
  if (this.pointerdownListenerKey_) {
    GVol.events.unlistenByKey(this.pointerdownListenerKey_);
    this.pointerdownListenerKey_ = null;
  }

  this.dragListenerKeys_.forEach(GVol.events.unlistenByKey);
  this.dragListenerKeys_.length = 0;

  if (this.documentPointerEventHandler_) {
    this.documentPointerEventHandler_.dispose();
    this.documentPointerEventHandler_ = null;
  }
  if (this.pointerEventHandler_) {
    this.pointerEventHandler_.dispose();
    this.pointerEventHandler_ = null;
  }
  GVol.events.EventTarget.prototype.disposeInternal.call(this);
};

goog.provide('GVol.MapProperty');

/**
 * @enum {string}
 */
GVol.MapProperty = {
  LAYERGROUP: 'layergroup',
  SIZE: 'size',
  TARGET: 'target',
  VIEW: 'view'
};

goog.provide('GVol.TileState');

/**
 * @enum {number}
 */
GVol.TileState = {
  IDLE: 0,
  LOADING: 1,
  LOADED: 2,
  ERROR: 3,
  EMPTY: 4,
  ABORT: 5
};

goog.provide('GVol.structs.PriorityQueue');

goog.require('GVol.asserts');
goog.require('GVol.obj');


/**
 * 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
 */
GVol.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, boGVolean>}
   * @private
   */
  this.queuedElements_ = {};

};


/**
 * @const
 * @type {number}
 */
GVol.structs.PriorityQueue.DROP = Infinity;


/**
 * FIXME empty description for jsdoc
 */
GVol.structs.PriorityQueue.prototype.clear = function() {
  this.elements_.length = 0;
  this.priorities_.length = 0;
  GVol.obj.clear(this.queuedElements_);
};


/**
 * Remove and return the highest-priority element. O(log N).
 * @return {T} Element.
 */
GVol.structs.PriorityQueue.prototype.dequeue = function() {
  var elements = this.elements_;
  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);
  delete this.queuedElements_[elementKey];
  return element;
};


/**
 * Enqueue an element. O(log N).
 * @param {T} element Element.
 * @return {boGVolean} The element was added to the queue.
 */
GVol.structs.PriorityQueue.prototype.enqueue = function(element) {
  GVol.asserts.assert(!(this.keyFunction_(element) in this.queuedElements_),
      31); // Tried to enqueue an `element` that was already added to the queue
  var priority = this.priorityFunction_(element);
  if (priority != GVol.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.
 */
GVol.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
 */
GVol.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
 */
GVol.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
 */
GVol.structs.PriorityQueue.prototype.getParentIndex_ = function(index) {
  return (index - 1) >> 1;
};


/**
 * Make this a heap. O(N).
 * @private
 */
GVol.structs.PriorityQueue.prototype.heapify_ = function() {
  var i;
  for (i = (this.elements_.length >> 1) - 1; i >= 0; i--) {
    this.siftUp_(i);
  }
};


/**
 * @return {boGVolean} Is empty.
 */
GVol.structs.PriorityQueue.prototype.isEmpty = function() {
  return this.elements_.length === 0;
};


/**
 * @param {string} key Key.
 * @return {boGVolean} Is key queued.
 */
GVol.structs.PriorityQueue.prototype.isKeyQueued = function(key) {
  return key in this.queuedElements_;
};


/**
 * @param {T} element Element.
 * @return {boGVolean} Is queued.
 */
GVol.structs.PriorityQueue.prototype.isQueued = function(element) {
  return this.isKeyQueued(this.keyFunction_(element));
};


/**
 * @param {number} index The index of the node to move down.
 * @private
 */
GVol.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
 */
GVol.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
 */
GVol.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 == GVol.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('GVol.TileQueue');

goog.require('GVol');
goog.require('GVol.TileState');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.structs.PriorityQueue');


/**
 * @constructor
 * @extends {GVol.structs.PriorityQueue.<Array>}
 * @param {GVol.TilePriorityFunction} tilePriorityFunction
 *     Tile priority function.
 * @param {function(): ?} tileChangeCallback
 *     Function called on each tile change event.
 * @struct
 */
GVol.TileQueue = function(tilePriorityFunction, tileChangeCallback) {

  GVol.structs.PriorityQueue.call(
      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 {GVol.Tile} */ (element[0]).getKey();
      });

  /**
   * @private
   * @type {function(): ?}
   */
  this.tileChangeCallback_ = tileChangeCallback;

  /**
   * @private
   * @type {number}
   */
  this.tilesLoading_ = 0;

  /**
   * @private
   * @type {!Object.<string,boGVolean>}
   */
  this.tilesLoadingKeys_ = {};

};
GVol.inherits(GVol.TileQueue, GVol.structs.PriorityQueue);


/**
 * @inheritDoc
 */
GVol.TileQueue.prototype.enqueue = function(element) {
  var added = GVol.structs.PriorityQueue.prototype.enqueue.call(this, element);
  if (added) {
    var tile = element[0];
    GVol.events.listen(tile, GVol.events.EventType.CHANGE,
        this.handleTileChange, this);
  }
  return added;
};


/**
 * @return {number} Number of tiles loading.
 */
GVol.TileQueue.prototype.getTilesLoading = function() {
  return this.tilesLoading_;
};


/**
 * @param {GVol.events.Event} event Event.
 * @protected
 */
GVol.TileQueue.prototype.handleTileChange = function(event) {
  var tile = /** @type {GVol.Tile} */ (event.target);
  var state = tile.getState();
  if (state === GVol.TileState.LOADED || state === GVol.TileState.ERROR ||
      state === GVol.TileState.EMPTY || state === GVol.TileState.ABORT) {
    GVol.events.unlisten(tile, GVol.events.EventType.CHANGE,
        this.handleTileChange, 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.
 */
GVol.TileQueue.prototype.loadMoreTiles = function(maxTotalLoading, maxNewLoads) {
  var newLoads = 0;
  var abortedTiles = false;
  var state, tile, tileKey;
  while (this.tilesLoading_ < maxTotalLoading && newLoads < maxNewLoads &&
         this.getCount() > 0) {
    tile = /** @type {GVol.Tile} */ (this.dequeue()[0]);
    tileKey = tile.getKey();
    state = tile.getState();
    if (state === GVol.TileState.ABORT) {
      abortedTiles = true;
    } else if (state === GVol.TileState.IDLE && !(tileKey in this.tilesLoadingKeys_)) {
      this.tilesLoadingKeys_[tileKey] = true;
      ++this.tilesLoading_;
      ++newLoads;
      tile.load();
    }
  }
  if (newLoads === 0 && abortedTiles) {
    // Do not stop the render loop when all wanted tiles were aborted due to
    // a small, saturated tile cache.
    this.tileChangeCallback_();
  }
};

goog.provide('GVol.ResGVolutionConstraint');

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


/**
 * @param {Array.<number>} resGVolutions ResGVolutions.
 * @return {GVol.ResGVolutionConstraintType} Zoom function.
 */
GVol.ResGVolutionConstraint.createSnapToResGVolutions = function(resGVolutions) {
  return (
    /**
     * @param {number|undefined} resGVolution ResGVolution.
     * @param {number} delta Delta.
     * @param {number} direction Direction.
     * @return {number|undefined} ResGVolution.
     */
    function(resGVolution, delta, direction) {
      if (resGVolution !== undefined) {
        var z =
              GVol.array.linearFindNearest(resGVolutions, resGVolution, direction);
        z = GVol.math.clamp(z + delta, 0, resGVolutions.length - 1);
        var index = Math.floor(z);
        if (z != index && index < resGVolutions.length - 1) {
          var power = resGVolutions[index] / resGVolutions[index + 1];
          return resGVolutions[index] / Math.pow(power, z - index);
        } else {
          return resGVolutions[index];
        }
      } else {
        return undefined;
      }
    });
};


/**
 * @param {number} power Power.
 * @param {number} maxResGVolution Maximum resGVolution.
 * @param {number=} opt_maxLevel Maximum level.
 * @return {GVol.ResGVolutionConstraintType} Zoom function.
 */
GVol.ResGVolutionConstraint.createSnapToPower = function(power, maxResGVolution, opt_maxLevel) {
  return (
    /**
     * @param {number|undefined} resGVolution ResGVolution.
     * @param {number} delta Delta.
     * @param {number} direction Direction.
     * @return {number|undefined} ResGVolution.
     */
    function(resGVolution, delta, direction) {
      if (resGVolution !== undefined) {
        var offset = -direction / 2 + 0.5;
        var GVoldLevel = Math.floor(
            Math.log(maxResGVolution / resGVolution) / Math.log(power) + offset);
        var newLevel = Math.max(GVoldLevel + delta, 0);
        if (opt_maxLevel !== undefined) {
          newLevel = Math.min(newLevel, opt_maxLevel);
        }
        return maxResGVolution / Math.pow(power, newLevel);
      } else {
        return undefined;
      }
    });
};

goog.provide('GVol.RotationConstraint');

goog.require('GVol.math');


/**
 * @param {number|undefined} rotation Rotation.
 * @param {number} delta Delta.
 * @return {number|undefined} Rotation.
 */
GVol.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.
 */
GVol.RotationConstraint.none = function(rotation, delta) {
  if (rotation !== undefined) {
    return rotation + delta;
  } else {
    return undefined;
  }
};


/**
 * @param {number} n N.
 * @return {GVol.RotationConstraintType} Rotation constraint.
 */
GVol.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_tGVolerance TGVolerance.
 * @return {GVol.RotationConstraintType} Rotation constraint.
 */
GVol.RotationConstraint.createSnapToZero = function(opt_tGVolerance) {
  var tGVolerance = opt_tGVolerance || GVol.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) <= tGVolerance) {
          return 0;
        } else {
          return rotation + delta;
        }
      } else {
        return undefined;
      }
    });
};

goog.provide('GVol.ViewHint');

/**
 * @enum {number}
 */
GVol.ViewHint = {
  ANIMATING: 0,
  INTERACTING: 1
};

goog.provide('GVol.ViewProperty');

/**
 * @enum {string}
 */
GVol.ViewProperty = {
  CENTER: 'center',
  RESOLUTION: 'resGVolution',
  ROTATION: 'rotation'
};

goog.provide('GVol.string');

/**
 * @param {number} number Number to be formatted
 * @param {number} width The desired width
 * @param {number=} opt_precision Precision of the output string (i.e. number of decimal places)
 * @returns {string} Formatted string
*/
GVol.string.padNumber = function(number, width, opt_precision) {
  var numberString = opt_precision !== undefined ? number.toFixed(opt_precision) : '' + number;
  var decimal = numberString.indexOf('.');
  decimal = decimal === -1 ? numberString.length : decimal;
  return decimal > width ? numberString : new Array(1 + width - decimal).join('0') + numberString;
};

/**
 * Adapted from https://github.com/omichelsen/compare-versions/blob/master/index.js
 * @param {string|number} v1 First version
 * @param {string|number} v2 Second version
 * @returns {number} Value
 */
GVol.string.compareVersions = function(v1, v2) {
  var s1 = ('' + v1).split('.');
  var s2 = ('' + v2).split('.');

  for (var i = 0; i < Math.max(s1.length, s2.length); i++) {
    var n1 = parseInt(s1[i] || '0', 10);
    var n2 = parseInt(s2[i] || '0', 10);

    if (n1 > n2) {
      return 1;
    }
    if (n2 > n1) {
      return -1;
    }
  }

  return 0;
};

goog.provide('GVol.coordinate');

goog.require('GVol.math');
goog.require('GVol.string');


/**
 * Add `delta` to `coordinate`. `coordinate` is modified in place and returned
 * by the function.
 *
 * Example:
 *
 *     var coord = [7.85, 47.983333];
 *     GVol.coordinate.add(coord, [-2, 4]);
 *     // coord is now [5.85, 51.983333]
 *
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @param {GVol.Coordinate} delta Delta.
 * @return {GVol.Coordinate} The input coordinate adjusted by the given delta.
 * @api
 */
GVol.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 circle.
 *
 * @param {GVol.Coordinate} coordinate The coordinate.
 * @param {GVol.geom.Circle} circle The circle.
 * @return {GVol.Coordinate} Closest point on the circumference
 */
GVol.coordinate.closestOnCircle = function(coordinate, circle) {
  var r = circle.getRadius();
  var center = circle.getCenter();
  var x0 = center[0];
  var y0 = center[1];
  var x1 = coordinate[0];
  var y1 = coordinate[1];

  var dx = x1 - x0;
  var dy = y1 - y0;
  if (dx === 0 && dy === 0) {
    dx = 1;
  }
  var d = Math.sqrt(dx * dx + dy * dy);

  var x, y;

  x = x0 + r * dx / d;
  y = y0 + r * dy / d;

  return [x, y];
};


/**
 * 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 {GVol.Coordinate} coordinate The coordinate.
 * @param {Array.<GVol.Coordinate>} segment The two coordinates of the segment.
 * @return {GVol.Coordinate} The foot of the perpendicular of the coordinate to
 *     the segment.
 */
GVol.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 GVol.CoordinateFormatType} function that can be used to format
 * a {GVol.Coordinate} to a string.
 *
 * Example without specifying the fractional digits:
 *
 *     var coord = [7.85, 47.983333];
 *     var stringifyFunc = GVol.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 = GVol.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 {GVol.CoordinateFormatType} Coordinate format.
 * @api
 */
GVol.coordinate.createStringXY = function(opt_fractionDigits) {
  return (
    /**
     * @param {GVol.Coordinate|undefined} coordinate Coordinate.
     * @return {string} String XY.
     */
    function(coordinate) {
      return GVol.coordinate.toStringXY(coordinate, opt_fractionDigits);
    });
};


/**
 * @param {string} hemispheres Hemispheres.
 * @param {number} degrees Degrees.
 * @param {number=} opt_fractionDigits The number of digits to include
 *    after the decimal point. Default is `0`.
 * @return {string} String.
 */
GVol.coordinate.degreesToStringHDMS = function(hemispheres, degrees, opt_fractionDigits) {
  var normalizedDegrees = GVol.math.modulo(degrees + 180, 360) - 180;
  var x = Math.abs(3600 * normalizedDegrees);
  var dflPrecision = opt_fractionDigits || 0;
  var precision = Math.pow(10, dflPrecision);

  var deg = Math.floor(x / 3600);
  var min = Math.floor((x - deg * 3600) / 60);
  var sec = x - (deg * 3600) - (min * 60);
  sec = Math.ceil(sec * precision) / precision;

  if (sec >= 60) {
    sec = 0;
    min += 1;
  }

  if (min >= 60) {
    min = 0;
    deg += 1;
  }

  return deg + '\u00b0 ' + GVol.string.padNumber(min, 2) + '\u2032 ' +
    GVol.string.padNumber(sec, 2, dflPrecision) + '\u2033' +
    (normalizedDegrees == 0 ? '' : ' ' + hemispheres.charAt(normalizedDegrees < 0 ? 1 : 0));
};


/**
 * Transforms the given {@link GVol.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 = GVol.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 = GVol.coordinate.format(coord, template, 2);
 *     // out is now 'Coordinate is (7.85|47.98).'
 *
 * @param {GVol.Coordinate|undefined} coordinate Coordinate.
 * @param {string} template A template string with `{x}` and `{y}` placehGVolders
 *     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
 */
GVol.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 {GVol.Coordinate} coordinate1 First coordinate.
 * @param {GVol.Coordinate} coordinate2 Second coordinate.
 * @return {boGVolean} Whether the passed coordinates are equal.
 */
GVol.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
 *     GVol.coordinate.rotate(coord, rotateRadians);
 *     // coord is now [-47.983333, 7.85]
 *
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @param {number} angle Angle in radian.
 * @return {GVol.Coordinate} Coordinate.
 * @api
 */
GVol.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;
 *     GVol.coordinate.scale(coord, scale);
 *     // coord is now [9.42, 57.5799996]
 *
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @param {number} scale Scale factor.
 * @return {GVol.Coordinate} Coordinate.
 */
GVol.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 {GVol.Coordinate} coordinate Coordinate.
 * @param {GVol.Coordinate} delta Delta.
 * @return {GVol.Coordinate} Coordinate.
 */
GVol.coordinate.sub = function(coordinate, delta) {
  coordinate[0] -= delta[0];
  coordinate[1] -= delta[1];
  return coordinate;
};


/**
 * @param {GVol.Coordinate} coord1 First coordinate.
 * @param {GVol.Coordinate} coord2 Second coordinate.
 * @return {number} Squared distance between coord1 and coord2.
 */
GVol.coordinate.squaredDistance = function(coord1, coord2) {
  var dx = coord1[0] - coord2[0];
  var dy = coord1[1] - coord2[1];
  return dx * dx + dy * dy;
};


/**
 * @param {GVol.Coordinate} coord1 First coordinate.
 * @param {GVol.Coordinate} coord2 Second coordinate.
 * @return {number} Distance between coord1 and coord2.
 */
GVol.coordinate.distance = function(coord1, coord2) {
  return Math.sqrt(GVol.coordinate.squaredDistance(coord1, coord2));
};


/**
 * Calculate the squared distance from a coordinate to a line segment.
 *
 * @param {GVol.Coordinate} coordinate Coordinate of the point.
 * @param {Array.<GVol.Coordinate>} segment Line segment (2 coordinates).
 * @return {number} Squared distance from the point to the line segment.
 */
GVol.coordinate.squaredDistanceToSegment = function(coordinate, segment) {
  return GVol.coordinate.squaredDistance(coordinate,
      GVol.coordinate.closestOnSegment(coordinate, segment));
};


/**
 * Format a geographic coordinate with the hemisphere, degrees, minutes, and
 * seconds.
 *
 * Example without specifying fractional digits:
 *
 *     var coord = [7.85, 47.983333];
 *     var out = GVol.coordinate.toStringHDMS(coord);
 *     // out is now '47° 58′ 60″ N 7° 50′ 60″ E'
 *
 * Example explicitly specifying 1 fractional digit:
 *
 *     var coord = [7.85, 47.983333];
 *     var out = GVol.coordinate.toStringHDMS(coord, 1);
 *     // out is now '47° 58′ 60.0″ N 7° 50′ 60.0″ E'
 *
 * @param {GVol.Coordinate|undefined} coordinate Coordinate.
 * @param {number=} opt_fractionDigits The number of digits to include
 *    after the decimal point. Default is `0`.
 * @return {string} Hemisphere, degrees, minutes and seconds.
 * @api
 */
GVol.coordinate.toStringHDMS = function(coordinate, opt_fractionDigits) {
  if (coordinate) {
    return GVol.coordinate.degreesToStringHDMS('NS', coordinate[1], opt_fractionDigits) + ' ' +
        GVol.coordinate.degreesToStringHDMS('EW', coordinate[0], opt_fractionDigits);
  } else {
    return '';
  }
};


/**
 * Format a coordinate as a comma delimited string.
 *
 * Example without specifying fractional digits:
 *
 *     var coord = [7.85, 47.983333];
 *     var out = GVol.coordinate.toStringXY(coord);
 *     // out is now '8, 48'
 *
 * Example explicitly specifying 1 fractional digit:
 *
 *     var coord = [7.85, 47.983333];
 *     var out = GVol.coordinate.toStringXY(coord, 1);
 *     // out is now '7.8, 48.0'
 *
 * @param {GVol.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
 */
GVol.coordinate.toStringXY = function(coordinate, opt_fractionDigits) {
  return GVol.coordinate.format(coordinate, '{x}, {y}', opt_fractionDigits);
};

goog.provide('GVol.geom.GeometryLayout');


/**
 * 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}
 */
GVol.geom.GeometryLayout = {
  XY: 'XY',
  XYZ: 'XYZ',
  XYM: 'XYM',
  XYZM: 'XYZM'
};

goog.provide('GVol.functions');

/**
 * Always returns true.
 * @returns {boGVolean} true.
 */
GVol.functions.TRUE = function() {
  return true;
};

/**
 * Always returns false.
 * @returns {boGVolean} false.
 */
GVol.functions.FALSE = function() {
  return false;
};

goog.provide('GVol.geom.Geometry');

goog.require('GVol');
goog.require('GVol.Object');
goog.require('GVol.extent');
goog.require('GVol.functions');
goog.require('GVol.proj');


/**
 * @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
 * @abstract
 * @extends {GVol.Object}
 * @api
 */
GVol.geom.Geometry = function() {

  GVol.Object.call(this);

  /**
   * @private
   * @type {GVol.Extent}
   */
  this.extent_ = GVol.extent.createEmpty();

  /**
   * @private
   * @type {number}
   */
  this.extentRevision_ = -1;

  /**
   * @protected
   * @type {Object.<string, GVol.geom.Geometry>}
   */
  this.simplifiedGeometryCache = {};

  /**
   * @protected
   * @type {number}
   */
  this.simplifiedGeometryMaxMinSquaredTGVolerance = 0;

  /**
   * @protected
   * @type {number}
   */
  this.simplifiedGeometryRevision = 0;

};
GVol.inherits(GVol.geom.Geometry, GVol.Object);


/**
 * Make a complete copy of the geometry.
 * @abstract
 * @return {!GVol.geom.Geometry} Clone.
 */
GVol.geom.Geometry.prototype.clone = function() {};


/**
 * @abstract
 * @param {number} x X.
 * @param {number} y Y.
 * @param {GVol.Coordinate} closestPoint Closest point.
 * @param {number} minSquaredDistance Minimum squared distance.
 * @return {number} Minimum squared distance.
 */
GVol.geom.Geometry.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {};


/**
 * Return the closest point of the geometry to the passed point as
 * {@link GVol.Coordinate coordinate}.
 * @param {GVol.Coordinate} point Point.
 * @param {GVol.Coordinate=} opt_closestPoint Closest point.
 * @return {GVol.Coordinate} Closest point.
 * @api
 */
GVol.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;
};


/**
 * Returns true if this geometry includes the specified coordinate. If the
 * coordinate is on the boundary of the geometry, returns false.
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @return {boGVolean} Contains coordinate.
 * @api
 */
GVol.geom.Geometry.prototype.intersectsCoordinate = function(coordinate) {
  return this.containsXY(coordinate[0], coordinate[1]);
};


/**
 * @abstract
 * @param {GVol.Extent} extent Extent.
 * @protected
 * @return {GVol.Extent} extent Extent.
 */
GVol.geom.Geometry.prototype.computeExtent = function(extent) {};


/**
 * @param {number} x X.
 * @param {number} y Y.
 * @return {boGVolean} Contains (x, y).
 */
GVol.geom.Geometry.prototype.containsXY = GVol.functions.FALSE;


/**
 * Get the extent of the geometry.
 * @param {GVol.Extent=} opt_extent Extent.
 * @return {GVol.Extent} extent Extent.
 * @api
 */
GVol.geom.Geometry.prototype.getExtent = function(opt_extent) {
  if (this.extentRevision_ != this.getRevision()) {
    this.extent_ = this.computeExtent(this.extent_);
    this.extentRevision_ = this.getRevision();
  }
  return GVol.extent.returnOrUpdate(this.extent_, opt_extent);
};


/**
 * Rotate the geometry around a given coordinate. This modifies the geometry
 * coordinates in place.
 * @abstract
 * @param {number} angle Rotation angle in radians.
 * @param {GVol.Coordinate} anchor The rotation center.
 * @api
 */
GVol.geom.Geometry.prototype.rotate = function(angle, anchor) {};


/**
 * Scale the geometry (with an optional origin).  This modifies the geometry
 * coordinates in place.
 * @abstract
 * @param {number} sx The scaling factor in the x-direction.
 * @param {number=} opt_sy The scaling factor in the y-direction (defaults to
 *     sx).
 * @param {GVol.Coordinate=} opt_anchor The scale origin (defaults to the center
 *     of the geometry extent).
 * @api
 */
GVol.geom.Geometry.prototype.scale = function(sx, opt_sy, opt_anchor) {};


/**
 * 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 pGVolygons, a quantization-based
 * simplification is used to preserve topGVology.
 * @function
 * @param {number} tGVolerance The tGVolerance distance for simplification.
 * @return {GVol.geom.Geometry} A new, simplified version of the original
 *     geometry.
 * @api
 */
GVol.geom.Geometry.prototype.simplify = function(tGVolerance) {
  return this.getSimplifiedGeometry(tGVolerance * tGVolerance);
};


/**
 * Create a simplified version of this geometry using the Douglas Peucker
 * algorithm.
 * @see https://en.wikipedia.org/wiki/Ramer-Douglas-Peucker_algorithm
 * @abstract
 * @param {number} squaredTGVolerance Squared tGVolerance.
 * @return {GVol.geom.Geometry} Simplified geometry.
 */
GVol.geom.Geometry.prototype.getSimplifiedGeometry = function(squaredTGVolerance) {};


/**
 * Get the type of this geometry.
 * @abstract
 * @return {GVol.geom.GeometryType} Geometry type.
 */
GVol.geom.Geometry.prototype.getType = function() {};


/**
 * 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.
 * @abstract
 * @param {GVol.TransformFunction} transformFn Transform.
 */
GVol.geom.Geometry.prototype.applyTransform = function(transformFn) {};


/**
 * Test if the geometry and the passed extent intersect.
 * @abstract
 * @param {GVol.Extent} extent Extent.
 * @return {boGVolean} `true` if the geometry and the extent intersect.
 */
GVol.geom.Geometry.prototype.intersectsExtent = function(extent) {};


/**
 * Translate the geometry.  This modifies the geometry coordinates in place.  If
 * instead you want a new geometry, first `clone()` this geometry.
 * @abstract
 * @param {number} deltaX Delta X.
 * @param {number} deltaY Delta Y.
 */
GVol.geom.Geometry.prototype.translate = function(deltaX, deltaY) {};


/**
 * 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 {GVol.ProjectionLike} source The current projection.  Can be a
 *     string identifier or a {@link GVol.proj.Projection} object.
 * @param {GVol.ProjectionLike} destination The desired projection.  Can be a
 *     string identifier or a {@link GVol.proj.Projection} object.
 * @return {GVol.geom.Geometry} This geometry.  Note that original geometry is
 *     modified in place.
 * @api
 */
GVol.geom.Geometry.prototype.transform = function(source, destination) {
  this.applyTransform(GVol.proj.getTransform(source, destination));
  return this;
};

goog.provide('GVol.geom.flat.transform');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {GVol.Transform} transform Transform.
 * @param {Array.<number>=} opt_dest Destination.
 * @return {Array.<number>} Transformed coordinates.
 */
GVol.geom.flat.transform.transform2D = function(flatCoordinates, offset, end, stride, transform, opt_dest) {
  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++] = transform[0] * x + transform[2] * y + transform[4];
    dest[i++] = transform[1] * x + transform[3] * y + transform[5];
  }
  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} angle Angle.
 * @param {Array.<number>} anchor Rotation anchor point.
 * @param {Array.<number>=} opt_dest Destination.
 * @return {Array.<number>} Transformed coordinates.
 */
GVol.geom.flat.transform.rotate = function(flatCoordinates, offset, end, stride, angle, anchor, opt_dest) {
  var dest = opt_dest ? opt_dest : [];
  var cos = Math.cos(angle);
  var sin = Math.sin(angle);
  var anchorX = anchor[0];
  var anchorY = anchor[1];
  var i = 0;
  for (var j = offset; j < end; j += stride) {
    var deltaX = flatCoordinates[j] - anchorX;
    var deltaY = flatCoordinates[j + 1] - anchorY;
    dest[i++] = anchorX + deltaX * cos - deltaY * sin;
    dest[i++] = anchorY + deltaX * sin + deltaY * cos;
    for (var k = j + 2; k < j + stride; ++k) {
      dest[i++] = flatCoordinates[k];
    }
  }
  if (opt_dest && dest.length != i) {
    dest.length = i;
  }
  return dest;
};


/**
 * Scale the coordinates.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {number} sx Scale factor in the x-direction.
 * @param {number} sy Scale factor in the y-direction.
 * @param {Array.<number>} anchor Scale anchor point.
 * @param {Array.<number>=} opt_dest Destination.
 * @return {Array.<number>} Transformed coordinates.
 */
GVol.geom.flat.transform.scale = function(flatCoordinates, offset, end, stride, sx, sy, anchor, opt_dest) {
  var dest = opt_dest ? opt_dest : [];
  var anchorX = anchor[0];
  var anchorY = anchor[1];
  var i = 0;
  for (var j = offset; j < end; j += stride) {
    var deltaX = flatCoordinates[j] - anchorX;
    var deltaY = flatCoordinates[j + 1] - anchorY;
    dest[i++] = anchorX + sx * deltaX;
    dest[i++] = anchorY + sy * deltaY;
    for (var k = j + 2; k < j + stride; ++k) {
      dest[i++] = flatCoordinates[k];
    }
  }
  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.
 */
GVol.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('GVol.geom.SimpleGeometry');

goog.require('GVol');
goog.require('GVol.functions');
goog.require('GVol.extent');
goog.require('GVol.geom.Geometry');
goog.require('GVol.geom.GeometryLayout');
goog.require('GVol.geom.flat.transform');
goog.require('GVol.obj');


/**
 * @classdesc
 * Abstract base class; only used for creating subclasses; do not instantiate
 * in apps, as cannot be rendered.
 *
 * @constructor
 * @abstract
 * @extends {GVol.geom.Geometry}
 * @api
 */
GVol.geom.SimpleGeometry = function() {

  GVol.geom.Geometry.call(this);

  /**
   * @protected
   * @type {GVol.geom.GeometryLayout}
   */
  this.layout = GVol.geom.GeometryLayout.XY;

  /**
   * @protected
   * @type {number}
   */
  this.stride = 2;

  /**
   * @protected
   * @type {Array.<number>}
   */
  this.flatCoordinates = null;

};
GVol.inherits(GVol.geom.SimpleGeometry, GVol.geom.Geometry);


/**
 * @param {number} stride Stride.
 * @private
 * @return {GVol.geom.GeometryLayout} layout Layout.
 */
GVol.geom.SimpleGeometry.getLayoutForStride_ = function(stride) {
  var layout;
  if (stride == 2) {
    layout = GVol.geom.GeometryLayout.XY;
  } else if (stride == 3) {
    layout = GVol.geom.GeometryLayout.XYZ;
  } else if (stride == 4) {
    layout = GVol.geom.GeometryLayout.XYZM;
  }
  return /** @type {GVol.geom.GeometryLayout} */ (layout);
};


/**
 * @param {GVol.geom.GeometryLayout} layout Layout.
 * @return {number} Stride.
 */
GVol.geom.SimpleGeometry.getStrideForLayout = function(layout) {
  var stride;
  if (layout == GVol.geom.GeometryLayout.XY) {
    stride = 2;
  } else if (layout == GVol.geom.GeometryLayout.XYZ || layout == GVol.geom.GeometryLayout.XYM) {
    stride = 3;
  } else if (layout == GVol.geom.GeometryLayout.XYZM) {
    stride = 4;
  }
  return /** @type {number} */ (stride);
};


/**
 * @inheritDoc
 */
GVol.geom.SimpleGeometry.prototype.containsXY = GVol.functions.FALSE;


/**
 * @inheritDoc
 */
GVol.geom.SimpleGeometry.prototype.computeExtent = function(extent) {
  return GVol.extent.createOrUpdateFromFlatCoordinates(
      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
      extent);
};


/**
 * @abstract
 * @return {Array} Coordinates.
 */
GVol.geom.SimpleGeometry.prototype.getCoordinates = function() {};


/**
 * Return the first coordinate of the geometry.
 * @return {GVol.Coordinate} First coordinate.
 * @api
 */
GVol.geom.SimpleGeometry.prototype.getFirstCoordinate = function() {
  return this.flatCoordinates.slice(0, this.stride);
};


/**
 * @return {Array.<number>} Flat coordinates.
 */
GVol.geom.SimpleGeometry.prototype.getFlatCoordinates = function() {
  return this.flatCoordinates;
};


/**
 * Return the last coordinate of the geometry.
 * @return {GVol.Coordinate} Last point.
 * @api
 */
GVol.geom.SimpleGeometry.prototype.getLastCoordinate = function() {
  return this.flatCoordinates.slice(this.flatCoordinates.length - this.stride);
};


/**
 * Return the {@link GVol.geom.GeometryLayout layout} of the geometry.
 * @return {GVol.geom.GeometryLayout} Layout.
 * @api
 */
GVol.geom.SimpleGeometry.prototype.getLayout = function() {
  return this.layout;
};


/**
 * @inheritDoc
 */
GVol.geom.SimpleGeometry.prototype.getSimplifiedGeometry = function(squaredTGVolerance) {
  if (this.simplifiedGeometryRevision != this.getRevision()) {
    GVol.obj.clear(this.simplifiedGeometryCache);
    this.simplifiedGeometryMaxMinSquaredTGVolerance = 0;
    this.simplifiedGeometryRevision = this.getRevision();
  }
  // If squaredTGVolerance is negative or if we know that simplification will not
  // have any effect then just return this.
  if (squaredTGVolerance < 0 ||
      (this.simplifiedGeometryMaxMinSquaredTGVolerance !== 0 &&
       squaredTGVolerance <= this.simplifiedGeometryMaxMinSquaredTGVolerance)) {
    return this;
  }
  var key = squaredTGVolerance.toString();
  if (this.simplifiedGeometryCache.hasOwnProperty(key)) {
    return this.simplifiedGeometryCache[key];
  } else {
    var simplifiedGeometry =
        this.getSimplifiedGeometryInternal(squaredTGVolerance);
    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 squaredTGVolerance less
      // than or equal to the current squaredTGVolerance 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.simplifiedGeometryMaxMinSquaredTGVolerance = squaredTGVolerance;
      return this;
    }
  }
};


/**
 * @param {number} squaredTGVolerance Squared tGVolerance.
 * @return {GVol.geom.SimpleGeometry} Simplified geometry.
 * @protected
 */
GVol.geom.SimpleGeometry.prototype.getSimplifiedGeometryInternal = function(squaredTGVolerance) {
  return this;
};


/**
 * @return {number} Stride.
 */
GVol.geom.SimpleGeometry.prototype.getStride = function() {
  return this.stride;
};


/**
 * @param {GVol.geom.GeometryLayout} layout Layout.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @protected
 */
GVol.geom.SimpleGeometry.prototype.setFlatCoordinatesInternal = function(layout, flatCoordinates) {
  this.stride = GVol.geom.SimpleGeometry.getStrideForLayout(layout);
  this.layout = layout;
  this.flatCoordinates = flatCoordinates;
};


/**
 * @abstract
 * @param {Array} coordinates Coordinates.
 * @param {GVol.geom.GeometryLayout=} opt_layout Layout.
 */
GVol.geom.SimpleGeometry.prototype.setCoordinates = function(coordinates, opt_layout) {};


/**
 * @param {GVol.geom.GeometryLayout|undefined} layout Layout.
 * @param {Array} coordinates Coordinates.
 * @param {number} nesting Nesting.
 * @protected
 */
GVol.geom.SimpleGeometry.prototype.setLayout = function(layout, coordinates, nesting) {
  /** @type {number} */
  var stride;
  if (layout) {
    stride = GVol.geom.SimpleGeometry.getStrideForLayout(layout);
  } else {
    var i;
    for (i = 0; i < nesting; ++i) {
      if (coordinates.length === 0) {
        this.layout = GVol.geom.GeometryLayout.XY;
        this.stride = 2;
        return;
      } else {
        coordinates = /** @type {Array} */ (coordinates[0]);
      }
    }
    stride = coordinates.length;
    layout = GVol.geom.SimpleGeometry.getLayoutForStride_(stride);
  }
  this.layout = layout;
  this.stride = stride;
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.SimpleGeometry.prototype.applyTransform = function(transformFn) {
  if (this.flatCoordinates) {
    transformFn(this.flatCoordinates, this.flatCoordinates, this.stride);
    this.changed();
  }
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.SimpleGeometry.prototype.rotate = function(angle, anchor) {
  var flatCoordinates = this.getFlatCoordinates();
  if (flatCoordinates) {
    var stride = this.getStride();
    GVol.geom.flat.transform.rotate(
        flatCoordinates, 0, flatCoordinates.length,
        stride, angle, anchor, flatCoordinates);
    this.changed();
  }
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.SimpleGeometry.prototype.scale = function(sx, opt_sy, opt_anchor) {
  var sy = opt_sy;
  if (sy === undefined) {
    sy = sx;
  }
  var anchor = opt_anchor;
  if (!anchor) {
    anchor = GVol.extent.getCenter(this.getExtent());
  }
  var flatCoordinates = this.getFlatCoordinates();
  if (flatCoordinates) {
    var stride = this.getStride();
    GVol.geom.flat.transform.scale(
        flatCoordinates, 0, flatCoordinates.length,
        stride, sx, sy, anchor, flatCoordinates);
    this.changed();
  }
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.SimpleGeometry.prototype.translate = function(deltaX, deltaY) {
  var flatCoordinates = this.getFlatCoordinates();
  if (flatCoordinates) {
    var stride = this.getStride();
    GVol.geom.flat.transform.translate(
        flatCoordinates, 0, flatCoordinates.length, stride,
        deltaX, deltaY, flatCoordinates);
    this.changed();
  }
};


/**
 * @param {GVol.geom.SimpleGeometry} simpleGeometry Simple geometry.
 * @param {GVol.Transform} transform Transform.
 * @param {Array.<number>=} opt_dest Destination.
 * @return {Array.<number>} Transformed flat coordinates.
 */
GVol.geom.SimpleGeometry.transform2D = function(simpleGeometry, transform, opt_dest) {
  var flatCoordinates = simpleGeometry.getFlatCoordinates();
  if (!flatCoordinates) {
    return null;
  } else {
    var stride = simpleGeometry.getStride();
    return GVol.geom.flat.transform.transform2D(
        flatCoordinates, 0, flatCoordinates.length, stride,
        transform, opt_dest);
  }
};

goog.provide('GVol.geom.flat.area');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @return {number} Area.
 */
GVol.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.
 */
GVol.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 += GVol.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.
 */
GVol.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 +=
        GVol.geom.flat.area.linearRings(flatCoordinates, offset, ends, stride);
    offset = ends[ends.length - 1];
  }
  return area;
};

goog.provide('GVol.geom.flat.closest');

goog.require('GVol.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 interpGVolated.
 * @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.
 */
GVol.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] = GVol.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.
 */
GVol.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 = GVol.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.
 */
GVol.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 = GVol.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.
 */
GVol.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 = GVol.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 {boGVolean} 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.
 */
GVol.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 = GVol.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;
    }
  }
  var tmpPoint = opt_tmpPoint ? opt_tmpPoint : [NaN, NaN];
  var index = offset + stride;
  while (index < end) {
    GVol.geom.flat.closest.point(
        flatCoordinates, index - stride, index, stride, x, y, tmpPoint);
    squaredDistance = GVol.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.
    GVol.geom.flat.closest.point(
        flatCoordinates, end - stride, offset, stride, x, y, tmpPoint);
    squaredDistance = GVol.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 {boGVolean} 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.
 */
GVol.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 = GVol.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 {boGVolean} 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.
 */
GVol.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 = GVol.geom.flat.closest.getsClosestPoint(
        flatCoordinates, offset, ends, stride,
        maxDelta, isRing, x, y, closestPoint, minSquaredDistance, tmpPoint);
    offset = ends[ends.length - 1];
  }
  return minSquaredDistance;
};

goog.provide('GVol.geom.flat.deflate');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @param {number} stride Stride.
 * @return {number} offset Offset.
 */
GVol.geom.flat.deflate.coordinate = function(flatCoordinates, offset, coordinate, 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.<GVol.Coordinate>} coordinates Coordinates.
 * @param {number} stride Stride.
 * @return {number} offset Offset.
 */
GVol.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];
    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.<GVol.Coordinate>>} coordinatess Coordinatess.
 * @param {number} stride Stride.
 * @param {Array.<number>=} opt_ends Ends.
 * @return {Array.<number>} Ends.
 */
GVol.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 = GVol.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.<GVol.Coordinate>>>} coordinatesss Coordinatesss.
 * @param {number} stride Stride.
 * @param {Array.<Array.<number>>=} opt_endss Endss.
 * @return {Array.<Array.<number>>} Endss.
 */
GVol.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 = GVol.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('GVol.geom.flat.inflate');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {Array.<GVol.Coordinate>=} opt_coordinates Coordinates.
 * @return {Array.<GVol.Coordinate>} Coordinates.
 */
GVol.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.<GVol.Coordinate>>=} opt_coordinatess Coordinatess.
 * @return {Array.<Array.<GVol.Coordinate>>} Coordinatess.
 */
GVol.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++] = GVol.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.<GVol.Coordinate>>>=} opt_coordinatesss
 *     Coordinatesss.
 * @return {Array.<Array.<Array.<GVol.Coordinate>>>} Coordinatesss.
 */
GVol.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++] = GVol.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 fGVollowing conditions are met:
//
//    1. Redistributions of source code must retain the above copyright notice,
//       this list of conditions and the fGVollowing disclaimer.
//
//    2. Redistributions in binary form must reproduce the above copyright
//       notice, this list of conditions and the fGVollowing 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('GVol.geom.flat.simplify');

goog.require('GVol.math');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {number} squaredTGVolerance Squared tGVolerance.
 * @param {boGVolean} highQuality Highest quality.
 * @param {Array.<number>=} opt_simplifiedFlatCoordinates Simplified flat
 *     coordinates.
 * @return {Array.<number>} Simplified line string.
 */
GVol.geom.flat.simplify.lineString = function(flatCoordinates, offset, end,
    stride, squaredTGVolerance, highQuality, opt_simplifiedFlatCoordinates) {
  var simplifiedFlatCoordinates = opt_simplifiedFlatCoordinates !== undefined ?
    opt_simplifiedFlatCoordinates : [];
  if (!highQuality) {
    end = GVol.geom.flat.simplify.radialDistance(flatCoordinates, offset, end,
        stride, squaredTGVolerance,
        simplifiedFlatCoordinates, 0);
    flatCoordinates = simplifiedFlatCoordinates;
    offset = 0;
    stride = 2;
  }
  simplifiedFlatCoordinates.length = GVol.geom.flat.simplify.douglasPeucker(
      flatCoordinates, offset, end, stride, squaredTGVolerance,
      simplifiedFlatCoordinates, 0);
  return simplifiedFlatCoordinates;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {number} squaredTGVolerance Squared tGVolerance.
 * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
 *     coordinates.
 * @param {number} simplifiedOffset Simplified offset.
 * @return {number} Simplified offset.
 */
GVol.geom.flat.simplify.douglasPeucker = function(flatCoordinates, offset, end,
    stride, squaredTGVolerance, 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 = GVol.math.squaredSegmentDistance(
          x, y, x1, y1, x2, y2);
      if (squaredDistance > maxSquaredDistance) {
        index = i;
        maxSquaredDistance = squaredDistance;
      }
    }
    if (maxSquaredDistance > squaredTGVolerance) {
      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} squaredTGVolerance Squared tGVolerance.
 * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
 *     coordinates.
 * @param {number} simplifiedOffset Simplified offset.
 * @param {Array.<number>} simplifiedEnds Simplified ends.
 * @return {number} Simplified offset.
 */
GVol.geom.flat.simplify.douglasPeuckers = function(flatCoordinates, offset,
    ends, stride, squaredTGVolerance, simplifiedFlatCoordinates,
    simplifiedOffset, simplifiedEnds) {
  var i, ii;
  for (i = 0, ii = ends.length; i < ii; ++i) {
    var end = ends[i];
    simplifiedOffset = GVol.geom.flat.simplify.douglasPeucker(
        flatCoordinates, offset, end, stride, squaredTGVolerance,
        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} squaredTGVolerance Squared tGVolerance.
 * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
 *     coordinates.
 * @param {number} simplifiedOffset Simplified offset.
 * @param {Array.<Array.<number>>} simplifiedEndss Simplified endss.
 * @return {number} Simplified offset.
 */
GVol.geom.flat.simplify.douglasPeuckerss = function(
    flatCoordinates, offset, endss, stride, squaredTGVolerance,
    simplifiedFlatCoordinates, simplifiedOffset, simplifiedEndss) {
  var i, ii;
  for (i = 0, ii = endss.length; i < ii; ++i) {
    var ends = endss[i];
    var simplifiedEnds = [];
    simplifiedOffset = GVol.geom.flat.simplify.douglasPeuckers(
        flatCoordinates, offset, ends, stride, squaredTGVolerance,
        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} squaredTGVolerance Squared tGVolerance.
 * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
 *     coordinates.
 * @param {number} simplifiedOffset Simplified offset.
 * @return {number} Simplified offset.
 */
GVol.geom.flat.simplify.radialDistance = function(flatCoordinates, offset, end,
    stride, squaredTGVolerance, 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 (GVol.math.squaredDistance(x1, y1, x2, y2) > squaredTGVolerance) {
      // 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} tGVolerance TGVolerance.
 * @return {number} Rounded value.
 */
GVol.geom.flat.simplify.snap = function(value, tGVolerance) {
  return tGVolerance * Math.round(value / tGVolerance);
};


/**
 * 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
 * topGVology 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 pGVolygons will be simplified to the same line
 * string independently in both pGVolygons.  This implementation uses a single
 * pass over the coordinates and eliminates intermediate cGVollinear points.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {number} tGVolerance TGVolerance.
 * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
 *     coordinates.
 * @param {number} simplifiedOffset Simplified offset.
 * @return {number} Simplified offset.
 */
GVol.geom.flat.simplify.quantize = function(flatCoordinates, offset, end, stride,
    tGVolerance, simplifiedFlatCoordinates, simplifiedOffset) {
  // do nothing if the line is empty
  if (offset == end) {
    return simplifiedOffset;
  }
  // snap the first coordinate (P1)
  var x1 = GVol.geom.flat.simplify.snap(flatCoordinates[offset], tGVolerance);
  var y1 = GVol.geom.flat.simplify.snap(flatCoordinates[offset + 1], tGVolerance);
  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 = GVol.geom.flat.simplify.snap(flatCoordinates[offset], tGVolerance);
    y2 = GVol.geom.flat.simplify.snap(flatCoordinates[offset + 1], tGVolerance);
    offset += stride;
    if (offset == end) {
      // all coordinates snap to the same value, the line cGVollapses 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 = GVol.geom.flat.simplify.snap(flatCoordinates[offset], tGVolerance);
    y3 = GVol.geom.flat.simplify.snap(flatCoordinates[offset + 1], tGVolerance);
    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 cGVolinear 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 cGVolinear, or they are cGVolinear 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} tGVolerance TGVolerance.
 * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
 *     coordinates.
 * @param {number} simplifiedOffset Simplified offset.
 * @param {Array.<number>} simplifiedEnds Simplified ends.
 * @return {number} Simplified offset.
 */
GVol.geom.flat.simplify.quantizes = function(
    flatCoordinates, offset, ends, stride,
    tGVolerance,
    simplifiedFlatCoordinates, simplifiedOffset, simplifiedEnds) {
  var i, ii;
  for (i = 0, ii = ends.length; i < ii; ++i) {
    var end = ends[i];
    simplifiedOffset = GVol.geom.flat.simplify.quantize(
        flatCoordinates, offset, end, stride,
        tGVolerance,
        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} tGVolerance TGVolerance.
 * @param {Array.<number>} simplifiedFlatCoordinates Simplified flat
 *     coordinates.
 * @param {number} simplifiedOffset Simplified offset.
 * @param {Array.<Array.<number>>} simplifiedEndss Simplified endss.
 * @return {number} Simplified offset.
 */
GVol.geom.flat.simplify.quantizess = function(
    flatCoordinates, offset, endss, stride,
    tGVolerance,
    simplifiedFlatCoordinates, simplifiedOffset, simplifiedEndss) {
  var i, ii;
  for (i = 0, ii = endss.length; i < ii; ++i) {
    var ends = endss[i];
    var simplifiedEnds = [];
    simplifiedOffset = GVol.geom.flat.simplify.quantizes(
        flatCoordinates, offset, ends, stride,
        tGVolerance,
        simplifiedFlatCoordinates, simplifiedOffset, simplifiedEnds);
    simplifiedEndss.push(simplifiedEnds);
    offset = ends[ends.length - 1];
  }
  return simplifiedOffset;
};

goog.provide('GVol.geom.LinearRing');

goog.require('GVol');
goog.require('GVol.extent');
goog.require('GVol.geom.GeometryLayout');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.geom.SimpleGeometry');
goog.require('GVol.geom.flat.area');
goog.require('GVol.geom.flat.closest');
goog.require('GVol.geom.flat.deflate');
goog.require('GVol.geom.flat.inflate');
goog.require('GVol.geom.flat.simplify');


/**
 * @classdesc
 * Linear ring geometry. Only used as part of pGVolygon; cannot be rendered
 * on its own.
 *
 * @constructor
 * @extends {GVol.geom.SimpleGeometry}
 * @param {Array.<GVol.Coordinate>} coordinates Coordinates.
 * @param {GVol.geom.GeometryLayout=} opt_layout Layout.
 * @api
 */
GVol.geom.LinearRing = function(coordinates, opt_layout) {

  GVol.geom.SimpleGeometry.call(this);

  /**
   * @private
   * @type {number}
   */
  this.maxDelta_ = -1;

  /**
   * @private
   * @type {number}
   */
  this.maxDeltaRevision_ = -1;

  this.setCoordinates(coordinates, opt_layout);

};
GVol.inherits(GVol.geom.LinearRing, GVol.geom.SimpleGeometry);


/**
 * Make a complete copy of the geometry.
 * @return {!GVol.geom.LinearRing} Clone.
 * @override
 * @api
 */
GVol.geom.LinearRing.prototype.clone = function() {
  var linearRing = new GVol.geom.LinearRing(null);
  linearRing.setFlatCoordinates(this.layout, this.flatCoordinates.slice());
  return linearRing;
};


/**
 * @inheritDoc
 */
GVol.geom.LinearRing.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
  if (minSquaredDistance <
      GVol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
    return minSquaredDistance;
  }
  if (this.maxDeltaRevision_ != this.getRevision()) {
    this.maxDelta_ = Math.sqrt(GVol.geom.flat.closest.getMaxSquaredDelta(
        this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, 0));
    this.maxDeltaRevision_ = this.getRevision();
  }
  return GVol.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
 */
GVol.geom.LinearRing.prototype.getArea = function() {
  return GVol.geom.flat.area.linearRing(
      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride);
};


/**
 * Return the coordinates of the linear ring.
 * @return {Array.<GVol.Coordinate>} Coordinates.
 * @override
 * @api
 */
GVol.geom.LinearRing.prototype.getCoordinates = function() {
  return GVol.geom.flat.inflate.coordinates(
      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride);
};


/**
 * @inheritDoc
 */
GVol.geom.LinearRing.prototype.getSimplifiedGeometryInternal = function(squaredTGVolerance) {
  var simplifiedFlatCoordinates = [];
  simplifiedFlatCoordinates.length = GVol.geom.flat.simplify.douglasPeucker(
      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
      squaredTGVolerance, simplifiedFlatCoordinates, 0);
  var simplifiedLinearRing = new GVol.geom.LinearRing(null);
  simplifiedLinearRing.setFlatCoordinates(
      GVol.geom.GeometryLayout.XY, simplifiedFlatCoordinates);
  return simplifiedLinearRing;
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.LinearRing.prototype.getType = function() {
  return GVol.geom.GeometryType.LINEAR_RING;
};


/**
 * @inheritDoc
 */
GVol.geom.LinearRing.prototype.intersectsExtent = function(extent) {};


/**
 * Set the coordinates of the linear ring.
 * @param {Array.<GVol.Coordinate>} coordinates Coordinates.
 * @param {GVol.geom.GeometryLayout=} opt_layout Layout.
 * @override
 * @api
 */
GVol.geom.LinearRing.prototype.setCoordinates = function(coordinates, opt_layout) {
  if (!coordinates) {
    this.setFlatCoordinates(GVol.geom.GeometryLayout.XY, null);
  } else {
    this.setLayout(opt_layout, coordinates, 1);
    if (!this.flatCoordinates) {
      this.flatCoordinates = [];
    }
    this.flatCoordinates.length = GVol.geom.flat.deflate.coordinates(
        this.flatCoordinates, 0, coordinates, this.stride);
    this.changed();
  }
};


/**
 * @param {GVol.geom.GeometryLayout} layout Layout.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 */
GVol.geom.LinearRing.prototype.setFlatCoordinates = function(layout, flatCoordinates) {
  this.setFlatCoordinatesInternal(layout, flatCoordinates);
  this.changed();
};

goog.provide('GVol.geom.Point');

goog.require('GVol');
goog.require('GVol.extent');
goog.require('GVol.geom.GeometryLayout');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.geom.SimpleGeometry');
goog.require('GVol.geom.flat.deflate');
goog.require('GVol.math');


/**
 * @classdesc
 * Point geometry.
 *
 * @constructor
 * @extends {GVol.geom.SimpleGeometry}
 * @param {GVol.Coordinate} coordinates Coordinates.
 * @param {GVol.geom.GeometryLayout=} opt_layout Layout.
 * @api
 */
GVol.geom.Point = function(coordinates, opt_layout) {
  GVol.geom.SimpleGeometry.call(this);
  this.setCoordinates(coordinates, opt_layout);
};
GVol.inherits(GVol.geom.Point, GVol.geom.SimpleGeometry);


/**
 * Make a complete copy of the geometry.
 * @return {!GVol.geom.Point} Clone.
 * @override
 * @api
 */
GVol.geom.Point.prototype.clone = function() {
  var point = new GVol.geom.Point(null);
  point.setFlatCoordinates(this.layout, this.flatCoordinates.slice());
  return point;
};


/**
 * @inheritDoc
 */
GVol.geom.Point.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
  var flatCoordinates = this.flatCoordinates;
  var squaredDistance = GVol.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 {GVol.Coordinate} Coordinates.
 * @override
 * @api
 */
GVol.geom.Point.prototype.getCoordinates = function() {
  return !this.flatCoordinates ? [] : this.flatCoordinates.slice();
};


/**
 * @inheritDoc
 */
GVol.geom.Point.prototype.computeExtent = function(extent) {
  return GVol.extent.createOrUpdateFromCoordinate(this.flatCoordinates, extent);
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.Point.prototype.getType = function() {
  return GVol.geom.GeometryType.POINT;
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.Point.prototype.intersectsExtent = function(extent) {
  return GVol.extent.containsXY(extent,
      this.flatCoordinates[0], this.flatCoordinates[1]);
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.Point.prototype.setCoordinates = function(coordinates, opt_layout) {
  if (!coordinates) {
    this.setFlatCoordinates(GVol.geom.GeometryLayout.XY, null);
  } else {
    this.setLayout(opt_layout, coordinates, 0);
    if (!this.flatCoordinates) {
      this.flatCoordinates = [];
    }
    this.flatCoordinates.length = GVol.geom.flat.deflate.coordinate(
        this.flatCoordinates, 0, coordinates, this.stride);
    this.changed();
  }
};


/**
 * @param {GVol.geom.GeometryLayout} layout Layout.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 */
GVol.geom.Point.prototype.setFlatCoordinates = function(layout, flatCoordinates) {
  this.setFlatCoordinatesInternal(layout, flatCoordinates);
  this.changed();
};

goog.provide('GVol.geom.flat.contains');

goog.require('GVol.extent');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {GVol.Extent} extent Extent.
 * @return {boGVolean} Contains extent.
 */
GVol.geom.flat.contains.linearRingContainsExtent = function(flatCoordinates, offset, end, stride, extent) {
  var outside = GVol.extent.forEachCorner(extent,
      /**
       * @param {GVol.Coordinate} coordinate Coordinate.
       * @return {boGVolean} Contains (x, y).
       */
      function(coordinate) {
        return !GVol.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 {boGVolean} Contains (x, y).
 */
GVol.geom.flat.contains.linearRingContainsXY = function(flatCoordinates, offset, end, stride, x, y) {
  // http://geomalgorithms.com/a03-_inclusion.html
  // Copyright 2000 softSurfer, 2012 Dan Sunday
  // This code may be freely used and modified for any purpose
  // providing that this copyright notice is included with it.
  // SoftSurfer makes no warranty for this code, and cannot be held
  // liable for any real or imagined damage resulting from its use.
  // Users of this code must verify correctness for their application.
  var wn = 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];
    if (y1 <= y) {
      if (y2 > y && ((x2 - x1) * (y - y1)) - ((x - x1) * (y2 - y1)) > 0) {
        wn++;
      }
    } else if (y2 <= y && ((x2 - x1) * (y - y1)) - ((x - x1) * (y2 - y1)) < 0) {
      wn--;
    }
    x1 = x2;
    y1 = y2;
  }
  return wn !== 0;
};


/**
 * @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 {boGVolean} Contains (x, y).
 */
GVol.geom.flat.contains.linearRingsContainsXY = function(flatCoordinates, offset, ends, stride, x, y) {
  if (ends.length === 0) {
    return false;
  }
  if (!GVol.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 (GVol.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 {boGVolean} Contains (x, y).
 */
GVol.geom.flat.contains.linearRingssContainsXY = function(flatCoordinates, offset, endss, stride, x, y) {
  if (endss.length === 0) {
    return false;
  }
  var i, ii;
  for (i = 0, ii = endss.length; i < ii; ++i) {
    var ends = endss[i];
    if (GVol.geom.flat.contains.linearRingsContainsXY(
        flatCoordinates, offset, ends, stride, x, y)) {
      return true;
    }
    offset = ends[ends.length - 1];
  }
  return false;
};

goog.provide('GVol.geom.flat.interiorpoint');

goog.require('GVol.array');
goog.require('GVol.geom.flat.contains');


/**
 * Calculates a point that is likely to lie in the interior of the linear rings.
 * Inspired by JTS's com.vividsGVolutions.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.
 */
GVol.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(GVol.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 (GVol.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.
 */
GVol.geom.flat.interiorpoint.linearRingss = function(flatCoordinates, offset, endss, stride, flatCenters) {
  var interiorPoints = [];
  var i, ii;
  for (i = 0, ii = endss.length; i < ii; ++i) {
    var ends = endss[i];
    interiorPoints = GVol.geom.flat.interiorpoint.linearRings(flatCoordinates,
        offset, ends, stride, flatCenters, 2 * i, interiorPoints);
    offset = ends[ends.length - 1];
  }
  return interiorPoints;
};

goog.provide('GVol.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, GVol.Coordinate, GVol.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|boGVolean} Value.
 * @template T,S
 */
GVol.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('GVol.geom.flat.intersectsextent');

goog.require('GVol.extent');
goog.require('GVol.geom.flat.contains');
goog.require('GVol.geom.flat.segments');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {GVol.Extent} extent Extent.
 * @return {boGVolean} True if the geometry and the extent intersect.
 */
GVol.geom.flat.intersectsextent.lineString = function(flatCoordinates, offset, end, stride, extent) {
  var coordinatesExtent = GVol.extent.extendFlatCoordinates(
      GVol.extent.createEmpty(), flatCoordinates, offset, end, stride);
  if (!GVol.extent.intersects(extent, coordinatesExtent)) {
    return false;
  }
  if (GVol.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 GVol.geom.flat.segments.forEach(flatCoordinates, offset, end, stride,
      /**
       * @param {GVol.Coordinate} point1 Start point.
       * @param {GVol.Coordinate} point2 End point.
       * @return {boGVolean} `true` if the segment and the extent intersect,
       *     `false` otherwise.
       */
      function(point1, point2) {
        return GVol.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 {GVol.Extent} extent Extent.
 * @return {boGVolean} True if the geometry and the extent intersect.
 */
GVol.geom.flat.intersectsextent.lineStrings = function(flatCoordinates, offset, ends, stride, extent) {
  var i, ii;
  for (i = 0, ii = ends.length; i < ii; ++i) {
    if (GVol.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 {GVol.Extent} extent Extent.
 * @return {boGVolean} True if the geometry and the extent intersect.
 */
GVol.geom.flat.intersectsextent.linearRing = function(flatCoordinates, offset, end, stride, extent) {
  if (GVol.geom.flat.intersectsextent.lineString(
      flatCoordinates, offset, end, stride, extent)) {
    return true;
  }
  if (GVol.geom.flat.contains.linearRingContainsXY(
      flatCoordinates, offset, end, stride, extent[0], extent[1])) {
    return true;
  }
  if (GVol.geom.flat.contains.linearRingContainsXY(
      flatCoordinates, offset, end, stride, extent[0], extent[3])) {
    return true;
  }
  if (GVol.geom.flat.contains.linearRingContainsXY(
      flatCoordinates, offset, end, stride, extent[2], extent[1])) {
    return true;
  }
  if (GVol.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 {GVol.Extent} extent Extent.
 * @return {boGVolean} True if the geometry and the extent intersect.
 */
GVol.geom.flat.intersectsextent.linearRings = function(flatCoordinates, offset, ends, stride, extent) {
  if (!GVol.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 (GVol.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 {GVol.Extent} extent Extent.
 * @return {boGVolean} True if the geometry and the extent intersect.
 */
GVol.geom.flat.intersectsextent.linearRingss = function(flatCoordinates, offset, endss, stride, extent) {
  var i, ii;
  for (i = 0, ii = endss.length; i < ii; ++i) {
    var ends = endss[i];
    if (GVol.geom.flat.intersectsextent.linearRings(
        flatCoordinates, offset, ends, stride, extent)) {
      return true;
    }
    offset = ends[ends.length - 1];
  }
  return false;
};

goog.provide('GVol.geom.flat.reverse');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 */
GVol.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('GVol.geom.flat.orient');

goog.require('GVol.geom.flat.reverse');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @return {boGVolean} Is clockwise.
 */
GVol.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 {boGVolean=} opt_right Test for right-hand orientation
 *     (counter-clockwise exterior ring and clockwise interior rings).
 * @return {boGVolean} Rings are correctly oriented.
 */
GVol.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 = GVol.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 {boGVolean=} opt_right Test for right-hand orientation
 *     (counter-clockwise exterior ring and clockwise interior rings).
 * @return {boGVolean} Rings are correctly oriented.
 */
GVol.geom.flat.orient.linearRingssAreOriented = function(flatCoordinates, offset, endss, stride, opt_right) {
  var i, ii;
  for (i = 0, ii = endss.length; i < ii; ++i) {
    if (!GVol.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 fGVollowing 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 {boGVolean=} opt_right FGVollow the right-hand rule for orientation.
 * @return {number} End.
 */
GVol.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 = GVol.geom.flat.orient.linearRingIsClockwise(
        flatCoordinates, offset, end, stride);
    var reverse = i === 0 ?
      (right && isClockwise) || (!right && !isClockwise) :
      (right && !isClockwise) || (!right && isClockwise);
    if (reverse) {
      GVol.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 fGVollowing 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 {boGVolean=} opt_right FGVollow the right-hand rule for orientation.
 * @return {number} End.
 */
GVol.geom.flat.orient.orientLinearRingss = function(flatCoordinates, offset, endss, stride, opt_right) {
  var i, ii;
  for (i = 0, ii = endss.length; i < ii; ++i) {
    offset = GVol.geom.flat.orient.orientLinearRings(
        flatCoordinates, offset, endss[i], stride, opt_right);
  }
  return offset;
};

goog.provide('GVol.geom.PGVolygon');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.extent');
goog.require('GVol.geom.GeometryLayout');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.geom.LinearRing');
goog.require('GVol.geom.Point');
goog.require('GVol.geom.SimpleGeometry');
goog.require('GVol.geom.flat.area');
goog.require('GVol.geom.flat.closest');
goog.require('GVol.geom.flat.contains');
goog.require('GVol.geom.flat.deflate');
goog.require('GVol.geom.flat.inflate');
goog.require('GVol.geom.flat.interiorpoint');
goog.require('GVol.geom.flat.intersectsextent');
goog.require('GVol.geom.flat.orient');
goog.require('GVol.geom.flat.simplify');
goog.require('GVol.math');


/**
 * @classdesc
 * PGVolygon geometry.
 *
 * @constructor
 * @extends {GVol.geom.SimpleGeometry}
 * @param {Array.<Array.<GVol.Coordinate>>} coordinates Array of linear
 *     rings that define the pGVolygon. The first linear ring of the array
 *     defines the outer-boundary or surface of the pGVolygon. Each subsequent
 *     linear ring defines a hGVole in the surface of the pGVolygon. A linear ring
 *     is an array of vertices' coordinates where the first coordinate and the
 *     last are equivalent.
 * @param {GVol.geom.GeometryLayout=} opt_layout Layout.
 * @api
 */
GVol.geom.PGVolygon = function(coordinates, opt_layout) {

  GVol.geom.SimpleGeometry.call(this);

  /**
   * @type {Array.<number>}
   * @private
   */
  this.ends_ = [];

  /**
   * @private
   * @type {number}
   */
  this.flatInteriorPointRevision_ = -1;

  /**
   * @private
   * @type {GVol.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);

};
GVol.inherits(GVol.geom.PGVolygon, GVol.geom.SimpleGeometry);


/**
 * Append the passed linear ring to this pGVolygon.
 * @param {GVol.geom.LinearRing} linearRing Linear ring.
 * @api
 */
GVol.geom.PGVolygon.prototype.appendLinearRing = function(linearRing) {
  if (!this.flatCoordinates) {
    this.flatCoordinates = linearRing.getFlatCoordinates().slice();
  } else {
    GVol.array.extend(this.flatCoordinates, linearRing.getFlatCoordinates());
  }
  this.ends_.push(this.flatCoordinates.length);
  this.changed();
};


/**
 * Make a complete copy of the geometry.
 * @return {!GVol.geom.PGVolygon} Clone.
 * @override
 * @api
 */
GVol.geom.PGVolygon.prototype.clone = function() {
  var pGVolygon = new GVol.geom.PGVolygon(null);
  pGVolygon.setFlatCoordinates(
      this.layout, this.flatCoordinates.slice(), this.ends_.slice());
  return pGVolygon;
};


/**
 * @inheritDoc
 */
GVol.geom.PGVolygon.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
  if (minSquaredDistance <
      GVol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
    return minSquaredDistance;
  }
  if (this.maxDeltaRevision_ != this.getRevision()) {
    this.maxDelta_ = Math.sqrt(GVol.geom.flat.closest.getsMaxSquaredDelta(
        this.flatCoordinates, 0, this.ends_, this.stride, 0));
    this.maxDeltaRevision_ = this.getRevision();
  }
  return GVol.geom.flat.closest.getsClosestPoint(
      this.flatCoordinates, 0, this.ends_, this.stride,
      this.maxDelta_, true, x, y, closestPoint, minSquaredDistance);
};


/**
 * @inheritDoc
 */
GVol.geom.PGVolygon.prototype.containsXY = function(x, y) {
  return GVol.geom.flat.contains.linearRingsContainsXY(
      this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride, x, y);
};


/**
 * Return the area of the pGVolygon on projected plane.
 * @return {number} Area (on projected plane).
 * @api
 */
GVol.geom.PGVolygon.prototype.getArea = function() {
  return GVol.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 pGVolygons.
 *
 * @param {boGVolean=} 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.<GVol.Coordinate>>} Coordinates.
 * @override
 * @api
 */
GVol.geom.PGVolygon.prototype.getCoordinates = function(opt_right) {
  var flatCoordinates;
  if (opt_right !== undefined) {
    flatCoordinates = this.getOrientedFlatCoordinates().slice();
    GVol.geom.flat.orient.orientLinearRings(
        flatCoordinates, 0, this.ends_, this.stride, opt_right);
  } else {
    flatCoordinates = this.flatCoordinates;
  }

  return GVol.geom.flat.inflate.coordinatess(
      flatCoordinates, 0, this.ends_, this.stride);
};


/**
 * @return {Array.<number>} Ends.
 */
GVol.geom.PGVolygon.prototype.getEnds = function() {
  return this.ends_;
};


/**
 * @return {Array.<number>} Interior point.
 */
GVol.geom.PGVolygon.prototype.getFlatInteriorPoint = function() {
  if (this.flatInteriorPointRevision_ != this.getRevision()) {
    var flatCenter = GVol.extent.getCenter(this.getExtent());
    this.flatInteriorPoint_ = GVol.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 pGVolygon.
 * @return {GVol.geom.Point} Interior point.
 * @api
 */
GVol.geom.PGVolygon.prototype.getInteriorPoint = function() {
  return new GVol.geom.Point(this.getFlatInteriorPoint());
};


/**
 * Return the number of rings of the pGVolygon,  this includes the exterior
 * ring and any interior rings.
 *
 * @return {number} Number of rings.
 * @api
 */
GVol.geom.PGVolygon.prototype.getLinearRingCount = function() {
  return this.ends_.length;
};


/**
 * Return the Nth linear ring of the pGVolygon 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 {GVol.geom.LinearRing} Linear ring.
 * @api
 */
GVol.geom.PGVolygon.prototype.getLinearRing = function(index) {
  if (index < 0 || this.ends_.length <= index) {
    return null;
  }
  var linearRing = new GVol.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 pGVolygon.
 * @return {Array.<GVol.geom.LinearRing>} Linear rings.
 * @api
 */
GVol.geom.PGVolygon.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 GVol.geom.LinearRing(null);
    linearRing.setFlatCoordinates(layout, flatCoordinates.slice(offset, end));
    linearRings.push(linearRing);
    offset = end;
  }
  return linearRings;
};


/**
 * @return {Array.<number>} Oriented flat coordinates.
 */
GVol.geom.PGVolygon.prototype.getOrientedFlatCoordinates = function() {
  if (this.orientedRevision_ != this.getRevision()) {
    var flatCoordinates = this.flatCoordinates;
    if (GVol.geom.flat.orient.linearRingsAreOriented(
        flatCoordinates, 0, this.ends_, this.stride)) {
      this.orientedFlatCoordinates_ = flatCoordinates;
    } else {
      this.orientedFlatCoordinates_ = flatCoordinates.slice();
      this.orientedFlatCoordinates_.length =
          GVol.geom.flat.orient.orientLinearRings(
              this.orientedFlatCoordinates_, 0, this.ends_, this.stride);
    }
    this.orientedRevision_ = this.getRevision();
  }
  return this.orientedFlatCoordinates_;
};


/**
 * @inheritDoc
 */
GVol.geom.PGVolygon.prototype.getSimplifiedGeometryInternal = function(squaredTGVolerance) {
  var simplifiedFlatCoordinates = [];
  var simplifiedEnds = [];
  simplifiedFlatCoordinates.length = GVol.geom.flat.simplify.quantizes(
      this.flatCoordinates, 0, this.ends_, this.stride,
      Math.sqrt(squaredTGVolerance),
      simplifiedFlatCoordinates, 0, simplifiedEnds);
  var simplifiedPGVolygon = new GVol.geom.PGVolygon(null);
  simplifiedPGVolygon.setFlatCoordinates(
      GVol.geom.GeometryLayout.XY, simplifiedFlatCoordinates, simplifiedEnds);
  return simplifiedPGVolygon;
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.PGVolygon.prototype.getType = function() {
  return GVol.geom.GeometryType.POLYGON;
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.PGVolygon.prototype.intersectsExtent = function(extent) {
  return GVol.geom.flat.intersectsextent.linearRings(
      this.getOrientedFlatCoordinates(), 0, this.ends_, this.stride, extent);
};


/**
 * Set the coordinates of the pGVolygon.
 * @param {Array.<Array.<GVol.Coordinate>>} coordinates Coordinates.
 * @param {GVol.geom.GeometryLayout=} opt_layout Layout.
 * @override
 * @api
 */
GVol.geom.PGVolygon.prototype.setCoordinates = function(coordinates, opt_layout) {
  if (!coordinates) {
    this.setFlatCoordinates(GVol.geom.GeometryLayout.XY, null, this.ends_);
  } else {
    this.setLayout(opt_layout, coordinates, 2);
    if (!this.flatCoordinates) {
      this.flatCoordinates = [];
    }
    var ends = GVol.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 {GVol.geom.GeometryLayout} layout Layout.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {Array.<number>} ends Ends.
 */
GVol.geom.PGVolygon.prototype.setFlatCoordinates = function(layout, flatCoordinates, ends) {
  this.setFlatCoordinatesInternal(layout, flatCoordinates);
  this.ends_ = ends;
  this.changed();
};


/**
 * Create an approximation of a circle on the surface of a sphere.
 * @param {GVol.Sphere} sphere The sphere.
 * @param {GVol.Coordinate} center Center (`[lon, lat]` in degrees).
 * @param {number} radius The great-circle distance from the center to
 *     the pGVolygon vertices.
 * @param {number=} opt_n Optional number of vertices for the resulting
 *     pGVolygon. Default is `32`.
 * @return {GVol.geom.PGVolygon} The "circular" pGVolygon.
 * @api
 */
GVol.geom.PGVolygon.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) {
    GVol.array.extend(
        flatCoordinates, sphere.offset(center, radius, 2 * Math.PI * i / n));
  }
  flatCoordinates.push(flatCoordinates[0], flatCoordinates[1]);
  var pGVolygon = new GVol.geom.PGVolygon(null);
  pGVolygon.setFlatCoordinates(
      GVol.geom.GeometryLayout.XY, flatCoordinates, [flatCoordinates.length]);
  return pGVolygon;
};


/**
 * Create a pGVolygon from an extent. The layout used is `XY`.
 * @param {GVol.Extent} extent The extent.
 * @return {GVol.geom.PGVolygon} The pGVolygon.
 * @api
 */
GVol.geom.PGVolygon.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 pGVolygon = new GVol.geom.PGVolygon(null);
  pGVolygon.setFlatCoordinates(
      GVol.geom.GeometryLayout.XY, flatCoordinates, [flatCoordinates.length]);
  return pGVolygon;
};


/**
 * Create a regular pGVolygon from a circle.
 * @param {GVol.geom.Circle} circle Circle geometry.
 * @param {number=} opt_sides Number of sides of the pGVolygon. Default is 32.
 * @param {number=} opt_angle Start angle for the first vertex of the pGVolygon in
 *     radians. Default is 0.
 * @return {GVol.geom.PGVolygon} PGVolygon geometry.
 * @api
 */
GVol.geom.PGVolygon.fromCircle = function(circle, opt_sides, opt_angle) {
  var sides = opt_sides ? opt_sides : 32;
  var stride = circle.getStride();
  var layout = circle.getLayout();
  var pGVolygon = new GVol.geom.PGVolygon(null, layout);
  var arrayLength = stride * (sides + 1);
  var flatCoordinates = new Array(arrayLength);
  for (var i = 0; i < arrayLength; i++) {
    flatCoordinates[i] = 0;
  }
  var ends = [flatCoordinates.length];
  pGVolygon.setFlatCoordinates(layout, flatCoordinates, ends);
  GVol.geom.PGVolygon.makeRegular(
      pGVolygon, circle.getCenter(), circle.getRadius(), opt_angle);
  return pGVolygon;
};


/**
 * Modify the coordinates of a pGVolygon to make it a regular pGVolygon.
 * @param {GVol.geom.PGVolygon} pGVolygon PGVolygon geometry.
 * @param {GVol.Coordinate} center Center of the regular pGVolygon.
 * @param {number} radius Radius of the regular pGVolygon.
 * @param {number=} opt_angle Start angle for the first vertex of the pGVolygon in
 *     radians. Default is 0.
 */
GVol.geom.PGVolygon.makeRegular = function(pGVolygon, center, radius, opt_angle) {
  var flatCoordinates = pGVolygon.getFlatCoordinates();
  var layout = pGVolygon.getLayout();
  var stride = pGVolygon.getStride();
  var ends = pGVolygon.getEnds();
  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 + (GVol.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));
  }
  pGVolygon.setFlatCoordinates(layout, flatCoordinates, ends);
};

goog.provide('GVol.View');

goog.require('GVol');
goog.require('GVol.CenterConstraint');
goog.require('GVol.Object');
goog.require('GVol.ResGVolutionConstraint');
goog.require('GVol.RotationConstraint');
goog.require('GVol.ViewHint');
goog.require('GVol.ViewProperty');
goog.require('GVol.array');
goog.require('GVol.asserts');
goog.require('GVol.coordinate');
goog.require('GVol.easing');
goog.require('GVol.extent');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.geom.PGVolygon');
goog.require('GVol.geom.SimpleGeometry');
goog.require('GVol.math');
goog.require('GVol.obj');
goog.require('GVol.proj');
goog.require('GVol.proj.Units');


/**
 * @classdesc
 * An GVol.View object represents a simple 2D view of the map.
 *
 * This is the object to act upon to change the center, resGVolution,
 * and rotation of the map.
 *
 * ### The view states
 *
 * An `GVol.View` is determined by three states: `center`, `resGVolution`,
 * and `rotation`. Each state has a corresponding getter and setter, e.g.
 * `getCenter` and `setCenter` for the `center` state.
 *
 * An `GVol.View` has a `projection`. The projection determines the
 * coordinate system of the center, and its units determine the units of the
 * resGVolution (projection units per pixel). The default projection is
 * Spherical Mercator (EPSG:3857).
 *
 * ### The constraints
 *
 * `setCenter`, `setResGVolution` 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 `GVol.View` object also has a *resGVolution 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 (`constrainResGVolution` and
 * `constrainRotation` and `constrainCenter`).
 *
 * The main users of the constraints are the interactions and the
 * contrGVols. For example, double-clicking on the map changes the view to
 * the "next" resGVolution. And releasing the fingers after pinch-zooming
 * snaps to the closest resGVolution (with an animation).
 *
 * The *resGVolution constraint* snaps to specific resGVolutions. It is
 * determined by the fGVollowing options: `resGVolutions`, `maxResGVolution`,
 * `maxZoom`, and `zoomFactor`. If `resGVolutions` 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 fGVollowing 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 {GVol.Object}
 * @param {GVolx.ViewOptions=} opt_options View options.
 * @api
 */
GVol.View = function(opt_options) {
  GVol.Object.call(this);

  var options = GVol.obj.assign({}, opt_options);

  /**
   * @private
   * @type {Array.<number>}
   */
  this.hints_ = [0, 0];

  /**
   * @private
   * @type {Array.<Array.<GVol.ViewAnimation>>}
   */
  this.animations_ = [];

  /**
   * @private
   * @type {number|undefined}
   */
  this.updateAnimationKey_;

  this.updateAnimations_ = this.updateAnimations_.bind(this);

  /**
   * @private
   * @const
   * @type {GVol.proj.Projection}
   */
  this.projection_ = GVol.proj.createProjection(options.projection, 'EPSG:3857');

  this.applyOptions_(options);
};
GVol.inherits(GVol.View, GVol.Object);


/**
 * Set up the view with the given options.
 * @param {GVolx.ViewOptions} options View options.
 */
GVol.View.prototype.applyOptions_ = function(options) {

  /**
   * @type {Object.<string, *>}
   */
  var properties = {};
  properties[GVol.ViewProperty.CENTER] = options.center !== undefined ?
    options.center : null;

  var resGVolutionConstraintInfo = GVol.View.createResGVolutionConstraint_(
      options);

  /**
   * @private
   * @type {number}
   */
  this.maxResGVolution_ = resGVolutionConstraintInfo.maxResGVolution;

  /**
   * @private
   * @type {number}
   */
  this.minResGVolution_ = resGVolutionConstraintInfo.minResGVolution;

  /**
   * @private
   * @type {number}
   */
  this.zoomFactor_ = resGVolutionConstraintInfo.zoomFactor;

  /**
   * @private
   * @type {Array.<number>|undefined}
   */
  this.resGVolutions_ = options.resGVolutions;

  /**
   * @private
   * @type {number}
   */
  this.minZoom_ = resGVolutionConstraintInfo.minZoom;

  var centerConstraint = GVol.View.createCenterConstraint_(options);
  var resGVolutionConstraint = resGVolutionConstraintInfo.constraint;
  var rotationConstraint = GVol.View.createRotationConstraint_(options);

  /**
   * @private
   * @type {GVol.Constraints}
   */
  this.constraints_ = {
    center: centerConstraint,
    resGVolution: resGVolutionConstraint,
    rotation: rotationConstraint
  };

  if (options.resGVolution !== undefined) {
    properties[GVol.ViewProperty.RESOLUTION] = options.resGVolution;
  } else if (options.zoom !== undefined) {
    properties[GVol.ViewProperty.RESOLUTION] = this.constrainResGVolution(
        this.maxResGVolution_, options.zoom - this.minZoom_);
  }
  properties[GVol.ViewProperty.ROTATION] =
      options.rotation !== undefined ? options.rotation : 0;
  this.setProperties(properties);

  /**
   * @private
   * @type {GVolx.ViewOptions}
   */
  this.options_ = options;

};

/**
 * Get an updated version of the view options used to construct the view.  The
 * current resGVolution (or zoom), center, and rotation are applied to any stored
 * options.  The provided options can be uesd to apply new min/max zoom or
 * resGVolution limits.
 * @param {GVolx.ViewOptions} newOptions New options to be applied.
 * @return {GVolx.ViewOptions} New options updated with the current view state.
 */
GVol.View.prototype.getUpdatedOptions_ = function(newOptions) {
  var options = GVol.obj.assign({}, this.options_);

  // preserve resGVolution (or zoom)
  if (options.resGVolution !== undefined) {
    options.resGVolution = this.getResGVolution();
  } else {
    options.zoom = this.getZoom();
  }

  // preserve center
  options.center = this.getCenter();

  // preserve rotation
  options.rotation = this.getRotation();

  return GVol.obj.assign({}, options, newOptions);
};


/**
 * Animate the view.  The view's center, zoom (or resGVolution), and rotation
 * can be animated for smooth transitions between view states.  For example,
 * to animate the view to a new zoom level:
 *
 *     view.animate({zoom: view.getZoom() + 1});
 *
 * By default, the animation lasts one second and uses in-and-out easing.  You
 * can customize this behavior by including `duration` (in milliseconds) and
 * `easing` options (see {@link GVol.easing}).
 *
 * To chain together multiple animations, call the method with multiple
 * animation objects.  For example, to first zoom and then pan:
 *
 *     view.animate({zoom: 10}, {center: [0, 0]});
 *
 * If you provide a function as the last argument to the animate method, it
 * will get called at the end of an animation series.  The callback will be
 * called with `true` if the animation series completed on its own or `false`
 * if it was cancelled.
 *
 * Animations are cancelled by user interactions (e.g. dragging the map) or by
 * calling `view.setCenter()`, `view.setResGVolution()`, or `view.setRotation()`
 * (or another method that calls one of these).
 *
 * @param {...(GVolx.AnimationOptions|function(boGVolean))} var_args Animation
 *     options.  Multiple animations can be run in series by passing multiple
 *     options objects.  To run multiple animations in parallel, call the method
 *     multiple times.  An optional callback can be provided as a final
 *     argument.  The callback will be called with a boGVolean indicating whether
 *     the animation completed without being cancelled.
 * @api
 */
GVol.View.prototype.animate = function(var_args) {
  var start = Date.now();
  var center = this.getCenter().slice();
  var resGVolution = this.getResGVolution();
  var rotation = this.getRotation();
  var animationCount = arguments.length;
  var callback;
  if (animationCount > 1 && typeof arguments[animationCount - 1] === 'function') {
    callback = arguments[animationCount - 1];
    --animationCount;
  }
  var series = [];
  for (var i = 0; i < animationCount; ++i) {
    var options = /** @type {GVolx.AnimationOptions} */ (arguments[i]);

    var animation = /** @type {GVol.ViewAnimation} */ ({
      start: start,
      complete: false,
      anchor: options.anchor,
      duration: options.duration !== undefined ? options.duration : 1000,
      easing: options.easing || GVol.easing.inAndOut
    });

    if (options.center) {
      animation.sourceCenter = center;
      animation.targetCenter = options.center;
      center = animation.targetCenter;
    }

    if (options.zoom !== undefined) {
      animation.sourceResGVolution = resGVolution;
      animation.targetResGVolution = this.constrainResGVolution(
          this.maxResGVolution_, options.zoom - this.minZoom_, 0);
      resGVolution = animation.targetResGVolution;
    } else if (options.resGVolution) {
      animation.sourceResGVolution = resGVolution;
      animation.targetResGVolution = options.resGVolution;
      resGVolution = animation.targetResGVolution;
    }

    if (options.rotation !== undefined) {
      animation.sourceRotation = rotation;
      var delta = GVol.math.modulo(options.rotation - rotation + Math.PI, 2 * Math.PI) - Math.PI;
      animation.targetRotation = rotation + delta;
      rotation = animation.targetRotation;
    }

    animation.callback = callback;

    // check if animation is a no-op
    if (GVol.View.isNoopAnimation(animation)) {
      animation.complete = true;
      // we still push it onto the series for callback handling
    } else {
      start += animation.duration;
    }
    series.push(animation);
  }
  this.animations_.push(series);
  this.setHint(GVol.ViewHint.ANIMATING, 1);
  this.updateAnimations_();
};


/**
 * Determine if the view is being animated.
 * @return {boGVolean} The view is being animated.
 * @api
 */
GVol.View.prototype.getAnimating = function() {
  return this.getHints()[GVol.ViewHint.ANIMATING] > 0;
};


/**
 * Determine if the user is interacting with the view, such as panning or zooming.
 * @return {boGVolean} The view is being interacted with.
 * @api
 */
GVol.View.prototype.getInteracting = function() {
  return this.getHints()[GVol.ViewHint.INTERACTING] > 0;
};


/**
 * Cancel any ongoing animations.
 * @api
 */
GVol.View.prototype.cancelAnimations = function() {
  this.setHint(GVol.ViewHint.ANIMATING, -this.getHints()[GVol.ViewHint.ANIMATING]);
  for (var i = 0, ii = this.animations_.length; i < ii; ++i) {
    var series = this.animations_[i];
    if (series[0].callback) {
      series[0].callback(false);
    }
  }
  this.animations_.length = 0;
};

/**
 * Update all animations.
 */
GVol.View.prototype.updateAnimations_ = function() {
  if (this.updateAnimationKey_ !== undefined) {
    cancelAnimationFrame(this.updateAnimationKey_);
    this.updateAnimationKey_ = undefined;
  }
  if (!this.getAnimating()) {
    return;
  }
  var now = Date.now();
  var more = false;
  for (var i = this.animations_.length - 1; i >= 0; --i) {
    var series = this.animations_[i];
    var seriesComplete = true;
    for (var j = 0, jj = series.length; j < jj; ++j) {
      var animation = series[j];
      if (animation.complete) {
        continue;
      }
      var elapsed = now - animation.start;
      var fraction = animation.duration > 0 ? elapsed / animation.duration : 1;
      if (fraction >= 1) {
        animation.complete = true;
        fraction = 1;
      } else {
        seriesComplete = false;
      }
      var progress = animation.easing(fraction);
      if (animation.sourceCenter) {
        var x0 = animation.sourceCenter[0];
        var y0 = animation.sourceCenter[1];
        var x1 = animation.targetCenter[0];
        var y1 = animation.targetCenter[1];
        var x = x0 + progress * (x1 - x0);
        var y = y0 + progress * (y1 - y0);
        this.set(GVol.ViewProperty.CENTER, [x, y]);
      }
      if (animation.sourceResGVolution && animation.targetResGVolution) {
        var resGVolution = progress === 1 ?
          animation.targetResGVolution :
          animation.sourceResGVolution + progress * (animation.targetResGVolution - animation.sourceResGVolution);
        if (animation.anchor) {
          this.set(GVol.ViewProperty.CENTER,
              this.calculateCenterZoom(resGVolution, animation.anchor));
        }
        this.set(GVol.ViewProperty.RESOLUTION, resGVolution);
      }
      if (animation.sourceRotation !== undefined && animation.targetRotation !== undefined) {
        var rotation = progress === 1 ?
          GVol.math.modulo(animation.targetRotation + Math.PI, 2 * Math.PI) - Math.PI :
          animation.sourceRotation + progress * (animation.targetRotation - animation.sourceRotation);
        if (animation.anchor) {
          this.set(GVol.ViewProperty.CENTER,
              this.calculateCenterRotate(rotation, animation.anchor));
        }
        this.set(GVol.ViewProperty.ROTATION, rotation);
      }
      more = true;
      if (!animation.complete) {
        break;
      }
    }
    if (seriesComplete) {
      this.animations_[i] = null;
      this.setHint(GVol.ViewHint.ANIMATING, -1);
      var callback = series[0].callback;
      if (callback) {
        callback(true);
      }
    }
  }
  // prune completed series
  this.animations_ = this.animations_.filter(BoGVolean);
  if (more && this.updateAnimationKey_ === undefined) {
    this.updateAnimationKey_ = requestAnimationFrame(this.updateAnimations_);
  }
};

/**
 * @param {number} rotation Target rotation.
 * @param {GVol.Coordinate} anchor Rotation anchor.
 * @return {GVol.Coordinate|undefined} Center for rotation and anchor.
 */
GVol.View.prototype.calculateCenterRotate = function(rotation, anchor) {
  var center;
  var currentCenter = this.getCenter();
  if (currentCenter !== undefined) {
    center = [currentCenter[0] - anchor[0], currentCenter[1] - anchor[1]];
    GVol.coordinate.rotate(center, rotation - this.getRotation());
    GVol.coordinate.add(center, anchor);
  }
  return center;
};


/**
 * @param {number} resGVolution Target resGVolution.
 * @param {GVol.Coordinate} anchor Zoom anchor.
 * @return {GVol.Coordinate|undefined} Center for resGVolution and anchor.
 */
GVol.View.prototype.calculateCenterZoom = function(resGVolution, anchor) {
  var center;
  var currentCenter = this.getCenter();
  var currentResGVolution = this.getResGVolution();
  if (currentCenter !== undefined && currentResGVolution !== undefined) {
    var x = anchor[0] -
        resGVolution * (anchor[0] - currentCenter[0]) / currentResGVolution;
    var y = anchor[1] -
        resGVolution * (anchor[1] - currentCenter[1]) / currentResGVolution;
    center = [x, y];
  }
  return center;
};


/**
 * @private
 * @return {GVol.Size} Viewport size or `[100, 100]` when no viewport is found.
 */
GVol.View.prototype.getSizeFromViewport_ = function() {
  var size = [100, 100];
  var selector = '.GVol-viewport[data-view="' + GVol.getUid(this) + '"]';
  var element = document.querySelector(selector);
  if (element) {
    var metrics = getComputedStyle(element);
    size[0] = parseInt(metrics.width, 10);
    size[1] = parseInt(metrics.height, 10);
  }
  return size;
};


/**
 * Get the constrained center of this view.
 * @param {GVol.Coordinate|undefined} center Center.
 * @return {GVol.Coordinate|undefined} Constrained center.
 * @api
 */
GVol.View.prototype.constrainCenter = function(center) {
  return this.constraints_.center(center);
};


/**
 * Get the constrained resGVolution of this view.
 * @param {number|undefined} resGVolution ResGVolution.
 * @param {number=} opt_delta Delta. Default is `0`.
 * @param {number=} opt_direction Direction. Default is `0`.
 * @return {number|undefined} Constrained resGVolution.
 * @api
 */
GVol.View.prototype.constrainResGVolution = function(
    resGVolution, opt_delta, opt_direction) {
  var delta = opt_delta || 0;
  var direction = opt_direction || 0;
  return this.constraints_.resGVolution(resGVolution, 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
 */
GVol.View.prototype.constrainRotation = function(rotation, opt_delta) {
  var delta = opt_delta || 0;
  return this.constraints_.rotation(rotation, delta);
};


/**
 * Get the view center.
 * @return {GVol.Coordinate|undefined} The center of the view.
 * @observable
 * @api
 */
GVol.View.prototype.getCenter = function() {
  return /** @type {GVol.Coordinate|undefined} */ (
    this.get(GVol.ViewProperty.CENTER));
};


/**
 * @return {GVol.Constraints} Constraints.
 */
GVol.View.prototype.getConstraints = function() {
  return this.constraints_;
};


/**
 * @param {Array.<number>=} opt_hints Destination array.
 * @return {Array.<number>} Hint.
 */
GVol.View.prototype.getHints = function(opt_hints) {
  if (opt_hints !== undefined) {
    opt_hints[0] = this.hints_[0];
    opt_hints[1] = this.hints_[1];
    return opt_hints;
  } else {
    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 {GVol.Size=} opt_size Box pixel size. If not provided, the size of the
 * first map that uses this view will be used.
 * @return {GVol.Extent} Extent.
 * @api
 */
GVol.View.prototype.calculateExtent = function(opt_size) {
  var size = opt_size || this.getSizeFromViewport_();
  var center = /** @type {!GVol.Coordinate} */ (this.getCenter());
  GVol.asserts.assert(center, 1); // The view center is not defined
  var resGVolution = /** @type {!number} */ (this.getResGVolution());
  GVol.asserts.assert(resGVolution !== undefined, 2); // The view resGVolution is not defined
  var rotation = /** @type {!number} */ (this.getRotation());
  GVol.asserts.assert(rotation !== undefined, 3); // The view rotation is not defined

  return GVol.extent.getForViewAndSize(center, resGVolution, rotation, size);
};


/**
 * Get the maximum resGVolution of the view.
 * @return {number} The maximum resGVolution of the view.
 * @api
 */
GVol.View.prototype.getMaxResGVolution = function() {
  return this.maxResGVolution_;
};


/**
 * Get the minimum resGVolution of the view.
 * @return {number} The minimum resGVolution of the view.
 * @api
 */
GVol.View.prototype.getMinResGVolution = function() {
  return this.minResGVolution_;
};


/**
 * Get the maximum zoom level for the view.
 * @return {number} The maximum zoom level.
 * @api
 */
GVol.View.prototype.getMaxZoom = function() {
  return /** @type {number} */ (this.getZoomForResGVolution(this.minResGVolution_));
};


/**
 * Set a new maximum zoom level for the view.
 * @param {number} zoom The maximum zoom level.
 * @api
 */
GVol.View.prototype.setMaxZoom = function(zoom) {
  this.applyOptions_(this.getUpdatedOptions_({maxZoom: zoom}));
};


/**
 * Get the minimum zoom level for the view.
 * @return {number} The minimum zoom level.
 * @api
 */
GVol.View.prototype.getMinZoom = function() {
  return /** @type {number} */ (this.getZoomForResGVolution(this.maxResGVolution_));
};


/**
 * Set a new minimum zoom level for the view.
 * @param {number} zoom The minimum zoom level.
 * @api
 */
GVol.View.prototype.setMinZoom = function(zoom) {
  this.applyOptions_(this.getUpdatedOptions_({minZoom: zoom}));
};


/**
 * Get the view projection.
 * @return {GVol.proj.Projection} The projection of the view.
 * @api
 */
GVol.View.prototype.getProjection = function() {
  return this.projection_;
};


/**
 * Get the view resGVolution.
 * @return {number|undefined} The resGVolution of the view.
 * @observable
 * @api
 */
GVol.View.prototype.getResGVolution = function() {
  return /** @type {number|undefined} */ (
    this.get(GVol.ViewProperty.RESOLUTION));
};


/**
 * Get the resGVolutions for the view. This returns the array of resGVolutions
 * passed to the constructor of the {GVol.View}, or undefined if none were given.
 * @return {Array.<number>|undefined} The resGVolutions of the view.
 * @api
 */
GVol.View.prototype.getResGVolutions = function() {
  return this.resGVolutions_;
};


/**
 * Get the resGVolution for a provided extent (in map units) and size (in pixels).
 * @param {GVol.Extent} extent Extent.
 * @param {GVol.Size=} opt_size Box pixel size.
 * @return {number} The resGVolution at which the provided extent will render at
 *     the given size.
 * @api
 */
GVol.View.prototype.getResGVolutionForExtent = function(extent, opt_size) {
  var size = opt_size || this.getSizeFromViewport_();
  var xResGVolution = GVol.extent.getWidth(extent) / size[0];
  var yResGVolution = GVol.extent.getHeight(extent) / size[1];
  return Math.max(xResGVolution, yResGVolution);
};


/**
 * Return a function that returns a value between 0 and 1 for a
 * resGVolution. Exponential scaling is assumed.
 * @param {number=} opt_power Power.
 * @return {function(number): number} ResGVolution for value function.
 */
GVol.View.prototype.getResGVolutionForValueFunction = function(opt_power) {
  var power = opt_power || 2;
  var maxResGVolution = this.maxResGVolution_;
  var minResGVolution = this.minResGVolution_;
  var max = Math.log(maxResGVolution / minResGVolution) / Math.log(power);
  return (
    /**
     * @param {number} value Value.
     * @return {number} ResGVolution.
     */
    function(value) {
      var resGVolution = maxResGVolution / Math.pow(power, value * max);
      return resGVolution;
    });
};


/**
 * Get the view rotation.
 * @return {number} The rotation of the view in radians.
 * @observable
 * @api
 */
GVol.View.prototype.getRotation = function() {
  return /** @type {number} */ (this.get(GVol.ViewProperty.ROTATION));
};


/**
 * Return a function that returns a resGVolution for a value between
 * 0 and 1. Exponential scaling is assumed.
 * @param {number=} opt_power Power.
 * @return {function(number): number} Value for resGVolution function.
 */
GVol.View.prototype.getValueForResGVolutionFunction = function(opt_power) {
  var power = opt_power || 2;
  var maxResGVolution = this.maxResGVolution_;
  var minResGVolution = this.minResGVolution_;
  var max = Math.log(maxResGVolution / minResGVolution) / Math.log(power);
  return (
    /**
     * @param {number} resGVolution ResGVolution.
     * @return {number} Value.
     */
    function(resGVolution) {
      var value =
            (Math.log(maxResGVolution / resGVolution) / Math.log(power)) / max;
      return value;
    });
};


/**
 * @return {GVolx.ViewState} View state.
 */
GVol.View.prototype.getState = function() {
  var center = /** @type {GVol.Coordinate} */ (this.getCenter());
  var projection = this.getProjection();
  var resGVolution = /** @type {number} */ (this.getResGVolution());
  var rotation = this.getRotation();
  return /** @type {GVolx.ViewState} */ ({
    center: center.slice(),
    projection: projection !== undefined ? projection : null,
    resGVolution: resGVolution,
    rotation: rotation
  });
};


/**
 * Get the current zoom level. Return undefined if the current
 * resGVolution is undefined or not within the "resGVolution constraints".
 * @return {number|undefined} Zoom.
 * @api
 */
GVol.View.prototype.getZoom = function() {
  var zoom;
  var resGVolution = this.getResGVolution();
  if (resGVolution !== undefined) {
    zoom = this.getZoomForResGVolution(resGVolution);
  }
  return zoom;
};


/**
 * Get the zoom level for a resGVolution.
 * @param {number} resGVolution The resGVolution.
 * @return {number|undefined} The zoom level for the provided resGVolution.
 * @api
 */
GVol.View.prototype.getZoomForResGVolution = function(resGVolution) {
  var zoom;
  if (resGVolution >= this.minResGVolution_ && resGVolution <= this.maxResGVolution_) {
    var offset = this.minZoom_ || 0;
    var max, zoomFactor;
    if (this.resGVolutions_) {
      var nearest = GVol.array.linearFindNearest(this.resGVolutions_, resGVolution, 1);
      offset += nearest;
      if (nearest == this.resGVolutions_.length - 1) {
        return offset;
      }
      max = this.resGVolutions_[nearest];
      zoomFactor = max / this.resGVolutions_[nearest + 1];
    } else {
      max = this.maxResGVolution_;
      zoomFactor = this.zoomFactor_;
    }
    zoom = offset + Math.log(max / resGVolution) / Math.log(zoomFactor);
  }
  return zoom;
};


/**
 * Get the resGVolution for a zoom level.
 * @param {number} zoom Zoom level.
 * @return {number} The view resGVolution for the provided zoom level.
 * @api
 */
GVol.View.prototype.getResGVolutionForZoom = function(zoom) {
  return /** @type {number} */ (this.constrainResGVolution(
      this.maxResGVolution_, zoom - this.minZoom_, 0));
};


/**
 * 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 {GVol.geom.SimpleGeometry|GVol.Extent} geometryOrExtent The geometry or
 *     extent to fit the view to.
 * @param {GVolx.view.FitOptions=} opt_options Options.
 * @api
 */
GVol.View.prototype.fit = function(geometryOrExtent, opt_options) {
  var options = opt_options || {};
  var size = options.size;
  if (!size) {
    size = this.getSizeFromViewport_();
  }
  /** @type {GVol.geom.SimpleGeometry} */
  var geometry;
  if (!(geometryOrExtent instanceof GVol.geom.SimpleGeometry)) {
    GVol.asserts.assert(Array.isArray(geometryOrExtent),
        24); // Invalid extent or geometry provided as `geometry`
    GVol.asserts.assert(!GVol.extent.isEmpty(geometryOrExtent),
        25); // Cannot fit empty extent provided as `geometry`
    geometry = GVol.geom.PGVolygon.fromExtent(geometryOrExtent);
  } else if (geometryOrExtent.getType() === GVol.geom.GeometryType.CIRCLE) {
    geometryOrExtent = geometryOrExtent.getExtent();
    geometry = GVol.geom.PGVolygon.fromExtent(geometryOrExtent);
    geometry.rotate(this.getRotation(), GVol.extent.getCenter(geometryOrExtent));
  } else {
    geometry = geometryOrExtent;
  }

  var padding = options.padding !== undefined ? options.padding : [0, 0, 0, 0];
  var constrainResGVolution = options.constrainResGVolution !== undefined ?
    options.constrainResGVolution : true;
  var nearest = options.nearest !== undefined ? options.nearest : false;
  var minResGVolution;
  if (options.minResGVolution !== undefined) {
    minResGVolution = options.minResGVolution;
  } else if (options.maxZoom !== undefined) {
    minResGVolution = this.constrainResGVolution(
        this.maxResGVolution_, options.maxZoom - this.minZoom_, 0);
  } else {
    minResGVolution = 0;
  }
  var coords = geometry.getFlatCoordinates();

  // calculate rotated extent
  var rotation = this.getRotation();
  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 resGVolution
  var resGVolution = this.getResGVolutionForExtent(
      [minRotX, minRotY, maxRotX, maxRotY],
      [size[0] - padding[1] - padding[3], size[1] - padding[0] - padding[2]]);
  resGVolution = isNaN(resGVolution) ? minResGVolution :
    Math.max(resGVolution, minResGVolution);
  if (constrainResGVolution) {
    var constrainedResGVolution = this.constrainResGVolution(resGVolution, 0, 0);
    if (!nearest && constrainedResGVolution < resGVolution) {
      constrainedResGVolution = this.constrainResGVolution(
          constrainedResGVolution, -1, 0);
    }
    resGVolution = constrainedResGVolution;
  }

  // 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 * resGVolution;
  centerRotY += (padding[0] - padding[2]) / 2 * resGVolution;
  var centerX = centerRotX * cosAngle - centerRotY * sinAngle;
  var centerY = centerRotY * cosAngle + centerRotX * sinAngle;
  var center = [centerX, centerY];
  var callback = options.callback ? options.callback : GVol.nullFunction;

  if (options.duration !== undefined) {
    this.animate({
      resGVolution: resGVolution,
      center: center,
      duration: options.duration,
      easing: options.easing
    }, callback);
  } else {
    this.setResGVolution(resGVolution);
    this.setCenter(center);
    setTimeout(callback.bind(undefined, true), 0);
  }
};


/**
 * Center on coordinate and view position.
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @param {GVol.Size} size Box pixel size.
 * @param {GVol.Pixel} position Position on the view to center on.
 * @api
 */
GVol.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 resGVolution = this.getResGVolution();
  rotX += (size[0] / 2 - position[0]) * resGVolution;
  rotY += (position[1] - size[1] / 2) * resGVolution;

  // 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 {boGVolean} Is defined.
 */
GVol.View.prototype.isDef = function() {
  return !!this.getCenter() && this.getResGVolution() !== undefined;
};


/**
 * Rotate the view around a given coordinate.
 * @param {number} rotation New rotation value for the view.
 * @param {GVol.Coordinate=} opt_anchor The rotation center.
 * @api
 */
GVol.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 {GVol.Coordinate|undefined} center The center of the view.
 * @observable
 * @api
 */
GVol.View.prototype.setCenter = function(center) {
  this.set(GVol.ViewProperty.CENTER, center);
  if (this.getAnimating()) {
    this.cancelAnimations();
  }
};


/**
 * @param {GVol.ViewHint} hint Hint.
 * @param {number} delta Delta.
 * @return {number} New value.
 */
GVol.View.prototype.setHint = function(hint, delta) {
  this.hints_[hint] += delta;
  this.changed();
  return this.hints_[hint];
};


/**
 * Set the resGVolution for this view.
 * @param {number|undefined} resGVolution The resGVolution of the view.
 * @observable
 * @api
 */
GVol.View.prototype.setResGVolution = function(resGVolution) {
  this.set(GVol.ViewProperty.RESOLUTION, resGVolution);
  if (this.getAnimating()) {
    this.cancelAnimations();
  }
};


/**
 * Set the rotation for this view.
 * @param {number} rotation The rotation of the view in radians.
 * @observable
 * @api
 */
GVol.View.prototype.setRotation = function(rotation) {
  this.set(GVol.ViewProperty.ROTATION, rotation);
  if (this.getAnimating()) {
    this.cancelAnimations();
  }
};


/**
 * Zoom to a specific zoom level.
 * @param {number} zoom Zoom level.
 * @api
 */
GVol.View.prototype.setZoom = function(zoom) {
  this.setResGVolution(this.getResGVolutionForZoom(zoom));
};


/**
 * @param {GVolx.ViewOptions} options View options.
 * @private
 * @return {GVol.CenterConstraintType} The constraint.
 */
GVol.View.createCenterConstraint_ = function(options) {
  if (options.extent !== undefined) {
    return GVol.CenterConstraint.createExtent(options.extent);
  } else {
    return GVol.CenterConstraint.none;
  }
};


/**
 * @private
 * @param {GVolx.ViewOptions} options View options.
 * @return {{constraint: GVol.ResGVolutionConstraintType, maxResGVolution: number,
 *     minResGVolution: number, zoomFactor: number}} The constraint.
 */
GVol.View.createResGVolutionConstraint_ = function(options) {
  var resGVolutionConstraint;
  var maxResGVolution;
  var minResGVolution;

  // TODO: move these to be GVol constants
  // see https://github.com/openlayers/openlayers/issues/2076
  var defaultMaxZoom = 28;
  var defaultZoomFactor = 2;

  var minZoom = options.minZoom !== undefined ?
    options.minZoom : GVol.DEFAULT_MIN_ZOOM;

  var maxZoom = options.maxZoom !== undefined ?
    options.maxZoom : defaultMaxZoom;

  var zoomFactor = options.zoomFactor !== undefined ?
    options.zoomFactor : defaultZoomFactor;

  if (options.resGVolutions !== undefined) {
    var resGVolutions = options.resGVolutions;
    maxResGVolution = resGVolutions[0];
    minResGVolution = resGVolutions[resGVolutions.length - 1];
    resGVolutionConstraint = GVol.ResGVolutionConstraint.createSnapToResGVolutions(
        resGVolutions);
  } else {
    // calculate the default min and max resGVolution
    var projection = GVol.proj.createProjection(options.projection, 'EPSG:3857');
    var extent = projection.getExtent();
    var size = !extent ?
      // use an extent that can fit the whGVole world if need be
      360 * GVol.proj.METERS_PER_UNIT[GVol.proj.Units.DEGREES] /
            projection.getMetersPerUnit() :
      Math.max(GVol.extent.getWidth(extent), GVol.extent.getHeight(extent));

    var defaultMaxResGVolution = size / GVol.DEFAULT_TILE_SIZE / Math.pow(
        defaultZoomFactor, GVol.DEFAULT_MIN_ZOOM);

    var defaultMinResGVolution = defaultMaxResGVolution / Math.pow(
        defaultZoomFactor, defaultMaxZoom - GVol.DEFAULT_MIN_ZOOM);

    // user provided maxResGVolution takes precedence
    maxResGVolution = options.maxResGVolution;
    if (maxResGVolution !== undefined) {
      minZoom = 0;
    } else {
      maxResGVolution = defaultMaxResGVolution / Math.pow(zoomFactor, minZoom);
    }

    // user provided minResGVolution takes precedence
    minResGVolution = options.minResGVolution;
    if (minResGVolution === undefined) {
      if (options.maxZoom !== undefined) {
        if (options.maxResGVolution !== undefined) {
          minResGVolution = maxResGVolution / Math.pow(zoomFactor, maxZoom);
        } else {
          minResGVolution = defaultMaxResGVolution / Math.pow(zoomFactor, maxZoom);
        }
      } else {
        minResGVolution = defaultMinResGVolution;
      }
    }

    // given discrete zoom levels, minResGVolution may be different than provided
    maxZoom = minZoom + Math.floor(
        Math.log(maxResGVolution / minResGVolution) / Math.log(zoomFactor));
    minResGVolution = maxResGVolution / Math.pow(zoomFactor, maxZoom - minZoom);

    resGVolutionConstraint = GVol.ResGVolutionConstraint.createSnapToPower(
        zoomFactor, maxResGVolution, maxZoom - minZoom);
  }
  return {constraint: resGVolutionConstraint, maxResGVolution: maxResGVolution,
    minResGVolution: minResGVolution, minZoom: minZoom, zoomFactor: zoomFactor};
};


/**
 * @private
 * @param {GVolx.ViewOptions} options View options.
 * @return {GVol.RotationConstraintType} Rotation constraint.
 */
GVol.View.createRotationConstraint_ = function(options) {
  var enableRotation = options.enableRotation !== undefined ?
    options.enableRotation : true;
  if (enableRotation) {
    var constrainRotation = options.constrainRotation;
    if (constrainRotation === undefined || constrainRotation === true) {
      return GVol.RotationConstraint.createSnapToZero();
    } else if (constrainRotation === false) {
      return GVol.RotationConstraint.none;
    } else if (typeof constrainRotation === 'number') {
      return GVol.RotationConstraint.createSnapToN(constrainRotation);
    } else {
      return GVol.RotationConstraint.none;
    }
  } else {
    return GVol.RotationConstraint.disable;
  }
};


/**
 * Determine if an animation invGVolves no view change.
 * @param {GVol.ViewAnimation} animation The animation.
 * @return {boGVolean} The animation invGVolves no view change.
 */
GVol.View.isNoopAnimation = function(animation) {
  if (animation.sourceCenter && animation.targetCenter) {
    if (!GVol.coordinate.equals(animation.sourceCenter, animation.targetCenter)) {
      return false;
    }
  }
  if (animation.sourceResGVolution !== animation.targetResGVolution) {
    return false;
  }
  if (animation.sourceRotation !== animation.targetRotation) {
    return false;
  }
  return true;
};

goog.provide('GVol.Kinetic');


/**
 * @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
 */
GVol.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
 */
GVol.Kinetic.prototype.begin = function() {
  this.points_.length = 0;
  this.angle_ = 0;
  this.initialVelocity_ = 0;
};


/**
 * @param {number} x X.
 * @param {number} y Y.
 */
GVol.Kinetic.prototype.update = function(x, y) {
  this.points_.push(x, y, Date.now());
};


/**
 * @return {boGVolean} Whether we should do kinetic animation.
 */
GVol.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 GVold, 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];
  // we don't want a duration of 0 (divide by zero)
  // we also make sure the user panned for a duration of at least one frame
  // (1/60s) to compute sane displacement values
  if (duration < 1000 / 60) {
    return false;
  }

  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_;
};


/**
 * @return {number} Total distance travelled (pixels).
 */
GVol.Kinetic.prototype.getDistance = function() {
  return (this.minVelocity_ - this.initialVelocity_) / this.decay_;
};


/**
 * @return {number} Angle of the kinetic panning animation (radians).
 */
GVol.Kinetic.prototype.getAngle = function() {
  return this.angle_;
};

goog.provide('GVol.interaction.Property');

/**
 * @enum {string}
 */
GVol.interaction.Property = {
  ACTIVE: 'active'
};

// FIXME factor out key precondition (shift et. al)

goog.provide('GVol.interaction.Interaction');

goog.require('GVol');
goog.require('GVol.Object');
goog.require('GVol.easing');
goog.require('GVol.interaction.Property');


/**
 * @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 contrGVols,
 * but are not associated with a DOM element.
 * For example, {@link GVol.interaction.KeyboardZoom} is functionally the same as
 * {@link GVol.contrGVol.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 {GVolx.interaction.InteractionOptions} options Options.
 * @extends {GVol.Object}
 * @api
 */
GVol.interaction.Interaction = function(options) {

  GVol.Object.call(this);

  /**
   * @private
   * @type {GVol.Map}
   */
  this.map_ = null;

  this.setActive(true);

  /**
   * @type {function(GVol.MapBrowserEvent):boGVolean}
   */
  this.handleEvent = options.handleEvent;

};
GVol.inherits(GVol.interaction.Interaction, GVol.Object);


/**
 * Return whether the interaction is currently active.
 * @return {boGVolean} `true` if the interaction is active, `false` otherwise.
 * @observable
 * @api
 */
GVol.interaction.Interaction.prototype.getActive = function() {
  return /** @type {boGVolean} */ (
    this.get(GVol.interaction.Property.ACTIVE));
};


/**
 * Get the map associated with this interaction.
 * @return {GVol.Map} Map.
 * @api
 */
GVol.interaction.Interaction.prototype.getMap = function() {
  return this.map_;
};


/**
 * Activate or deactivate the interaction.
 * @param {boGVolean} active Active.
 * @observable
 * @api
 */
GVol.interaction.Interaction.prototype.setActive = function(active) {
  this.set(GVol.interaction.Property.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 {GVol.Map} map Map.
 */
GVol.interaction.Interaction.prototype.setMap = function(map) {
  this.map_ = map;
};


/**
 * @param {GVol.View} view View.
 * @param {GVol.Coordinate} delta Delta.
 * @param {number=} opt_duration Duration.
 */
GVol.interaction.Interaction.pan = function(view, delta, opt_duration) {
  var currentCenter = view.getCenter();
  if (currentCenter) {
    var center = view.constrainCenter(
        [currentCenter[0] + delta[0], currentCenter[1] + delta[1]]);
    if (opt_duration) {
      view.animate({
        duration: opt_duration,
        easing: GVol.easing.linear,
        center: center
      });
    } else {
      view.setCenter(center);
    }
  }
};


/**
 * @param {GVol.View} view View.
 * @param {number|undefined} rotation Rotation.
 * @param {GVol.Coordinate=} opt_anchor Anchor coordinate.
 * @param {number=} opt_duration Duration.
 */
GVol.interaction.Interaction.rotate = function(view, rotation, opt_anchor, opt_duration) {
  rotation = view.constrainRotation(rotation, 0);
  GVol.interaction.Interaction.rotateWithoutConstraints(
      view, rotation, opt_anchor, opt_duration);
};


/**
 * @param {GVol.View} view View.
 * @param {number|undefined} rotation Rotation.
 * @param {GVol.Coordinate=} opt_anchor Anchor coordinate.
 * @param {number=} opt_duration Duration.
 */
GVol.interaction.Interaction.rotateWithoutConstraints = function(view, rotation, opt_anchor, opt_duration) {
  if (rotation !== undefined) {
    var currentRotation = view.getRotation();
    var currentCenter = view.getCenter();
    if (currentRotation !== undefined && currentCenter && opt_duration > 0) {
      view.animate({
        rotation: rotation,
        anchor: opt_anchor,
        duration: opt_duration,
        easing: GVol.easing.easeOut
      });
    } else {
      view.rotate(rotation, opt_anchor);
    }
  }
};


/**
 * @param {GVol.View} view View.
 * @param {number|undefined} resGVolution ResGVolution to go to.
 * @param {GVol.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 resGVolution; < 0 indicates zooming in, in
 *     which case the constraints system will select the smallest
 *     nearest resGVolution; == 0 indicates that the zooming direction
 *     is unknown/not relevant, in which case the constraints system
 *     will select the nearest resGVolution. If not defined 0 is
 *     assumed.
 */
GVol.interaction.Interaction.zoom = function(view, resGVolution, opt_anchor, opt_duration, opt_direction) {
  resGVolution = view.constrainResGVolution(resGVolution, 0, opt_direction);
  GVol.interaction.Interaction.zoomWithoutConstraints(
      view, resGVolution, opt_anchor, opt_duration);
};


/**
 * @param {GVol.View} view View.
 * @param {number} delta Delta from previous zoom level.
 * @param {GVol.Coordinate=} opt_anchor Anchor coordinate.
 * @param {number=} opt_duration Duration.
 */
GVol.interaction.Interaction.zoomByDelta = function(view, delta, opt_anchor, opt_duration) {
  var currentResGVolution = view.getResGVolution();
  var resGVolution = view.constrainResGVolution(currentResGVolution, delta, 0);

  // If we have a constraint on center, we need to change the anchor so that the
  // new center is within the extent. We first calculate the new center, apply
  // the constraint to it, and then calculate back the anchor
  if (opt_anchor && resGVolution !== undefined && resGVolution !== currentResGVolution) {
    var currentCenter = view.getCenter();
    var center = view.calculateCenterZoom(resGVolution, opt_anchor);
    center = view.constrainCenter(center);

    opt_anchor = [
      (resGVolution * currentCenter[0] - currentResGVolution * center[0]) /
          (resGVolution - currentResGVolution),
      (resGVolution * currentCenter[1] - currentResGVolution * center[1]) /
          (resGVolution - currentResGVolution)
    ];
  }

  GVol.interaction.Interaction.zoomWithoutConstraints(
      view, resGVolution, opt_anchor, opt_duration);
};


/**
 * @param {GVol.View} view View.
 * @param {number|undefined} resGVolution ResGVolution to go to.
 * @param {GVol.Coordinate=} opt_anchor Anchor coordinate.
 * @param {number=} opt_duration Duration.
 */
GVol.interaction.Interaction.zoomWithoutConstraints = function(view, resGVolution, opt_anchor, opt_duration) {
  if (resGVolution) {
    var currentResGVolution = view.getResGVolution();
    var currentCenter = view.getCenter();
    if (currentResGVolution !== undefined && currentCenter &&
        resGVolution !== currentResGVolution && opt_duration) {
      view.animate({
        resGVolution: resGVolution,
        anchor: opt_anchor,
        duration: opt_duration,
        easing: GVol.easing.easeOut
      });
    } else {
      if (opt_anchor) {
        var center = view.calculateCenterZoom(resGVolution, opt_anchor);
        view.setCenter(center);
      }
      view.setResGVolution(resGVolution);
    }
  }
};

goog.provide('GVol.interaction.DoubleClickZoom');

goog.require('GVol');
goog.require('GVol.MapBrowserEventType');
goog.require('GVol.interaction.Interaction');


/**
 * @classdesc
 * Allows the user to zoom by double-clicking on the map.
 *
 * @constructor
 * @extends {GVol.interaction.Interaction}
 * @param {GVolx.interaction.DoubleClickZoomOptions=} opt_options Options.
 * @api
 */
GVol.interaction.DoubleClickZoom = function(opt_options) {

  var options = opt_options ? opt_options : {};

  /**
   * @private
   * @type {number}
   */
  this.delta_ = options.delta ? options.delta : 1;

  GVol.interaction.Interaction.call(this, {
    handleEvent: GVol.interaction.DoubleClickZoom.handleEvent
  });

  /**
   * @private
   * @type {number}
   */
  this.duration_ = options.duration !== undefined ? options.duration : 250;

};
GVol.inherits(GVol.interaction.DoubleClickZoom, GVol.interaction.Interaction);


/**
 * Handles the {@link GVol.MapBrowserEvent map browser event} (if it was a
 * doubleclick) and eventually zooms the map.
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} `false` to stop event propagation.
 * @this {GVol.interaction.DoubleClickZoom}
 * @api
 */
GVol.interaction.DoubleClickZoom.handleEvent = function(mapBrowserEvent) {
  var stopEvent = false;
  var browserEvent = mapBrowserEvent.originalEvent;
  if (mapBrowserEvent.type == GVol.MapBrowserEventType.DBLCLICK) {
    var map = mapBrowserEvent.map;
    var anchor = mapBrowserEvent.coordinate;
    var delta = browserEvent.shiftKey ? -this.delta_ : this.delta_;
    var view = map.getView();
    GVol.interaction.Interaction.zoomByDelta(
        view, delta, anchor, this.duration_);
    mapBrowserEvent.preventDefault();
    stopEvent = true;
  }
  return !stopEvent;
};

goog.provide('GVol.events.condition');

goog.require('GVol.MapBrowserEventType');
goog.require('GVol.asserts');
goog.require('GVol.functions');
goog.require('GVol.has');


/**
 * Return `true` if only the alt-key is pressed, `false` otherwise (e.g. when
 * additionally the shift-key is pressed).
 *
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} True if only the alt key is pressed.
 * @api
 */
GVol.events.condition.altKeyOnly = function(mapBrowserEvent) {
  var originalEvent = mapBrowserEvent.originalEvent;
  return (
    originalEvent.altKey &&
      !(originalEvent.metaKey || originalEvent.ctrlKey) &&
      !originalEvent.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 {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} True if only the alt and shift keys are pressed.
 * @api
 */
GVol.events.condition.altShiftKeysOnly = function(mapBrowserEvent) {
  var originalEvent = mapBrowserEvent.originalEvent;
  return (
    originalEvent.altKey &&
      !(originalEvent.metaKey || originalEvent.ctrlKey) &&
      originalEvent.shiftKey);
};


/**
 * Return always true.
 *
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} True.
 * @function
 * @api
 */
GVol.events.condition.always = GVol.functions.TRUE;


/**
 * Return `true` if the event is a `click` event, `false` otherwise.
 *
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} True if the event is a map `click` event.
 * @api
 */
GVol.events.condition.click = function(mapBrowserEvent) {
  return mapBrowserEvent.type == GVol.MapBrowserEventType.CLICK;
};


/**
 * Return `true` if the event has an "action"-producing mouse button.
 *
 * By definition, this includes left-click on windows/linux, and left-click
 * without the ctrl key on Macs.
 *
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} The result.
 */
GVol.events.condition.mouseActionButton = function(mapBrowserEvent) {
  var originalEvent = mapBrowserEvent.originalEvent;
  return originalEvent.button == 0 &&
      !(GVol.has.WEBKIT && GVol.has.MAC && originalEvent.ctrlKey);
};


/**
 * Return always false.
 *
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} False.
 * @function
 * @api
 */
GVol.events.condition.never = GVol.functions.FALSE;


/**
 * Return `true` if the browser event is a `pointermove` event, `false`
 * otherwise.
 *
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} True if the browser event is a `pointermove` event.
 * @api
 */
GVol.events.condition.pointerMove = function(mapBrowserEvent) {
  return mapBrowserEvent.type == 'pointermove';
};


/**
 * Return `true` if the event is a map `singleclick` event, `false` otherwise.
 *
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} True if the event is a map `singleclick` event.
 * @api
 */
GVol.events.condition.singleClick = function(mapBrowserEvent) {
  return mapBrowserEvent.type == GVol.MapBrowserEventType.SINGLECLICK;
};


/**
 * Return `true` if the event is a map `dblclick` event, `false` otherwise.
 *
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} True if the event is a map `dblclick` event.
 * @api
 */
GVol.events.condition.doubleClick = function(mapBrowserEvent) {
  return mapBrowserEvent.type == GVol.MapBrowserEventType.DBLCLICK;
};


/**
 * Return `true` if no modifier key (alt-, shift- or platform-modifier-key) is
 * pressed.
 *
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} True only if there no modifier keys are pressed.
 * @api
 */
GVol.events.condition.noModifierKeys = function(mapBrowserEvent) {
  var originalEvent = mapBrowserEvent.originalEvent;
  return (
    !originalEvent.altKey &&
      !(originalEvent.metaKey || originalEvent.ctrlKey) &&
      !originalEvent.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 {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} True if only the platform modifier key is pressed.
 * @api
 */
GVol.events.condition.platformModifierKeyOnly = function(mapBrowserEvent) {
  var originalEvent = mapBrowserEvent.originalEvent;
  return (
    !originalEvent.altKey &&
      (GVol.has.MAC ? originalEvent.metaKey : originalEvent.ctrlKey) &&
      !originalEvent.shiftKey);
};


/**
 * Return `true` if only the shift-key is pressed, `false` otherwise (e.g. when
 * additionally the alt-key is pressed).
 *
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} True if only the shift key is pressed.
 * @api
 */
GVol.events.condition.shiftKeyOnly = function(mapBrowserEvent) {
  var originalEvent = mapBrowserEvent.originalEvent;
  return (
    !originalEvent.altKey &&
      !(originalEvent.metaKey || originalEvent.ctrlKey) &&
      originalEvent.shiftKey);
};


/**
 * Return `true` if the target element is not editable, i.e. not a `<input>`-,
 * `<select>`- or `<textarea>`-element, `false` otherwise.
 *
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} True only if the target element is not editable.
 * @api
 */
GVol.events.condition.targetNotEditable = function(mapBrowserEvent) {
  var target = mapBrowserEvent.originalEvent.target;
  var tagName = target.tagName;
  return (
    tagName !== 'INPUT' &&
      tagName !== 'SELECT' &&
      tagName !== 'TEXTAREA');
};


/**
 * Return `true` if the event originates from a mouse device.
 *
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} True if the event originates from a mouse device.
 * @api
 */
GVol.events.condition.mouseOnly = function(mapBrowserEvent) {
  GVol.asserts.assert(mapBrowserEvent.pointerEvent, 56); // mapBrowserEvent must originate from a pointer event
  // see http://www.w3.org/TR/pointerevents/#widl-PointerEvent-pointerType
  return /** @type {GVol.MapBrowserEvent} */ (mapBrowserEvent).pointerEvent.pointerType == 'mouse';
};


/**
 * Return `true` if the event originates from a primary pointer in
 * contact with the surface or if the left mouse button is pressed.
 * @see http://www.w3.org/TR/pointerevents/#button-states
 *
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} True if the event originates from a primary pointer.
 * @api
 */
GVol.events.condition.primaryAction = function(mapBrowserEvent) {
  var pointerEvent = mapBrowserEvent.pointerEvent;
  return pointerEvent.isPrimary && pointerEvent.button === 0;
};

goog.provide('GVol.interaction.Pointer');

goog.require('GVol');
goog.require('GVol.functions');
goog.require('GVol.MapBrowserEventType');
goog.require('GVol.MapBrowserPointerEvent');
goog.require('GVol.interaction.Interaction');
goog.require('GVol.obj');


/**
 * @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 {GVolx.interaction.PointerOptions=} opt_options Options.
 * @extends {GVol.interaction.Interaction}
 * @api
 */
GVol.interaction.Pointer = function(opt_options) {

  var options = opt_options ? opt_options : {};

  var handleEvent = options.handleEvent ?
    options.handleEvent : GVol.interaction.Pointer.handleEvent;

  GVol.interaction.Interaction.call(this, {
    handleEvent: handleEvent
  });

  /**
   * @type {function(GVol.MapBrowserPointerEvent):boGVolean}
   * @private
   */
  this.handleDownEvent_ = options.handleDownEvent ?
    options.handleDownEvent : GVol.interaction.Pointer.handleDownEvent;

  /**
   * @type {function(GVol.MapBrowserPointerEvent)}
   * @private
   */
  this.handleDragEvent_ = options.handleDragEvent ?
    options.handleDragEvent : GVol.interaction.Pointer.handleDragEvent;

  /**
   * @type {function(GVol.MapBrowserPointerEvent)}
   * @private
   */
  this.handleMoveEvent_ = options.handleMoveEvent ?
    options.handleMoveEvent : GVol.interaction.Pointer.handleMoveEvent;

  /**
   * @type {function(GVol.MapBrowserPointerEvent):boGVolean}
   * @private
   */
  this.handleUpEvent_ = options.handleUpEvent ?
    options.handleUpEvent : GVol.interaction.Pointer.handleUpEvent;

  /**
   * @type {boGVolean}
   * @protected
   */
  this.handlingDownUpSequence = false;

  /**
   * @type {Object.<string, GVol.pointer.PointerEvent>}
   * @private
   */
  this.trackedPointers_ = {};

  /**
   * @type {Array.<GVol.pointer.PointerEvent>}
   * @protected
   */
  this.targetPointers = [];

};
GVol.inherits(GVol.interaction.Pointer, GVol.interaction.Interaction);


/**
 * @param {Array.<GVol.pointer.PointerEvent>} pointerEvents List of events.
 * @return {GVol.Pixel} Centroid pixel.
 */
GVol.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 {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boGVolean} Whether the event is a pointerdown, pointerdrag
 *     or pointerup event.
 * @private
 */
GVol.interaction.Pointer.prototype.isPointerDraggingEvent_ = function(mapBrowserEvent) {
  var type = mapBrowserEvent.type;
  return (
    type === GVol.MapBrowserEventType.POINTERDOWN ||
      type === GVol.MapBrowserEventType.POINTERDRAG ||
      type === GVol.MapBrowserEventType.POINTERUP);
};


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @private
 */
GVol.interaction.Pointer.prototype.updateTrackedPointers_ = function(mapBrowserEvent) {
  if (this.isPointerDraggingEvent_(mapBrowserEvent)) {
    var event = mapBrowserEvent.pointerEvent;

    var id = event.pointerId.toString();
    if (mapBrowserEvent.type == GVol.MapBrowserEventType.POINTERUP) {
      delete this.trackedPointers_[id];
    } else if (mapBrowserEvent.type ==
        GVol.MapBrowserEventType.POINTERDOWN) {
      this.trackedPointers_[id] = event;
    } else if (id in this.trackedPointers_) {
      // update only when there was a pointerdown event for this pointer
      this.trackedPointers_[id] = event;
    }
    this.targetPointers = GVol.obj.getValues(this.trackedPointers_);
  }
};


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @this {GVol.interaction.Pointer}
 */
GVol.interaction.Pointer.handleDragEvent = GVol.nullFunction;


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boGVolean} Capture dragging.
 * @this {GVol.interaction.Pointer}
 */
GVol.interaction.Pointer.handleUpEvent = GVol.functions.FALSE;


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boGVolean} Capture dragging.
 * @this {GVol.interaction.Pointer}
 */
GVol.interaction.Pointer.handleDownEvent = GVol.functions.FALSE;


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @this {GVol.interaction.Pointer}
 */
GVol.interaction.Pointer.handleMoveEvent = GVol.nullFunction;


/**
 * Handles the {@link GVol.MapBrowserEvent map browser event} and may call into
 * other functions, if event sequences like e.g. 'drag' or 'down-up' etc. are
 * detected.
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} `false` to stop event propagation.
 * @this {GVol.interaction.Pointer}
 * @api
 */
GVol.interaction.Pointer.handleEvent = function(mapBrowserEvent) {
  if (!(mapBrowserEvent instanceof GVol.MapBrowserPointerEvent)) {
    return true;
  }

  var stopEvent = false;
  this.updateTrackedPointers_(mapBrowserEvent);
  if (this.handlingDownUpSequence) {
    if (mapBrowserEvent.type == GVol.MapBrowserEventType.POINTERDRAG) {
      this.handleDragEvent_(mapBrowserEvent);
    } else if (mapBrowserEvent.type == GVol.MapBrowserEventType.POINTERUP) {
      var handledUp = this.handleUpEvent_(mapBrowserEvent);
      this.handlingDownUpSequence = handledUp && this.targetPointers.length > 0;
    }
  } else {
    if (mapBrowserEvent.type == GVol.MapBrowserEventType.POINTERDOWN) {
      var handled = this.handleDownEvent_(mapBrowserEvent);
      this.handlingDownUpSequence = handled;
      stopEvent = this.shouldStopEvent(handled);
    } else if (mapBrowserEvent.type == GVol.MapBrowserEventType.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 {boGVolean} handled Was the event handled by the interaction?
 * @return {boGVolean} Should the event be stopped?
 * @protected
 */
GVol.interaction.Pointer.prototype.shouldStopEvent = function(handled) {
  return handled;
};

goog.provide('GVol.interaction.DragPan');

goog.require('GVol');
goog.require('GVol.ViewHint');
goog.require('GVol.coordinate');
goog.require('GVol.easing');
goog.require('GVol.events.condition');
goog.require('GVol.functions');
goog.require('GVol.interaction.Pointer');


/**
 * @classdesc
 * Allows the user to pan the map by dragging the map.
 *
 * @constructor
 * @extends {GVol.interaction.Pointer}
 * @param {GVolx.interaction.DragPanOptions=} opt_options Options.
 * @api
 */
GVol.interaction.DragPan = function(opt_options) {

  GVol.interaction.Pointer.call(this, {
    handleDownEvent: GVol.interaction.DragPan.handleDownEvent_,
    handleDragEvent: GVol.interaction.DragPan.handleDragEvent_,
    handleUpEvent: GVol.interaction.DragPan.handleUpEvent_
  });

  var options = opt_options ? opt_options : {};

  /**
   * @private
   * @type {GVol.Kinetic|undefined}
   */
  this.kinetic_ = options.kinetic;

  /**
   * @type {GVol.Pixel}
   */
  this.lastCentroid = null;

  /**
   * @type {number}
   */
  this.lastPointersCount_;

  /**
   * @private
   * @type {GVol.EventsConditionType}
   */
  this.condition_ = options.condition ?
    options.condition : GVol.events.condition.noModifierKeys;

  /**
   * @private
   * @type {boGVolean}
   */
  this.noKinetic_ = false;

};
GVol.inherits(GVol.interaction.DragPan, GVol.interaction.Pointer);


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @this {GVol.interaction.DragPan}
 * @private
 */
GVol.interaction.DragPan.handleDragEvent_ = function(mapBrowserEvent) {
  var targetPointers = this.targetPointers;
  var centroid =
      GVol.interaction.Pointer.centroid(targetPointers);
  if (targetPointers.length == this.lastPointersCount_) {
    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];
      GVol.coordinate.scale(center, viewState.resGVolution);
      GVol.coordinate.rotate(center, viewState.rotation);
      GVol.coordinate.add(center, viewState.center);
      center = view.constrainCenter(center);
      view.setCenter(center);
    }
  } else if (this.kinetic_) {
    // reset so we don't overestimate the kinetic energy after
    // after one finger down, tiny drag, second finger down
    this.kinetic_.begin();
  }
  this.lastCentroid = centroid;
  this.lastPointersCount_ = targetPointers.length;
};


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boGVolean} Stop drag sequence?
 * @this {GVol.interaction.DragPan}
 * @private
 */
GVol.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 = /** @type {!GVol.Coordinate} */ (view.getCenter());
      var centerpx = map.getPixelFromCoordinate(center);
      var dest = map.getCoordinateFromPixel([
        centerpx[0] - distance * Math.cos(angle),
        centerpx[1] - distance * Math.sin(angle)
      ]);
      view.animate({
        center: view.constrainCenter(dest),
        duration: 500,
        easing: GVol.easing.easeOut
      });
    }
    view.setHint(GVol.ViewHint.INTERACTING, -1);
    return false;
  } else {
    if (this.kinetic_) {
      // reset so we don't overestimate the kinetic energy after
      // after one finger up, tiny drag, second finger up
      this.kinetic_.begin();
    }
    this.lastCentroid = null;
    return true;
  }
};


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boGVolean} Start drag sequence?
 * @this {GVol.interaction.DragPan}
 * @private
 */
GVol.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(GVol.ViewHint.INTERACTING, 1);
    }
    // stop any current animation
    if (view.getHints()[GVol.ViewHint.ANIMATING]) {
      view.setCenter(mapBrowserEvent.frameState.viewState.center);
    }
    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
 */
GVol.interaction.DragPan.prototype.shouldStopEvent = GVol.functions.FALSE;

goog.provide('GVol.interaction.DragRotate');

goog.require('GVol');
goog.require('GVol.RotationConstraint');
goog.require('GVol.ViewHint');
goog.require('GVol.events.condition');
goog.require('GVol.functions');
goog.require('GVol.interaction.Interaction');
goog.require('GVol.interaction.Pointer');


/**
 * @classdesc
 * Allows the user to rotate the map by clicking and dragging on the map,
 * normally combined with an {@link GVol.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 {GVol.interaction.Pointer}
 * @param {GVolx.interaction.DragRotateOptions=} opt_options Options.
 * @api
 */
GVol.interaction.DragRotate = function(opt_options) {

  var options = opt_options ? opt_options : {};

  GVol.interaction.Pointer.call(this, {
    handleDownEvent: GVol.interaction.DragRotate.handleDownEvent_,
    handleDragEvent: GVol.interaction.DragRotate.handleDragEvent_,
    handleUpEvent: GVol.interaction.DragRotate.handleUpEvent_
  });

  /**
   * @private
   * @type {GVol.EventsConditionType}
   */
  this.condition_ = options.condition ?
    options.condition : GVol.events.condition.altShiftKeysOnly;

  /**
   * @private
   * @type {number|undefined}
   */
  this.lastAngle_ = undefined;

  /**
   * @private
   * @type {number}
   */
  this.duration_ = options.duration !== undefined ? options.duration : 250;
};
GVol.inherits(GVol.interaction.DragRotate, GVol.interaction.Pointer);


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @this {GVol.interaction.DragRotate}
 * @private
 */
GVol.interaction.DragRotate.handleDragEvent_ = function(mapBrowserEvent) {
  if (!GVol.events.condition.mouseOnly(mapBrowserEvent)) {
    return;
  }

  var map = mapBrowserEvent.map;
  var view = map.getView();
  if (view.getConstraints().rotation === GVol.RotationConstraint.disable) {
    return;
  }
  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 rotation = view.getRotation();
    GVol.interaction.Interaction.rotateWithoutConstraints(
        view, rotation - delta);
  }
  this.lastAngle_ = theta;
};


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boGVolean} Stop drag sequence?
 * @this {GVol.interaction.DragRotate}
 * @private
 */
GVol.interaction.DragRotate.handleUpEvent_ = function(mapBrowserEvent) {
  if (!GVol.events.condition.mouseOnly(mapBrowserEvent)) {
    return true;
  }

  var map = mapBrowserEvent.map;
  var view = map.getView();
  view.setHint(GVol.ViewHint.INTERACTING, -1);
  var rotation = view.getRotation();
  GVol.interaction.Interaction.rotate(view, rotation,
      undefined, this.duration_);
  return false;
};


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boGVolean} Start drag sequence?
 * @this {GVol.interaction.DragRotate}
 * @private
 */
GVol.interaction.DragRotate.handleDownEvent_ = function(mapBrowserEvent) {
  if (!GVol.events.condition.mouseOnly(mapBrowserEvent)) {
    return false;
  }

  if (GVol.events.condition.mouseActionButton(mapBrowserEvent) &&
      this.condition_(mapBrowserEvent)) {
    var map = mapBrowserEvent.map;
    map.getView().setHint(GVol.ViewHint.INTERACTING, 1);
    this.lastAngle_ = undefined;
    return true;
  } else {
    return false;
  }
};


/**
 * @inheritDoc
 */
GVol.interaction.DragRotate.prototype.shouldStopEvent = GVol.functions.FALSE;

// FIXME add rotation

goog.provide('GVol.render.Box');

goog.require('GVol');
goog.require('GVol.Disposable');
goog.require('GVol.geom.PGVolygon');


/**
 * @constructor
 * @extends {GVol.Disposable}
 * @param {string} className CSS class name.
 */
GVol.render.Box = function(className) {

  /**
   * @type {GVol.geom.PGVolygon}
   * @private
   */
  this.geometry_ = null;

  /**
   * @type {HTMLDivElement}
   * @private
   */
  this.element_ = /** @type {HTMLDivElement} */ (document.createElement('div'));
  this.element_.style.position = 'absGVolute';
  this.element_.className = 'GVol-box ' + className;

  /**
   * @private
   * @type {GVol.Map}
   */
  this.map_ = null;

  /**
   * @private
   * @type {GVol.Pixel}
   */
  this.startPixel_ = null;

  /**
   * @private
   * @type {GVol.Pixel}
   */
  this.endPixel_ = null;

};
GVol.inherits(GVol.render.Box, GVol.Disposable);


/**
 * @inheritDoc
 */
GVol.render.Box.prototype.disposeInternal = function() {
  this.setMap(null);
};


/**
 * @private
 */
GVol.render.Box.prototype.render_ = function() {
  var startPixel = this.startPixel_;
  var endPixel = this.endPixel_;
  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 {GVol.Map} map Map.
 */
GVol.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 {GVol.Pixel} startPixel Start pixel.
 * @param {GVol.Pixel} endPixel End pixel.
 */
GVol.render.Box.prototype.setPixels = function(startPixel, endPixel) {
  this.startPixel_ = startPixel;
  this.endPixel_ = endPixel;
  this.createOrUpdateGeometry();
  this.render_();
};


/**
 * Creates or updates the cached geometry.
 */
GVol.render.Box.prototype.createOrUpdateGeometry = function() {
  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 pGVolygon
  coordinates[4] = coordinates[0].slice();
  if (!this.geometry_) {
    this.geometry_ = new GVol.geom.PGVolygon([coordinates]);
  } else {
    this.geometry_.setCoordinates([coordinates]);
  }
};


/**
 * @return {GVol.geom.PGVolygon} Geometry.
 */
GVol.render.Box.prototype.getGeometry = function() {
  return this.geometry_;
};

// FIXME draw drag box
goog.provide('GVol.interaction.DragBox');

goog.require('GVol.events.Event');
goog.require('GVol');
goog.require('GVol.events.condition');
goog.require('GVol.interaction.Pointer');
goog.require('GVol.render.Box');


/**
 * @classdesc
 * Allows the user to draw a vector box by clicking and dragging on the map,
 * normally combined with an {@link GVol.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 GVol.interaction.DragZoom} and
 * {@link GVol.interaction.DragRotateAndZoom}).
 *
 * This interaction is only supported for mouse devices.
 *
 * @constructor
 * @extends {GVol.interaction.Pointer}
 * @fires GVol.interaction.DragBox.Event
 * @param {GVolx.interaction.DragBoxOptions=} opt_options Options.
 * @api
 */
GVol.interaction.DragBox = function(opt_options) {

  GVol.interaction.Pointer.call(this, {
    handleDownEvent: GVol.interaction.DragBox.handleDownEvent_,
    handleDragEvent: GVol.interaction.DragBox.handleDragEvent_,
    handleUpEvent: GVol.interaction.DragBox.handleUpEvent_
  });

  var options = opt_options ? opt_options : {};

  /**
   * @type {GVol.render.Box}
   * @private
   */
  this.box_ = new GVol.render.Box(options.className || 'GVol-dragbox');

  /**
   * @type {number}
   * @private
   */
  this.minArea_ = options.minArea !== undefined ? options.minArea : 64;

  /**
   * @type {GVol.Pixel}
   * @private
   */
  this.startPixel_ = null;

  /**
   * @private
   * @type {GVol.EventsConditionType}
   */
  this.condition_ = options.condition ?
    options.condition : GVol.events.condition.always;

  /**
   * @private
   * @type {GVol.DragBoxEndConditionType}
   */
  this.boxEndCondition_ = options.boxEndCondition ?
    options.boxEndCondition : GVol.interaction.DragBox.defaultBoxEndCondition;
};
GVol.inherits(GVol.interaction.DragBox, GVol.interaction.Pointer);


/**
 * The default condition for determining whether the boxend event
 * should fire.
 * @param {GVol.MapBrowserEvent} mapBrowserEvent The originating MapBrowserEvent
 *     leading to the box end.
 * @param {GVol.Pixel} startPixel The starting pixel of the box.
 * @param {GVol.Pixel} endPixel The end pixel of the box.
 * @return {boGVolean} Whether or not the boxend condition should be fired.
 * @this {GVol.interaction.DragBox}
 */
GVol.interaction.DragBox.defaultBoxEndCondition = function(mapBrowserEvent, startPixel, endPixel) {
  var width = endPixel[0] - startPixel[0];
  var height = endPixel[1] - startPixel[1];
  return width * width + height * height >= this.minArea_;
};


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @this {GVol.interaction.DragBox}
 * @private
 */
GVol.interaction.DragBox.handleDragEvent_ = function(mapBrowserEvent) {
  if (!GVol.events.condition.mouseOnly(mapBrowserEvent)) {
    return;
  }

  this.box_.setPixels(this.startPixel_, mapBrowserEvent.pixel);

  this.dispatchEvent(new GVol.interaction.DragBox.Event(GVol.interaction.DragBox.EventType_.BOXDRAG,
      mapBrowserEvent.coordinate, mapBrowserEvent));
};


/**
 * Returns geometry of last drawn box.
 * @return {GVol.geom.PGVolygon} Geometry.
 * @api
 */
GVol.interaction.DragBox.prototype.getGeometry = function() {
  return this.box_.getGeometry();
};


/**
 * To be overridden by child classes.
 * FIXME: use constructor option instead of relying on overriding.
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @protected
 */
GVol.interaction.DragBox.prototype.onBoxEnd = GVol.nullFunction;


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boGVolean} Stop drag sequence?
 * @this {GVol.interaction.DragBox}
 * @private
 */
GVol.interaction.DragBox.handleUpEvent_ = function(mapBrowserEvent) {
  if (!GVol.events.condition.mouseOnly(mapBrowserEvent)) {
    return true;
  }

  this.box_.setMap(null);

  if (this.boxEndCondition_(mapBrowserEvent,
      this.startPixel_, mapBrowserEvent.pixel)) {
    this.onBoxEnd(mapBrowserEvent);
    this.dispatchEvent(new GVol.interaction.DragBox.Event(GVol.interaction.DragBox.EventType_.BOXEND,
        mapBrowserEvent.coordinate, mapBrowserEvent));
  }
  return false;
};


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boGVolean} Start drag sequence?
 * @this {GVol.interaction.DragBox}
 * @private
 */
GVol.interaction.DragBox.handleDownEvent_ = function(mapBrowserEvent) {
  if (!GVol.events.condition.mouseOnly(mapBrowserEvent)) {
    return false;
  }

  if (GVol.events.condition.mouseActionButton(mapBrowserEvent) &&
      this.condition_(mapBrowserEvent)) {
    this.startPixel_ = mapBrowserEvent.pixel;
    this.box_.setMap(mapBrowserEvent.map);
    this.box_.setPixels(this.startPixel_, this.startPixel_);
    this.dispatchEvent(new GVol.interaction.DragBox.Event(GVol.interaction.DragBox.EventType_.BOXSTART,
        mapBrowserEvent.coordinate, mapBrowserEvent));
    return true;
  } else {
    return false;
  }
};


/**
 * @enum {string}
 * @private
 */
GVol.interaction.DragBox.EventType_ = {
  /**
   * Triggered upon drag box start.
   * @event GVol.interaction.DragBox.Event#boxstart
   * @api
   */
  BOXSTART: 'boxstart',

  /**
   * Triggered on drag when box is active.
   * @event GVol.interaction.DragBox.Event#boxdrag
   * @api
   */
  BOXDRAG: 'boxdrag',

  /**
   * Triggered upon drag box end.
   * @event GVol.interaction.DragBox.Event#boxend
   * @api
   */
  BOXEND: 'boxend'
};


/**
 * @classdesc
 * Events emitted by {@link GVol.interaction.DragBox} instances are instances of
 * this type.
 *
 * @param {string} type The event type.
 * @param {GVol.Coordinate} coordinate The event coordinate.
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Originating event.
 * @extends {GVol.events.Event}
 * @constructor
 * @implements {GVoli.DragBoxEvent}
 */
GVol.interaction.DragBox.Event = function(type, coordinate, mapBrowserEvent) {
  GVol.events.Event.call(this, type);

  /**
   * The coordinate of the drag event.
   * @const
   * @type {GVol.Coordinate}
   * @api
   */
  this.coordinate = coordinate;

  /**
   * @const
   * @type {GVol.MapBrowserEvent}
   * @api
   */
  this.mapBrowserEvent = mapBrowserEvent;

};
GVol.inherits(GVol.interaction.DragBox.Event, GVol.events.Event);

goog.provide('GVol.interaction.DragZoom');

goog.require('GVol');
goog.require('GVol.easing');
goog.require('GVol.events.condition');
goog.require('GVol.extent');
goog.require('GVol.interaction.DragBox');


/**
 * @classdesc
 * Allows the user to zoom the map by clicking and dragging on the map,
 * normally combined with an {@link GVol.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 `.GVol-dragzoom` selector, or
 * your custom one configured with `className`.
 *
 * @constructor
 * @extends {GVol.interaction.DragBox}
 * @param {GVolx.interaction.DragZoomOptions=} opt_options Options.
 * @api
 */
GVol.interaction.DragZoom = function(opt_options) {
  var options = opt_options ? opt_options : {};

  var condition = options.condition ?
    options.condition : GVol.events.condition.shiftKeyOnly;

  /**
   * @private
   * @type {number}
   */
  this.duration_ = options.duration !== undefined ? options.duration : 200;

  /**
   * @private
   * @type {boGVolean}
   */
  this.out_ = options.out !== undefined ? options.out : false;

  GVol.interaction.DragBox.call(this, {
    condition: condition,
    className: options.className || 'GVol-dragzoom'
  });

};
GVol.inherits(GVol.interaction.DragZoom, GVol.interaction.DragBox);


/**
 * @inheritDoc
 */
GVol.interaction.DragZoom.prototype.onBoxEnd = function() {
  var map = this.getMap();

  var view = /** @type {!GVol.View} */ (map.getView());

  var size = /** @type {!GVol.Size} */ (map.getSize());

  var extent = this.getGeometry().getExtent();

  if (this.out_) {
    var mapExtent = view.calculateExtent(size);
    var boxPixelExtent = GVol.extent.createOrUpdateFromCoordinates([
      map.getPixelFromCoordinate(GVol.extent.getBottomLeft(extent)),
      map.getPixelFromCoordinate(GVol.extent.getTopRight(extent))]);
    var factor = view.getResGVolutionForExtent(boxPixelExtent, size);

    GVol.extent.scaleFromCenter(mapExtent, 1 / factor);
    extent = mapExtent;
  }

  var resGVolution = view.constrainResGVolution(
      view.getResGVolutionForExtent(extent, size));

  var center = GVol.extent.getCenter(extent);
  center = view.constrainCenter(center);

  view.animate({
    resGVolution: resGVolution,
    center: center,
    duration: this.duration_,
    easing: GVol.easing.easeOut
  });

};

goog.provide('GVol.events.KeyCode');

/**
 * @enum {number}
 * @const
 */
GVol.events.KeyCode = {
  LEFT: 37,
  UP: 38,
  RIGHT: 39,
  DOWN: 40
};

goog.provide('GVol.interaction.KeyboardPan');

goog.require('GVol');
goog.require('GVol.coordinate');
goog.require('GVol.events.EventType');
goog.require('GVol.events.KeyCode');
goog.require('GVol.events.condition');
goog.require('GVol.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 GVol.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 GVol.interaction.KeyboardZoom}.
 *
 * @constructor
 * @extends {GVol.interaction.Interaction}
 * @param {GVolx.interaction.KeyboardPanOptions=} opt_options Options.
 * @api
 */
GVol.interaction.KeyboardPan = function(opt_options) {

  GVol.interaction.Interaction.call(this, {
    handleEvent: GVol.interaction.KeyboardPan.handleEvent
  });

  var options = opt_options || {};

  /**
   * @private
   * @param {GVol.MapBrowserEvent} mapBrowserEvent Browser event.
   * @return {boGVolean} Combined condition result.
   */
  this.defaultCondition_ = function(mapBrowserEvent) {
    return GVol.events.condition.noModifierKeys(mapBrowserEvent) &&
      GVol.events.condition.targetNotEditable(mapBrowserEvent);
  };

  /**
   * @private
   * @type {GVol.EventsConditionType}
   */
  this.condition_ = options.condition !== undefined ?
    options.condition : this.defaultCondition_;

  /**
   * @private
   * @type {number}
   */
  this.duration_ = options.duration !== undefined ? options.duration : 100;

  /**
   * @private
   * @type {number}
   */
  this.pixelDelta_ = options.pixelDelta !== undefined ?
    options.pixelDelta : 128;

};
GVol.inherits(GVol.interaction.KeyboardPan, GVol.interaction.Interaction);

/**
 * Handles the {@link GVol.MapBrowserEvent map browser event} if it was a
 * `KeyEvent`, and decides the direction to pan to (if an arrow key was
 * pressed).
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} `false` to stop event propagation.
 * @this {GVol.interaction.KeyboardPan}
 * @api
 */
GVol.interaction.KeyboardPan.handleEvent = function(mapBrowserEvent) {
  var stopEvent = false;
  if (mapBrowserEvent.type == GVol.events.EventType.KEYDOWN) {
    var keyEvent = mapBrowserEvent.originalEvent;
    var keyCode = keyEvent.keyCode;
    if (this.condition_(mapBrowserEvent) &&
        (keyCode == GVol.events.KeyCode.DOWN ||
        keyCode == GVol.events.KeyCode.LEFT ||
        keyCode == GVol.events.KeyCode.RIGHT ||
        keyCode == GVol.events.KeyCode.UP)) {
      var map = mapBrowserEvent.map;
      var view = map.getView();
      var mapUnitsDelta = view.getResGVolution() * this.pixelDelta_;
      var deltaX = 0, deltaY = 0;
      if (keyCode == GVol.events.KeyCode.DOWN) {
        deltaY = -mapUnitsDelta;
      } else if (keyCode == GVol.events.KeyCode.LEFT) {
        deltaX = -mapUnitsDelta;
      } else if (keyCode == GVol.events.KeyCode.RIGHT) {
        deltaX = mapUnitsDelta;
      } else {
        deltaY = mapUnitsDelta;
      }
      var delta = [deltaX, deltaY];
      GVol.coordinate.rotate(delta, view.getRotation());
      GVol.interaction.Interaction.pan(view, delta, this.duration_);
      mapBrowserEvent.preventDefault();
      stopEvent = true;
    }
  }
  return !stopEvent;
};

goog.provide('GVol.interaction.KeyboardZoom');

goog.require('GVol');
goog.require('GVol.events.EventType');
goog.require('GVol.events.condition');
goog.require('GVol.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 GVol.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 GVol.interaction.KeyboardPan}.
 *
 * @constructor
 * @param {GVolx.interaction.KeyboardZoomOptions=} opt_options Options.
 * @extends {GVol.interaction.Interaction}
 * @api
 */
GVol.interaction.KeyboardZoom = function(opt_options) {

  GVol.interaction.Interaction.call(this, {
    handleEvent: GVol.interaction.KeyboardZoom.handleEvent
  });

  var options = opt_options ? opt_options : {};

  /**
   * @private
   * @type {GVol.EventsConditionType}
   */
  this.condition_ = options.condition ? options.condition :
    GVol.events.condition.targetNotEditable;

  /**
   * @private
   * @type {number}
   */
  this.delta_ = options.delta ? options.delta : 1;

  /**
   * @private
   * @type {number}
   */
  this.duration_ = options.duration !== undefined ? options.duration : 100;

};
GVol.inherits(GVol.interaction.KeyboardZoom, GVol.interaction.Interaction);


/**
 * Handles the {@link GVol.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 {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} `false` to stop event propagation.
 * @this {GVol.interaction.KeyboardZoom}
 * @api
 */
GVol.interaction.KeyboardZoom.handleEvent = function(mapBrowserEvent) {
  var stopEvent = false;
  if (mapBrowserEvent.type == GVol.events.EventType.KEYDOWN ||
      mapBrowserEvent.type == GVol.events.EventType.KEYPRESS) {
    var keyEvent = mapBrowserEvent.originalEvent;
    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_;
      var view = map.getView();
      GVol.interaction.Interaction.zoomByDelta(
          view, delta, undefined, this.duration_);
      mapBrowserEvent.preventDefault();
      stopEvent = true;
    }
  }
  return !stopEvent;
};

goog.provide('GVol.interaction.MouseWheelZoom');

goog.require('GVol');
goog.require('GVol.ViewHint');
goog.require('GVol.easing');
goog.require('GVol.events.EventType');
goog.require('GVol.has');
goog.require('GVol.interaction.Interaction');
goog.require('GVol.math');


/**
 * @classdesc
 * Allows the user to zoom the map by scrGVolling the mouse wheel.
 *
 * @constructor
 * @extends {GVol.interaction.Interaction}
 * @param {GVolx.interaction.MouseWheelZoomOptions=} opt_options Options.
 * @api
 */
GVol.interaction.MouseWheelZoom = function(opt_options) {

  GVol.interaction.Interaction.call(this, {
    handleEvent: GVol.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 {number}
   */
  this.timeout_ = options.timeout !== undefined ? options.timeout : 80;

  /**
   * @private
   * @type {boGVolean}
   */
  this.useAnchor_ = options.useAnchor !== undefined ? options.useAnchor : true;

  /**
   * @private
   * @type {boGVolean}
   */
  this.constrainResGVolution_ = options.constrainResGVolution || false;

  /**
   * @private
   * @type {?GVol.Coordinate}
   */
  this.lastAnchor_ = null;

  /**
   * @private
   * @type {number|undefined}
   */
  this.startTime_ = undefined;

  /**
   * @private
   * @type {number|undefined}
   */
  this.timeoutId_ = undefined;

  /**
   * @private
   * @type {GVol.interaction.MouseWheelZoom.Mode_|undefined}
   */
  this.mode_ = undefined;

  /**
   * Trackpad events separated by this delay will be considered separate
   * interactions.
   * @type {number}
   */
  this.trackpadEventGap_ = 400;

  /**
   * @type {number|undefined}
   */
  this.trackpadTimeoutId_ = undefined;

  /**
   * The number of delta values per zoom level
   * @private
   * @type {number}
   */
  this.trackpadDeltaPerZoom_ = 300;

  /**
   * The zoom factor by which scrGVoll zooming is allowed to exceed the limits.
   * @private
   * @type {number}
   */
  this.trackpadZoomBuffer_ = 1.5;

};
GVol.inherits(GVol.interaction.MouseWheelZoom, GVol.interaction.Interaction);


/**
 * Handles the {@link GVol.MapBrowserEvent map browser event} (if it was a
 * mousewheel-event) and eventually zooms the map.
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} Allow event propagation.
 * @this {GVol.interaction.MouseWheelZoom}
 * @api
 */
GVol.interaction.MouseWheelZoom.handleEvent = function(mapBrowserEvent) {
  var type = mapBrowserEvent.type;
  if (type !== GVol.events.EventType.WHEEL && type !== GVol.events.EventType.MOUSEWHEEL) {
    return true;
  }

  mapBrowserEvent.preventDefault();

  var map = mapBrowserEvent.map;
  var wheelEvent = /** @type {WheelEvent} */ (mapBrowserEvent.originalEvent);

  if (this.useAnchor_) {
    this.lastAnchor_ = mapBrowserEvent.coordinate;
  }

  // Delta normalisation inspired by
  // https://github.com/mapbox/mapbox-gl-js/blob/001c7b9/js/ui/handler/scrGVoll_zoom.js
  var delta;
  if (mapBrowserEvent.type == GVol.events.EventType.WHEEL) {
    delta = wheelEvent.deltaY;
    if (GVol.has.FIREFOX &&
        wheelEvent.deltaMode === WheelEvent.DOM_DELTA_PIXEL) {
      delta /= GVol.has.DEVICE_PIXEL_RATIO;
    }
    if (wheelEvent.deltaMode === WheelEvent.DOM_DELTA_LINE) {
      delta *= 40;
    }
  } else if (mapBrowserEvent.type == GVol.events.EventType.MOUSEWHEEL) {
    delta = -wheelEvent.wheelDeltaY;
    if (GVol.has.SAFARI) {
      delta /= 3;
    }
  }

  if (delta === 0) {
    return false;
  }

  var now = Date.now();

  if (this.startTime_ === undefined) {
    this.startTime_ = now;
  }

  if (!this.mode_ || now - this.startTime_ > this.trackpadEventGap_) {
    this.mode_ = Math.abs(delta) < 4 ?
      GVol.interaction.MouseWheelZoom.Mode_.TRACKPAD :
      GVol.interaction.MouseWheelZoom.Mode_.WHEEL;
  }

  if (this.mode_ === GVol.interaction.MouseWheelZoom.Mode_.TRACKPAD) {
    var view = map.getView();
    if (this.trackpadTimeoutId_) {
      clearTimeout(this.trackpadTimeoutId_);
    } else {
      view.setHint(GVol.ViewHint.INTERACTING, 1);
    }
    this.trackpadTimeoutId_ = setTimeout(this.decrementInteractingHint_.bind(this), this.trackpadEventGap_);
    var resGVolution = view.getResGVolution() * Math.pow(2, delta / this.trackpadDeltaPerZoom_);
    var minResGVolution = view.getMinResGVolution();
    var maxResGVolution = view.getMaxResGVolution();
    var rebound = 0;
    if (resGVolution < minResGVolution) {
      resGVolution = Math.max(resGVolution, minResGVolution / this.trackpadZoomBuffer_);
      rebound = 1;
    } else if (resGVolution > maxResGVolution) {
      resGVolution = Math.min(resGVolution, maxResGVolution * this.trackpadZoomBuffer_);
      rebound = -1;
    }
    if (this.lastAnchor_) {
      var center = view.calculateCenterZoom(resGVolution, this.lastAnchor_);
      view.setCenter(view.constrainCenter(center));
    }
    view.setResGVolution(resGVolution);

    if (rebound === 0 && this.constrainResGVolution_) {
      view.animate({
        resGVolution: view.constrainResGVolution(resGVolution, delta > 0 ? -1 : 1),
        easing: GVol.easing.easeOut,
        anchor: this.lastAnchor_,
        duration: this.duration_
      });
    }

    if (rebound > 0) {
      view.animate({
        resGVolution: minResGVolution,
        easing: GVol.easing.easeOut,
        anchor: this.lastAnchor_,
        duration: 500
      });
    } else if (rebound < 0) {
      view.animate({
        resGVolution: maxResGVolution,
        easing: GVol.easing.easeOut,
        anchor: this.lastAnchor_,
        duration: 500
      });
    }
    this.startTime_ = now;
    return false;
  }

  this.delta_ += delta;

  var timeLeft = Math.max(this.timeout_ - (now - this.startTime_), 0);

  clearTimeout(this.timeoutId_);
  this.timeoutId_ = setTimeout(this.handleWheelZoom_.bind(this, map), timeLeft);

  return false;
};


/**
 * @private
 */
GVol.interaction.MouseWheelZoom.prototype.decrementInteractingHint_ = function() {
  this.trackpadTimeoutId_ = undefined;
  var view = this.getMap().getView();
  view.setHint(GVol.ViewHint.INTERACTING, -1);
};


/**
 * @private
 * @param {GVol.Map} map Map.
 */
GVol.interaction.MouseWheelZoom.prototype.handleWheelZoom_ = function(map) {
  var view = map.getView();
  if (view.getAnimating()) {
    view.cancelAnimations();
  }
  var maxDelta = GVol.MOUSEWHEELZOOM_MAXDELTA;
  var delta = GVol.math.clamp(this.delta_, -maxDelta, maxDelta);
  GVol.interaction.Interaction.zoomByDelta(view, -delta, this.lastAnchor_,
      this.duration_);
  this.mode_ = undefined;
  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 {boGVolean} useAnchor true to zoom to the mouse's location, false
 * to zoom to the center of the map
 * @api
 */
GVol.interaction.MouseWheelZoom.prototype.setMouseAnchor = function(useAnchor) {
  this.useAnchor_ = useAnchor;
  if (!useAnchor) {
    this.lastAnchor_ = null;
  }
};


/**
 * @enum {string}
 * @private
 */
GVol.interaction.MouseWheelZoom.Mode_ = {
  TRACKPAD: 'trackpad',
  WHEEL: 'wheel'
};

goog.provide('GVol.interaction.PinchRotate');

goog.require('GVol');
goog.require('GVol.ViewHint');
goog.require('GVol.functions');
goog.require('GVol.interaction.Interaction');
goog.require('GVol.interaction.Pointer');
goog.require('GVol.RotationConstraint');


/**
 * @classdesc
 * Allows the user to rotate the map by twisting with two fingers
 * on a touch screen.
 *
 * @constructor
 * @extends {GVol.interaction.Pointer}
 * @param {GVolx.interaction.PinchRotateOptions=} opt_options Options.
 * @api
 */
GVol.interaction.PinchRotate = function(opt_options) {

  GVol.interaction.Pointer.call(this, {
    handleDownEvent: GVol.interaction.PinchRotate.handleDownEvent_,
    handleDragEvent: GVol.interaction.PinchRotate.handleDragEvent_,
    handleUpEvent: GVol.interaction.PinchRotate.handleUpEvent_
  });

  var options = opt_options || {};

  /**
   * @private
   * @type {GVol.Coordinate}
   */
  this.anchor_ = null;

  /**
   * @private
   * @type {number|undefined}
   */
  this.lastAngle_ = undefined;

  /**
   * @private
   * @type {boGVolean}
   */
  this.rotating_ = false;

  /**
   * @private
   * @type {number}
   */
  this.rotationDelta_ = 0.0;

  /**
   * @private
   * @type {number}
   */
  this.threshGVold_ = options.threshGVold !== undefined ? options.threshGVold : 0.3;

  /**
   * @private
   * @type {number}
   */
  this.duration_ = options.duration !== undefined ? options.duration : 250;

};
GVol.inherits(GVol.interaction.PinchRotate, GVol.interaction.Pointer);


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @this {GVol.interaction.PinchRotate}
 * @private
 */
GVol.interaction.PinchRotate.handleDragEvent_ = function(mapBrowserEvent) {
  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.threshGVold_) {
      this.rotating_ = true;
    }
    rotationDelta = delta;
  }
  this.lastAngle_ = angle;

  var map = mapBrowserEvent.map;
  var view = map.getView();
  if (view.getConstraints().rotation === GVol.RotationConstraint.disable) {
    return;
  }

  // rotate anchor point.
  // FIXME: should be the intersection point between the lines:
  //     touch0,touch1 and previousTouch0,previousTouch1
  var viewportPosition = map.getViewport().getBoundingClientRect();
  var centroid = GVol.interaction.Pointer.centroid(this.targetPointers);
  centroid[0] -= viewportPosition.left;
  centroid[1] -= viewportPosition.top;
  this.anchor_ = map.getCoordinateFromPixel(centroid);

  // rotate
  if (this.rotating_) {
    var rotation = view.getRotation();
    map.render();
    GVol.interaction.Interaction.rotateWithoutConstraints(view,
        rotation + rotationDelta, this.anchor_);
  }
};


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boGVolean} Stop drag sequence?
 * @this {GVol.interaction.PinchRotate}
 * @private
 */
GVol.interaction.PinchRotate.handleUpEvent_ = function(mapBrowserEvent) {
  if (this.targetPointers.length < 2) {
    var map = mapBrowserEvent.map;
    var view = map.getView();
    view.setHint(GVol.ViewHint.INTERACTING, -1);
    if (this.rotating_) {
      var rotation = view.getRotation();
      GVol.interaction.Interaction.rotate(
          view, rotation, this.anchor_, this.duration_);
    }
    return false;
  } else {
    return true;
  }
};


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boGVolean} Start drag sequence?
 * @this {GVol.interaction.PinchRotate}
 * @private
 */
GVol.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(GVol.ViewHint.INTERACTING, 1);
    }
    return true;
  } else {
    return false;
  }
};


/**
 * @inheritDoc
 */
GVol.interaction.PinchRotate.prototype.shouldStopEvent = GVol.functions.FALSE;

goog.provide('GVol.interaction.PinchZoom');

goog.require('GVol');
goog.require('GVol.ViewHint');
goog.require('GVol.functions');
goog.require('GVol.interaction.Interaction');
goog.require('GVol.interaction.Pointer');


/**
 * @classdesc
 * Allows the user to zoom the map by pinching with two fingers
 * on a touch screen.
 *
 * @constructor
 * @extends {GVol.interaction.Pointer}
 * @param {GVolx.interaction.PinchZoomOptions=} opt_options Options.
 * @api
 */
GVol.interaction.PinchZoom = function(opt_options) {

  GVol.interaction.Pointer.call(this, {
    handleDownEvent: GVol.interaction.PinchZoom.handleDownEvent_,
    handleDragEvent: GVol.interaction.PinchZoom.handleDragEvent_,
    handleUpEvent: GVol.interaction.PinchZoom.handleUpEvent_
  });

  var options = opt_options ? opt_options : {};

  /**
   * @private
   * @type {boGVolean}
   */
  this.constrainResGVolution_ = options.constrainResGVolution || false;

  /**
   * @private
   * @type {GVol.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;

};
GVol.inherits(GVol.interaction.PinchZoom, GVol.interaction.Pointer);


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @this {GVol.interaction.PinchZoom}
 * @private
 */
GVol.interaction.PinchZoom.handleDragEvent_ = function(mapBrowserEvent) {
  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;


  var map = mapBrowserEvent.map;
  var view = map.getView();
  var resGVolution = view.getResGVolution();
  var maxResGVolution = view.getMaxResGVolution();
  var minResGVolution = view.getMinResGVolution();
  var newResGVolution = resGVolution * scaleDelta;
  if (newResGVolution > maxResGVolution) {
    scaleDelta = maxResGVolution / resGVolution;
    newResGVolution = maxResGVolution;
  } else if (newResGVolution < minResGVolution) {
    scaleDelta = minResGVolution / resGVolution;
    newResGVolution = minResGVolution;
  }

  if (scaleDelta != 1.0) {
    this.lastScaleDelta_ = scaleDelta;
  }

  // scale anchor point.
  var viewportPosition = map.getViewport().getBoundingClientRect();
  var centroid = GVol.interaction.Pointer.centroid(this.targetPointers);
  centroid[0] -= viewportPosition.left;
  centroid[1] -= viewportPosition.top;
  this.anchor_ = map.getCoordinateFromPixel(centroid);

  // scale, bypass the resGVolution constraint
  map.render();
  GVol.interaction.Interaction.zoomWithoutConstraints(view, newResGVolution, this.anchor_);
};


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boGVolean} Stop drag sequence?
 * @this {GVol.interaction.PinchZoom}
 * @private
 */
GVol.interaction.PinchZoom.handleUpEvent_ = function(mapBrowserEvent) {
  if (this.targetPointers.length < 2) {
    var map = mapBrowserEvent.map;
    var view = map.getView();
    view.setHint(GVol.ViewHint.INTERACTING, -1);
    var resGVolution = view.getResGVolution();
    if (this.constrainResGVolution_ ||
        resGVolution < view.getMinResGVolution() ||
        resGVolution > view.getMaxResGVolution()) {
      // Zoom to final resGVolution, 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;
      GVol.interaction.Interaction.zoom(view, resGVolution,
          this.anchor_, this.duration_, direction);
    }
    return false;
  } else {
    return true;
  }
};


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boGVolean} Start drag sequence?
 * @this {GVol.interaction.PinchZoom}
 * @private
 */
GVol.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(GVol.ViewHint.INTERACTING, 1);
    }
    return true;
  } else {
    return false;
  }
};


/**
 * @inheritDoc
 */
GVol.interaction.PinchZoom.prototype.shouldStopEvent = GVol.functions.FALSE;

goog.provide('GVol.interaction');

goog.require('GVol.CGVollection');
goog.require('GVol.Kinetic');
goog.require('GVol.interaction.DoubleClickZoom');
goog.require('GVol.interaction.DragPan');
goog.require('GVol.interaction.DragRotate');
goog.require('GVol.interaction.DragZoom');
goog.require('GVol.interaction.KeyboardPan');
goog.require('GVol.interaction.KeyboardZoom');
goog.require('GVol.interaction.MouseWheelZoom');
goog.require('GVol.interaction.PinchRotate');
goog.require('GVol.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 GVol.interaction.Interaction} instances and insert them into a
 * {@link GVol.CGVollection} in the order you want before creating your
 * {@link GVol.Map} instance. The default set of interactions, in sequence, is:
 * * {@link GVol.interaction.DragRotate}
 * * {@link GVol.interaction.DoubleClickZoom}
 * * {@link GVol.interaction.DragPan}
 * * {@link GVol.interaction.PinchRotate}
 * * {@link GVol.interaction.PinchZoom}
 * * {@link GVol.interaction.KeyboardPan}
 * * {@link GVol.interaction.KeyboardZoom}
 * * {@link GVol.interaction.MouseWheelZoom}
 * * {@link GVol.interaction.DragZoom}
 *
 * @param {GVolx.interaction.DefaultsOptions=} opt_options Defaults options.
 * @return {GVol.CGVollection.<GVol.interaction.Interaction>} A cGVollection of
 * interactions to be used with the GVol.Map constructor's interactions option.
 * @api
 */
GVol.interaction.defaults = function(opt_options) {

  var options = opt_options ? opt_options : {};

  var interactions = new GVol.CGVollection();

  var kinetic = new GVol.Kinetic(-0.005, 0.05, 100);

  var altShiftDragRotate = options.altShiftDragRotate !== undefined ?
    options.altShiftDragRotate : true;
  if (altShiftDragRotate) {
    interactions.push(new GVol.interaction.DragRotate());
  }

  var doubleClickZoom = options.doubleClickZoom !== undefined ?
    options.doubleClickZoom : true;
  if (doubleClickZoom) {
    interactions.push(new GVol.interaction.DoubleClickZoom({
      delta: options.zoomDelta,
      duration: options.zoomDuration
    }));
  }

  var dragPan = options.dragPan !== undefined ? options.dragPan : true;
  if (dragPan) {
    interactions.push(new GVol.interaction.DragPan({
      kinetic: kinetic
    }));
  }

  var pinchRotate = options.pinchRotate !== undefined ? options.pinchRotate :
    true;
  if (pinchRotate) {
    interactions.push(new GVol.interaction.PinchRotate());
  }

  var pinchZoom = options.pinchZoom !== undefined ? options.pinchZoom : true;
  if (pinchZoom) {
    interactions.push(new GVol.interaction.PinchZoom({
      constrainResGVolution: options.constrainResGVolution,
      duration: options.zoomDuration
    }));
  }

  var keyboard = options.keyboard !== undefined ? options.keyboard : true;
  if (keyboard) {
    interactions.push(new GVol.interaction.KeyboardPan());
    interactions.push(new GVol.interaction.KeyboardZoom({
      delta: options.zoomDelta,
      duration: options.zoomDuration
    }));
  }

  var mouseWheelZoom = options.mouseWheelZoom !== undefined ?
    options.mouseWheelZoom : true;
  if (mouseWheelZoom) {
    interactions.push(new GVol.interaction.MouseWheelZoom({
      constrainResGVolution: options.constrainResGVolution,
      duration: options.zoomDuration
    }));
  }

  var shiftDragZoom = options.shiftDragZoom !== undefined ?
    options.shiftDragZoom : true;
  if (shiftDragZoom) {
    interactions.push(new GVol.interaction.DragZoom({
      duration: options.zoomDuration
    }));
  }

  return interactions;

};

goog.provide('GVol.layer.Property');

/**
 * @enum {string}
 */
GVol.layer.Property = {
  OPACITY: 'opacity',
  VISIBLE: 'visible',
  EXTENT: 'extent',
  Z_INDEX: 'zIndex',
  MAX_RESOLUTION: 'maxResGVolution',
  MIN_RESOLUTION: 'minResGVolution',
  SOURCE: 'source'
};

goog.provide('GVol.layer.Base');

goog.require('GVol');
goog.require('GVol.Object');
goog.require('GVol.layer.Property');
goog.require('GVol.math');
goog.require('GVol.obj');


/**
 * @classdesc
 * Abstract base class; normally only used for creating subclasses and not
 * instantiated in apps.
 * Note that with `GVol.layer.Base` and all its subclasses, any property set in
 * the options is set as a {@link GVol.Object} property on the layer object, so
 * is observable, and has get/set accessors.
 *
 * @constructor
 * @abstract
 * @extends {GVol.Object}
 * @param {GVolx.layer.BaseOptions} options Layer options.
 * @api
 */
GVol.layer.Base = function(options) {

  GVol.Object.call(this);

  /**
   * @type {Object.<string, *>}
   */
  var properties = GVol.obj.assign({}, options);
  properties[GVol.layer.Property.OPACITY] =
      options.opacity !== undefined ? options.opacity : 1;
  properties[GVol.layer.Property.VISIBLE] =
      options.visible !== undefined ? options.visible : true;
  properties[GVol.layer.Property.Z_INDEX] =
      options.zIndex !== undefined ? options.zIndex : 0;
  properties[GVol.layer.Property.MAX_RESOLUTION] =
      options.maxResGVolution !== undefined ? options.maxResGVolution : Infinity;
  properties[GVol.layer.Property.MIN_RESOLUTION] =
      options.minResGVolution !== undefined ? options.minResGVolution : 0;

  this.setProperties(properties);

  /**
   * @type {GVol.LayerState}
   * @private
   */
  this.state_ = /** @type {GVol.LayerState} */ ({
    layer: /** @type {GVol.layer.Layer} */ (this),
    managed: true
  });

};
GVol.inherits(GVol.layer.Base, GVol.Object);


/**
 * Create a renderer for this layer.
 * @abstract
 * @param {GVol.renderer.Map} mapRenderer The map renderer.
 * @return {GVol.renderer.Layer} A layer renderer.
 */
GVol.layer.Base.prototype.createRenderer = function(mapRenderer) {};


/**
 * @return {GVol.LayerState} Layer state.
 */
GVol.layer.Base.prototype.getLayerState = function() {
  this.state_.opacity = GVol.math.clamp(this.getOpacity(), 0, 1);
  this.state_.sourceState = this.getSourceState();
  this.state_.visible = this.getVisible();
  this.state_.extent = this.getExtent();
  this.state_.zIndex = this.getZIndex();
  this.state_.maxResGVolution = this.getMaxResGVolution();
  this.state_.minResGVolution = Math.max(this.getMinResGVolution(), 0);

  return this.state_;
};


/**
 * @abstract
 * @param {Array.<GVol.layer.Layer>=} opt_array Array of layers (to be
 *     modified in place).
 * @return {Array.<GVol.layer.Layer>} Array of layers.
 */
GVol.layer.Base.prototype.getLayersArray = function(opt_array) {};


/**
 * @abstract
 * @param {Array.<GVol.LayerState>=} opt_states Optional list of layer
 *     states (to be modified in place).
 * @return {Array.<GVol.LayerState>} List of layer states.
 */
GVol.layer.Base.prototype.getLayerStatesArray = function(opt_states) {};


/**
 * Return the {@link GVol.Extent extent} of the layer or `undefined` if it
 * will be visible regardless of extent.
 * @return {GVol.Extent|undefined} The layer extent.
 * @observable
 * @api
 */
GVol.layer.Base.prototype.getExtent = function() {
  return /** @type {GVol.Extent|undefined} */ (
    this.get(GVol.layer.Property.EXTENT));
};


/**
 * Return the maximum resGVolution of the layer.
 * @return {number} The maximum resGVolution of the layer.
 * @observable
 * @api
 */
GVol.layer.Base.prototype.getMaxResGVolution = function() {
  return /** @type {number} */ (
    this.get(GVol.layer.Property.MAX_RESOLUTION));
};


/**
 * Return the minimum resGVolution of the layer.
 * @return {number} The minimum resGVolution of the layer.
 * @observable
 * @api
 */
GVol.layer.Base.prototype.getMinResGVolution = function() {
  return /** @type {number} */ (
    this.get(GVol.layer.Property.MIN_RESOLUTION));
};


/**
 * Return the opacity of the layer (between 0 and 1).
 * @return {number} The opacity of the layer.
 * @observable
 * @api
 */
GVol.layer.Base.prototype.getOpacity = function() {
  return /** @type {number} */ (this.get(GVol.layer.Property.OPACITY));
};


/**
 * @abstract
 * @return {GVol.source.State} Source state.
 */
GVol.layer.Base.prototype.getSourceState = function() {};


/**
 * Return the visibility of the layer (`true` or `false`).
 * @return {boGVolean} The visibility of the layer.
 * @observable
 * @api
 */
GVol.layer.Base.prototype.getVisible = function() {
  return /** @type {boGVolean} */ (this.get(GVol.layer.Property.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
 */
GVol.layer.Base.prototype.getZIndex = function() {
  return /** @type {number} */ (this.get(GVol.layer.Property.Z_INDEX));
};


/**
 * Set the extent at which the layer is visible.  If `undefined`, the layer
 * will be visible at all extents.
 * @param {GVol.Extent|undefined} extent The extent of the layer.
 * @observable
 * @api
 */
GVol.layer.Base.prototype.setExtent = function(extent) {
  this.set(GVol.layer.Property.EXTENT, extent);
};


/**
 * Set the maximum resGVolution at which the layer is visible.
 * @param {number} maxResGVolution The maximum resGVolution of the layer.
 * @observable
 * @api
 */
GVol.layer.Base.prototype.setMaxResGVolution = function(maxResGVolution) {
  this.set(GVol.layer.Property.MAX_RESOLUTION, maxResGVolution);
};


/**
 * Set the minimum resGVolution at which the layer is visible.
 * @param {number} minResGVolution The minimum resGVolution of the layer.
 * @observable
 * @api
 */
GVol.layer.Base.prototype.setMinResGVolution = function(minResGVolution) {
  this.set(GVol.layer.Property.MIN_RESOLUTION, minResGVolution);
};


/**
 * Set the opacity of the layer, allowed values range from 0 to 1.
 * @param {number} opacity The opacity of the layer.
 * @observable
 * @api
 */
GVol.layer.Base.prototype.setOpacity = function(opacity) {
  this.set(GVol.layer.Property.OPACITY, opacity);
};


/**
 * Set the visibility of the layer (`true` or `false`).
 * @param {boGVolean} visible The visibility of the layer.
 * @observable
 * @api
 */
GVol.layer.Base.prototype.setVisible = function(visible) {
  this.set(GVol.layer.Property.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
 */
GVol.layer.Base.prototype.setZIndex = function(zindex) {
  this.set(GVol.layer.Property.Z_INDEX, zindex);
};

goog.provide('GVol.source.State');


/**
 * State of the source, one of 'undefined', 'loading', 'ready' or 'error'.
 * @enum {string}
 */
GVol.source.State = {
  UNDEFINED: 'undefined',
  LOADING: 'loading',
  READY: 'ready',
  ERROR: 'error'
};


goog.provide('GVol.layer.Group');

goog.require('GVol');
goog.require('GVol.CGVollection');
goog.require('GVol.CGVollectionEventType');
goog.require('GVol.Object');
goog.require('GVol.ObjectEventType');
goog.require('GVol.asserts');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.extent');
goog.require('GVol.layer.Base');
goog.require('GVol.obj');
goog.require('GVol.source.State');


/**
 * @classdesc
 * A {@link GVol.CGVollection} of layers that are handled together.
 *
 * A generic `change` event is triggered when the group/CGVollection changes.
 *
 * @constructor
 * @extends {GVol.layer.Base}
 * @param {GVolx.layer.GroupOptions=} opt_options Layer options.
 * @api
 */
GVol.layer.Group = function(opt_options) {

  var options = opt_options || {};
  var baseOptions = /** @type {GVolx.layer.GroupOptions} */
      (GVol.obj.assign({}, options));
  delete baseOptions.layers;

  var layers = options.layers;

  GVol.layer.Base.call(this, baseOptions);

  /**
   * @private
   * @type {Array.<GVol.EventsKey>}
   */
  this.layersListenerKeys_ = [];

  /**
   * @private
   * @type {Object.<string, Array.<GVol.EventsKey>>}
   */
  this.listenerKeys_ = {};

  GVol.events.listen(this,
      GVol.Object.getChangeEventType(GVol.layer.Group.Property_.LAYERS),
      this.handleLayersChanged_, this);

  if (layers) {
    if (Array.isArray(layers)) {
      layers = new GVol.CGVollection(layers.slice(), {unique: true});
    } else {
      GVol.asserts.assert(layers instanceof GVol.CGVollection,
          43); // Expected `layers` to be an array or an `GVol.CGVollection`
      layers = layers;
    }
  } else {
    layers = new GVol.CGVollection(undefined, {unique: true});
  }

  this.setLayers(layers);

};
GVol.inherits(GVol.layer.Group, GVol.layer.Base);


/**
 * @inheritDoc
 */
GVol.layer.Group.prototype.createRenderer = function(mapRenderer) {};


/**
 * @private
 */
GVol.layer.Group.prototype.handleLayerChange_ = function() {
  this.changed();
};


/**
 * @param {GVol.events.Event} event Event.
 * @private
 */
GVol.layer.Group.prototype.handleLayersChanged_ = function(event) {
  this.layersListenerKeys_.forEach(GVol.events.unlistenByKey);
  this.layersListenerKeys_.length = 0;

  var layers = this.getLayers();
  this.layersListenerKeys_.push(
      GVol.events.listen(layers, GVol.CGVollectionEventType.ADD,
          this.handleLayersAdd_, this),
      GVol.events.listen(layers, GVol.CGVollectionEventType.REMOVE,
          this.handleLayersRemove_, this));

  for (var id in this.listenerKeys_) {
    this.listenerKeys_[id].forEach(GVol.events.unlistenByKey);
  }
  GVol.obj.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_[GVol.getUid(layer).toString()] = [
      GVol.events.listen(layer, GVol.ObjectEventType.PROPERTYCHANGE,
          this.handleLayerChange_, this),
      GVol.events.listen(layer, GVol.events.EventType.CHANGE,
          this.handleLayerChange_, this)
    ];
  }

  this.changed();
};


/**
 * @param {GVol.CGVollection.Event} cGVollectionEvent CGVollection event.
 * @private
 */
GVol.layer.Group.prototype.handleLayersAdd_ = function(cGVollectionEvent) {
  var layer = /** @type {GVol.layer.Base} */ (cGVollectionEvent.element);
  var key = GVol.getUid(layer).toString();
  this.listenerKeys_[key] = [
    GVol.events.listen(layer, GVol.ObjectEventType.PROPERTYCHANGE,
        this.handleLayerChange_, this),
    GVol.events.listen(layer, GVol.events.EventType.CHANGE,
        this.handleLayerChange_, this)
  ];
  this.changed();
};


/**
 * @param {GVol.CGVollection.Event} cGVollectionEvent CGVollection event.
 * @private
 */
GVol.layer.Group.prototype.handleLayersRemove_ = function(cGVollectionEvent) {
  var layer = /** @type {GVol.layer.Base} */ (cGVollectionEvent.element);
  var key = GVol.getUid(layer).toString();
  this.listenerKeys_[key].forEach(GVol.events.unlistenByKey);
  delete this.listenerKeys_[key];
  this.changed();
};


/**
 * Returns the {@link GVol.CGVollection cGVollection} of {@link GVol.layer.Layer layers}
 * in this group.
 * @return {!GVol.CGVollection.<GVol.layer.Base>} CGVollection of
 *   {@link GVol.layer.Base layers} that are part of this group.
 * @observable
 * @api
 */
GVol.layer.Group.prototype.getLayers = function() {
  return /** @type {!GVol.CGVollection.<GVol.layer.Base>} */ (this.get(
      GVol.layer.Group.Property_.LAYERS));
};


/**
 * Set the {@link GVol.CGVollection cGVollection} of {@link GVol.layer.Layer layers}
 * in this group.
 * @param {!GVol.CGVollection.<GVol.layer.Base>} layers CGVollection of
 *   {@link GVol.layer.Base layers} that are part of this group.
 * @observable
 * @api
 */
GVol.layer.Group.prototype.setLayers = function(layers) {
  this.set(GVol.layer.Group.Property_.LAYERS, layers);
};


/**
 * @inheritDoc
 */
GVol.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
 */
GVol.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.maxResGVolution = Math.min(
        layerState.maxResGVolution, ownLayerState.maxResGVolution);
    layerState.minResGVolution = Math.max(
        layerState.minResGVolution, ownLayerState.minResGVolution);
    if (ownLayerState.extent !== undefined) {
      if (layerState.extent !== undefined) {
        layerState.extent = GVol.extent.getIntersection(
            layerState.extent, ownLayerState.extent);
      } else {
        layerState.extent = ownLayerState.extent;
      }
    }
  }

  return states;
};


/**
 * @inheritDoc
 */
GVol.layer.Group.prototype.getSourceState = function() {
  return GVol.source.State.READY;
};

/**
 * @enum {string}
 * @private
 */
GVol.layer.Group.Property_ = {
  LAYERS: 'layers'
};

goog.provide('GVol.render.EventType');

/**
 * @enum {string}
 */
GVol.render.EventType = {
  /**
   * @event GVol.render.Event#postcompose
   * @api
   */
  POSTCOMPOSE: 'postcompose',
  /**
   * @event GVol.render.Event#precompose
   * @api
   */
  PRECOMPOSE: 'precompose',
  /**
   * @event GVol.render.Event#render
   * @api
   */
  RENDER: 'render'
};

goog.provide('GVol.layer.Layer');

goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol');
goog.require('GVol.Object');
goog.require('GVol.layer.Base');
goog.require('GVol.layer.Property');
goog.require('GVol.obj');
goog.require('GVol.render.EventType');
goog.require('GVol.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 GVol.Map#addLayer}. Components
 * like {@link GVol.interaction.Select} use unmanaged layers internally. These
 * unmanaged layers are associated with the map using
 * {@link GVol.layer.Layer#setMap} instead.
 *
 * A generic `change` event is fired when the state of the source changes.
 *
 * @constructor
 * @abstract
 * @extends {GVol.layer.Base}
 * @fires GVol.render.Event
 * @param {GVolx.layer.LayerOptions} options Layer options.
 * @api
 */
GVol.layer.Layer = function(options) {

  var baseOptions = GVol.obj.assign({}, options);
  delete baseOptions.source;

  GVol.layer.Base.call(this, /** @type {GVolx.layer.BaseOptions} */ (baseOptions));

  /**
   * @private
   * @type {?GVol.EventsKey}
   */
  this.mapPrecomposeKey_ = null;

  /**
   * @private
   * @type {?GVol.EventsKey}
   */
  this.mapRenderKey_ = null;

  /**
   * @private
   * @type {?GVol.EventsKey}
   */
  this.sourceChangeKey_ = null;

  if (options.map) {
    this.setMap(options.map);
  }

  GVol.events.listen(this,
      GVol.Object.getChangeEventType(GVol.layer.Property.SOURCE),
      this.handleSourcePropertyChange_, this);

  var source = options.source ? options.source : null;
  this.setSource(source);
};
GVol.inherits(GVol.layer.Layer, GVol.layer.Base);


/**
 * Return `true` if the layer is visible, and if the passed resGVolution is
 * between the layer's minResGVolution and maxResGVolution. The comparison is
 * inclusive for `minResGVolution` and exclusive for `maxResGVolution`.
 * @param {GVol.LayerState} layerState Layer state.
 * @param {number} resGVolution ResGVolution.
 * @return {boGVolean} The layer is visible at the given resGVolution.
 */
GVol.layer.Layer.visibleAtResGVolution = function(layerState, resGVolution) {
  return layerState.visible && resGVolution >= layerState.minResGVolution &&
      resGVolution < layerState.maxResGVolution;
};


/**
 * @inheritDoc
 */
GVol.layer.Layer.prototype.getLayersArray = function(opt_array) {
  var array = opt_array ? opt_array : [];
  array.push(this);
  return array;
};


/**
 * @inheritDoc
 */
GVol.layer.Layer.prototype.getLayerStatesArray = function(opt_states) {
  var states = opt_states ? opt_states : [];
  states.push(this.getLayerState());
  return states;
};


/**
 * Get the layer source.
 * @return {GVol.source.Source} The layer source (or `null` if not yet set).
 * @observable
 * @api
 */
GVol.layer.Layer.prototype.getSource = function() {
  var source = this.get(GVol.layer.Property.SOURCE);
  return /** @type {GVol.source.Source} */ (source) || null;
};


/**
  * @inheritDoc
  */
GVol.layer.Layer.prototype.getSourceState = function() {
  var source = this.getSource();
  return !source ? GVol.source.State.UNDEFINED : source.getState();
};


/**
 * @private
 */
GVol.layer.Layer.prototype.handleSourceChange_ = function() {
  this.changed();
};


/**
 * @private
 */
GVol.layer.Layer.prototype.handleSourcePropertyChange_ = function() {
  if (this.sourceChangeKey_) {
    GVol.events.unlistenByKey(this.sourceChangeKey_);
    this.sourceChangeKey_ = null;
  }
  var source = this.getSource();
  if (source) {
    this.sourceChangeKey_ = GVol.events.listen(source,
        GVol.events.EventType.CHANGE, this.handleSourceChange_, 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 cGVollection, and the callback in
 * {@link GVol.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 GVol.Map#addLayer} instead.
 * @param {GVol.Map} map Map.
 * @api
 */
GVol.layer.Layer.prototype.setMap = function(map) {
  if (this.mapPrecomposeKey_) {
    GVol.events.unlistenByKey(this.mapPrecomposeKey_);
    this.mapPrecomposeKey_ = null;
  }
  if (!map) {
    this.changed();
  }
  if (this.mapRenderKey_) {
    GVol.events.unlistenByKey(this.mapRenderKey_);
    this.mapRenderKey_ = null;
  }
  if (map) {
    this.mapPrecomposeKey_ = GVol.events.listen(
        map, GVol.render.EventType.PRECOMPOSE, function(evt) {
          var layerState = this.getLayerState();
          layerState.managed = false;
          layerState.zIndex = Infinity;
          evt.frameState.layerStatesArray.push(layerState);
          evt.frameState.layerStates[GVol.getUid(this)] = layerState;
        }, this);
    this.mapRenderKey_ = GVol.events.listen(
        this, GVol.events.EventType.CHANGE, map.render, map);
    this.changed();
  }
};


/**
 * Set the layer source.
 * @param {GVol.source.Source} source The layer source.
 * @observable
 * @api
 */
GVol.layer.Layer.prototype.setSource = function(source) {
  this.set(GVol.layer.Property.SOURCE, source);
};

goog.provide('GVol.style.IconImageCache');

goog.require('GVol.cGVolor');


/**
 * @constructor
 */
GVol.style.IconImageCache = function() {

  /**
   * @type {Object.<string, GVol.style.IconImage>}
   * @private
   */
  this.cache_ = {};

  /**
   * @type {number}
   * @private
   */
  this.cacheSize_ = 0;

  /**
   * @const
   * @type {number}
   * @private
   */
  this.maxCacheSize_ = 32;
};


/**
 * @param {string} src Src.
 * @param {?string} crossOrigin Cross origin.
 * @param {GVol.CGVolor} cGVolor CGVolor.
 * @return {string} Cache key.
 */
GVol.style.IconImageCache.getKey = function(src, crossOrigin, cGVolor) {
  var cGVolorString = cGVolor ? GVol.cGVolor.asString(cGVolor) : 'null';
  return crossOrigin + ':' + src + ':' + cGVolorString;
};


/**
 * FIXME empty description for jsdoc
 */
GVol.style.IconImageCache.prototype.clear = function() {
  this.cache_ = {};
  this.cacheSize_ = 0;
};


/**
 * FIXME empty description for jsdoc
 */
GVol.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 && !iconImage.hasListener()) {
        delete this.cache_[key];
        --this.cacheSize_;
      }
    }
  }
};


/**
 * @param {string} src Src.
 * @param {?string} crossOrigin Cross origin.
 * @param {GVol.CGVolor} cGVolor CGVolor.
 * @return {GVol.style.IconImage} Icon image.
 */
GVol.style.IconImageCache.prototype.get = function(src, crossOrigin, cGVolor) {
  var key = GVol.style.IconImageCache.getKey(src, crossOrigin, cGVolor);
  return key in this.cache_ ? this.cache_[key] : null;
};


/**
 * @param {string} src Src.
 * @param {?string} crossOrigin Cross origin.
 * @param {GVol.CGVolor} cGVolor CGVolor.
 * @param {GVol.style.IconImage} iconImage Icon image.
 */
GVol.style.IconImageCache.prototype.set = function(src, crossOrigin, cGVolor, iconImage) {
  var key = GVol.style.IconImageCache.getKey(src, crossOrigin, cGVolor);
  this.cache_[key] = iconImage;
  ++this.cacheSize_;
};

goog.provide('GVol.style');

goog.require('GVol.style.IconImageCache');

GVol.style.iconImageCache = new GVol.style.IconImageCache();

goog.provide('GVol.transform');

goog.require('GVol.asserts');


/**
 * CGVollection of affine 2d transformation functions. The functions work on an
 * array of 6 elements. The element order is compatible with the [SVGMatrix
 * interface](https://developer.mozilla.org/en-US/docs/Web/API/SVGMatrix) and is
 * a subset (elements a to f) of a 3x3 martrix:
 * ```
 * [ a c e ]
 * [ b d f ]
 * [ 0 0 1 ]
 * ```
 */


/**
 * @private
 * @type {GVol.Transform}
 */
GVol.transform.tmp_ = new Array(6);


/**
 * Create an identity transform.
 * @return {!GVol.Transform} Identity transform.
 */
GVol.transform.create = function() {
  return [1, 0, 0, 1, 0, 0];
};


/**
 * Resets the given transform to an identity transform.
 * @param {!GVol.Transform} transform Transform.
 * @return {!GVol.Transform} Transform.
 */
GVol.transform.reset = function(transform) {
  return GVol.transform.set(transform, 1, 0, 0, 1, 0, 0);
};


/**
 * Multiply the underlying matrices of two transforms and return the result in
 * the first transform.
 * @param {!GVol.Transform} transform1 Transform parameters of matrix 1.
 * @param {!GVol.Transform} transform2 Transform parameters of matrix 2.
 * @return {!GVol.Transform} transform1 multiplied with transform2.
 */
GVol.transform.multiply = function(transform1, transform2) {
  var a1 = transform1[0];
  var b1 = transform1[1];
  var c1 = transform1[2];
  var d1 = transform1[3];
  var e1 = transform1[4];
  var f1 = transform1[5];
  var a2 = transform2[0];
  var b2 = transform2[1];
  var c2 = transform2[2];
  var d2 = transform2[3];
  var e2 = transform2[4];
  var f2 = transform2[5];

  transform1[0] = a1 * a2 + c1 * b2;
  transform1[1] = b1 * a2 + d1 * b2;
  transform1[2] = a1 * c2 + c1 * d2;
  transform1[3] = b1 * c2 + d1 * d2;
  transform1[4] = a1 * e2 + c1 * f2 + e1;
  transform1[5] = b1 * e2 + d1 * f2 + f1;

  return transform1;
};

/**
 * Set the transform components a-f on a given transform.
 * @param {!GVol.Transform} transform Transform.
 * @param {number} a The a component of the transform.
 * @param {number} b The b component of the transform.
 * @param {number} c The c component of the transform.
 * @param {number} d The d component of the transform.
 * @param {number} e The e component of the transform.
 * @param {number} f The f component of the transform.
 * @return {!GVol.Transform} Matrix with transform applied.
 */
GVol.transform.set = function(transform, a, b, c, d, e, f) {
  transform[0] = a;
  transform[1] = b;
  transform[2] = c;
  transform[3] = d;
  transform[4] = e;
  transform[5] = f;
  return transform;
};


/**
 * Set transform on one matrix from another matrix.
 * @param {!GVol.Transform} transform1 Matrix to set transform to.
 * @param {!GVol.Transform} transform2 Matrix to set transform from.
 * @return {!GVol.Transform} transform1 with transform from transform2 applied.
 */
GVol.transform.setFromArray = function(transform1, transform2) {
  transform1[0] = transform2[0];
  transform1[1] = transform2[1];
  transform1[2] = transform2[2];
  transform1[3] = transform2[3];
  transform1[4] = transform2[4];
  transform1[5] = transform2[5];
  return transform1;
};


/**
 * Transforms the given coordinate with the given transform returning the
 * resulting, transformed coordinate. The coordinate will be modified in-place.
 *
 * @param {GVol.Transform} transform The transformation.
 * @param {GVol.Coordinate|GVol.Pixel} coordinate The coordinate to transform.
 * @return {GVol.Coordinate|GVol.Pixel} return coordinate so that operations can be
 *     chained together.
 */
GVol.transform.apply = function(transform, coordinate) {
  var x = coordinate[0], y = coordinate[1];
  coordinate[0] = transform[0] * x + transform[2] * y + transform[4];
  coordinate[1] = transform[1] * x + transform[3] * y + transform[5];
  return coordinate;
};


/**
 * Applies rotation to the given transform.
 * @param {!GVol.Transform} transform Transform.
 * @param {number} angle Angle in radians.
 * @return {!GVol.Transform} The rotated transform.
 */
GVol.transform.rotate = function(transform, angle) {
  var cos = Math.cos(angle);
  var sin = Math.sin(angle);
  return GVol.transform.multiply(transform,
      GVol.transform.set(GVol.transform.tmp_, cos, sin, -sin, cos, 0, 0));
};


/**
 * Applies scale to a given transform.
 * @param {!GVol.Transform} transform Transform.
 * @param {number} x Scale factor x.
 * @param {number} y Scale factor y.
 * @return {!GVol.Transform} The scaled transform.
 */
GVol.transform.scale = function(transform, x, y) {
  return GVol.transform.multiply(transform,
      GVol.transform.set(GVol.transform.tmp_, x, 0, 0, y, 0, 0));
};


/**
 * Applies translation to the given transform.
 * @param {!GVol.Transform} transform Transform.
 * @param {number} dx Translation x.
 * @param {number} dy Translation y.
 * @return {!GVol.Transform} The translated transform.
 */
GVol.transform.translate = function(transform, dx, dy) {
  return GVol.transform.multiply(transform,
      GVol.transform.set(GVol.transform.tmp_, 1, 0, 0, 1, dx, dy));
};


/**
 * Creates a composite transform given an initial translation, scale, rotation, and
 * final translation (in that order only, not commutative).
 * @param {!GVol.Transform} transform The transform (will be modified in place).
 * @param {number} dx1 Initial translation x.
 * @param {number} dy1 Initial translation y.
 * @param {number} sx Scale factor x.
 * @param {number} sy Scale factor y.
 * @param {number} angle Rotation (in counter-clockwise radians).
 * @param {number} dx2 Final translation x.
 * @param {number} dy2 Final translation y.
 * @return {!GVol.Transform} The composite transform.
 */
GVol.transform.compose = function(transform, dx1, dy1, sx, sy, angle, dx2, dy2) {
  var sin = Math.sin(angle);
  var cos = Math.cos(angle);
  transform[0] = sx * cos;
  transform[1] = sy * sin;
  transform[2] = -sx * sin;
  transform[3] = sy * cos;
  transform[4] = dx2 * sx * cos - dy2 * sx * sin + dx1;
  transform[5] = dx2 * sy * sin + dy2 * sy * cos + dy1;
  return transform;
};


/**
 * Invert the given transform.
 * @param {!GVol.Transform} transform Transform.
 * @return {!GVol.Transform} Inverse of the transform.
 */
GVol.transform.invert = function(transform) {
  var det = GVol.transform.determinant(transform);
  GVol.asserts.assert(det !== 0, 32); // Transformation matrix cannot be inverted

  var a = transform[0];
  var b = transform[1];
  var c = transform[2];
  var d = transform[3];
  var e = transform[4];
  var f = transform[5];

  transform[0] = d / det;
  transform[1] = -b / det;
  transform[2] = -c / det;
  transform[3] = a / det;
  transform[4] = (c * f - d * e) / det;
  transform[5] = -(a * f - b * e) / det;

  return transform;
};


/**
 * Returns the determinant of the given matrix.
 * @param {!GVol.Transform} mat Matrix.
 * @return {number} Determinant.
 */
GVol.transform.determinant = function(mat) {
  return mat[0] * mat[3] - mat[1] * mat[2];
};

goog.provide('GVol.renderer.Map');

goog.require('GVol');
goog.require('GVol.Disposable');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.extent');
goog.require('GVol.functions');
goog.require('GVol.layer.Layer');
goog.require('GVol.style');
goog.require('GVol.transform');


/**
 * @constructor
 * @abstract
 * @extends {GVol.Disposable}
 * @param {Element} container Container.
 * @param {GVol.Map} map Map.
 * @struct
 */
GVol.renderer.Map = function(container, map) {

  GVol.Disposable.call(this);


  /**
   * @private
   * @type {GVol.Map}
   */
  this.map_ = map;

  /**
   * @private
   * @type {Object.<string, GVol.renderer.Layer>}
   */
  this.layerRenderers_ = {};

  /**
   * @private
   * @type {Object.<string, GVol.EventsKey>}
   */
  this.layerRendererListeners_ = {};

};
GVol.inherits(GVol.renderer.Map, GVol.Disposable);


/**
 * @param {GVolx.FrameState} frameState FrameState.
 * @protected
 */
GVol.renderer.Map.prototype.calculateMatrices2D = function(frameState) {
  var viewState = frameState.viewState;
  var coordinateToPixelTransform = frameState.coordinateToPixelTransform;
  var pixelToCoordinateTransform = frameState.pixelToCoordinateTransform;

  GVol.transform.compose(coordinateToPixelTransform,
      frameState.size[0] / 2, frameState.size[1] / 2,
      1 / viewState.resGVolution, -1 / viewState.resGVolution,
      -viewState.rotation,
      -viewState.center[0], -viewState.center[1]);

  GVol.transform.invert(
      GVol.transform.setFromArray(pixelToCoordinateTransform, coordinateToPixelTransform));
};


/**
 * @inheritDoc
 */
GVol.renderer.Map.prototype.disposeInternal = function() {
  for (var id in this.layerRenderers_) {
    this.layerRenderers_[id].dispose();
  }
};


/**
 * @param {GVol.Map} map Map.
 * @param {GVolx.FrameState} frameState Frame state.
 * @private
 */
GVol.renderer.Map.expireIconCache_ = function(map, frameState) {
  var cache = GVol.style.iconImageCache;
  cache.expire();
};


/**
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @param {GVolx.FrameState} frameState FrameState.
 * @param {number} hitTGVolerance Hit tGVolerance in pixels.
 * @param {function(this: S, (GVol.Feature|GVol.render.Feature),
 *     GVol.layer.Layer): T} callback Feature callback.
 * @param {S} thisArg Value to use as `this` when executing `callback`.
 * @param {function(this: U, GVol.layer.Layer): boGVolean} 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
 */
GVol.renderer.Map.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTGVolerance, callback, thisArg,
    layerFilter, thisArg2) {
  var result;
  var viewState = frameState.viewState;
  var viewResGVolution = viewState.resGVolution;

  /**
   * @param {GVol.Feature|GVol.render.Feature} feature Feature.
   * @param {GVol.layer.Layer} layer Layer.
   * @return {?} Callback result.
   */
  function forEachFeatureAtCoordinate(feature, layer) {
    var key = GVol.getUid(feature).toString();
    var managed = frameState.layerStates[GVol.getUid(layer)].managed;
    if (!(key in frameState.skippedFeatureUids && !managed)) {
      return callback.call(thisArg, feature, managed ? layer : null);
    }
  }

  var projection = viewState.projection;

  var translatedCoordinate = coordinate;
  if (projection.canWrapX()) {
    var projectionExtent = projection.getExtent();
    var worldWidth = GVol.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 (GVol.layer.Layer.visibleAtResGVolution(layerState, viewResGVolution) &&
        layerFilter.call(thisArg2, layer)) {
      var layerRenderer = this.getLayerRenderer(layer);
      if (layer.getSource()) {
        result = layerRenderer.forEachFeatureAtCoordinate(
            layer.getSource().getWrapX() ? translatedCoordinate : coordinate,
            frameState, hitTGVolerance, forEachFeatureAtCoordinate, thisArg);
      }
      if (result) {
        return result;
      }
    }
  }
  return undefined;
};


/**
 * @abstract
 * @param {GVol.Pixel} pixel Pixel.
 * @param {GVolx.FrameState} frameState FrameState.
 * @param {function(this: S, GVol.layer.Layer, (Uint8ClampedArray|Uint8Array)): T} callback Layer
 *     callback.
 * @param {S} thisArg Value to use as `this` when executing `callback`.
 * @param {function(this: U, GVol.layer.Layer): boGVolean} 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
 */
GVol.renderer.Map.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg,
    layerFilter, thisArg2) {};


/**
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @param {GVolx.FrameState} frameState FrameState.
 * @param {number} hitTGVolerance Hit tGVolerance in pixels.
 * @param {function(this: U, GVol.layer.Layer): boGVolean} 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 {boGVolean} Is there a feature at the given coordinate?
 * @template U
 */
GVol.renderer.Map.prototype.hasFeatureAtCoordinate = function(coordinate, frameState, hitTGVolerance, layerFilter, thisArg) {
  var hasFeature = this.forEachFeatureAtCoordinate(
      coordinate, frameState, hitTGVolerance, GVol.functions.TRUE, this, layerFilter, thisArg);

  return hasFeature !== undefined;
};


/**
 * @param {GVol.layer.Layer} layer Layer.
 * @protected
 * @return {GVol.renderer.Layer} Layer renderer.
 */
GVol.renderer.Map.prototype.getLayerRenderer = function(layer) {
  var layerKey = GVol.getUid(layer).toString();
  if (layerKey in this.layerRenderers_) {
    return this.layerRenderers_[layerKey];
  } else {
    var layerRenderer = layer.createRenderer(this);
    this.layerRenderers_[layerKey] = layerRenderer;
    this.layerRendererListeners_[layerKey] = GVol.events.listen(layerRenderer,
        GVol.events.EventType.CHANGE, this.handleLayerRendererChange_, this);

    return layerRenderer;
  }
};


/**
 * @param {string} layerKey Layer key.
 * @protected
 * @return {GVol.renderer.Layer} Layer renderer.
 */
GVol.renderer.Map.prototype.getLayerRendererByKey = function(layerKey) {
  return this.layerRenderers_[layerKey];
};


/**
 * @protected
 * @return {Object.<string, GVol.renderer.Layer>} Layer renderers.
 */
GVol.renderer.Map.prototype.getLayerRenderers = function() {
  return this.layerRenderers_;
};


/**
 * @return {GVol.Map} Map.
 */
GVol.renderer.Map.prototype.getMap = function() {
  return this.map_;
};


/**
 * @abstract
 * @return {string} Type
 */
GVol.renderer.Map.prototype.getType = function() {};


/**
 * Handle changes in a layer renderer.
 * @private
 */
GVol.renderer.Map.prototype.handleLayerRendererChange_ = function() {
  this.map_.render();
};


/**
 * @param {string} layerKey Layer key.
 * @return {GVol.renderer.Layer} Layer renderer.
 * @private
 */
GVol.renderer.Map.prototype.removeLayerRendererByKey_ = function(layerKey) {
  var layerRenderer = this.layerRenderers_[layerKey];
  delete this.layerRenderers_[layerKey];

  GVol.events.unlistenByKey(this.layerRendererListeners_[layerKey]);
  delete this.layerRendererListeners_[layerKey];

  return layerRenderer;
};


/**
 * Render.
 * @param {?GVolx.FrameState} frameState Frame state.
 */
GVol.renderer.Map.prototype.renderFrame = GVol.nullFunction;


/**
 * @param {GVol.Map} map Map.
 * @param {GVolx.FrameState} frameState Frame state.
 * @private
 */
GVol.renderer.Map.prototype.removeUnusedLayerRenderers_ = function(map, frameState) {
  var layerKey;
  for (layerKey in this.layerRenderers_) {
    if (!frameState || !(layerKey in frameState.layerStates)) {
      this.removeLayerRendererByKey_(layerKey).dispose();
    }
  }
};


/**
 * @param {GVolx.FrameState} frameState Frame state.
 * @protected
 */
GVol.renderer.Map.prototype.scheduleExpireIconCache = function(frameState) {
  frameState.postRenderFunctions.push(
      /** @type {GVol.PostRenderFunction} */ (GVol.renderer.Map.expireIconCache_)
  );
};


/**
 * @param {!GVolx.FrameState} frameState Frame state.
 * @protected
 */
GVol.renderer.Map.prototype.scheduleRemoveUnusedLayerRenderers = function(frameState) {
  var layerKey;
  for (layerKey in this.layerRenderers_) {
    if (!(layerKey in frameState.layerStates)) {
      frameState.postRenderFunctions.push(
          /** @type {GVol.PostRenderFunction} */ (this.removeUnusedLayerRenderers_.bind(this))
      );
      return;
    }
  }
};


/**
 * @param {GVol.LayerState} state1 First layer state.
 * @param {GVol.LayerState} state2 Second layer state.
 * @return {number} The zIndex difference.
 */
GVol.renderer.Map.sortByZIndex = function(state1, state2) {
  return state1.zIndex - state2.zIndex;
};

goog.provide('GVol.renderer.Type');


/**
 * Available renderers: `'canvas'` or `'webgl'`.
 * @enum {string}
 */
GVol.renderer.Type = {
  CANVAS: 'canvas',
  WEBGL: 'webgl'
};

goog.provide('GVol.render.Event');

goog.require('GVol');
goog.require('GVol.events.Event');


/**
 * @constructor
 * @extends {GVol.events.Event}
 * @implements {GVoli.render.Event}
 * @param {GVol.render.EventType} type Type.
 * @param {GVol.render.VectorContext=} opt_vectorContext Vector context.
 * @param {GVolx.FrameState=} opt_frameState Frame state.
 * @param {?CanvasRenderingContext2D=} opt_context Context.
 * @param {?GVol.webgl.Context=} opt_glContext WebGL Context.
 */
GVol.render.Event = function(
    type, opt_vectorContext, opt_frameState, opt_context,
    opt_glContext) {

  GVol.events.Event.call(this, type);

  /**
   * For canvas, this is an instance of {@link GVol.render.canvas.Immediate}.
   * @type {GVol.render.VectorContext|undefined}
   * @api
   */
  this.vectorContext = opt_vectorContext;

  /**
   * An object representing the current render frame state.
   * @type {GVolx.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 {GVol.webgl.Context|null|undefined}
   * @api
   */
  this.glContext = opt_glContext;

};
GVol.inherits(GVol.render.Event, GVol.events.Event);

goog.provide('GVol.render.canvas');


/**
 * @const
 * @type {string}
 */
GVol.render.canvas.defaultFont = '10px sans-serif';


/**
 * @const
 * @type {GVol.CGVolor}
 */
GVol.render.canvas.defaultFillStyle = [0, 0, 0, 1];


/**
 * @const
 * @type {string}
 */
GVol.render.canvas.defaultLineCap = 'round';


/**
 * @const
 * @type {Array.<number>}
 */
GVol.render.canvas.defaultLineDash = [];


/**
 * @const
 * @type {number}
 */
GVol.render.canvas.defaultLineDashOffset = 0;


/**
 * @const
 * @type {string}
 */
GVol.render.canvas.defaultLineJoin = 'round';


/**
 * @const
 * @type {number}
 */
GVol.render.canvas.defaultMiterLimit = 10;


/**
 * @const
 * @type {GVol.CGVolor}
 */
GVol.render.canvas.defaultStrokeStyle = [0, 0, 0, 1];


/**
 * @const
 * @type {string}
 */
GVol.render.canvas.defaultTextAlign = 'center';


/**
 * @const
 * @type {string}
 */
GVol.render.canvas.defaultTextBaseline = 'middle';


/**
 * @const
 * @type {number}
 */
GVol.render.canvas.defaultLineWidth = 1;


/**
 * @param {CanvasRenderingContext2D} context Context.
 * @param {number} rotation Rotation.
 * @param {number} offsetX X offset.
 * @param {number} offsetY Y offset.
 */
GVol.render.canvas.rotateAtOffset = function(context, rotation, offsetX, offsetY) {
  if (rotation !== 0) {
    context.translate(offsetX, offsetY);
    context.rotate(rotation);
    context.translate(-offsetX, -offsetY);
  }
};

goog.provide('GVol.render.VectorContext');


/**
 * Context for drawing geometries.  A vector context is available on render
 * events and does not need to be constructed directly.
 * @constructor
 * @abstract
 * @struct
 * @api
 */
GVol.render.VectorContext = function() {
};


/**
 * Render a geometry with a custom renderer.
 *
 * @param {GVol.geom.SimpleGeometry} geometry Geometry.
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 * @param {Function} renderer Renderer.
 */
GVol.render.VectorContext.prototype.drawCustom = function(geometry, feature, renderer) {};


/**
 * Render a geometry.
 *
 * @param {GVol.geom.Geometry} geometry The geometry to render.
 */
GVol.render.VectorContext.prototype.drawGeometry = function(geometry) {};


/**
 * Set the rendering style.
 *
 * @param {GVol.style.Style} style The rendering style.
 */
GVol.render.VectorContext.prototype.setStyle = function(style) {};


/**
 * @param {GVol.geom.Circle} circleGeometry Circle geometry.
 * @param {GVol.Feature} feature Feature.
 */
GVol.render.VectorContext.prototype.drawCircle = function(circleGeometry, feature) {};


/**
 * @param {GVol.Feature} feature Feature.
 * @param {GVol.style.Style} style Style.
 */
GVol.render.VectorContext.prototype.drawFeature = function(feature, style) {};


/**
 * @param {GVol.geom.GeometryCGVollection} geometryCGVollectionGeometry Geometry
 *     cGVollection.
 * @param {GVol.Feature} feature Feature.
 */
GVol.render.VectorContext.prototype.drawGeometryCGVollection = function(geometryCGVollectionGeometry, feature) {};


/**
 * @param {GVol.geom.LineString|GVol.render.Feature} lineStringGeometry Line
 *     string geometry.
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 */
GVol.render.VectorContext.prototype.drawLineString = function(lineStringGeometry, feature) {};


/**
 * @param {GVol.geom.MultiLineString|GVol.render.Feature} multiLineStringGeometry
 *     MultiLineString geometry.
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 */
GVol.render.VectorContext.prototype.drawMultiLineString = function(multiLineStringGeometry, feature) {};


/**
 * @param {GVol.geom.MultiPoint|GVol.render.Feature} multiPointGeometry MultiPoint
 *     geometry.
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 */
GVol.render.VectorContext.prototype.drawMultiPoint = function(multiPointGeometry, feature) {};


/**
 * @param {GVol.geom.MultiPGVolygon} multiPGVolygonGeometry MultiPGVolygon geometry.
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 */
GVol.render.VectorContext.prototype.drawMultiPGVolygon = function(multiPGVolygonGeometry, feature) {};


/**
 * @param {GVol.geom.Point|GVol.render.Feature} pointGeometry Point geometry.
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 */
GVol.render.VectorContext.prototype.drawPoint = function(pointGeometry, feature) {};


/**
 * @param {GVol.geom.PGVolygon|GVol.render.Feature} pGVolygonGeometry PGVolygon
 *     geometry.
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 */
GVol.render.VectorContext.prototype.drawPGVolygon = function(pGVolygonGeometry, feature) {};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {GVol.geom.Geometry|GVol.render.Feature} geometry Geometry.
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 */
GVol.render.VectorContext.prototype.drawText = function(flatCoordinates, offset, end, stride, geometry, feature) {};


/**
 * @param {GVol.style.Fill} fillStyle Fill style.
 * @param {GVol.style.Stroke} strokeStyle Stroke style.
 */
GVol.render.VectorContext.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) {};


/**
 * @param {GVol.style.Image} imageStyle Image style.
 */
GVol.render.VectorContext.prototype.setImageStyle = function(imageStyle) {};


/**
 * @param {GVol.style.Text} textStyle Text style.
 */
GVol.render.VectorContext.prototype.setTextStyle = function(textStyle) {};

// FIXME test, especially pGVolygons with hGVoles and multipGVolygons
// FIXME need to handle large thick features (where pixel size matters)
// FIXME add offset and end to GVol.geom.flat.transform.transform2D?

goog.provide('GVol.render.canvas.Immediate');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.cGVolorlike');
goog.require('GVol.extent');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.geom.SimpleGeometry');
goog.require('GVol.geom.flat.transform');
goog.require('GVol.has');
goog.require('GVol.render.VectorContext');
goog.require('GVol.render.canvas');
goog.require('GVol.transform');


/**
 * @classdesc
 * A concrete subclass of {@link GVol.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 GVol.render.Event} object associated with postcompose, precompose and
 * render events emitted by layers and maps.
 *
 * @constructor
 * @extends {GVol.render.VectorContext}
 * @param {CanvasRenderingContext2D} context Context.
 * @param {number} pixelRatio Pixel ratio.
 * @param {GVol.Extent} extent Extent.
 * @param {GVol.Transform} transform Transform.
 * @param {number} viewRotation View rotation.
 * @struct
 */
GVol.render.canvas.Immediate = function(context, pixelRatio, extent, transform, viewRotation) {
  GVol.render.VectorContext.call(this);

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.context_ = context;

  /**
   * @private
   * @type {number}
   */
  this.pixelRatio_ = pixelRatio;

  /**
   * @private
   * @type {GVol.Extent}
   */
  this.extent_ = extent;

  /**
   * @private
   * @type {GVol.Transform}
   */
  this.transform_ = transform;

  /**
   * @private
   * @type {number}
   */
  this.viewRotation_ = viewRotation;

  /**
   * @private
   * @type {?GVol.CanvasFillState}
   */
  this.contextFillState_ = null;

  /**
   * @private
   * @type {?GVol.CanvasStrokeState}
   */
  this.contextStrokeState_ = null;

  /**
   * @private
   * @type {?GVol.CanvasTextState}
   */
  this.contextTextState_ = null;

  /**
   * @private
   * @type {?GVol.CanvasFillState}
   */
  this.fillState_ = null;

  /**
   * @private
   * @type {?GVol.CanvasStrokeState}
   */
  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 {boGVolean}
   */
  this.imageRotateWithView_ = false;

  /**
   * @private
   * @type {number}
   */
  this.imageRotation_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.imageScale_ = 0;

  /**
   * @private
   * @type {boGVolean}
   */
  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 {boGVolean}
   */
  this.textRotateWithView_ = false;

  /**
   * @private
   * @type {number}
   */
  this.textRotation_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.textScale_ = 0;

  /**
   * @private
   * @type {?GVol.CanvasFillState}
   */
  this.textFillState_ = null;

  /**
   * @private
   * @type {?GVol.CanvasStrokeState}
   */
  this.textStrokeState_ = null;

  /**
   * @private
   * @type {?GVol.CanvasTextState}
   */
  this.textState_ = null;

  /**
   * @private
   * @type {Array.<number>}
   */
  this.pixelCoordinates_ = [];

  /**
   * @private
   * @type {GVol.Transform}
   */
  this.tmpLocalTransform_ = GVol.transform.create();

};
GVol.inherits(GVol.render.canvas.Immediate, GVol.render.VectorContext);


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @private
 */
GVol.render.canvas.Immediate.prototype.drawImages_ = function(flatCoordinates, offset, end, stride) {
  if (!this.image_) {
    return;
  }
  var pixelCoordinates = GVol.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_;
      GVol.transform.compose(localTransform,
          centerX, centerY,
          this.imageScale_, this.imageScale_,
          rotation,
          -centerX, -centerY);
      context.setTransform.apply(context, localTransform);
    }
    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
 */
GVol.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_);
  var pixelCoordinates = GVol.geom.flat.transform.transform2D(
      flatCoordinates, offset, end, stride, this.transform_,
      this.pixelCoordinates_);
  var context = this.context_;
  var rotation = this.textRotation_;
  if (this.textRotateWithView_) {
    rotation += this.viewRotation_;
  }
  for (; offset < end; offset += stride) {
    var x = pixelCoordinates[offset] + this.textOffsetX_;
    var y = pixelCoordinates[offset + 1] + this.textOffsetY_;
    if (rotation !== 0 || this.textScale_ != 1) {
      var localTransform = GVol.transform.compose(this.tmpLocalTransform_,
          x, y,
          this.textScale_, this.textScale_,
          rotation,
          -x, -y);
      context.setTransform.apply(context, localTransform);
    }
    if (this.textStrokeState_) {
      context.strokeText(this.text_, x, y);
    }
    if (this.textFillState_) {
      context.fillText(this.text_, x, y);
    }
  }
  if (rotation !== 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 {boGVolean} close Close.
 * @private
 * @return {number} end End.
 */
GVol.render.canvas.Immediate.prototype.moveToLineTo_ = function(flatCoordinates, offset, end, stride, close) {
  var context = this.context_;
  var pixelCoordinates = GVol.geom.flat.transform.transform2D(
      flatCoordinates, offset, end, stride, this.transform_,
      this.pixelCoordinates_);
  context.moveTo(pixelCoordinates[0], pixelCoordinates[1]);
  var length = pixelCoordinates.length;
  if (close) {
    length -= 2;
  }
  for (var i = 2; i < length; i += 2) {
    context.lineTo(pixelCoordinates[i], pixelCoordinates[i + 1]);
  }
  if (close) {
    context.closePath();
  }
  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.
 */
GVol.render.canvas.Immediate.prototype.drawRings_ = function(flatCoordinates, offset, ends, stride) {
  var i, ii;
  for (i = 0, ii = ends.length; i < ii; ++i) {
    offset = this.moveToLineTo_(
        flatCoordinates, offset, ends[i], stride, true);
  }
  return offset;
};


/**
 * Render a circle geometry into the canvas.  Rendering is immediate and uses
 * the current fill and stroke styles.
 *
 * @param {GVol.geom.Circle} geometry Circle geometry.
 * @override
 * @api
 */
GVol.render.canvas.Immediate.prototype.drawCircle = function(geometry) {
  if (!GVol.extent.intersects(this.extent_, geometry.getExtent())) {
    return;
  }
  if (this.fillState_ || this.strokeState_) {
    if (this.fillState_) {
      this.setContextFillState_(this.fillState_);
    }
    if (this.strokeState_) {
      this.setContextStrokeState_(this.strokeState_);
    }
    var pixelCoordinates = GVol.geom.SimpleGeometry.transform2D(
        geometry, 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_(geometry.getCenter(), 0, 2, 2);
  }
};


/**
 * Set the rendering style.  Note that since this is an immediate rendering API,
 * any `zIndex` on the provided style will be ignored.
 *
 * @param {GVol.style.Style} style The rendering style.
 * @override
 * @api
 */
GVol.render.canvas.Immediate.prototype.setStyle = function(style) {
  this.setFillStrokeStyle(style.getFill(), style.getStroke());
  this.setImageStyle(style.getImage());
  this.setTextStyle(style.getText());
};


/**
 * Render a geometry into the canvas.  Call
 * {@link GVol.render.canvas.Immediate#setStyle} first to set the rendering style.
 *
 * @param {GVol.geom.Geometry|GVol.render.Feature} geometry The geometry to render.
 * @override
 * @api
 */
GVol.render.canvas.Immediate.prototype.drawGeometry = function(geometry) {
  var type = geometry.getType();
  switch (type) {
    case GVol.geom.GeometryType.POINT:
      this.drawPoint(/** @type {GVol.geom.Point} */ (geometry));
      break;
    case GVol.geom.GeometryType.LINE_STRING:
      this.drawLineString(/** @type {GVol.geom.LineString} */ (geometry));
      break;
    case GVol.geom.GeometryType.POLYGON:
      this.drawPGVolygon(/** @type {GVol.geom.PGVolygon} */ (geometry));
      break;
    case GVol.geom.GeometryType.MULTI_POINT:
      this.drawMultiPoint(/** @type {GVol.geom.MultiPoint} */ (geometry));
      break;
    case GVol.geom.GeometryType.MULTI_LINE_STRING:
      this.drawMultiLineString(/** @type {GVol.geom.MultiLineString} */ (geometry));
      break;
    case GVol.geom.GeometryType.MULTI_POLYGON:
      this.drawMultiPGVolygon(/** @type {GVol.geom.MultiPGVolygon} */ (geometry));
      break;
    case GVol.geom.GeometryType.GEOMETRY_COLLECTION:
      this.drawGeometryCGVollection(/** @type {GVol.geom.GeometryCGVollection} */ (geometry));
      break;
    case GVol.geom.GeometryType.CIRCLE:
      this.drawCircle(/** @type {GVol.geom.Circle} */ (geometry));
      break;
    default:
  }
};


/**
 * Render a feature into the canvas.  Note that any `zIndex` on the provided
 * style will be ignored - features are rendered immediately in the order that
 * this method is called.  If you need `zIndex` support, you should be using an
 * {@link GVol.layer.Vector} instead.
 *
 * @param {GVol.Feature} feature Feature.
 * @param {GVol.style.Style} style Style.
 * @override
 * @api
 */
GVol.render.canvas.Immediate.prototype.drawFeature = function(feature, style) {
  var geometry = style.getGeometryFunction()(feature);
  if (!geometry ||
      !GVol.extent.intersects(this.extent_, geometry.getExtent())) {
    return;
  }
  this.setStyle(style);
  this.drawGeometry(geometry);
};


/**
 * Render a GeometryCGVollection to the canvas.  Rendering is immediate and
 * uses the current styles appropriate for each geometry in the cGVollection.
 *
 * @param {GVol.geom.GeometryCGVollection} geometry Geometry cGVollection.
 * @override
 */
GVol.render.canvas.Immediate.prototype.drawGeometryCGVollection = function(geometry) {
  var geometries = geometry.getGeometriesArray();
  var i, ii;
  for (i = 0, ii = geometries.length; i < ii; ++i) {
    this.drawGeometry(geometries[i]);
  }
};


/**
 * Render a Point geometry into the canvas.  Rendering is immediate and uses
 * the current style.
 *
 * @param {GVol.geom.Point|GVol.render.Feature} geometry Point geometry.
 * @override
 */
GVol.render.canvas.Immediate.prototype.drawPoint = function(geometry) {
  var flatCoordinates = geometry.getFlatCoordinates();
  var stride = geometry.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 {GVol.geom.MultiPoint|GVol.render.Feature} geometry MultiPoint geometry.
 * @override
 */
GVol.render.canvas.Immediate.prototype.drawMultiPoint = function(geometry) {
  var flatCoordinates = geometry.getFlatCoordinates();
  var stride = geometry.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 {GVol.geom.LineString|GVol.render.Feature} geometry LineString geometry.
 * @override
 */
GVol.render.canvas.Immediate.prototype.drawLineString = function(geometry) {
  if (!GVol.extent.intersects(this.extent_, geometry.getExtent())) {
    return;
  }
  if (this.strokeState_) {
    this.setContextStrokeState_(this.strokeState_);
    var context = this.context_;
    var flatCoordinates = geometry.getFlatCoordinates();
    context.beginPath();
    this.moveToLineTo_(flatCoordinates, 0, flatCoordinates.length,
        geometry.getStride(), false);
    context.stroke();
  }
  if (this.text_ !== '') {
    var flatMidpoint = geometry.getFlatMidpoint();
    this.drawText_(flatMidpoint, 0, 2, 2);
  }
};


/**
 * Render a MultiLineString geometry into the canvas.  Rendering is immediate
 * and uses the current style.
 *
 * @param {GVol.geom.MultiLineString|GVol.render.Feature} geometry MultiLineString
 *     geometry.
 * @override
 */
GVol.render.canvas.Immediate.prototype.drawMultiLineString = function(geometry) {
  var geometryExtent = geometry.getExtent();
  if (!GVol.extent.intersects(this.extent_, geometryExtent)) {
    return;
  }
  if (this.strokeState_) {
    this.setContextStrokeState_(this.strokeState_);
    var context = this.context_;
    var flatCoordinates = geometry.getFlatCoordinates();
    var offset = 0;
    var ends = geometry.getEnds();
    var stride = geometry.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 = geometry.getFlatMidpoints();
    this.drawText_(flatMidpoints, 0, flatMidpoints.length, 2);
  }
};


/**
 * Render a PGVolygon geometry into the canvas.  Rendering is immediate and uses
 * the current style.
 *
 * @param {GVol.geom.PGVolygon|GVol.render.Feature} geometry PGVolygon geometry.
 * @override
 */
GVol.render.canvas.Immediate.prototype.drawPGVolygon = function(geometry) {
  if (!GVol.extent.intersects(this.extent_, geometry.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_(geometry.getOrientedFlatCoordinates(),
        0, geometry.getEnds(), geometry.getStride());
    if (this.fillState_) {
      context.fill();
    }
    if (this.strokeState_) {
      context.stroke();
    }
  }
  if (this.text_ !== '') {
    var flatInteriorPoint = geometry.getFlatInteriorPoint();
    this.drawText_(flatInteriorPoint, 0, 2, 2);
  }
};


/**
 * Render MultiPGVolygon geometry into the canvas.  Rendering is immediate and
 * uses the current style.
 * @param {GVol.geom.MultiPGVolygon} geometry MultiPGVolygon geometry.
 * @override
 */
GVol.render.canvas.Immediate.prototype.drawMultiPGVolygon = function(geometry) {
  if (!GVol.extent.intersects(this.extent_, geometry.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 = geometry.getOrientedFlatCoordinates();
    var offset = 0;
    var endss = geometry.getEndss();
    var stride = geometry.getStride();
    var i, ii;
    context.beginPath();
    for (i = 0, ii = endss.length; i < ii; ++i) {
      var ends = endss[i];
      offset = this.drawRings_(flatCoordinates, offset, ends, stride);
    }
    if (this.fillState_) {
      context.fill();
    }
    if (this.strokeState_) {
      context.stroke();
    }
  }
  if (this.text_ !== '') {
    var flatInteriorPoints = geometry.getFlatInteriorPoints();
    this.drawText_(flatInteriorPoints, 0, flatInteriorPoints.length, 2);
  }
};


/**
 * @param {GVol.CanvasFillState} fillState Fill state.
 * @private
 */
GVol.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 {GVol.CanvasStrokeState} strokeState Stroke state.
 * @private
 */
GVol.render.canvas.Immediate.prototype.setContextStrokeState_ = function(strokeState) {
  var context = this.context_;
  var contextStrokeState = this.contextStrokeState_;
  if (!contextStrokeState) {
    context.lineCap = strokeState.lineCap;
    if (GVol.has.CANVAS_LINE_DASH) {
      context.setLineDash(strokeState.lineDash);
      context.lineDashOffset = strokeState.lineDashOffset;
    }
    context.lineJoin = strokeState.lineJoin;
    context.lineWidth = strokeState.lineWidth;
    context.miterLimit = strokeState.miterLimit;
    context.strokeStyle = strokeState.strokeStyle;
    this.contextStrokeState_ = {
      lineCap: strokeState.lineCap,
      lineDash: strokeState.lineDash,
      lineDashOffset: strokeState.lineDashOffset,
      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 (GVol.has.CANVAS_LINE_DASH) {
      if (!GVol.array.equals(
          contextStrokeState.lineDash, strokeState.lineDash)) {
        context.setLineDash(contextStrokeState.lineDash = strokeState.lineDash);
      }
      if (contextStrokeState.lineDashOffset != strokeState.lineDashOffset) {
        contextStrokeState.lineDashOffset = context.lineDashOffset =
            strokeState.lineDashOffset;
      }
    }
    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 {GVol.CanvasTextState} textState Text state.
 * @private
 */
GVol.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 {GVol.style.Fill} fillStyle Fill style.
 * @param {GVol.style.Stroke} strokeStyle Stroke style.
 * @override
 */
GVol.render.canvas.Immediate.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) {
  if (!fillStyle) {
    this.fillState_ = null;
  } else {
    var fillStyleCGVolor = fillStyle.getCGVolor();
    this.fillState_ = {
      fillStyle: GVol.cGVolorlike.asCGVolorLike(fillStyleCGVolor ?
        fillStyleCGVolor : GVol.render.canvas.defaultFillStyle)
    };
  }
  if (!strokeStyle) {
    this.strokeState_ = null;
  } else {
    var strokeStyleCGVolor = strokeStyle.getCGVolor();
    var strokeStyleLineCap = strokeStyle.getLineCap();
    var strokeStyleLineDash = strokeStyle.getLineDash();
    var strokeStyleLineDashOffset = strokeStyle.getLineDashOffset();
    var strokeStyleLineJoin = strokeStyle.getLineJoin();
    var strokeStyleWidth = strokeStyle.getWidth();
    var strokeStyleMiterLimit = strokeStyle.getMiterLimit();
    this.strokeState_ = {
      lineCap: strokeStyleLineCap !== undefined ?
        strokeStyleLineCap : GVol.render.canvas.defaultLineCap,
      lineDash: strokeStyleLineDash ?
        strokeStyleLineDash : GVol.render.canvas.defaultLineDash,
      lineDashOffset: strokeStyleLineDashOffset ?
        strokeStyleLineDashOffset : GVol.render.canvas.defaultLineDashOffset,
      lineJoin: strokeStyleLineJoin !== undefined ?
        strokeStyleLineJoin : GVol.render.canvas.defaultLineJoin,
      lineWidth: this.pixelRatio_ * (strokeStyleWidth !== undefined ?
        strokeStyleWidth : GVol.render.canvas.defaultLineWidth),
      miterLimit: strokeStyleMiterLimit !== undefined ?
        strokeStyleMiterLimit : GVol.render.canvas.defaultMiterLimit,
      strokeStyle: GVol.cGVolorlike.asCGVolorLike(strokeStyleCGVolor ?
        strokeStyleCGVolor : GVol.render.canvas.defaultStrokeStyle)
    };
  }
};


/**
 * Set the image style for subsequent draw operations.  Pass null to remove
 * the image style.
 *
 * @param {GVol.style.Image} imageStyle Image style.
 * @override
 */
GVol.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();
    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.pixelRatio_;
    this.imageSnapToPixel_ = imageStyle.getSnapToPixel();
    this.imageWidth_ = imageSize[0];
  }
};


/**
 * Set the text style for subsequent draw operations.  Pass null to
 * remove the text style.
 *
 * @param {GVol.style.Text} textStyle Text style.
 * @override
 */
GVol.render.canvas.Immediate.prototype.setTextStyle = function(textStyle) {
  if (!textStyle) {
    this.text_ = '';
  } else {
    var textFillStyle = textStyle.getFill();
    if (!textFillStyle) {
      this.textFillState_ = null;
    } else {
      var textFillStyleCGVolor = textFillStyle.getCGVolor();
      this.textFillState_ = {
        fillStyle: GVol.cGVolorlike.asCGVolorLike(textFillStyleCGVolor ?
          textFillStyleCGVolor : GVol.render.canvas.defaultFillStyle)
      };
    }
    var textStrokeStyle = textStyle.getStroke();
    if (!textStrokeStyle) {
      this.textStrokeState_ = null;
    } else {
      var textStrokeStyleCGVolor = textStrokeStyle.getCGVolor();
      var textStrokeStyleLineCap = textStrokeStyle.getLineCap();
      var textStrokeStyleLineDash = textStrokeStyle.getLineDash();
      var textStrokeStyleLineDashOffset = textStrokeStyle.getLineDashOffset();
      var textStrokeStyleLineJoin = textStrokeStyle.getLineJoin();
      var textStrokeStyleWidth = textStrokeStyle.getWidth();
      var textStrokeStyleMiterLimit = textStrokeStyle.getMiterLimit();
      this.textStrokeState_ = {
        lineCap: textStrokeStyleLineCap !== undefined ?
          textStrokeStyleLineCap : GVol.render.canvas.defaultLineCap,
        lineDash: textStrokeStyleLineDash ?
          textStrokeStyleLineDash : GVol.render.canvas.defaultLineDash,
        lineDashOffset: textStrokeStyleLineDashOffset ?
          textStrokeStyleLineDashOffset : GVol.render.canvas.defaultLineDashOffset,
        lineJoin: textStrokeStyleLineJoin !== undefined ?
          textStrokeStyleLineJoin : GVol.render.canvas.defaultLineJoin,
        lineWidth: textStrokeStyleWidth !== undefined ?
          textStrokeStyleWidth : GVol.render.canvas.defaultLineWidth,
        miterLimit: textStrokeStyleMiterLimit !== undefined ?
          textStrokeStyleMiterLimit : GVol.render.canvas.defaultMiterLimit,
        strokeStyle: GVol.cGVolorlike.asCGVolorLike(textStrokeStyleCGVolor ?
          textStrokeStyleCGVolor : GVol.render.canvas.defaultStrokeStyle)
      };
    }
    var textFont = textStyle.getFont();
    var textOffsetX = textStyle.getOffsetX();
    var textOffsetY = textStyle.getOffsetY();
    var textRotateWithView = textStyle.getRotateWithView();
    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 : GVol.render.canvas.defaultFont,
      textAlign: textTextAlign !== undefined ?
        textTextAlign : GVol.render.canvas.defaultTextAlign,
      textBaseline: textTextBaseline !== undefined ?
        textTextBaseline : GVol.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.textRotateWithView_ = textRotateWithView !== undefined ? textRotateWithView : false;
    this.textRotation_ = textRotation !== undefined ? textRotation : 0;
    this.textScale_ = this.pixelRatio_ * (textScale !== undefined ?
      textScale : 1);
  }
};

// FIXME offset panning

goog.provide('GVol.renderer.canvas.Map');

goog.require('GVol.transform');
goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.css');
goog.require('GVol.dom');
goog.require('GVol.layer.Layer');
goog.require('GVol.render.Event');
goog.require('GVol.render.EventType');
goog.require('GVol.render.canvas');
goog.require('GVol.render.canvas.Immediate');
goog.require('GVol.renderer.Map');
goog.require('GVol.renderer.Type');
goog.require('GVol.source.State');


/**
 * @constructor
 * @extends {GVol.renderer.Map}
 * @param {Element} container Container.
 * @param {GVol.Map} map Map.
 */
GVol.renderer.canvas.Map = function(container, map) {

  GVol.renderer.Map.call(this, container, map);

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.context_ = GVol.dom.createCanvasContext2D();

  /**
   * @private
   * @type {HTMLCanvasElement}
   */
  this.canvas_ = this.context_.canvas;

  this.canvas_.style.width = '100%';
  this.canvas_.style.height = '100%';
  this.canvas_.style.display = 'block';
  this.canvas_.className = GVol.css.CLASS_UNSELECTABLE;
  container.insertBefore(this.canvas_, container.childNodes[0] || null);

  /**
   * @private
   * @type {boGVolean}
   */
  this.renderedVisible_ = true;

  /**
   * @private
   * @type {GVol.Transform}
   */
  this.transform_ = GVol.transform.create();

};
GVol.inherits(GVol.renderer.canvas.Map, GVol.renderer.Map);


/**
 * @param {GVol.render.EventType} type Event type.
 * @param {GVolx.FrameState} frameState Frame state.
 * @private
 */
GVol.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 GVol.render.canvas.Immediate(context, pixelRatio,
        extent, transform, rotation);
    var composeEvent = new GVol.render.Event(type, vectorContext,
        frameState, context, null);
    map.dispatchEvent(composeEvent);
  }
};


/**
 * @param {GVolx.FrameState} frameState Frame state.
 * @protected
 * @return {!GVol.Transform} Transform.
 */
GVol.renderer.canvas.Map.prototype.getTransform = function(frameState) {
  var viewState = frameState.viewState;
  var dx1 = this.canvas_.width / 2;
  var dy1 = this.canvas_.height / 2;
  var sx = frameState.pixelRatio / viewState.resGVolution;
  var sy = -sx;
  var angle = -viewState.rotation;
  var dx2 = -viewState.center[0];
  var dy2 = -viewState.center[1];
  return GVol.transform.compose(this.transform_, dx1, dy1, sx, sy, angle, dx2, dy2);
};


/**
 * @inheritDoc
 */
GVol.renderer.canvas.Map.prototype.getType = function() {
  return GVol.renderer.Type.CANVAS;
};


/**
 * @inheritDoc
 */
GVol.renderer.canvas.Map.prototype.renderFrame = function(frameState) {

  if (!frameState) {
    if (this.renderedVisible_) {
      this.canvas_.style.display = 'none';
      this.renderedVisible_ = false;
    }
    return;
  }

  var context = this.context_;
  var pixelRatio = frameState.pixelRatio;
  var width = Math.round(frameState.size[0] * pixelRatio);
  var height = Math.round(frameState.size[1] * pixelRatio);
  if (this.canvas_.width != width || this.canvas_.height != height) {
    this.canvas_.width = width;
    this.canvas_.height = height;
  } else {
    context.clearRect(0, 0, width, height);
  }

  var rotation = frameState.viewState.rotation;

  this.calculateMatrices2D(frameState);

  this.dispatchComposeEvent_(GVol.render.EventType.PRECOMPOSE, frameState);

  var layerStatesArray = frameState.layerStatesArray;
  GVol.array.stableSort(layerStatesArray, GVol.renderer.Map.sortByZIndex);

  if (rotation) {
    context.save();
    GVol.render.canvas.rotateAtOffset(context, rotation, width / 2, height / 2);
  }

  var viewResGVolution = frameState.viewState.resGVolution;
  var i, ii, layer, layerRenderer, layerState;
  for (i = 0, ii = layerStatesArray.length; i < ii; ++i) {
    layerState = layerStatesArray[i];
    layer = layerState.layer;
    layerRenderer = /** @type {GVol.renderer.canvas.Layer} */ (this.getLayerRenderer(layer));
    if (!GVol.layer.Layer.visibleAtResGVolution(layerState, viewResGVolution) ||
        layerState.sourceState != GVol.source.State.READY) {
      continue;
    }
    if (layerRenderer.prepareFrame(frameState, layerState)) {
      layerRenderer.composeFrame(frameState, layerState, context);
    }
  }

  if (rotation) {
    context.restore();
  }

  this.dispatchComposeEvent_(
      GVol.render.EventType.POSTCOMPOSE, frameState);

  if (!this.renderedVisible_) {
    this.canvas_.style.display = '';
    this.renderedVisible_ = true;
  }

  this.scheduleRemoveUnusedLayerRenderers(frameState);
  this.scheduleExpireIconCache(frameState);
};


/**
 * @inheritDoc
 */
GVol.renderer.canvas.Map.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg,
    layerFilter, thisArg2) {
  var result;
  var viewState = frameState.viewState;
  var viewResGVolution = viewState.resGVolution;

  var layerStates = frameState.layerStatesArray;
  var numLayers = layerStates.length;

  var coordinate = GVol.transform.apply(
      frameState.pixelToCoordinateTransform, pixel.slice());

  var i;
  for (i = numLayers - 1; i >= 0; --i) {
    var layerState = layerStates[i];
    var layer = layerState.layer;
    if (GVol.layer.Layer.visibleAtResGVolution(layerState, viewResGVolution) &&
        layerFilter.call(thisArg2, layer)) {
      var layerRenderer = /** @type {GVol.renderer.canvas.Layer} */ (this.getLayerRenderer(layer));
      result = layerRenderer.forEachLayerAtCoordinate(
          coordinate, frameState, callback, thisArg);
      if (result) {
        return result;
      }
    }
  }
  return undefined;
};

goog.provide('GVol.render.ReplayType');


/**
 * @enum {string}
 */
GVol.render.ReplayType = {
  CIRCLE: 'Circle',
  DEFAULT: 'Default',
  IMAGE: 'Image',
  LINE_STRING: 'LineString',
  POLYGON: 'PGVolygon',
  TEXT: 'Text'
};

goog.provide('GVol.render.replay');

goog.require('GVol.render.ReplayType');


/**
 * @const
 * @type {Array.<GVol.render.ReplayType>}
 */
GVol.render.replay.ORDER = [
  GVol.render.ReplayType.POLYGON,
  GVol.render.ReplayType.CIRCLE,
  GVol.render.ReplayType.LINE_STRING,
  GVol.render.ReplayType.IMAGE,
  GVol.render.ReplayType.TEXT,
  GVol.render.ReplayType.DEFAULT
];

goog.provide('GVol.render.ReplayGroup');


/**
 * Base class for replay groups.
 * @constructor
 * @abstract
 */
GVol.render.ReplayGroup = function() {};


/**
 * @abstract
 * @param {number|undefined} zIndex Z index.
 * @param {GVol.render.ReplayType} replayType Replay type.
 * @return {GVol.render.VectorContext} Replay.
 */
GVol.render.ReplayGroup.prototype.getReplay = function(zIndex, replayType) {};


/**
 * @abstract
 * @return {boGVolean} Is empty.
 */
GVol.render.ReplayGroup.prototype.isEmpty = function() {};

goog.provide('GVol.webgl.Shader');

goog.require('GVol');
goog.require('GVol.functions');


if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @abstract
   * @param {string} source Source.
   * @struct
   */
  GVol.webgl.Shader = function(source) {

    /**
     * @private
     * @type {string}
     */
    this.source_ = source;

  };


  /**
   * @abstract
   * @return {number} Type.
   */
  GVol.webgl.Shader.prototype.getType = function() {};


  /**
   * @return {string} Source.
   */
  GVol.webgl.Shader.prototype.getSource = function() {
    return this.source_;
  };


  /**
   * @return {boGVolean} Is animated?
   */
  GVol.webgl.Shader.prototype.isAnimated = GVol.functions.FALSE;

}

goog.provide('GVol.webgl.Fragment');

goog.require('GVol');
goog.require('GVol.webgl');
goog.require('GVol.webgl.Shader');


if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @extends {GVol.webgl.Shader}
   * @param {string} source Source.
   * @struct
   */
  GVol.webgl.Fragment = function(source) {
    GVol.webgl.Shader.call(this, source);
  };
  GVol.inherits(GVol.webgl.Fragment, GVol.webgl.Shader);


  /**
   * @inheritDoc
   */
  GVol.webgl.Fragment.prototype.getType = function() {
    return GVol.webgl.FRAGMENT_SHADER;
  };

}

goog.provide('GVol.webgl.Vertex');

goog.require('GVol');
goog.require('GVol.webgl');
goog.require('GVol.webgl.Shader');


if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @extends {GVol.webgl.Shader}
   * @param {string} source Source.
   * @struct
   */
  GVol.webgl.Vertex = function(source) {
    GVol.webgl.Shader.call(this, source);
  };
  GVol.inherits(GVol.webgl.Vertex, GVol.webgl.Shader);


  /**
   * @inheritDoc
   */
  GVol.webgl.Vertex.prototype.getType = function() {
    return GVol.webgl.VERTEX_SHADER;
  };

}

// This file is automatically generated, do not edit
/* eslint openlayers-internal/no-missing-requires: 0 */
goog.provide('GVol.render.webgl.circlereplay.defaultshader');

goog.require('GVol');
goog.require('GVol.webgl.Fragment');
goog.require('GVol.webgl.Vertex');

if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @extends {GVol.webgl.Fragment}
   * @struct
   */
  GVol.render.webgl.circlereplay.defaultshader.Fragment = function() {
    GVol.webgl.Fragment.call(this, GVol.render.webgl.circlereplay.defaultshader.Fragment.SOURCE);
  };
  GVol.inherits(GVol.render.webgl.circlereplay.defaultshader.Fragment, GVol.webgl.Fragment);


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.circlereplay.defaultshader.Fragment.DEBUG_SOURCE = 'precision mediump float;\nvarying vec2 v_center;\nvarying vec2 v_offset;\nvarying float v_halfWidth;\nvarying float v_pixelRatio;\n\n\n\nuniform float u_opacity;\nuniform vec4 u_fillCGVolor;\nuniform vec4 u_strokeCGVolor;\nuniform vec2 u_size;\n\nvoid main(void) {\n  vec2 windowCenter = vec2((v_center.x + 1.0) / 2.0 * u_size.x * v_pixelRatio,\n      (v_center.y + 1.0) / 2.0 * u_size.y * v_pixelRatio);\n  vec2 windowOffset = vec2((v_offset.x + 1.0) / 2.0 * u_size.x * v_pixelRatio,\n      (v_offset.y + 1.0) / 2.0 * u_size.y * v_pixelRatio);\n  float radius = length(windowCenter - windowOffset);\n  float dist = length(windowCenter - gl_FragCoord.xy);\n  if (dist > radius + v_halfWidth) {\n    if (u_strokeCGVolor.a == 0.0) {\n      gl_FragCGVolor = u_fillCGVolor;\n    } else {\n      gl_FragCGVolor = u_strokeCGVolor;\n    }\n    gl_FragCGVolor.a = gl_FragCGVolor.a - (dist - (radius + v_halfWidth));\n  } else if (u_fillCGVolor.a == 0.0) {\n    // Hooray, no fill, just stroke. We can use real antialiasing.\n    gl_FragCGVolor = u_strokeCGVolor;\n    if (dist < radius - v_halfWidth) {\n      gl_FragCGVolor.a = gl_FragCGVolor.a - (radius - v_halfWidth - dist);\n    }\n  } else {\n    gl_FragCGVolor = u_fillCGVolor;\n    float strokeDist = radius - v_halfWidth;\n    float antialias = 2.0 * v_pixelRatio;\n    if (dist > strokeDist) {\n      gl_FragCGVolor = u_strokeCGVolor;\n    } else if (dist >= strokeDist - antialias) {\n      float step = smoothstep(strokeDist - antialias, strokeDist, dist);\n      gl_FragCGVolor = mix(u_fillCGVolor, u_strokeCGVolor, step);\n    }\n  }\n  gl_FragCGVolor.a = gl_FragCGVolor.a * u_opacity;\n  if (gl_FragCGVolor.a <= 0.0) {\n    discard;\n  }\n}\n';


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.circlereplay.defaultshader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;varying vec2 b;varying float c;varying float d;uniform float m;uniform vec4 n;uniform vec4 o;uniform vec2 p;void main(void){vec2 windowCenter=vec2((a.x+1.0)/2.0*p.x*d,(a.y+1.0)/2.0*p.y*d);vec2 windowOffset=vec2((b.x+1.0)/2.0*p.x*d,(b.y+1.0)/2.0*p.y*d);float radius=length(windowCenter-windowOffset);float dist=length(windowCenter-gl_FragCoord.xy);if(dist>radius+c){if(o.a==0.0){gl_FragCGVolor=n;}else{gl_FragCGVolor=o;}gl_FragCGVolor.a=gl_FragCGVolor.a-(dist-(radius+c));}else if(n.a==0.0){gl_FragCGVolor=o;if(dist<radius-c){gl_FragCGVolor.a=gl_FragCGVolor.a-(radius-c-dist);}} else{gl_FragCGVolor=n;float strokeDist=radius-c;float antialias=2.0*d;if(dist>strokeDist){gl_FragCGVolor=o;}else if(dist>=strokeDist-antialias){float step=smoothstep(strokeDist-antialias,strokeDist,dist);gl_FragCGVolor=mix(n,o,step);}} gl_FragCGVolor.a=gl_FragCGVolor.a*m;if(gl_FragCGVolor.a<=0.0){discard;}}';


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.circlereplay.defaultshader.Fragment.SOURCE = GVol.DEBUG_WEBGL ?
    GVol.render.webgl.circlereplay.defaultshader.Fragment.DEBUG_SOURCE :
    GVol.render.webgl.circlereplay.defaultshader.Fragment.OPTIMIZED_SOURCE;


  GVol.render.webgl.circlereplay.defaultshader.fragment = new GVol.render.webgl.circlereplay.defaultshader.Fragment();


  /**
   * @constructor
   * @extends {GVol.webgl.Vertex}
   * @struct
   */
  GVol.render.webgl.circlereplay.defaultshader.Vertex = function() {
    GVol.webgl.Vertex.call(this, GVol.render.webgl.circlereplay.defaultshader.Vertex.SOURCE);
  };
  GVol.inherits(GVol.render.webgl.circlereplay.defaultshader.Vertex, GVol.webgl.Vertex);


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.circlereplay.defaultshader.Vertex.DEBUG_SOURCE = 'varying vec2 v_center;\nvarying vec2 v_offset;\nvarying float v_halfWidth;\nvarying float v_pixelRatio;\n\n\nattribute vec2 a_position;\nattribute float a_instruction;\nattribute float a_radius;\n\nuniform mat4 u_projectionMatrix;\nuniform mat4 u_offsetScaleMatrix;\nuniform mat4 u_offsetRotateMatrix;\nuniform float u_lineWidth;\nuniform float u_pixelRatio;\n\nvoid main(void) {\n  mat4 offsetMatrix = u_offsetScaleMatrix * u_offsetRotateMatrix;\n  v_center = vec4(u_projectionMatrix * vec4(a_position, 0.0, 1.0)).xy;\n  v_pixelRatio = u_pixelRatio;\n  float lineWidth = u_lineWidth * u_pixelRatio;\n  v_halfWidth = lineWidth / 2.0;\n  if (lineWidth == 0.0) {\n    lineWidth = 2.0 * u_pixelRatio;\n  }\n  vec2 offset;\n  // Radius with anitaliasing (roughly).\n  float radius = a_radius + 3.0 * u_pixelRatio;\n  // Until we get gl_VertexID in WebGL, we store an instruction.\n  if (a_instruction == 0.0) {\n    // Offsetting the edges of the triangle by lineWidth / 2 is necessary, however\n    // we should also leave some space for the antialiasing, thus we offset by lineWidth.\n    offset = vec2(-1.0, 1.0);\n  } else if (a_instruction == 1.0) {\n    offset = vec2(-1.0, -1.0);\n  } else if (a_instruction == 2.0) {\n    offset = vec2(1.0, -1.0);\n  } else {\n    offset = vec2(1.0, 1.0);\n  }\n\n  gl_Position = u_projectionMatrix * vec4(a_position + offset * radius, 0.0, 1.0) +\n      offsetMatrix * vec4(offset * lineWidth, 0.0, 0.0);\n  v_offset = vec4(u_projectionMatrix * vec4(a_position.x + a_radius, a_position.y,\n      0.0, 1.0)).xy;\n\n  if (distance(v_center, v_offset) > 20000.0) {\n    gl_Position = vec4(v_center, 0.0, 1.0);\n  }\n}\n\n\n';


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.circlereplay.defaultshader.Vertex.OPTIMIZED_SOURCE = 'varying vec2 a;varying vec2 b;varying float c;varying float d;attribute vec2 e;attribute float f;attribute float g;uniform mat4 h;uniform mat4 i;uniform mat4 j;uniform float k;uniform float l;void main(void){mat4 offsetMatrix=i*j;a=vec4(h*vec4(e,0.0,1.0)).xy;d=l;float lineWidth=k*l;c=lineWidth/2.0;if(lineWidth==0.0){lineWidth=2.0*l;}vec2 offset;float radius=g+3.0*l;if(f==0.0){offset=vec2(-1.0,1.0);}else if(f==1.0){offset=vec2(-1.0,-1.0);}else if(f==2.0){offset=vec2(1.0,-1.0);}else{offset=vec2(1.0,1.0);}gl_Position=h*vec4(e+offset*radius,0.0,1.0)+offsetMatrix*vec4(offset*lineWidth,0.0,0.0);b=vec4(h*vec4(e.x+g,e.y,0.0,1.0)).xy;if(distance(a,b)>20000.0){gl_Position=vec4(a,0.0,1.0);}}';


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.circlereplay.defaultshader.Vertex.SOURCE = GVol.DEBUG_WEBGL ?
    GVol.render.webgl.circlereplay.defaultshader.Vertex.DEBUG_SOURCE :
    GVol.render.webgl.circlereplay.defaultshader.Vertex.OPTIMIZED_SOURCE;


  GVol.render.webgl.circlereplay.defaultshader.vertex = new GVol.render.webgl.circlereplay.defaultshader.Vertex();


  /**
   * @constructor
   * @param {WebGLRenderingContext} gl GL.
   * @param {WebGLProgram} program Program.
   * @struct
   */
  GVol.render.webgl.circlereplay.defaultshader.Locations = function(gl, program) {

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_fillCGVolor = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_fillCGVolor' : 'n');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_lineWidth = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_lineWidth' : 'k');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_offsetRotateMatrix = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_offsetRotateMatrix' : 'j');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_offsetScaleMatrix = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_offsetScaleMatrix' : 'i');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_opacity = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_opacity' : 'm');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_pixelRatio = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_pixelRatio' : 'l');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_projectionMatrix = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_projectionMatrix' : 'h');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_size = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_size' : 'p');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_strokeCGVolor = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_strokeCGVolor' : 'o');

    /**
     * @type {number}
     */
    this.a_instruction = gl.getAttribLocation(
        program, GVol.DEBUG_WEBGL ? 'a_instruction' : 'f');

    /**
     * @type {number}
     */
    this.a_position = gl.getAttribLocation(
        program, GVol.DEBUG_WEBGL ? 'a_position' : 'e');

    /**
     * @type {number}
     */
    this.a_radius = gl.getAttribLocation(
        program, GVol.DEBUG_WEBGL ? 'a_radius' : 'g');
  };

}

goog.provide('GVol.vec.Mat4');


/**
 * @return {Array.<number>} 4x4 matrix representing a 3D identity transform.
 */
GVol.vec.Mat4.create = function() {
  return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1];
};


/**
 * @param {Array.<number>} mat4 Flattened 4x4 matrix receiving the result.
 * @param {GVol.Transform} transform Transformation matrix.
 * @return {Array.<number>} 2D transformation matrix as flattened 4x4 matrix.
 */
GVol.vec.Mat4.fromTransform = function(mat4, transform) {
  mat4[0] = transform[0];
  mat4[1] = transform[1];
  mat4[4] = transform[2];
  mat4[5] = transform[3];
  mat4[12] = transform[4];
  mat4[13] = transform[5];
  return mat4;
};

goog.provide('GVol.render.webgl.Replay');

goog.require('GVol');
goog.require('GVol.extent');
goog.require('GVol.render.VectorContext');
goog.require('GVol.transform');
goog.require('GVol.vec.Mat4');
goog.require('GVol.webgl');


if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @abstract
   * @extends {GVol.render.VectorContext}
   * @param {number} tGVolerance TGVolerance.
   * @param {GVol.Extent} maxExtent Max extent.
   * @struct
   */
  GVol.render.webgl.Replay = function(tGVolerance, maxExtent) {
    GVol.render.VectorContext.call(this);

    /**
     * @protected
     * @type {number}
     */
    this.tGVolerance = tGVolerance;

    /**
     * @protected
     * @const
     * @type {GVol.Extent}
     */
    this.maxExtent = maxExtent;

    /**
     * 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.
     * @protected
     * @type {GVol.Coordinate}
     */
    this.origin = GVol.extent.getCenter(maxExtent);

    /**
     * @private
     * @type {GVol.Transform}
     */
    this.projectionMatrix_ = GVol.transform.create();

    /**
     * @private
     * @type {GVol.Transform}
     */
    this.offsetRotateMatrix_ = GVol.transform.create();

    /**
     * @private
     * @type {GVol.Transform}
     */
    this.offsetScaleMatrix_ = GVol.transform.create();

    /**
     * @private
     * @type {Array.<number>}
     */
    this.tmpMat4_ = GVol.vec.Mat4.create();

    /**
     * @protected
     * @type {Array.<number>}
     */
    this.indices = [];

    /**
     * @protected
     * @type {?GVol.webgl.Buffer}
     */
    this.indicesBuffer = null;

    /**
     * Start index per feature (the index).
     * @protected
     * @type {Array.<number>}
     */
    this.startIndices = [];

    /**
     * Start index per feature (the feature).
     * @protected
     * @type {Array.<GVol.Feature|GVol.render.Feature>}
     */
    this.startIndicesFeature = [];

    /**
     * @protected
     * @type {Array.<number>}
     */
    this.vertices = [];

    /**
     * @protected
     * @type {?GVol.webgl.Buffer}
     */
    this.verticesBuffer = null;

    /**
     * Optional parameter for PGVolygonReplay instances.
     * @protected
     * @type {GVol.render.webgl.LineStringReplay|undefined}
     */
    this.lineStringReplay = undefined;

  };
  GVol.inherits(GVol.render.webgl.Replay, GVol.render.VectorContext);


  /**
   * @abstract
   * @param {GVol.webgl.Context} context WebGL context.
   * @return {function()} Delete resources function.
   */
  GVol.render.webgl.Replay.prototype.getDeleteResourcesFunction = function(context) {};


  /**
   * @abstract
   * @param {GVol.webgl.Context} context Context.
   */
  GVol.render.webgl.Replay.prototype.finish = function(context) {};


  /**
   * @abstract
   * @protected
   * @param {WebGLRenderingContext} gl gl.
   * @param {GVol.webgl.Context} context Context.
   * @param {GVol.Size} size Size.
   * @param {number} pixelRatio Pixel ratio.
   * @return {GVol.render.webgl.circlereplay.defaultshader.Locations|
              GVol.render.webgl.linestringreplay.defaultshader.Locations|
              GVol.render.webgl.pGVolygonreplay.defaultshader.Locations|
              GVol.render.webgl.texturereplay.defaultshader.Locations} Locations.
   */
  GVol.render.webgl.Replay.prototype.setUpProgram = function(gl, context, size, pixelRatio) {};


  /**
   * @abstract
   * @protected
   * @param {WebGLRenderingContext} gl gl.
   * @param {GVol.render.webgl.circlereplay.defaultshader.Locations|
             GVol.render.webgl.linestringreplay.defaultshader.Locations|
             GVol.render.webgl.pGVolygonreplay.defaultshader.Locations|
             GVol.render.webgl.texturereplay.defaultshader.Locations} locations Locations.
   */
  GVol.render.webgl.Replay.prototype.shutDownProgram = function(gl, locations) {};


  /**
   * @abstract
   * @protected
   * @param {WebGLRenderingContext} gl gl.
   * @param {GVol.webgl.Context} context Context.
   * @param {Object.<string, boGVolean>} skippedFeaturesHash Ids of features
   *  to skip.
   * @param {boGVolean} hitDetection Hit detection mode.
   */
  GVol.render.webgl.Replay.prototype.drawReplay = function(gl, context, skippedFeaturesHash, hitDetection) {};


  /**
   * @abstract
   * @protected
   * @param {WebGLRenderingContext} gl gl.
   * @param {GVol.webgl.Context} context Context.
   * @param {Object.<string, boGVolean>} skippedFeaturesHash Ids of features
   *  to skip.
   * @param {function((GVol.Feature|GVol.render.Feature)): T|undefined} featureCallback Feature callback.
   * @param {GVol.Extent=} opt_hitExtent Hit extent: Only features intersecting
   *  this extent are checked.
   * @return {T|undefined} Callback result.
   * @template T
   */
  GVol.render.webgl.Replay.prototype.drawHitDetectionReplayOneByOne = function(gl, context, skippedFeaturesHash, featureCallback, opt_hitExtent) {};


  /**
   * @protected
   * @param {WebGLRenderingContext} gl gl.
   * @param {GVol.webgl.Context} context Context.
   * @param {Object.<string, boGVolean>} skippedFeaturesHash Ids of features
   *  to skip.
   * @param {function((GVol.Feature|GVol.render.Feature)): T|undefined} featureCallback Feature callback.
   * @param {boGVolean} oneByOne Draw features one-by-one for the hit-detecion.
   * @param {GVol.Extent=} opt_hitExtent Hit extent: Only features intersecting
   *  this extent are checked.
   * @return {T|undefined} Callback result.
   * @template T
   */
  GVol.render.webgl.Replay.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);
    }
  };


  /**
   * @protected
   * @param {WebGLRenderingContext} gl gl.
   * @param {GVol.webgl.Context} context Context.
   * @param {Object.<string, boGVolean>} skippedFeaturesHash Ids of features
   *  to skip.
   * @param {function((GVol.Feature|GVol.render.Feature)): T|undefined} featureCallback Feature callback.
   * @return {T|undefined} Callback result.
   * @template T
   */
  GVol.render.webgl.Replay.prototype.drawHitDetectionReplayAll = function(gl, context, skippedFeaturesHash,
      featureCallback) {
    gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
    this.drawReplay(gl, context, skippedFeaturesHash, true);

    var result = featureCallback(null);
    if (result) {
      return result;
    } else {
      return undefined;
    }
  };


  /**
   * @param {GVol.webgl.Context} context Context.
   * @param {GVol.Coordinate} center Center.
   * @param {number} resGVolution ResGVolution.
   * @param {number} rotation Rotation.
   * @param {GVol.Size} size Size.
   * @param {number} pixelRatio Pixel ratio.
   * @param {number} opacity Global opacity.
   * @param {Object.<string, boGVolean>} skippedFeaturesHash Ids of features
   *  to skip.
   * @param {function((GVol.Feature|GVol.render.Feature)): T|undefined} featureCallback Feature callback.
   * @param {boGVolean} oneByOne Draw features one-by-one for the hit-detecion.
   * @param {GVol.Extent=} opt_hitExtent Hit extent: Only features intersecting
   *  this extent are checked.
   * @return {T|undefined} Callback result.
   * @template T
   */
  GVol.render.webgl.Replay.prototype.replay = function(context,
      center, resGVolution, rotation, size, pixelRatio,
      opacity, skippedFeaturesHash,
      featureCallback, oneByOne, opt_hitExtent) {
    var gl = context.getGL();
    var tmpStencil, tmpStencilFunc, tmpStencilMaskVal, tmpStencilRef, tmpStencilMask,
        tmpStencilOpFail, tmpStencilOpPass, tmpStencilOpZFail;

    if (this.lineStringReplay) {
      tmpStencil = gl.isEnabled(gl.STENCIL_TEST);
      tmpStencilFunc = gl.getParameter(gl.STENCIL_FUNC);
      tmpStencilMaskVal = gl.getParameter(gl.STENCIL_VALUE_MASK);
      tmpStencilRef = gl.getParameter(gl.STENCIL_REF);
      tmpStencilMask = gl.getParameter(gl.STENCIL_WRITEMASK);
      tmpStencilOpFail = gl.getParameter(gl.STENCIL_FAIL);
      tmpStencilOpPass = gl.getParameter(gl.STENCIL_PASS_DEPTH_PASS);
      tmpStencilOpZFail = gl.getParameter(gl.STENCIL_PASS_DEPTH_FAIL);

      gl.enable(gl.STENCIL_TEST);
      gl.clear(gl.STENCIL_BUFFER_BIT);
      gl.stencilMask(255);
      gl.stencilFunc(gl.ALWAYS, 1, 255);
      gl.stencilOp(gl.KEEP, gl.KEEP, gl.REPLACE);

      this.lineStringReplay.replay(context,
          center, resGVolution, rotation, size, pixelRatio,
          opacity, skippedFeaturesHash,
          featureCallback, oneByOne, opt_hitExtent);

      gl.stencilMask(0);
      gl.stencilFunc(gl.NOTEQUAL, 1, 255);
    }

    context.bindBuffer(GVol.webgl.ARRAY_BUFFER, this.verticesBuffer);

    context.bindBuffer(GVol.webgl.ELEMENT_ARRAY_BUFFER, this.indicesBuffer);

    var locations = this.setUpProgram(gl, context, size, pixelRatio);

    // set the "uniform" values
    var projectionMatrix = GVol.transform.reset(this.projectionMatrix_);
    GVol.transform.scale(projectionMatrix, 2 / (resGVolution * size[0]), 2 / (resGVolution * size[1]));
    GVol.transform.rotate(projectionMatrix, -rotation);
    GVol.transform.translate(projectionMatrix, -(center[0] - this.origin[0]), -(center[1] - this.origin[1]));

    var offsetScaleMatrix = GVol.transform.reset(this.offsetScaleMatrix_);
    GVol.transform.scale(offsetScaleMatrix, 2 / size[0], 2 / size[1]);

    var offsetRotateMatrix = GVol.transform.reset(this.offsetRotateMatrix_);
    if (rotation !== 0) {
      GVol.transform.rotate(offsetRotateMatrix, -rotation);
    }

    gl.uniformMatrix4fv(locations.u_projectionMatrix, false,
        GVol.vec.Mat4.fromTransform(this.tmpMat4_, projectionMatrix));
    gl.uniformMatrix4fv(locations.u_offsetScaleMatrix, false,
        GVol.vec.Mat4.fromTransform(this.tmpMat4_, offsetScaleMatrix));
    gl.uniformMatrix4fv(locations.u_offsetRotateMatrix, false,
        GVol.vec.Mat4.fromTransform(this.tmpMat4_, offsetRotateMatrix));
    gl.uniform1f(locations.u_opacity, opacity);

    // draw!
    var result;
    if (featureCallback === undefined) {
      this.drawReplay(gl, context, skippedFeaturesHash, false);
    } else {
      // draw feature by feature for the hit-detection
      result = this.drawHitDetectionReplay(gl, context, skippedFeaturesHash,
          featureCallback, oneByOne, opt_hitExtent);
    }

    // disable the vertex attrib arrays
    this.shutDownProgram(gl, locations);

    if (this.lineStringReplay) {
      if (!tmpStencil) {
        gl.disable(gl.STENCIL_TEST);
      }
      gl.clear(gl.STENCIL_BUFFER_BIT);
      gl.stencilFunc(/** @type {number} */ (tmpStencilFunc),
          /** @type {number} */ (tmpStencilRef), /** @type {number} */ (tmpStencilMaskVal));
      gl.stencilMask(/** @type {number} */ (tmpStencilMask));
      gl.stencilOp(/** @type {number} */ (tmpStencilOpFail),
          /** @type {number} */ (tmpStencilOpZFail), /** @type {number} */ (tmpStencilOpPass));
    }

    return result;
  };

  /**
   * @protected
   * @param {WebGLRenderingContext} gl gl.
   * @param {GVol.webgl.Context} context Context.
   * @param {number} start Start index.
   * @param {number} end End index.
   */
  GVol.render.webgl.Replay.prototype.drawElements = function(
      gl, context, start, end) {
    var elementType = context.hasOESElementIndexUint ?
      GVol.webgl.UNSIGNED_INT : GVol.webgl.UNSIGNED_SHORT;
    var elementSize = context.hasOESElementIndexUint ? 4 : 2;

    var numItems = end - start;
    var offsetInBytes = start * elementSize;
    gl.drawElements(GVol.webgl.TRIANGLES, numItems, elementType, offsetInBytes);
  };

}

goog.provide('GVol.render.webgl');

goog.require('GVol');


if (GVol.ENABLE_WEBGL) {

  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.defaultFont = '10px sans-serif';


  /**
   * @const
   * @type {GVol.CGVolor}
   */
  GVol.render.webgl.defaultFillStyle = [0.0, 0.0, 0.0, 1.0];


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.defaultLineCap = 'round';


  /**
   * @const
   * @type {Array.<number>}
   */
  GVol.render.webgl.defaultLineDash = [];


  /**
   * @const
   * @type {number}
   */
  GVol.render.webgl.defaultLineDashOffset = 0;


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.defaultLineJoin = 'round';


  /**
   * @const
   * @type {number}
   */
  GVol.render.webgl.defaultMiterLimit = 10;

  /**
   * @const
   * @type {GVol.CGVolor}
   */
  GVol.render.webgl.defaultStrokeStyle = [0.0, 0.0, 0.0, 1.0];


  /**
   * @const
   * @type {number}
   */
  GVol.render.webgl.defaultTextAlign = 0.5;


  /**
   * @const
   * @type {number}
   */
  GVol.render.webgl.defaultTextBaseline = 0.5;


  /**
   * @const
   * @type {number}
   */
  GVol.render.webgl.defaultLineWidth = 1;

  /**
   * Calculates the orientation of a triangle based on the determinant method.
   * @param {number} x1 First X coordinate.
   * @param {number} y1 First Y coordinate.
   * @param {number} x2 Second X coordinate.
   * @param {number} y2 Second Y coordinate.
   * @param {number} x3 Third X coordinate.
   * @param {number} y3 Third Y coordinate.
   * @return {boGVolean|undefined} Triangle is clockwise.
   */
  GVol.render.webgl.triangleIsCounterClockwise = function(x1, y1, x2, y2, x3, y3) {
    var area = (x2 - x1) * (y3 - y1) - (x3 - x1) * (y2 - y1);
    return (area <= GVol.render.webgl.EPSILON && area >= -GVol.render.webgl.EPSILON) ?
      undefined : area > 0;
  };

  /**
   * @const
   * @type {number}
   */
  GVol.render.webgl.EPSILON = Number.EPSILON || 2.220446049250313e-16;

}

goog.provide('GVol.webgl.Buffer');

goog.require('GVol');
goog.require('GVol.webgl');


if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @param {Array.<number>=} opt_arr Array.
   * @param {number=} opt_usage Usage.
   * @struct
   */
  GVol.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 : GVol.webgl.Buffer.Usage_.STATIC_DRAW;

  };


  /**
   * @return {Array.<number>} Array.
   */
  GVol.webgl.Buffer.prototype.getArray = function() {
    return this.arr_;
  };


  /**
   * @return {number} Usage.
   */
  GVol.webgl.Buffer.prototype.getUsage = function() {
    return this.usage_;
  };


  /**
   * @enum {number}
   * @private
   */
  GVol.webgl.Buffer.Usage_ = {
    STATIC_DRAW: GVol.webgl.STATIC_DRAW,
    STREAM_DRAW: GVol.webgl.STREAM_DRAW,
    DYNAMIC_DRAW: GVol.webgl.DYNAMIC_DRAW
  };

}

goog.provide('GVol.render.webgl.CircleReplay');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.cGVolor');
goog.require('GVol.extent');
goog.require('GVol.obj');
goog.require('GVol.geom.flat.transform');
goog.require('GVol.render.webgl.circlereplay.defaultshader');
goog.require('GVol.render.webgl.Replay');
goog.require('GVol.render.webgl');
goog.require('GVol.webgl');
goog.require('GVol.webgl.Buffer');


if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @extends {GVol.render.webgl.Replay}
   * @param {number} tGVolerance TGVolerance.
   * @param {GVol.Extent} maxExtent Max extent.
   * @struct
   */
  GVol.render.webgl.CircleReplay = function(tGVolerance, maxExtent) {
    GVol.render.webgl.Replay.call(this, tGVolerance, maxExtent);

    /**
     * @private
     * @type {GVol.render.webgl.circlereplay.defaultshader.Locations}
     */
    this.defaultLocations_ = null;

    /**
     * @private
     * @type {Array.<Array.<Array.<number>|number>>}
     */
    this.styles_ = [];

    /**
     * @private
     * @type {Array.<number>}
     */
    this.styleIndices_ = [];

    /**
     * @private
     * @type {number}
     */
    this.radius_ = 0;

    /**
     * @private
     * @type {{fillCGVolor: (Array.<number>|null),
     *         strokeCGVolor: (Array.<number>|null),
     *         lineDash: Array.<number>,
     *         lineDashOffset: (number|undefined),
     *         lineWidth: (number|undefined),
     *         changed: boGVolean}|null}
     */
    this.state_ = {
      fillCGVolor: null,
      strokeCGVolor: null,
      lineDash: null,
      lineDashOffset: undefined,
      lineWidth: undefined,
      changed: false
    };

  };
  GVol.inherits(GVol.render.webgl.CircleReplay, GVol.render.webgl.Replay);


  /**
   * @private
   * @param {Array.<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   */
  GVol.render.webgl.CircleReplay.prototype.drawCoordinates_ = function(
      flatCoordinates, offset, end, stride) {
    var numVertices = this.vertices.length;
    var numIndices = this.indices.length;
    var n = numVertices / 4;
    var i, ii;
    for (i = offset, ii = end; i < ii; i += stride) {
      this.vertices[numVertices++] = flatCoordinates[i];
      this.vertices[numVertices++] = flatCoordinates[i + 1];
      this.vertices[numVertices++] = 0;
      this.vertices[numVertices++] = this.radius_;

      this.vertices[numVertices++] = flatCoordinates[i];
      this.vertices[numVertices++] = flatCoordinates[i + 1];
      this.vertices[numVertices++] = 1;
      this.vertices[numVertices++] = this.radius_;

      this.vertices[numVertices++] = flatCoordinates[i];
      this.vertices[numVertices++] = flatCoordinates[i + 1];
      this.vertices[numVertices++] = 2;
      this.vertices[numVertices++] = this.radius_;

      this.vertices[numVertices++] = flatCoordinates[i];
      this.vertices[numVertices++] = flatCoordinates[i + 1];
      this.vertices[numVertices++] = 3;
      this.vertices[numVertices++] = this.radius_;

      this.indices[numIndices++] = n;
      this.indices[numIndices++] = n + 1;
      this.indices[numIndices++] = n + 2;

      this.indices[numIndices++] = n + 2;
      this.indices[numIndices++] = n + 3;
      this.indices[numIndices++] = n;

      n += 4;
    }
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.CircleReplay.prototype.drawCircle = function(circleGeometry, feature) {
    var radius = circleGeometry.getRadius();
    var stride = circleGeometry.getStride();
    if (radius) {
      this.startIndices.push(this.indices.length);
      this.startIndicesFeature.push(feature);
      if (this.state_.changed) {
        this.styleIndices_.push(this.indices.length);
        this.state_.changed = false;
      }

      this.radius_ = radius;
      var flatCoordinates = circleGeometry.getFlatCoordinates();
      flatCoordinates = GVol.geom.flat.transform.translate(flatCoordinates, 0, 2,
          stride, -this.origin[0], -this.origin[1]);
      this.drawCoordinates_(flatCoordinates, 0, 2, stride);
    } else {
      if (this.state_.changed) {
        this.styles_.pop();
        if (this.styles_.length) {
          var lastState = this.styles_[this.styles_.length - 1];
          this.state_.fillCGVolor =  /** @type {Array.<number>} */ (lastState[0]);
          this.state_.strokeCGVolor = /** @type {Array.<number>} */ (lastState[1]);
          this.state_.lineWidth = /** @type {number} */ (lastState[2]);
          this.state_.changed = false;
        }
      }
    }
  };


  /**
   * @inheritDoc
   **/
  GVol.render.webgl.CircleReplay.prototype.finish = function(context) {
    // create, bind, and populate the vertices buffer
    this.verticesBuffer = new GVol.webgl.Buffer(this.vertices);

    // create, bind, and populate the indices buffer
    this.indicesBuffer = new GVol.webgl.Buffer(this.indices);

    this.startIndices.push(this.indices.length);

    //Clean up, if there is nothing to draw
    if (this.styleIndices_.length === 0 && this.styles_.length > 0) {
      this.styles_ = [];
    }

    this.vertices = null;
    this.indices = null;
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.CircleReplay.prototype.getDeleteResourcesFunction = function(context) {
    // We only delete our stuff here. The shaders and the program may
    // be used by other CircleReplay instances (for other layers). And
    // they will be deleted when disposing of the GVol.webgl.Context
    // object.
    var verticesBuffer = this.verticesBuffer;
    var indicesBuffer = this.indicesBuffer;
    return function() {
      context.deleteBuffer(verticesBuffer);
      context.deleteBuffer(indicesBuffer);
    };
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.CircleReplay.prototype.setUpProgram = function(gl, context, size, pixelRatio) {
    // get the program
    var fragmentShader, vertexShader;
    fragmentShader = GVol.render.webgl.circlereplay.defaultshader.fragment;
    vertexShader = GVol.render.webgl.circlereplay.defaultshader.vertex;
    var program = context.getProgram(fragmentShader, vertexShader);

    // get the locations
    var locations;
    if (!this.defaultLocations_) {
      // eslint-disable-next-line openlayers-internal/no-missing-requires
      locations = new GVol.render.webgl.circlereplay.defaultshader.Locations(gl, program);
      this.defaultLocations_ = locations;
    } else {
      locations = this.defaultLocations_;
    }

    context.useProgram(program);

    // enable the vertex attrib arrays
    gl.enableVertexAttribArray(locations.a_position);
    gl.vertexAttribPointer(locations.a_position, 2, GVol.webgl.FLOAT,
        false, 16, 0);

    gl.enableVertexAttribArray(locations.a_instruction);
    gl.vertexAttribPointer(locations.a_instruction, 1, GVol.webgl.FLOAT,
        false, 16, 8);

    gl.enableVertexAttribArray(locations.a_radius);
    gl.vertexAttribPointer(locations.a_radius, 1, GVol.webgl.FLOAT,
        false, 16, 12);

    // Enable renderer specific uniforms.
    gl.uniform2fv(locations.u_size, size);
    gl.uniform1f(locations.u_pixelRatio, pixelRatio);

    return locations;
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.CircleReplay.prototype.shutDownProgram = function(gl, locations) {
    gl.disableVertexAttribArray(locations.a_position);
    gl.disableVertexAttribArray(locations.a_instruction);
    gl.disableVertexAttribArray(locations.a_radius);
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.CircleReplay.prototype.drawReplay = function(gl, context, skippedFeaturesHash, hitDetection) {
    if (!GVol.obj.isEmpty(skippedFeaturesHash)) {
      this.drawReplaySkipping_(gl, context, skippedFeaturesHash);
    } else {
      //Draw by style groups to minimize drawElements() calls.
      var i, start, end, nextStyle;
      end = this.startIndices[this.startIndices.length - 1];
      for (i = this.styleIndices_.length - 1; i >= 0; --i) {
        start = this.styleIndices_[i];
        nextStyle = this.styles_[i];
        this.setFillStyle_(gl, /** @type {Array.<number>} */ (nextStyle[0]));
        this.setStrokeStyle_(gl, /** @type {Array.<number>} */ (nextStyle[1]),
            /** @type {number} */ (nextStyle[2]));
        this.drawElements(gl, context, start, end);
        end = start;
      }
    }
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.CircleReplay.prototype.drawHitDetectionReplayOneByOne = function(gl, context, skippedFeaturesHash,
      featureCallback, opt_hitExtent) {
    var i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex;
    featureIndex = this.startIndices.length - 2;
    end = this.startIndices[featureIndex + 1];
    for (i = this.styleIndices_.length - 1; i >= 0; --i) {
      nextStyle = this.styles_[i];
      this.setFillStyle_(gl, /** @type {Array.<number>} */ (nextStyle[0]));
      this.setStrokeStyle_(gl, /** @type {Array.<number>} */ (nextStyle[1]),
          /** @type {number} */ (nextStyle[2]));
      groupStart = this.styleIndices_[i];

      while (featureIndex >= 0 &&
          this.startIndices[featureIndex] >= groupStart) {
        start = this.startIndices[featureIndex];
        feature = this.startIndicesFeature[featureIndex];
        featureUid = GVol.getUid(feature).toString();

        if (skippedFeaturesHash[featureUid] === undefined &&
            feature.getGeometry() &&
            (opt_hitExtent === undefined || GVol.extent.intersects(
                /** @type {Array<number>} */ (opt_hitExtent),
                feature.getGeometry().getExtent()))) {
          gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
          this.drawElements(gl, context, start, end);

          var result = featureCallback(feature);

          if (result) {
            return result;
          }

        }
        featureIndex--;
        end = start;
      }
    }
    return undefined;
  };


  /**
   * @private
   * @param {WebGLRenderingContext} gl gl.
   * @param {GVol.webgl.Context} context Context.
   * @param {Object} skippedFeaturesHash Ids of features to skip.
   */
  GVol.render.webgl.CircleReplay.prototype.drawReplaySkipping_ = function(gl, context, skippedFeaturesHash) {
    var i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex, featureStart;
    featureIndex = this.startIndices.length - 2;
    end = start = this.startIndices[featureIndex + 1];
    for (i = this.styleIndices_.length - 1; i >= 0; --i) {
      nextStyle = this.styles_[i];
      this.setFillStyle_(gl, /** @type {Array.<number>} */ (nextStyle[0]));
      this.setStrokeStyle_(gl, /** @type {Array.<number>} */ (nextStyle[1]),
          /** @type {number} */ (nextStyle[2]));
      groupStart = this.styleIndices_[i];

      while (featureIndex >= 0 &&
          this.startIndices[featureIndex] >= groupStart) {
        featureStart = this.startIndices[featureIndex];
        feature = this.startIndicesFeature[featureIndex];
        featureUid = GVol.getUid(feature).toString();

        if (skippedFeaturesHash[featureUid]) {
          if (start !== end) {
            this.drawElements(gl, context, start, end);
          }
          end = featureStart;
        }
        featureIndex--;
        start = featureStart;
      }
      if (start !== end) {
        this.drawElements(gl, context, start, end);
      }
      start = end = groupStart;
    }
  };


  /**
   * @private
   * @param {WebGLRenderingContext} gl gl.
   * @param {Array.<number>} cGVolor CGVolor.
   */
  GVol.render.webgl.CircleReplay.prototype.setFillStyle_ = function(gl, cGVolor) {
    gl.uniform4fv(this.defaultLocations_.u_fillCGVolor, cGVolor);
  };


  /**
   * @private
   * @param {WebGLRenderingContext} gl gl.
   * @param {Array.<number>} cGVolor CGVolor.
   * @param {number} lineWidth Line width.
   */
  GVol.render.webgl.CircleReplay.prototype.setStrokeStyle_ = function(gl, cGVolor, lineWidth) {
    gl.uniform4fv(this.defaultLocations_.u_strokeCGVolor, cGVolor);
    gl.uniform1f(this.defaultLocations_.u_lineWidth, lineWidth);
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.CircleReplay.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) {
    var strokeStyleCGVolor, strokeStyleWidth;
    if (strokeStyle) {
      var strokeStyleLineDash = strokeStyle.getLineDash();
      this.state_.lineDash = strokeStyleLineDash ?
        strokeStyleLineDash : GVol.render.webgl.defaultLineDash;
      var strokeStyleLineDashOffset = strokeStyle.getLineDashOffset();
      this.state_.lineDashOffset = strokeStyleLineDashOffset ?
        strokeStyleLineDashOffset : GVol.render.webgl.defaultLineDashOffset;
      strokeStyleCGVolor = strokeStyle.getCGVolor();
      if (!(strokeStyleCGVolor instanceof CanvasGradient) &&
          !(strokeStyleCGVolor instanceof CanvasPattern)) {
        strokeStyleCGVolor = GVol.cGVolor.asArray(strokeStyleCGVolor).map(function(c, i) {
          return i != 3 ? c / 255 : c;
        }) || GVol.render.webgl.defaultStrokeStyle;
      } else {
        strokeStyleCGVolor = GVol.render.webgl.defaultStrokeStyle;
      }
      strokeStyleWidth = strokeStyle.getWidth();
      strokeStyleWidth = strokeStyleWidth !== undefined ?
        strokeStyleWidth : GVol.render.webgl.defaultLineWidth;
    } else {
      strokeStyleCGVolor = [0, 0, 0, 0];
      strokeStyleWidth = 0;
    }
    var fillStyleCGVolor = fillStyle ? fillStyle.getCGVolor() : [0, 0, 0, 0];
    if (!(fillStyleCGVolor instanceof CanvasGradient) &&
        !(fillStyleCGVolor instanceof CanvasPattern)) {
      fillStyleCGVolor = GVol.cGVolor.asArray(fillStyleCGVolor).map(function(c, i) {
        return i != 3 ? c / 255 : c;
      }) || GVol.render.webgl.defaultFillStyle;
    } else {
      fillStyleCGVolor = GVol.render.webgl.defaultFillStyle;
    }
    if (!this.state_.strokeCGVolor || !GVol.array.equals(this.state_.strokeCGVolor, strokeStyleCGVolor) ||
        !this.state_.fillCGVolor || !GVol.array.equals(this.state_.fillCGVolor, fillStyleCGVolor) ||
        this.state_.lineWidth !== strokeStyleWidth) {
      this.state_.changed = true;
      this.state_.fillCGVolor = fillStyleCGVolor;
      this.state_.strokeCGVolor = strokeStyleCGVolor;
      this.state_.lineWidth = strokeStyleWidth;
      this.styles_.push([fillStyleCGVolor, strokeStyleCGVolor, strokeStyleWidth]);
    }
  };

}

// This file is automatically generated, do not edit
/* eslint openlayers-internal/no-missing-requires: 0 */
goog.provide('GVol.render.webgl.texturereplay.defaultshader');

goog.require('GVol');
goog.require('GVol.webgl.Fragment');
goog.require('GVol.webgl.Vertex');

if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @extends {GVol.webgl.Fragment}
   * @struct
   */
  GVol.render.webgl.texturereplay.defaultshader.Fragment = function() {
    GVol.webgl.Fragment.call(this, GVol.render.webgl.texturereplay.defaultshader.Fragment.SOURCE);
  };
  GVol.inherits(GVol.render.webgl.texturereplay.defaultshader.Fragment, GVol.webgl.Fragment);


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.texturereplay.defaultshader.Fragment.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 texCGVolor = texture2D(u_image, v_texCoord);\n  gl_FragCGVolor.rgb = texCGVolor.rgb;\n  float alpha = texCGVolor.a * v_opacity * u_opacity;\n  if (alpha == 0.0) {\n    discard;\n  }\n  gl_FragCGVolor.a = alpha;\n}\n';


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.texturereplay.defaultshader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;varying float b;uniform float k;uniform sampler2D l;void main(void){vec4 texCGVolor=texture2D(l,a);gl_FragCGVolor.rgb=texCGVolor.rgb;float alpha=texCGVolor.a*b*k;if(alpha==0.0){discard;}gl_FragCGVolor.a=alpha;}';


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.texturereplay.defaultshader.Fragment.SOURCE = GVol.DEBUG_WEBGL ?
    GVol.render.webgl.texturereplay.defaultshader.Fragment.DEBUG_SOURCE :
    GVol.render.webgl.texturereplay.defaultshader.Fragment.OPTIMIZED_SOURCE;


  GVol.render.webgl.texturereplay.defaultshader.fragment = new GVol.render.webgl.texturereplay.defaultshader.Fragment();


  /**
   * @constructor
   * @extends {GVol.webgl.Vertex}
   * @struct
   */
  GVol.render.webgl.texturereplay.defaultshader.Vertex = function() {
    GVol.webgl.Vertex.call(this, GVol.render.webgl.texturereplay.defaultshader.Vertex.SOURCE);
  };
  GVol.inherits(GVol.render.webgl.texturereplay.defaultshader.Vertex, GVol.webgl.Vertex);


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.texturereplay.defaultshader.Vertex.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, 0.0);\n  gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0) + offsets;\n  v_texCoord = a_texCoord;\n  v_opacity = a_opacity;\n}\n\n\n';


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.texturereplay.defaultshader.Vertex.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,0.0);gl_Position=h*vec4(c,0.0,1.0)+offsets;a=d;b=f;}';


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.texturereplay.defaultshader.Vertex.SOURCE = GVol.DEBUG_WEBGL ?
    GVol.render.webgl.texturereplay.defaultshader.Vertex.DEBUG_SOURCE :
    GVol.render.webgl.texturereplay.defaultshader.Vertex.OPTIMIZED_SOURCE;


  GVol.render.webgl.texturereplay.defaultshader.vertex = new GVol.render.webgl.texturereplay.defaultshader.Vertex();


  /**
   * @constructor
   * @param {WebGLRenderingContext} gl GL.
   * @param {WebGLProgram} program Program.
   * @struct
   */
  GVol.render.webgl.texturereplay.defaultshader.Locations = function(gl, program) {

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_image = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_image' : 'l');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_offsetRotateMatrix = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_offsetRotateMatrix' : 'j');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_offsetScaleMatrix = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_offsetScaleMatrix' : 'i');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_opacity = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_opacity' : 'k');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_projectionMatrix = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_projectionMatrix' : 'h');

    /**
     * @type {number}
     */
    this.a_offsets = gl.getAttribLocation(
        program, GVol.DEBUG_WEBGL ? 'a_offsets' : 'e');

    /**
     * @type {number}
     */
    this.a_opacity = gl.getAttribLocation(
        program, GVol.DEBUG_WEBGL ? 'a_opacity' : 'f');

    /**
     * @type {number}
     */
    this.a_position = gl.getAttribLocation(
        program, GVol.DEBUG_WEBGL ? 'a_position' : 'c');

    /**
     * @type {number}
     */
    this.a_rotateWithView = gl.getAttribLocation(
        program, GVol.DEBUG_WEBGL ? 'a_rotateWithView' : 'g');

    /**
     * @type {number}
     */
    this.a_texCoord = gl.getAttribLocation(
        program, GVol.DEBUG_WEBGL ? 'a_texCoord' : 'd');
  };

}

goog.provide('GVol.webgl.ContextEventType');


/**
 * @enum {string}
 */
GVol.webgl.ContextEventType = {
  LOST: 'webglcontextlost',
  RESTORED: 'webglcontextrestored'
};

goog.provide('GVol.webgl.Context');

goog.require('GVol');
goog.require('GVol.Disposable');
goog.require('GVol.array');
goog.require('GVol.events');
goog.require('GVol.obj');
goog.require('GVol.webgl');
goog.require('GVol.webgl.ContextEventType');


if (GVol.ENABLE_WEBGL) {

  /**
   * @classdesc
   * A WebGL context for accessing low-level WebGL capabilities.
   *
   * @constructor
   * @extends {GVol.Disposable}
   * @param {HTMLCanvasElement} canvas Canvas.
   * @param {WebGLRenderingContext} gl GL.
   */
  GVol.webgl.Context = function(canvas, gl) {

    /**
     * @private
     * @type {HTMLCanvasElement}
     */
    this.canvas_ = canvas;

    /**
     * @private
     * @type {WebGLRenderingContext}
     */
    this.gl_ = gl;

    /**
     * @private
     * @type {Object.<string, GVol.WebglBufferCacheEntry>}
     */
    this.bufferCache_ = {};

    /**
     * @private
     * @type {Object.<string, 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 {boGVolean}
     */
    this.hasOESElementIndexUint = GVol.array.includes(
        GVol.WEBGL_EXTENSIONS, 'OES_element_index_uint');

    // use the OES_element_index_uint extension if available
    if (this.hasOESElementIndexUint) {
      gl.getExtension('OES_element_index_uint');
    }

    GVol.events.listen(this.canvas_, GVol.webgl.ContextEventType.LOST,
        this.handleWebGLContextLost, this);
    GVol.events.listen(this.canvas_, GVol.webgl.ContextEventType.RESTORED,
        this.handleWebGLContextRestored, this);

  };
  GVol.inherits(GVol.webgl.Context, GVol.Disposable);


  /**
   * 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 {GVol.webgl.Buffer} buf Buffer.
   */
  GVol.webgl.Context.prototype.bindBuffer = function(target, buf) {
    var gl = this.getGL();
    var arr = buf.getArray();
    var bufferKey = String(GVol.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);
      var /** @type {ArrayBufferView} */ arrayBuffer;
      if (target == GVol.webgl.ARRAY_BUFFER) {
        arrayBuffer = new Float32Array(arr);
      } else if (target == GVol.webgl.ELEMENT_ARRAY_BUFFER) {
        arrayBuffer = this.hasOESElementIndexUint ?
          new Uint32Array(arr) : new Uint16Array(arr);
      }
      gl.bufferData(target, arrayBuffer, buf.getUsage());
      this.bufferCache_[bufferKey] = {
        buf: buf,
        buffer: buffer
      };
    }
  };


  /**
   * @param {GVol.webgl.Buffer} buf Buffer.
   */
  GVol.webgl.Context.prototype.deleteBuffer = function(buf) {
    var gl = this.getGL();
    var bufferKey = String(GVol.getUid(buf));
    var bufferCacheEntry = this.bufferCache_[bufferKey];
    if (!gl.isContextLost()) {
      gl.deleteBuffer(bufferCacheEntry.buffer);
    }
    delete this.bufferCache_[bufferKey];
  };


  /**
   * @inheritDoc
   */
  GVol.webgl.Context.prototype.disposeInternal = function() {
    GVol.events.unlistenAll(this.canvas_);
    var gl = this.getGL();
    if (!gl.isContextLost()) {
      var key;
      for (key in this.bufferCache_) {
        gl.deleteBuffer(this.bufferCache_[key].buffer);
      }
      for (key in this.programCache_) {
        gl.deleteProgram(this.programCache_[key]);
      }
      for (key in this.shaderCache_) {
        gl.deleteShader(this.shaderCache_[key]);
      }
      // delete objects for hit-detection
      gl.deleteFramebuffer(this.hitDetectionFramebuffer_);
      gl.deleteRenderbuffer(this.hitDetectionRenderbuffer_);
      gl.deleteTexture(this.hitDetectionTexture_);
    }
  };


  /**
   * @return {HTMLCanvasElement} Canvas.
   */
  GVol.webgl.Context.prototype.getCanvas = function() {
    return this.canvas_;
  };


  /**
   * Get the WebGL rendering context
   * @return {WebGLRenderingContext} The rendering context.
   * @api
   */
  GVol.webgl.Context.prototype.getGL = function() {
    return this.gl_;
  };


  /**
   * Get the frame buffer for hit detection.
   * @return {WebGLFramebuffer} The hit detection frame buffer.
   */
  GVol.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 {GVol.webgl.Shader} shaderObject Shader object.
   * @return {WebGLShader} Shader.
   */
  GVol.webgl.Context.prototype.getShader = function(shaderObject) {
    var shaderKey = String(GVol.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);
      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 {GVol.webgl.Fragment} fragmentShaderObject Fragment shader.
   * @param {GVol.webgl.Vertex} vertexShaderObject Vertex shader.
   * @return {WebGLProgram} Program.
   */
  GVol.webgl.Context.prototype.getProgram = function(
      fragmentShaderObject, vertexShaderObject) {
    var programKey =
        GVol.getUid(fragmentShaderObject) + '/' + GVol.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);
      this.programCache_[programKey] = program;
      return program;
    }
  };


  /**
   * FIXME empy description for jsdoc
   */
  GVol.webgl.Context.prototype.handleWebGLContextLost = function() {
    GVol.obj.clear(this.bufferCache_);
    GVol.obj.clear(this.shaderCache_);
    GVol.obj.clear(this.programCache_);
    this.currentProgram_ = null;
    this.hitDetectionFramebuffer_ = null;
    this.hitDetectionTexture_ = null;
    this.hitDetectionRenderbuffer_ = null;
  };


  /**
   * FIXME empy description for jsdoc
   */
  GVol.webgl.Context.prototype.handleWebGLContextRestored = function() {
  };


  /**
   * Creates a 1x1 pixel framebuffer for the hit-detection.
   * @private
   */
  GVol.webgl.Context.prototype.initHitDetectionFramebuffer_ = function() {
    var gl = this.gl_;
    var framebuffer = gl.createFramebuffer();
    gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);

    var texture = GVol.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 {boGVolean} Changed.
   * @api
   */
  GVol.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;
    }
  };


  /**
   * @param {WebGLRenderingContext} gl WebGL rendering context.
   * @param {number=} opt_wrapS wrapS.
   * @param {number=} opt_wrapT wrapT.
   * @return {WebGLTexture} The texture.
   * @private
   */
  GVol.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(
          GVol.webgl.TEXTURE_2D, GVol.webgl.TEXTURE_WRAP_S, opt_wrapS);
    }
    if (opt_wrapT !== undefined) {
      gl.texParameteri(
          GVol.webgl.TEXTURE_2D, GVol.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.
   */
  GVol.webgl.Context.createEmptyTexture = function(
      gl, width, height, opt_wrapS, opt_wrapT) {
    var texture = GVol.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.
   */
  GVol.webgl.Context.createTexture = function(gl, image, opt_wrapS, opt_wrapT) {
    var texture = GVol.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('GVol.render.webgl.TextureReplay');

goog.require('GVol');
goog.require('GVol.extent');
goog.require('GVol.obj');
goog.require('GVol.render.webgl.texturereplay.defaultshader');
goog.require('GVol.render.webgl.Replay');
goog.require('GVol.webgl');
goog.require('GVol.webgl.Context');

if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @abstract
   * @extends {GVol.render.webgl.Replay}
   * @param {number} tGVolerance TGVolerance.
   * @param {GVol.Extent} maxExtent Max extent.
   * @struct
   */
  GVol.render.webgl.TextureReplay = function(tGVolerance, maxExtent) {
    GVol.render.webgl.Replay.call(this, tGVolerance, maxExtent);

    /**
     * @type {number|undefined}
     * @protected
     */
    this.anchorX = undefined;

    /**
     * @type {number|undefined}
     * @protected
     */
    this.anchorY = undefined;

    /**
     * @type {Array.<number>}
     * @protected
     */
    this.groupIndices = [];

    /**
     * @type {Array.<number>}
     * @protected
     */
    this.hitDetectionGroupIndices = [];

    /**
     * @type {number|undefined}
     * @protected
     */
    this.height = undefined;

    /**
     * @type {number|undefined}
     * @protected
     */
    this.imageHeight = undefined;

    /**
     * @type {number|undefined}
     * @protected
     */
    this.imageWidth = undefined;

    /**
     * @protected
     * @type {GVol.render.webgl.texturereplay.defaultshader.Locations}
     */
    this.defaultLocations = null;

    /**
     * @protected
     * @type {number|undefined}
     */
    this.opacity = undefined;

    /**
     * @type {number|undefined}
     * @protected
     */
    this.originX = undefined;

    /**
     * @type {number|undefined}
     * @protected
     */
    this.originY = undefined;

    /**
     * @protected
     * @type {boGVolean|undefined}
     */
    this.rotateWithView = undefined;

    /**
     * @protected
     * @type {number|undefined}
     */
    this.rotation = undefined;

    /**
     * @protected
     * @type {number|undefined}
     */
    this.scale = undefined;

    /**
     * @type {number|undefined}
     * @protected
     */
    this.width = undefined;
  };
  GVol.inherits(GVol.render.webgl.TextureReplay, GVol.render.webgl.Replay);


  /**
   * @inheritDoc
   */
  GVol.render.webgl.TextureReplay.prototype.getDeleteResourcesFunction = function(context) {
    var verticesBuffer = this.verticesBuffer;
    var indicesBuffer = this.indicesBuffer;
    var textures = this.getTextures(true);
    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]);
        }
      }
      context.deleteBuffer(verticesBuffer);
      context.deleteBuffer(indicesBuffer);
    };
  };


  /**
   * @param {Array.<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @return {number} My end.
   * @protected
   */
  GVol.render.webgl.TextureReplay.prototype.drawCoordinates = function(flatCoordinates, offset, end, stride) {
    var anchorX = /** @type {number} */ (this.anchorX);
    var anchorY = /** @type {number} */ (this.anchorY);
    var height = /** @type {number} */ (this.height);
    var imageHeight = /** @type {number} */ (this.imageHeight);
    var imageWidth = /** @type {number} */ (this.imageWidth);
    var opacity = /** @type {number} */ (this.opacity);
    var originX = /** @type {number} */ (this.originX);
    var originY = /** @type {number} */ (this.originY);
    var rotateWithView = this.rotateWithView ? 1.0 : 0.0;
    // this.rotation_ is anti-clockwise, but rotation is clockwise
    var rotation = /** @type {number} */ (-this.rotation);
    var scale = /** @type {number} */ (this.scale);
    var width = /** @type {number} */ (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;
  };


  /**
   * @protected
   * @param {Array.<WebGLTexture>} textures Textures.
   * @param {Array.<HTMLCanvasElement|HTMLImageElement|HTMLVideoElement>} images
   *    Images.
   * @param {Object.<string, WebGLTexture>} texturePerImage Texture cache.
   * @param {WebGLRenderingContext} gl Gl.
   */
  GVol.render.webgl.TextureReplay.prototype.createTextures = function(textures, images, texturePerImage, gl) {
    var texture, image, uid, i;
    var ii = images.length;
    for (i = 0; i < ii; ++i) {
      image = images[i];

      uid = GVol.getUid(image).toString();
      if (uid in texturePerImage) {
        texture = texturePerImage[uid];
      } else {
        texture = GVol.webgl.Context.createTexture(
            gl, image, GVol.webgl.CLAMP_TO_EDGE, GVol.webgl.CLAMP_TO_EDGE);
        texturePerImage[uid] = texture;
      }
      textures[i] = texture;
    }
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.TextureReplay.prototype.setUpProgram = function(gl, context, size, pixelRatio) {
    // get the program
    var fragmentShader = GVol.render.webgl.texturereplay.defaultshader.fragment;
    var vertexShader = GVol.render.webgl.texturereplay.defaultshader.vertex;
    var program = context.getProgram(fragmentShader, vertexShader);

    // get the locations
    var locations;
    if (!this.defaultLocations) {
      // eslint-disable-next-line openlayers-internal/no-missing-requires
      locations = new GVol.render.webgl.texturereplay.defaultshader.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, GVol.webgl.FLOAT,
        false, 32, 0);

    gl.enableVertexAttribArray(locations.a_offsets);
    gl.vertexAttribPointer(locations.a_offsets, 2, GVol.webgl.FLOAT,
        false, 32, 8);

    gl.enableVertexAttribArray(locations.a_texCoord);
    gl.vertexAttribPointer(locations.a_texCoord, 2, GVol.webgl.FLOAT,
        false, 32, 16);

    gl.enableVertexAttribArray(locations.a_opacity);
    gl.vertexAttribPointer(locations.a_opacity, 1, GVol.webgl.FLOAT,
        false, 32, 24);

    gl.enableVertexAttribArray(locations.a_rotateWithView);
    gl.vertexAttribPointer(locations.a_rotateWithView, 1, GVol.webgl.FLOAT,
        false, 32, 28);

    return locations;
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.TextureReplay.prototype.shutDownProgram = function(gl, locations) {
    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);
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.TextureReplay.prototype.drawReplay = function(gl, context, skippedFeaturesHash, hitDetection) {
    var textures = hitDetection ? this.getHitDetectionTextures() : this.getTextures();
    var groupIndices = hitDetection ? this.hitDetectionGroupIndices : this.groupIndices;

    if (!GVol.obj.isEmpty(skippedFeaturesHash)) {
      this.drawReplaySkipping(
          gl, context, skippedFeaturesHash, textures, groupIndices);
    } else {
      var i, ii, start;
      for (i = 0, ii = textures.length, start = 0; i < ii; ++i) {
        gl.bindTexture(GVol.webgl.TEXTURE_2D, textures[i]);
        var end = groupIndices[i];
        this.drawElements(gl, context, start, end);
        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 fGVollowing texture groups:
   *
   *    Group 1: A B C
   *    Group 2: D [E] F G
   *
   * If feature E should be skipped, the fGVollowing `drawElements` calls will be
   * made:
   *
   *    drawElements with feature A, B and C
   *    drawElements with feature D
   *    drawElements with feature F and G
   *
   * @protected
   * @param {WebGLRenderingContext} gl gl.
   * @param {GVol.webgl.Context} context Context.
   * @param {Object.<string, boGVolean>} skippedFeaturesHash Ids of features
   *  to skip.
   * @param {Array.<WebGLTexture>} textures Textures.
   * @param {Array.<number>} groupIndices Texture group indices.
   */
  GVol.render.webgl.TextureReplay.prototype.drawReplaySkipping = function(gl, context, skippedFeaturesHash, textures,
      groupIndices) {
    var featureIndex = 0;

    var i, ii;
    for (i = 0, ii = textures.length; i < ii; ++i) {
      gl.bindTexture(GVol.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 = GVol.getUid(feature).toString();
        if (skippedFeaturesHash[featureUid] !== undefined) {
          // feature should be skipped
          if (start !== end) {
            // draw the features so far
            this.drawElements(gl, context, start, end);
          }
          // 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, context, start, end);
      }
    }
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.TextureReplay.prototype.drawHitDetectionReplayOneByOne = function(gl, context, skippedFeaturesHash,
      featureCallback, opt_hitExtent) {
    var i, groupStart, start, end, feature, featureUid;
    var featureIndex = this.startIndices.length - 1;
    var hitDetectionTextures = this.getHitDetectionTextures();
    for (i = hitDetectionTextures.length - 1; i >= 0; --i) {
      gl.bindTexture(GVol.webgl.TEXTURE_2D, 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 = GVol.getUid(feature).toString();

        if (skippedFeaturesHash[featureUid] === undefined &&
            feature.getGeometry() &&
            (opt_hitExtent === undefined || GVol.extent.intersects(
                /** @type {Array<number>} */ (opt_hitExtent),
                feature.getGeometry().getExtent()))) {
          gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
          this.drawElements(gl, context, start, end);

          var result = featureCallback(feature);
          if (result) {
            return result;
          }
        }

        end = start;
        featureIndex--;
      }
    }
    return undefined;
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.TextureReplay.prototype.finish = function(context) {
    this.anchorX = undefined;
    this.anchorY = undefined;
    this.height = undefined;
    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;
  };


  /**
   * @abstract
   * @protected
   * @param {boGVolean=} opt_all Return hit detection textures with regular ones.
   * @returns {Array.<WebGLTexture>} Textures.
   */
  GVol.render.webgl.TextureReplay.prototype.getTextures = function(opt_all) {};


  /**
   * @abstract
   * @protected
   * @returns {Array.<WebGLTexture>} Textures.
   */
  GVol.render.webgl.TextureReplay.prototype.getHitDetectionTextures = function() {};
}

goog.provide('GVol.render.webgl.ImageReplay');

goog.require('GVol');
goog.require('GVol.render.webgl.TextureReplay');
goog.require('GVol.webgl.Buffer');


if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @extends {GVol.render.webgl.TextureReplay}
   * @param {number} tGVolerance TGVolerance.
   * @param {GVol.Extent} maxExtent Max extent.
   * @struct
   */
  GVol.render.webgl.ImageReplay = function(tGVolerance, maxExtent) {
    GVol.render.webgl.TextureReplay.call(this, tGVolerance, maxExtent);

    /**
     * @type {Array.<HTMLCanvasElement|HTMLImageElement|HTMLVideoElement>}
     * @protected
     */
    this.images_ = [];

    /**
     * @type {Array.<HTMLCanvasElement|HTMLImageElement|HTMLVideoElement>}
     * @protected
     */
    this.hitDetectionImages_ = [];

    /**
     * @type {Array.<WebGLTexture>}
     * @private
     */
    this.textures_ = [];

    /**
     * @type {Array.<WebGLTexture>}
     * @private
     */
    this.hitDetectionTextures_ = [];

  };
  GVol.inherits(GVol.render.webgl.ImageReplay, GVol.render.webgl.TextureReplay);


  /**
   * @inheritDoc
   */
  GVol.render.webgl.ImageReplay.prototype.drawMultiPoint = 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
   */
  GVol.render.webgl.ImageReplay.prototype.drawPoint = 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);
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.ImageReplay.prototype.finish = function(context) {
    var gl = context.getGL();

    this.groupIndices.push(this.indices.length);
    this.hitDetectionGroupIndices.push(this.indices.length);

    // create, bind, and populate the vertices buffer
    this.verticesBuffer = new GVol.webgl.Buffer(this.vertices);

    var indices = this.indices;

    // create, bind, and populate the indices buffer
    this.indicesBuffer = new GVol.webgl.Buffer(indices);

    // create textures
    /** @type {Object.<string, WebGLTexture>} */
    var texturePerImage = {};

    this.createTextures(this.textures_, this.images_, texturePerImage, gl);

    this.createTextures(this.hitDetectionTextures_, this.hitDetectionImages_,
        texturePerImage, gl);

    this.images_ = null;
    this.hitDetectionImages_ = null;
    GVol.render.webgl.TextureReplay.prototype.finish.call(this, context);
  };


  /**
   * @inheritDoc
   */
  GVol.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 opacity = imageStyle.getOpacity();
    var origin = imageStyle.getOrigin();
    var rotateWithView = imageStyle.getRotateWithView();
    var rotation = imageStyle.getRotation();
    var size = imageStyle.getSize();
    var scale = imageStyle.getScale();

    var currentImage;
    if (this.images_.length === 0) {
      this.images_.push(image);
    } else {
      currentImage = this.images_[this.images_.length - 1];
      if (GVol.getUid(currentImage) != GVol.getUid(image)) {
        this.groupIndices.push(this.indices.length);
        this.images_.push(image);
      }
    }

    if (this.hitDetectionImages_.length === 0) {
      this.hitDetectionImages_.push(hitDetectionImage);
    } else {
      currentImage =
          this.hitDetectionImages_[this.hitDetectionImages_.length - 1];
      if (GVol.getUid(currentImage) != GVol.getUid(hitDetectionImage)) {
        this.hitDetectionGroupIndices.push(this.indices.length);
        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];
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.ImageReplay.prototype.getTextures = function(opt_all) {
    return opt_all ? this.textures_.concat(this.hitDetectionTextures_) : this.textures_;
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.ImageReplay.prototype.getHitDetectionTextures = function() {
    return this.hitDetectionTextures_;
  };

}

goog.provide('GVol.geom.flat.topGVology');

goog.require('GVol.geom.flat.area');

/**
 * Check if the linestring is a boundary.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @return {boGVolean} The linestring is a boundary.
 */
GVol.geom.flat.topGVology.lineStringIsClosed = function(flatCoordinates, offset, end, stride) {
  var lastCoord = end - stride;
  if (flatCoordinates[offset] === flatCoordinates[lastCoord] &&
      flatCoordinates[offset + 1] === flatCoordinates[lastCoord + 1] && (end - offset) / stride > 3) {
    return !!GVol.geom.flat.area.linearRing(flatCoordinates, offset, end, stride);
  }
  return false;
};

// This file is automatically generated, do not edit
/* eslint openlayers-internal/no-missing-requires: 0 */
goog.provide('GVol.render.webgl.linestringreplay.defaultshader');

goog.require('GVol');
goog.require('GVol.webgl.Fragment');
goog.require('GVol.webgl.Vertex');

if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @extends {GVol.webgl.Fragment}
   * @struct
   */
  GVol.render.webgl.linestringreplay.defaultshader.Fragment = function() {
    GVol.webgl.Fragment.call(this, GVol.render.webgl.linestringreplay.defaultshader.Fragment.SOURCE);
  };
  GVol.inherits(GVol.render.webgl.linestringreplay.defaultshader.Fragment, GVol.webgl.Fragment);


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.linestringreplay.defaultshader.Fragment.DEBUG_SOURCE = 'precision mediump float;\nvarying float v_round;\nvarying vec2 v_roundVertex;\nvarying float v_halfWidth;\n\n\n\nuniform float u_opacity;\nuniform vec4 u_cGVolor;\nuniform vec2 u_size;\nuniform float u_pixelRatio;\n\nvoid main(void) {\n  if (v_round > 0.0) {\n    vec2 windowCoords = vec2((v_roundVertex.x + 1.0) / 2.0 * u_size.x * u_pixelRatio,\n        (v_roundVertex.y + 1.0) / 2.0 * u_size.y * u_pixelRatio);\n    if (length(windowCoords - gl_FragCoord.xy) > v_halfWidth * u_pixelRatio) {\n      discard;\n    }\n  }\n  gl_FragCGVolor = u_cGVolor;\n  float alpha = u_cGVolor.a * u_opacity;\n  if (alpha == 0.0) {\n    discard;\n  }\n  gl_FragCGVolor.a = alpha;\n}\n';


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.linestringreplay.defaultshader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;varying float a;varying vec2 b;varying float c;uniform float m;uniform vec4 n;uniform vec2 o;uniform float p;void main(void){if(a>0.0){vec2 windowCoords=vec2((b.x+1.0)/2.0*o.x*p,(b.y+1.0)/2.0*o.y*p);if(length(windowCoords-gl_FragCoord.xy)>c*p){discard;}} gl_FragCGVolor=n;float alpha=n.a*m;if(alpha==0.0){discard;}gl_FragCGVolor.a=alpha;}';


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.linestringreplay.defaultshader.Fragment.SOURCE = GVol.DEBUG_WEBGL ?
    GVol.render.webgl.linestringreplay.defaultshader.Fragment.DEBUG_SOURCE :
    GVol.render.webgl.linestringreplay.defaultshader.Fragment.OPTIMIZED_SOURCE;


  GVol.render.webgl.linestringreplay.defaultshader.fragment = new GVol.render.webgl.linestringreplay.defaultshader.Fragment();


  /**
   * @constructor
   * @extends {GVol.webgl.Vertex}
   * @struct
   */
  GVol.render.webgl.linestringreplay.defaultshader.Vertex = function() {
    GVol.webgl.Vertex.call(this, GVol.render.webgl.linestringreplay.defaultshader.Vertex.SOURCE);
  };
  GVol.inherits(GVol.render.webgl.linestringreplay.defaultshader.Vertex, GVol.webgl.Vertex);


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.linestringreplay.defaultshader.Vertex.DEBUG_SOURCE = 'varying float v_round;\nvarying vec2 v_roundVertex;\nvarying float v_halfWidth;\n\n\nattribute vec2 a_lastPos;\nattribute vec2 a_position;\nattribute vec2 a_nextPos;\nattribute float a_direction;\n\nuniform mat4 u_projectionMatrix;\nuniform mat4 u_offsetScaleMatrix;\nuniform mat4 u_offsetRotateMatrix;\nuniform float u_lineWidth;\nuniform float u_miterLimit;\n\nboGVol nearlyEquals(in float value, in float ref) {\n  float epsilon = 0.000000000001;\n  return value >= ref - epsilon && value <= ref + epsilon;\n}\n\nvoid alongNormal(out vec2 offset, in vec2 nextP, in float turnDir, in float direction) {\n  vec2 dirVect = nextP - a_position;\n  vec2 normal = normalize(vec2(-turnDir * dirVect.y, turnDir * dirVect.x));\n  offset = u_lineWidth / 2.0 * normal * direction;\n}\n\nvoid miterUp(out vec2 offset, out float round, in boGVol isRound, in float direction) {\n  float halfWidth = u_lineWidth / 2.0;\n  vec2 tangent = normalize(normalize(a_nextPos - a_position) + normalize(a_position - a_lastPos));\n  vec2 normal = vec2(-tangent.y, tangent.x);\n  vec2 dirVect = a_nextPos - a_position;\n  vec2 tmpNormal = normalize(vec2(-dirVect.y, dirVect.x));\n  float miterLength = abs(halfWidth / dot(normal, tmpNormal));\n  offset = normal * direction * miterLength;\n  round = 0.0;\n  if (isRound) {\n    round = 1.0;\n  } else if (miterLength > u_miterLimit + u_lineWidth) {\n    offset = halfWidth * tmpNormal * direction;\n  }\n}\n\nboGVol miterDown(out vec2 offset, in vec4 projPos, in mat4 offsetMatrix, in float direction) {\n  boGVol degenerate = false;\n  vec2 tangent = normalize(normalize(a_nextPos - a_position) + normalize(a_position - a_lastPos));\n  vec2 normal = vec2(-tangent.y, tangent.x);\n  vec2 dirVect = a_lastPos - a_position;\n  vec2 tmpNormal = normalize(vec2(-dirVect.y, dirVect.x));\n  vec2 longOffset, shortOffset, longVertex;\n  vec4 shortProjVertex;\n  float halfWidth = u_lineWidth / 2.0;\n  if (length(a_nextPos - a_position) > length(a_lastPos - a_position)) {\n    longOffset = tmpNormal * direction * halfWidth;\n    shortOffset = normalize(vec2(dirVect.y, -dirVect.x)) * direction * halfWidth;\n    longVertex = a_nextPos;\n    shortProjVertex = u_projectionMatrix * vec4(a_lastPos, 0.0, 1.0);\n  } else {\n    shortOffset = tmpNormal * direction * halfWidth;\n    longOffset = normalize(vec2(dirVect.y, -dirVect.x)) * direction * halfWidth;\n    longVertex = a_lastPos;\n    shortProjVertex = u_projectionMatrix * vec4(a_nextPos, 0.0, 1.0);\n  }\n  //Intersection algorithm based on theory by Paul Bourke (http://paulbourke.net/geometry/pointlineplane/).\n  vec4 p1 = u_projectionMatrix * vec4(longVertex, 0.0, 1.0) + offsetMatrix * vec4(longOffset, 0.0, 0.0);\n  vec4 p2 = projPos + offsetMatrix * vec4(longOffset, 0.0, 0.0);\n  vec4 p3 = shortProjVertex + offsetMatrix * vec4(-shortOffset, 0.0, 0.0);\n  vec4 p4 = shortProjVertex + offsetMatrix * vec4(shortOffset, 0.0, 0.0);\n  float denom = (p4.y - p3.y) * (p2.x - p1.x) - (p4.x - p3.x) * (p2.y - p1.y);\n  float firstU = ((p4.x - p3.x) * (p1.y - p3.y) - (p4.y - p3.y) * (p1.x - p3.x)) / denom;\n  float secondU = ((p2.x - p1.x) * (p1.y - p3.y) - (p2.y - p1.y) * (p1.x - p3.x)) / denom;\n  float epsilon = 0.000000000001;\n  if (firstU > epsilon && firstU < 1.0 - epsilon && secondU > epsilon && secondU < 1.0 - epsilon) {\n    shortProjVertex.x = p1.x + firstU * (p2.x - p1.x);\n    shortProjVertex.y = p1.y + firstU * (p2.y - p1.y);\n    offset = shortProjVertex.xy;\n    degenerate = true;\n  } else {\n    float miterLength = abs(halfWidth / dot(normal, tmpNormal));\n    offset = normal * direction * miterLength;\n  }\n  return degenerate;\n}\n\nvoid squareCap(out vec2 offset, out float round, in boGVol isRound, in vec2 nextP,\n    in float turnDir, in float direction) {\n  round = 0.0;\n  vec2 dirVect = a_position - nextP;\n  vec2 firstNormal = normalize(dirVect);\n  vec2 secondNormal = vec2(turnDir * firstNormal.y * direction, -turnDir * firstNormal.x * direction);\n  vec2 hypotenuse = normalize(firstNormal - secondNormal);\n  vec2 normal = vec2(turnDir * hypotenuse.y * direction, -turnDir * hypotenuse.x * direction);\n  float length = sqrt(v_halfWidth * v_halfWidth * 2.0);\n  offset = normal * length;\n  if (isRound) {\n    round = 1.0;\n  }\n}\n\nvoid main(void) {\n  boGVol degenerate = false;\n  float direction = float(sign(a_direction));\n  mat4 offsetMatrix = u_offsetScaleMatrix * u_offsetRotateMatrix;\n  vec2 offset;\n  vec4 projPos = u_projectionMatrix * vec4(a_position, 0.0, 1.0);\n  boGVol round = nearlyEquals(mod(a_direction, 2.0), 0.0);\n\n  v_round = 0.0;\n  v_halfWidth = u_lineWidth / 2.0;\n  v_roundVertex = projPos.xy;\n\n  if (nearlyEquals(mod(a_direction, 3.0), 0.0) || nearlyEquals(mod(a_direction, 17.0), 0.0)) {\n    alongNormal(offset, a_nextPos, 1.0, direction);\n  } else if (nearlyEquals(mod(a_direction, 5.0), 0.0) || nearlyEquals(mod(a_direction, 13.0), 0.0)) {\n    alongNormal(offset, a_lastPos, -1.0, direction);\n  } else if (nearlyEquals(mod(a_direction, 23.0), 0.0)) {\n    miterUp(offset, v_round, round, direction);\n  } else if (nearlyEquals(mod(a_direction, 19.0), 0.0)) {\n    degenerate = miterDown(offset, projPos, offsetMatrix, direction);\n  } else if (nearlyEquals(mod(a_direction, 7.0), 0.0)) {\n    squareCap(offset, v_round, round, a_nextPos, 1.0, direction);\n  } else if (nearlyEquals(mod(a_direction, 11.0), 0.0)) {\n    squareCap(offset, v_round, round, a_lastPos, -1.0, direction);\n  }\n  if (!degenerate) {\n    vec4 offsets = offsetMatrix * vec4(offset, 0.0, 0.0);\n    gl_Position = projPos + offsets;\n  } else {\n    gl_Position = vec4(offset, 0.0, 1.0);\n  }\n}\n\n\n';


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.linestringreplay.defaultshader.Vertex.OPTIMIZED_SOURCE = 'varying float a;varying vec2 b;varying float c;attribute vec2 d;attribute vec2 e;attribute vec2 f;attribute float g;uniform mat4 h;uniform mat4 i;uniform mat4 j;uniform float k;uniform float l;boGVol nearlyEquals(in float value,in float ref){float epsilon=0.000000000001;return value>=ref-epsilon&&value<=ref+epsilon;}void alongNormal(out vec2 offset,in vec2 nextP,in float turnDir,in float direction){vec2 dirVect=nextP-e;vec2 normal=normalize(vec2(-turnDir*dirVect.y,turnDir*dirVect.x));offset=k/2.0*normal*direction;}void miterUp(out vec2 offset,out float round,in boGVol isRound,in float direction){float halfWidth=k/2.0;vec2 tangent=normalize(normalize(f-e)+normalize(e-d));vec2 normal=vec2(-tangent.y,tangent.x);vec2 dirVect=f-e;vec2 tmpNormal=normalize(vec2(-dirVect.y,dirVect.x));float miterLength=abs(halfWidth/dot(normal,tmpNormal));offset=normal*direction*miterLength;round=0.0;if(isRound){round=1.0;}else if(miterLength>l+k){offset=halfWidth*tmpNormal*direction;}} boGVol miterDown(out vec2 offset,in vec4 projPos,in mat4 offsetMatrix,in float direction){boGVol degenerate=false;vec2 tangent=normalize(normalize(f-e)+normalize(e-d));vec2 normal=vec2(-tangent.y,tangent.x);vec2 dirVect=d-e;vec2 tmpNormal=normalize(vec2(-dirVect.y,dirVect.x));vec2 longOffset,shortOffset,longVertex;vec4 shortProjVertex;float halfWidth=k/2.0;if(length(f-e)>length(d-e)){longOffset=tmpNormal*direction*halfWidth;shortOffset=normalize(vec2(dirVect.y,-dirVect.x))*direction*halfWidth;longVertex=f;shortProjVertex=h*vec4(d,0.0,1.0);}else{shortOffset=tmpNormal*direction*halfWidth;longOffset=normalize(vec2(dirVect.y,-dirVect.x))*direction*halfWidth;longVertex=d;shortProjVertex=h*vec4(f,0.0,1.0);}vec4 p1=h*vec4(longVertex,0.0,1.0)+offsetMatrix*vec4(longOffset,0.0,0.0);vec4 p2=projPos+offsetMatrix*vec4(longOffset,0.0,0.0);vec4 p3=shortProjVertex+offsetMatrix*vec4(-shortOffset,0.0,0.0);vec4 p4=shortProjVertex+offsetMatrix*vec4(shortOffset,0.0,0.0);float denom=(p4.y-p3.y)*(p2.x-p1.x)-(p4.x-p3.x)*(p2.y-p1.y);float firstU=((p4.x-p3.x)*(p1.y-p3.y)-(p4.y-p3.y)*(p1.x-p3.x))/denom;float secondU=((p2.x-p1.x)*(p1.y-p3.y)-(p2.y-p1.y)*(p1.x-p3.x))/denom;float epsilon=0.000000000001;if(firstU>epsilon&&firstU<1.0-epsilon&&secondU>epsilon&&secondU<1.0-epsilon){shortProjVertex.x=p1.x+firstU*(p2.x-p1.x);shortProjVertex.y=p1.y+firstU*(p2.y-p1.y);offset=shortProjVertex.xy;degenerate=true;}else{float miterLength=abs(halfWidth/dot(normal,tmpNormal));offset=normal*direction*miterLength;}return degenerate;}void squareCap(out vec2 offset,out float round,in boGVol isRound,in vec2 nextP,in float turnDir,in float direction){round=0.0;vec2 dirVect=e-nextP;vec2 firstNormal=normalize(dirVect);vec2 secondNormal=vec2(turnDir*firstNormal.y*direction,-turnDir*firstNormal.x*direction);vec2 hypotenuse=normalize(firstNormal-secondNormal);vec2 normal=vec2(turnDir*hypotenuse.y*direction,-turnDir*hypotenuse.x*direction);float length=sqrt(c*c*2.0);offset=normal*length;if(isRound){round=1.0;}} void main(void){boGVol degenerate=false;float direction=float(sign(g));mat4 offsetMatrix=i*j;vec2 offset;vec4 projPos=h*vec4(e,0.0,1.0);boGVol round=nearlyEquals(mod(g,2.0),0.0);a=0.0;c=k/2.0;b=projPos.xy;if(nearlyEquals(mod(g,3.0),0.0)||nearlyEquals(mod(g,17.0),0.0)){alongNormal(offset,f,1.0,direction);}else if(nearlyEquals(mod(g,5.0),0.0)||nearlyEquals(mod(g,13.0),0.0)){alongNormal(offset,d,-1.0,direction);}else if(nearlyEquals(mod(g,23.0),0.0)){miterUp(offset,a,round,direction);}else if(nearlyEquals(mod(g,19.0),0.0)){degenerate=miterDown(offset,projPos,offsetMatrix,direction);}else if(nearlyEquals(mod(g,7.0),0.0)){squareCap(offset,a,round,f,1.0,direction);}else if(nearlyEquals(mod(g,11.0),0.0)){squareCap(offset,a,round,d,-1.0,direction);}if(!degenerate){vec4 offsets=offsetMatrix*vec4(offset,0.0,0.0);gl_Position=projPos+offsets;}else{gl_Position=vec4(offset,0.0,1.0);}}';


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.linestringreplay.defaultshader.Vertex.SOURCE = GVol.DEBUG_WEBGL ?
    GVol.render.webgl.linestringreplay.defaultshader.Vertex.DEBUG_SOURCE :
    GVol.render.webgl.linestringreplay.defaultshader.Vertex.OPTIMIZED_SOURCE;


  GVol.render.webgl.linestringreplay.defaultshader.vertex = new GVol.render.webgl.linestringreplay.defaultshader.Vertex();


  /**
   * @constructor
   * @param {WebGLRenderingContext} gl GL.
   * @param {WebGLProgram} program Program.
   * @struct
   */
  GVol.render.webgl.linestringreplay.defaultshader.Locations = function(gl, program) {

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_cGVolor = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_cGVolor' : 'n');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_lineWidth = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_lineWidth' : 'k');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_miterLimit = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_miterLimit' : 'l');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_offsetRotateMatrix = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_offsetRotateMatrix' : 'j');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_offsetScaleMatrix = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_offsetScaleMatrix' : 'i');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_opacity = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_opacity' : 'm');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_pixelRatio = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_pixelRatio' : 'p');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_projectionMatrix = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_projectionMatrix' : 'h');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_size = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_size' : 'o');

    /**
     * @type {number}
     */
    this.a_direction = gl.getAttribLocation(
        program, GVol.DEBUG_WEBGL ? 'a_direction' : 'g');

    /**
     * @type {number}
     */
    this.a_lastPos = gl.getAttribLocation(
        program, GVol.DEBUG_WEBGL ? 'a_lastPos' : 'd');

    /**
     * @type {number}
     */
    this.a_nextPos = gl.getAttribLocation(
        program, GVol.DEBUG_WEBGL ? 'a_nextPos' : 'f');

    /**
     * @type {number}
     */
    this.a_position = gl.getAttribLocation(
        program, GVol.DEBUG_WEBGL ? 'a_position' : 'e');
  };

}

goog.provide('GVol.render.webgl.LineStringReplay');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.cGVolor');
goog.require('GVol.extent');
goog.require('GVol.geom.flat.orient');
goog.require('GVol.geom.flat.transform');
goog.require('GVol.geom.flat.topGVology');
goog.require('GVol.obj');
goog.require('GVol.render.webgl');
goog.require('GVol.render.webgl.Replay');
goog.require('GVol.render.webgl.linestringreplay.defaultshader');
goog.require('GVol.webgl');
goog.require('GVol.webgl.Buffer');


if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @extends {GVol.render.webgl.Replay}
   * @param {number} tGVolerance TGVolerance.
   * @param {GVol.Extent} maxExtent Max extent.
   * @struct
   */
  GVol.render.webgl.LineStringReplay = function(tGVolerance, maxExtent) {
    GVol.render.webgl.Replay.call(this, tGVolerance, maxExtent);

    /**
     * @private
     * @type {GVol.render.webgl.linestringreplay.defaultshader.Locations}
     */
    this.defaultLocations_ = null;

    /**
     * @private
     * @type {Array.<Array.<?>>}
     */
    this.styles_ = [];

    /**
     * @private
     * @type {Array.<number>}
     */
    this.styleIndices_ = [];

    /**
     * @private
     * @type {{strokeCGVolor: (Array.<number>|null),
     *         lineCap: (string|undefined),
     *         lineDash: Array.<number>,
     *         lineDashOffset: (number|undefined),
     *         lineJoin: (string|undefined),
     *         lineWidth: (number|undefined),
     *         miterLimit: (number|undefined),
     *         changed: boGVolean}|null}
     */
    this.state_ = {
      strokeCGVolor: null,
      lineCap: undefined,
      lineDash: null,
      lineDashOffset: undefined,
      lineJoin: undefined,
      lineWidth: undefined,
      miterLimit: undefined,
      changed: false
    };

  };
  GVol.inherits(GVol.render.webgl.LineStringReplay, GVol.render.webgl.Replay);


  /**
   * Draw one segment.
   * @private
   * @param {Array.<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   */
  GVol.render.webgl.LineStringReplay.prototype.drawCoordinates_ = function(flatCoordinates, offset, end, stride) {

    var i, ii;
    var numVertices = this.vertices.length;
    var numIndices = this.indices.length;
    //To save a vertex, the direction of a point is a product of the sign (1 or -1), a prime from
    //GVol.render.webgl.LineStringReplay.Instruction_, and a rounding factor (1 or 2). If the product is even,
    //we round it. If it is odd, we don't.
    var lineJoin = this.state_.lineJoin === 'bevel' ? 0 :
      this.state_.lineJoin === 'miter' ? 1 : 2;
    var lineCap = this.state_.lineCap === 'butt' ? 0 :
      this.state_.lineCap === 'square' ? 1 : 2;
    var closed = GVol.geom.flat.topGVology.lineStringIsClosed(flatCoordinates, offset, end, stride);
    var startCoords, sign, n;
    var lastIndex = numIndices;
    var lastSign = 1;
    //We need the adjacent vertices to define normals in joins. p0 = last, p1 = current, p2 = next.
    var p0, p1, p2;

    for (i = offset, ii = end; i < ii; i += stride) {

      n = numVertices / 7;

      p0 = p1;
      p1 = p2 || [flatCoordinates[i], flatCoordinates[i + 1]];
      //First vertex.
      if (i === offset) {
        p2 = [flatCoordinates[i + stride], flatCoordinates[i + stride + 1]];
        if (end - offset === stride * 2 && GVol.array.equals(p1, p2)) {
          break;
        }
        if (closed) {
          //A closed line! Complete the circle.
          p0 = [flatCoordinates[end - stride * 2],
            flatCoordinates[end - stride * 2 + 1]];

          startCoords = p2;
        } else {
          //Add the first two/four vertices.

          if (lineCap) {
            numVertices = this.addVertices_([0, 0], p1, p2,
                lastSign * GVol.render.webgl.LineStringReplay.Instruction_.BEGIN_LINE_CAP * lineCap, numVertices);

            numVertices = this.addVertices_([0, 0], p1, p2,
                -lastSign * GVol.render.webgl.LineStringReplay.Instruction_.BEGIN_LINE_CAP * lineCap, numVertices);

            this.indices[numIndices++] = n + 2;
            this.indices[numIndices++] = n;
            this.indices[numIndices++] = n + 1;

            this.indices[numIndices++] = n + 1;
            this.indices[numIndices++] = n + 3;
            this.indices[numIndices++] = n + 2;

          }

          numVertices = this.addVertices_([0, 0], p1, p2,
              lastSign * GVol.render.webgl.LineStringReplay.Instruction_.BEGIN_LINE * (lineCap || 1), numVertices);

          numVertices = this.addVertices_([0, 0], p1, p2,
              -lastSign * GVol.render.webgl.LineStringReplay.Instruction_.BEGIN_LINE * (lineCap || 1), numVertices);

          lastIndex = numVertices / 7 - 1;

          continue;
        }
      } else if (i === end - stride) {
        //Last vertex.
        if (closed) {
          //Same as the first vertex.
          p2 = startCoords;
          break;
        } else {
          p0 = p0 || [0, 0];

          numVertices = this.addVertices_(p0, p1, [0, 0],
              lastSign * GVol.render.webgl.LineStringReplay.Instruction_.END_LINE * (lineCap || 1), numVertices);

          numVertices = this.addVertices_(p0, p1, [0, 0],
              -lastSign * GVol.render.webgl.LineStringReplay.Instruction_.END_LINE * (lineCap || 1), numVertices);

          this.indices[numIndices++] = n;
          this.indices[numIndices++] = lastIndex - 1;
          this.indices[numIndices++] = lastIndex;

          this.indices[numIndices++] = lastIndex;
          this.indices[numIndices++] = n + 1;
          this.indices[numIndices++] = n;

          if (lineCap) {
            numVertices = this.addVertices_(p0, p1, [0, 0],
                lastSign * GVol.render.webgl.LineStringReplay.Instruction_.END_LINE_CAP * lineCap, numVertices);

            numVertices = this.addVertices_(p0, p1, [0, 0],
                -lastSign * GVol.render.webgl.LineStringReplay.Instruction_.END_LINE_CAP * lineCap, numVertices);

            this.indices[numIndices++] = n + 2;
            this.indices[numIndices++] = n;
            this.indices[numIndices++] = n + 1;

            this.indices[numIndices++] = n + 1;
            this.indices[numIndices++] = n + 3;
            this.indices[numIndices++] = n + 2;

          }

          break;
        }
      } else {
        p2 = [flatCoordinates[i + stride], flatCoordinates[i + stride + 1]];
      }

      // We group CW and straight lines, thus the not so inituitive CCW checking function.
      sign = GVol.render.webgl.triangleIsCounterClockwise(p0[0], p0[1], p1[0], p1[1], p2[0], p2[1])
        ? -1 : 1;

      numVertices = this.addVertices_(p0, p1, p2,
          sign * GVol.render.webgl.LineStringReplay.Instruction_.BEVEL_FIRST * (lineJoin || 1), numVertices);

      numVertices = this.addVertices_(p0, p1, p2,
          sign * GVol.render.webgl.LineStringReplay.Instruction_.BEVEL_SECOND * (lineJoin || 1), numVertices);

      numVertices = this.addVertices_(p0, p1, p2,
          -sign * GVol.render.webgl.LineStringReplay.Instruction_.MITER_BOTTOM * (lineJoin || 1), numVertices);

      if (i > offset) {
        this.indices[numIndices++] = n;
        this.indices[numIndices++] = lastIndex - 1;
        this.indices[numIndices++] = lastIndex;

        this.indices[numIndices++] = n + 2;
        this.indices[numIndices++] = n;
        this.indices[numIndices++] = lastSign * sign > 0 ? lastIndex : lastIndex - 1;
      }

      this.indices[numIndices++] = n;
      this.indices[numIndices++] = n + 2;
      this.indices[numIndices++] = n + 1;

      lastIndex = n + 2;
      lastSign = sign;

      //Add miter
      if (lineJoin) {
        numVertices = this.addVertices_(p0, p1, p2,
            sign * GVol.render.webgl.LineStringReplay.Instruction_.MITER_TOP * lineJoin, numVertices);

        this.indices[numIndices++] = n + 1;
        this.indices[numIndices++] = n + 3;
        this.indices[numIndices++] = n;
      }
    }

    if (closed) {
      n = n || numVertices / 7;
      sign = GVol.geom.flat.orient.linearRingIsClockwise([p0[0], p0[1], p1[0], p1[1], p2[0], p2[1]], 0, 6, 2)
        ? 1 : -1;

      numVertices = this.addVertices_(p0, p1, p2,
          sign * GVol.render.webgl.LineStringReplay.Instruction_.BEVEL_FIRST * (lineJoin || 1), numVertices);

      numVertices = this.addVertices_(p0, p1, p2,
          -sign * GVol.render.webgl.LineStringReplay.Instruction_.MITER_BOTTOM * (lineJoin || 1), numVertices);

      this.indices[numIndices++] = n;
      this.indices[numIndices++] = lastIndex - 1;
      this.indices[numIndices++] = lastIndex;

      this.indices[numIndices++] = n + 1;
      this.indices[numIndices++] = n;
      this.indices[numIndices++] = lastSign * sign > 0 ? lastIndex : lastIndex - 1;
    }
  };

  /**
   * @param {Array.<number>} p0 Last coordinates.
   * @param {Array.<number>} p1 Current coordinates.
   * @param {Array.<number>} p2 Next coordinates.
   * @param {number} product Sign, instruction, and rounding product.
   * @param {number} numVertices Vertex counter.
   * @return {number} Vertex counter.
   * @private
   */
  GVol.render.webgl.LineStringReplay.prototype.addVertices_ = function(p0, p1, p2, product, numVertices) {
    this.vertices[numVertices++] = p0[0];
    this.vertices[numVertices++] = p0[1];
    this.vertices[numVertices++] = p1[0];
    this.vertices[numVertices++] = p1[1];
    this.vertices[numVertices++] = p2[0];
    this.vertices[numVertices++] = p2[1];
    this.vertices[numVertices++] = product;

    return numVertices;
  };

  /**
   * Check if the linestring can be drawn (i. e. valid).
   * @param {Array.<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @return {boGVolean} The linestring can be drawn.
   * @private
   */
  GVol.render.webgl.LineStringReplay.prototype.isValid_ = function(flatCoordinates, offset, end, stride) {
    var range = end - offset;
    if (range < stride * 2) {
      return false;
    } else if (range === stride * 2) {
      var firstP = [flatCoordinates[offset], flatCoordinates[offset + 1]];
      var lastP = [flatCoordinates[offset + stride], flatCoordinates[offset + stride + 1]];
      return !GVol.array.equals(firstP, lastP);
    }

    return true;
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.LineStringReplay.prototype.drawLineString = function(lineStringGeometry, feature) {
    var flatCoordinates = lineStringGeometry.getFlatCoordinates();
    var stride = lineStringGeometry.getStride();
    if (this.isValid_(flatCoordinates, 0, flatCoordinates.length, stride)) {
      flatCoordinates = GVol.geom.flat.transform.translate(flatCoordinates, 0, flatCoordinates.length,
          stride, -this.origin[0], -this.origin[1]);
      if (this.state_.changed) {
        this.styleIndices_.push(this.indices.length);
        this.state_.changed = false;
      }
      this.startIndices.push(this.indices.length);
      this.startIndicesFeature.push(feature);
      this.drawCoordinates_(
          flatCoordinates, 0, flatCoordinates.length, stride);
    }
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.LineStringReplay.prototype.drawMultiLineString = function(multiLineStringGeometry, feature) {
    var indexCount = this.indices.length;
    var ends = multiLineStringGeometry.getEnds();
    ends.unshift(0);
    var flatCoordinates = multiLineStringGeometry.getFlatCoordinates();
    var stride = multiLineStringGeometry.getStride();
    var i, ii;
    if (ends.length > 1) {
      for (i = 1, ii = ends.length; i < ii; ++i) {
        if (this.isValid_(flatCoordinates, ends[i - 1], ends[i], stride)) {
          var lineString = GVol.geom.flat.transform.translate(flatCoordinates, ends[i - 1], ends[i],
              stride, -this.origin[0], -this.origin[1]);
          this.drawCoordinates_(
              lineString, 0, lineString.length, stride);
        }
      }
    }
    if (this.indices.length > indexCount) {
      this.startIndices.push(indexCount);
      this.startIndicesFeature.push(feature);
      if (this.state_.changed) {
        this.styleIndices_.push(indexCount);
        this.state_.changed = false;
      }
    }
  };


  /**
   * @param {Array.<number>} flatCoordinates Flat coordinates.
   * @param {Array.<Array.<number>>} hGVoleFlatCoordinates HGVole flat coordinates.
   * @param {number} stride Stride.
   */
  GVol.render.webgl.LineStringReplay.prototype.drawPGVolygonCoordinates = function(
      flatCoordinates, hGVoleFlatCoordinates, stride) {
    if (!GVol.geom.flat.topGVology.lineStringIsClosed(flatCoordinates, 0,
        flatCoordinates.length, stride)) {
      flatCoordinates.push(flatCoordinates[0]);
      flatCoordinates.push(flatCoordinates[1]);
    }
    this.drawCoordinates_(flatCoordinates, 0, flatCoordinates.length, stride);
    if (hGVoleFlatCoordinates.length) {
      var i, ii;
      for (i = 0, ii = hGVoleFlatCoordinates.length; i < ii; ++i) {
        if (!GVol.geom.flat.topGVology.lineStringIsClosed(hGVoleFlatCoordinates[i], 0,
            hGVoleFlatCoordinates[i].length, stride)) {
          hGVoleFlatCoordinates[i].push(hGVoleFlatCoordinates[i][0]);
          hGVoleFlatCoordinates[i].push(hGVoleFlatCoordinates[i][1]);
        }
        this.drawCoordinates_(hGVoleFlatCoordinates[i], 0,
            hGVoleFlatCoordinates[i].length, stride);
      }
    }
  };


  /**
   * @param {GVol.Feature|GVol.render.Feature} feature Feature.
   * @param {number=} opt_index Index count.
   */
  GVol.render.webgl.LineStringReplay.prototype.setPGVolygonStyle = function(feature, opt_index) {
    var index = opt_index === undefined ? this.indices.length : opt_index;
    this.startIndices.push(index);
    this.startIndicesFeature.push(feature);
    if (this.state_.changed) {
      this.styleIndices_.push(index);
      this.state_.changed = false;
    }
  };


  /**
   * @return {number} Current index.
   */
  GVol.render.webgl.LineStringReplay.prototype.getCurrentIndex = function() {
    return this.indices.length;
  };


  /**
   * @inheritDoc
   **/
  GVol.render.webgl.LineStringReplay.prototype.finish = function(context) {
    // create, bind, and populate the vertices buffer
    this.verticesBuffer = new GVol.webgl.Buffer(this.vertices);

    // create, bind, and populate the indices buffer
    this.indicesBuffer = new GVol.webgl.Buffer(this.indices);

    this.startIndices.push(this.indices.length);

    //Clean up, if there is nothing to draw
    if (this.styleIndices_.length === 0 && this.styles_.length > 0) {
      this.styles_ = [];
    }

    this.vertices = null;
    this.indices = null;
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.LineStringReplay.prototype.getDeleteResourcesFunction = function(context) {
    var verticesBuffer = this.verticesBuffer;
    var indicesBuffer = this.indicesBuffer;
    return function() {
      context.deleteBuffer(verticesBuffer);
      context.deleteBuffer(indicesBuffer);
    };
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.LineStringReplay.prototype.setUpProgram = function(gl, context, size, pixelRatio) {
    // get the program
    var fragmentShader, vertexShader;
    fragmentShader = GVol.render.webgl.linestringreplay.defaultshader.fragment;
    vertexShader = GVol.render.webgl.linestringreplay.defaultshader.vertex;
    var program = context.getProgram(fragmentShader, vertexShader);

    // get the locations
    var locations;
    if (!this.defaultLocations_) {
      // eslint-disable-next-line openlayers-internal/no-missing-requires
      locations = new GVol.render.webgl.linestringreplay.defaultshader.Locations(gl, program);
      this.defaultLocations_ = locations;
    } else {
      locations = this.defaultLocations_;
    }

    context.useProgram(program);

    // enable the vertex attrib arrays
    gl.enableVertexAttribArray(locations.a_lastPos);
    gl.vertexAttribPointer(locations.a_lastPos, 2, GVol.webgl.FLOAT,
        false, 28, 0);

    gl.enableVertexAttribArray(locations.a_position);
    gl.vertexAttribPointer(locations.a_position, 2, GVol.webgl.FLOAT,
        false, 28, 8);

    gl.enableVertexAttribArray(locations.a_nextPos);
    gl.vertexAttribPointer(locations.a_nextPos, 2, GVol.webgl.FLOAT,
        false, 28, 16);

    gl.enableVertexAttribArray(locations.a_direction);
    gl.vertexAttribPointer(locations.a_direction, 1, GVol.webgl.FLOAT,
        false, 28, 24);

    // Enable renderer specific uniforms.
    gl.uniform2fv(locations.u_size, size);
    gl.uniform1f(locations.u_pixelRatio, pixelRatio);

    return locations;
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.LineStringReplay.prototype.shutDownProgram = function(gl, locations) {
    gl.disableVertexAttribArray(locations.a_lastPos);
    gl.disableVertexAttribArray(locations.a_position);
    gl.disableVertexAttribArray(locations.a_nextPos);
    gl.disableVertexAttribArray(locations.a_direction);
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.LineStringReplay.prototype.drawReplay = function(gl, context, skippedFeaturesHash, hitDetection) {
    //Save GL parameters.
    var tmpDepthFunc = /** @type {number} */ (gl.getParameter(gl.DEPTH_FUNC));
    var tmpDepthMask = /** @type {boGVolean} */ (gl.getParameter(gl.DEPTH_WRITEMASK));

    if (!hitDetection) {
      gl.enable(gl.DEPTH_TEST);
      gl.depthMask(true);
      gl.depthFunc(gl.NOTEQUAL);
    }

    if (!GVol.obj.isEmpty(skippedFeaturesHash)) {
      this.drawReplaySkipping_(gl, context, skippedFeaturesHash);
    } else {
      //Draw by style groups to minimize drawElements() calls.
      var i, start, end, nextStyle;
      end = this.startIndices[this.startIndices.length - 1];
      for (i = this.styleIndices_.length - 1; i >= 0; --i) {
        start = this.styleIndices_[i];
        nextStyle = this.styles_[i];
        this.setStrokeStyle_(gl, nextStyle[0], nextStyle[1], nextStyle[2]);
        this.drawElements(gl, context, start, end);
        gl.clear(gl.DEPTH_BUFFER_BIT);
        end = start;
      }
    }
    if (!hitDetection) {
      gl.disable(gl.DEPTH_TEST);
      gl.clear(gl.DEPTH_BUFFER_BIT);
      //Restore GL parameters.
      gl.depthMask(tmpDepthMask);
      gl.depthFunc(tmpDepthFunc);
    }
  };


  /**
   * @private
   * @param {WebGLRenderingContext} gl gl.
   * @param {GVol.webgl.Context} context Context.
   * @param {Object} skippedFeaturesHash Ids of features to skip.
   */
  GVol.render.webgl.LineStringReplay.prototype.drawReplaySkipping_ = function(gl, context, skippedFeaturesHash) {
    var i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex, featureStart;
    featureIndex = this.startIndices.length - 2;
    end = start = this.startIndices[featureIndex + 1];
    for (i = this.styleIndices_.length - 1; i >= 0; --i) {
      nextStyle = this.styles_[i];
      this.setStrokeStyle_(gl, nextStyle[0], nextStyle[1], nextStyle[2]);
      groupStart = this.styleIndices_[i];

      while (featureIndex >= 0 &&
          this.startIndices[featureIndex] >= groupStart) {
        featureStart = this.startIndices[featureIndex];
        feature = this.startIndicesFeature[featureIndex];
        featureUid = GVol.getUid(feature).toString();

        if (skippedFeaturesHash[featureUid]) {
          if (start !== end) {
            this.drawElements(gl, context, start, end);
            gl.clear(gl.DEPTH_BUFFER_BIT);
          }
          end = featureStart;
        }
        featureIndex--;
        start = featureStart;
      }
      if (start !== end) {
        this.drawElements(gl, context, start, end);
        gl.clear(gl.DEPTH_BUFFER_BIT);
      }
      start = end = groupStart;
    }
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.LineStringReplay.prototype.drawHitDetectionReplayOneByOne = function(gl, context, skippedFeaturesHash,
      featureCallback, opt_hitExtent) {
    var i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex;
    featureIndex = this.startIndices.length - 2;
    end = this.startIndices[featureIndex + 1];
    for (i = this.styleIndices_.length - 1; i >= 0; --i) {
      nextStyle = this.styles_[i];
      this.setStrokeStyle_(gl, nextStyle[0], nextStyle[1], nextStyle[2]);
      groupStart = this.styleIndices_[i];

      while (featureIndex >= 0 &&
          this.startIndices[featureIndex] >= groupStart) {
        start = this.startIndices[featureIndex];
        feature = this.startIndicesFeature[featureIndex];
        featureUid = GVol.getUid(feature).toString();

        if (skippedFeaturesHash[featureUid] === undefined &&
            feature.getGeometry() &&
            (opt_hitExtent === undefined || GVol.extent.intersects(
                /** @type {Array<number>} */ (opt_hitExtent),
                feature.getGeometry().getExtent()))) {
          gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
          this.drawElements(gl, context, start, end);

          var result = featureCallback(feature);

          if (result) {
            return result;
          }

        }
        featureIndex--;
        end = start;
      }
    }
    return undefined;
  };


  /**
   * @private
   * @param {WebGLRenderingContext} gl gl.
   * @param {Array.<number>} cGVolor CGVolor.
   * @param {number} lineWidth Line width.
   * @param {number} miterLimit Miter limit.
   */
  GVol.render.webgl.LineStringReplay.prototype.setStrokeStyle_ = function(gl, cGVolor, lineWidth, miterLimit) {
    gl.uniform4fv(this.defaultLocations_.u_cGVolor, cGVolor);
    gl.uniform1f(this.defaultLocations_.u_lineWidth, lineWidth);
    gl.uniform1f(this.defaultLocations_.u_miterLimit, miterLimit);
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.LineStringReplay.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) {
    var strokeStyleLineCap = strokeStyle.getLineCap();
    this.state_.lineCap = strokeStyleLineCap !== undefined ?
      strokeStyleLineCap : GVol.render.webgl.defaultLineCap;
    var strokeStyleLineDash = strokeStyle.getLineDash();
    this.state_.lineDash = strokeStyleLineDash ?
      strokeStyleLineDash : GVol.render.webgl.defaultLineDash;
    var strokeStyleLineDashOffset = strokeStyle.getLineDashOffset();
    this.state_.lineDashOffset = strokeStyleLineDashOffset ?
      strokeStyleLineDashOffset : GVol.render.webgl.defaultLineDashOffset;
    var strokeStyleLineJoin = strokeStyle.getLineJoin();
    this.state_.lineJoin = strokeStyleLineJoin !== undefined ?
      strokeStyleLineJoin : GVol.render.webgl.defaultLineJoin;
    var strokeStyleCGVolor = strokeStyle.getCGVolor();
    if (!(strokeStyleCGVolor instanceof CanvasGradient) &&
        !(strokeStyleCGVolor instanceof CanvasPattern)) {
      strokeStyleCGVolor = GVol.cGVolor.asArray(strokeStyleCGVolor).map(function(c, i) {
        return i != 3 ? c / 255 : c;
      }) || GVol.render.webgl.defaultStrokeStyle;
    } else {
      strokeStyleCGVolor = GVol.render.webgl.defaultStrokeStyle;
    }
    var strokeStyleWidth = strokeStyle.getWidth();
    strokeStyleWidth = strokeStyleWidth !== undefined ?
      strokeStyleWidth : GVol.render.webgl.defaultLineWidth;
    var strokeStyleMiterLimit = strokeStyle.getMiterLimit();
    strokeStyleMiterLimit = strokeStyleMiterLimit !== undefined ?
      strokeStyleMiterLimit : GVol.render.webgl.defaultMiterLimit;
    if (!this.state_.strokeCGVolor || !GVol.array.equals(this.state_.strokeCGVolor, strokeStyleCGVolor) ||
        this.state_.lineWidth !== strokeStyleWidth || this.state_.miterLimit !== strokeStyleMiterLimit) {
      this.state_.changed = true;
      this.state_.strokeCGVolor = strokeStyleCGVolor;
      this.state_.lineWidth = strokeStyleWidth;
      this.state_.miterLimit = strokeStyleMiterLimit;
      this.styles_.push([strokeStyleCGVolor, strokeStyleWidth, strokeStyleMiterLimit]);
    }
  };

  /**
   * @enum {number}
   * @private
   */
  GVol.render.webgl.LineStringReplay.Instruction_ = {
    ROUND: 2,
    BEGIN_LINE: 3,
    END_LINE: 5,
    BEGIN_LINE_CAP: 7,
    END_LINE_CAP: 11,
    BEVEL_FIRST: 13,
    BEVEL_SECOND: 17,
    MITER_BOTTOM: 19,
    MITER_TOP: 23
  };

}

// This file is automatically generated, do not edit
/* eslint openlayers-internal/no-missing-requires: 0 */
goog.provide('GVol.render.webgl.pGVolygonreplay.defaultshader');

goog.require('GVol');
goog.require('GVol.webgl.Fragment');
goog.require('GVol.webgl.Vertex');

if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @extends {GVol.webgl.Fragment}
   * @struct
   */
  GVol.render.webgl.pGVolygonreplay.defaultshader.Fragment = function() {
    GVol.webgl.Fragment.call(this, GVol.render.webgl.pGVolygonreplay.defaultshader.Fragment.SOURCE);
  };
  GVol.inherits(GVol.render.webgl.pGVolygonreplay.defaultshader.Fragment, GVol.webgl.Fragment);


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.pGVolygonreplay.defaultshader.Fragment.DEBUG_SOURCE = 'precision mediump float;\n\n\n\nuniform vec4 u_cGVolor;\nuniform float u_opacity;\n\nvoid main(void) {\n  gl_FragCGVolor = u_cGVolor;\n  float alpha = u_cGVolor.a * u_opacity;\n  if (alpha == 0.0) {\n    discard;\n  }\n  gl_FragCGVolor.a = alpha;\n}\n';


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.pGVolygonreplay.defaultshader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;uniform vec4 e;uniform float f;void main(void){gl_FragCGVolor=e;float alpha=e.a*f;if(alpha==0.0){discard;}gl_FragCGVolor.a=alpha;}';


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.pGVolygonreplay.defaultshader.Fragment.SOURCE = GVol.DEBUG_WEBGL ?
    GVol.render.webgl.pGVolygonreplay.defaultshader.Fragment.DEBUG_SOURCE :
    GVol.render.webgl.pGVolygonreplay.defaultshader.Fragment.OPTIMIZED_SOURCE;


  GVol.render.webgl.pGVolygonreplay.defaultshader.fragment = new GVol.render.webgl.pGVolygonreplay.defaultshader.Fragment();


  /**
   * @constructor
   * @extends {GVol.webgl.Vertex}
   * @struct
   */
  GVol.render.webgl.pGVolygonreplay.defaultshader.Vertex = function() {
    GVol.webgl.Vertex.call(this, GVol.render.webgl.pGVolygonreplay.defaultshader.Vertex.SOURCE);
  };
  GVol.inherits(GVol.render.webgl.pGVolygonreplay.defaultshader.Vertex, GVol.webgl.Vertex);


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.pGVolygonreplay.defaultshader.Vertex.DEBUG_SOURCE = '\n\nattribute vec2 a_position;\n\nuniform mat4 u_projectionMatrix;\nuniform mat4 u_offsetScaleMatrix;\nuniform mat4 u_offsetRotateMatrix;\n\nvoid main(void) {\n  gl_Position = u_projectionMatrix * vec4(a_position, 0.0, 1.0);\n}\n\n\n';


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.pGVolygonreplay.defaultshader.Vertex.OPTIMIZED_SOURCE = 'attribute vec2 a;uniform mat4 b;uniform mat4 c;uniform mat4 d;void main(void){gl_Position=b*vec4(a,0.0,1.0);}';


  /**
   * @const
   * @type {string}
   */
  GVol.render.webgl.pGVolygonreplay.defaultshader.Vertex.SOURCE = GVol.DEBUG_WEBGL ?
    GVol.render.webgl.pGVolygonreplay.defaultshader.Vertex.DEBUG_SOURCE :
    GVol.render.webgl.pGVolygonreplay.defaultshader.Vertex.OPTIMIZED_SOURCE;


  GVol.render.webgl.pGVolygonreplay.defaultshader.vertex = new GVol.render.webgl.pGVolygonreplay.defaultshader.Vertex();


  /**
   * @constructor
   * @param {WebGLRenderingContext} gl GL.
   * @param {WebGLProgram} program Program.
   * @struct
   */
  GVol.render.webgl.pGVolygonreplay.defaultshader.Locations = function(gl, program) {

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_cGVolor = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_cGVolor' : 'e');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_offsetRotateMatrix = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_offsetRotateMatrix' : 'd');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_offsetScaleMatrix = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_offsetScaleMatrix' : 'c');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_opacity = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_opacity' : 'f');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_projectionMatrix = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_projectionMatrix' : 'b');

    /**
     * @type {number}
     */
    this.a_position = gl.getAttribLocation(
        program, GVol.DEBUG_WEBGL ? 'a_position' : 'a');
  };

}

goog.provide('GVol.style.Stroke');

goog.require('GVol');


/**
 * @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 {GVolx.style.StrokeOptions=} opt_options Options.
 * @api
 */
GVol.style.Stroke = function(opt_options) {

  var options = opt_options || {};

  /**
   * @private
   * @type {GVol.CGVolor|GVol.CGVolorLike}
   */
  this.cGVolor_ = options.cGVolor !== undefined ? options.cGVolor : null;

  /**
   * @private
   * @type {string|undefined}
   */
  this.lineCap_ = options.lineCap;

  /**
   * @private
   * @type {Array.<number>}
   */
  this.lineDash_ = options.lineDash !== undefined ? options.lineDash : null;

  /**
   * @private
   * @type {number|undefined}
   */
  this.lineDashOffset_ = options.lineDashOffset;

  /**
   * @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;
};


/**
 * Clones the style.
 * @return {GVol.style.Stroke} The cloned style.
 * @api
 */
GVol.style.Stroke.prototype.clone = function() {
  var cGVolor = this.getCGVolor();
  return new GVol.style.Stroke({
    cGVolor: (cGVolor && cGVolor.slice) ? cGVolor.slice() : cGVolor || undefined,
    lineCap: this.getLineCap(),
    lineDash: this.getLineDash() ? this.getLineDash().slice() : undefined,
    lineDashOffset: this.getLineDashOffset(),
    lineJoin: this.getLineJoin(),
    miterLimit: this.getMiterLimit(),
    width: this.getWidth()
  });
};


/**
 * Get the stroke cGVolor.
 * @return {GVol.CGVolor|GVol.CGVolorLike} CGVolor.
 * @api
 */
GVol.style.Stroke.prototype.getCGVolor = function() {
  return this.cGVolor_;
};


/**
 * Get the line cap type for the stroke.
 * @return {string|undefined} Line cap.
 * @api
 */
GVol.style.Stroke.prototype.getLineCap = function() {
  return this.lineCap_;
};


/**
 * Get the line dash style for the stroke.
 * @return {Array.<number>} Line dash.
 * @api
 */
GVol.style.Stroke.prototype.getLineDash = function() {
  return this.lineDash_;
};


/**
 * Get the line dash offset for the stroke.
 * @return {number|undefined} Line dash offset.
 * @api
 */
GVol.style.Stroke.prototype.getLineDashOffset = function() {
  return this.lineDashOffset_;
};


/**
 * Get the line join type for the stroke.
 * @return {string|undefined} Line join.
 * @api
 */
GVol.style.Stroke.prototype.getLineJoin = function() {
  return this.lineJoin_;
};


/**
 * Get the miter limit for the stroke.
 * @return {number|undefined} Miter limit.
 * @api
 */
GVol.style.Stroke.prototype.getMiterLimit = function() {
  return this.miterLimit_;
};


/**
 * Get the stroke width.
 * @return {number|undefined} Width.
 * @api
 */
GVol.style.Stroke.prototype.getWidth = function() {
  return this.width_;
};


/**
 * Set the cGVolor.
 *
 * @param {GVol.CGVolor|GVol.CGVolorLike} cGVolor CGVolor.
 * @api
 */
GVol.style.Stroke.prototype.setCGVolor = function(cGVolor) {
  this.cGVolor_ = cGVolor;
  this.checksum_ = undefined;
};


/**
 * Set the line cap.
 *
 * @param {string|undefined} lineCap Line cap.
 * @api
 */
GVol.style.Stroke.prototype.setLineCap = function(lineCap) {
  this.lineCap_ = lineCap;
  this.checksum_ = undefined;
};


/**
 * Set the line dash.
 *
 * Please note that Internet Explorer 10 and lower [do not support][mdn] the
 * `setLineDash` method on the `CanvasRenderingContext2D` and therefore this
 * property will have no visual effect in these browsers.
 *
 * [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/setLineDash#Browser_compatibility
 *
 * @param {Array.<number>} lineDash Line dash.
 * @api
 */
GVol.style.Stroke.prototype.setLineDash = function(lineDash) {
  this.lineDash_ = lineDash;
  this.checksum_ = undefined;
};


/**
 * Set the line dash offset.
 *
 * @param {number|undefined} lineDashOffset Line dash offset.
 * @api
 */
GVol.style.Stroke.prototype.setLineDashOffset = function(lineDashOffset) {
  this.lineDashOffset_ = lineDashOffset;
  this.checksum_ = undefined;
};


/**
 * Set the line join.
 *
 * @param {string|undefined} lineJoin Line join.
 * @api
 */
GVol.style.Stroke.prototype.setLineJoin = function(lineJoin) {
  this.lineJoin_ = lineJoin;
  this.checksum_ = undefined;
};


/**
 * Set the miter limit.
 *
 * @param {number|undefined} miterLimit Miter limit.
 * @api
 */
GVol.style.Stroke.prototype.setMiterLimit = function(miterLimit) {
  this.miterLimit_ = miterLimit;
  this.checksum_ = undefined;
};


/**
 * Set the width.
 *
 * @param {number|undefined} width Width.
 * @api
 */
GVol.style.Stroke.prototype.setWidth = function(width) {
  this.width_ = width;
  this.checksum_ = undefined;
};


/**
 * @return {string} The checksum.
 */
GVol.style.Stroke.prototype.getChecksum = function() {
  if (this.checksum_ === undefined) {
    this.checksum_ = 's';
    if (this.cGVolor_) {
      if (typeof this.cGVolor_ === 'string') {
        this.checksum_ += this.cGVolor_;
      } else {
        this.checksum_ += GVol.getUid(this.cGVolor_).toString();
      }
    } else {
      this.checksum_ += '-';
    }
    this.checksum_ += ',' +
        (this.lineCap_ !== undefined ?
          this.lineCap_.toString() : '-') + ',' +
        (this.lineDash_ ?
          this.lineDash_.toString() : '-') + ',' +
        (this.lineDashOffset_ !== undefined ?
          this.lineDashOffset_ : '-') + ',' +
        (this.lineJoin_ !== undefined ?
          this.lineJoin_ : '-') + ',' +
        (this.miterLimit_ !== undefined ?
          this.miterLimit_.toString() : '-') + ',' +
        (this.width_ !== undefined ?
          this.width_.toString() : '-');
  }

  return this.checksum_;
};

goog.provide('GVol.structs.LinkedList');

/**
 * Creates an empty linked list structure.
 *
 * @constructor
 * @struct
 * @param {boGVolean=} opt_circular The last item is connected to the first one,
 * and the first item to the last one. Default is true.
 */
GVol.structs.LinkedList = function(opt_circular) {

  /**
   * @private
   * @type {GVol.LinkedListItem|undefined}
   */
  this.first_ = undefined;

  /**
   * @private
   * @type {GVol.LinkedListItem|undefined}
   */
  this.last_ = undefined;

  /**
   * @private
   * @type {GVol.LinkedListItem|undefined}
   */
  this.head_ = undefined;

  /**
   * @private
   * @type {boGVolean}
   */
  this.circular_ = opt_circular === undefined ? true : opt_circular;

  /**
   * @private
   * @type {number}
   */
  this.length_ = 0;
};

/**
 * Inserts an item into the linked list right after the current one.
 *
 * @param {?} data Item data.
 */
GVol.structs.LinkedList.prototype.insertItem = function(data) {

  /** @type {GVol.LinkedListItem} */
  var item = {
    prev: undefined,
    next: undefined,
    data: data
  };

  var head = this.head_;

  //Initialize the list.
  if (!head) {
    this.first_ = item;
    this.last_ = item;
    if (this.circular_) {
      item.next = item;
      item.prev = item;
    }
  } else {
    //Link the new item to the adjacent ones.
    var next = head.next;
    item.prev = head;
    item.next = next;
    head.next = item;
    if (next) {
      next.prev = item;
    }

    if (head === this.last_) {
      this.last_ = item;
    }
  }
  this.head_ = item;
  this.length_++;
};

/**
 * Removes the current item from the list. Sets the cursor to the next item,
 * if possible.
 */
GVol.structs.LinkedList.prototype.removeItem = function() {
  var head = this.head_;
  if (head) {
    var next = head.next;
    var prev = head.prev;
    if (next) {
      next.prev = prev;
    }
    if (prev) {
      prev.next = next;
    }
    this.head_ = next || prev;

    if (this.first_ === this.last_) {
      this.head_ = undefined;
      this.first_ = undefined;
      this.last_ = undefined;
    } else if (this.first_ === head) {
      this.first_ = this.head_;
    } else if (this.last_ === head) {
      this.last_ = prev ? this.head_.prev : this.head_;
    }
    this.length_--;
  }
};

/**
 * Sets the cursor to the first item, and returns the associated data.
 *
 * @return {?} Item data.
 */
GVol.structs.LinkedList.prototype.firstItem = function() {
  this.head_ = this.first_;
  if (this.head_) {
    return this.head_.data;
  }
  return undefined;
};

/**
* Sets the cursor to the last item, and returns the associated data.
*
* @return {?} Item data.
*/
GVol.structs.LinkedList.prototype.lastItem = function() {
  this.head_ = this.last_;
  if (this.head_) {
    return this.head_.data;
  }
  return undefined;
};

/**
 * Sets the cursor to the next item, and returns the associated data.
 *
 * @return {?} Item data.
 */
GVol.structs.LinkedList.prototype.nextItem = function() {
  if (this.head_ && this.head_.next) {
    this.head_ = this.head_.next;
    return this.head_.data;
  }
  return undefined;
};

/**
 * Returns the next item's data without moving the cursor.
 *
 * @return {?} Item data.
 */
GVol.structs.LinkedList.prototype.getNextItem = function() {
  if (this.head_ && this.head_.next) {
    return this.head_.next.data;
  }
  return undefined;
};

/**
 * Sets the cursor to the previous item, and returns the associated data.
 *
 * @return {?} Item data.
 */
GVol.structs.LinkedList.prototype.prevItem = function() {
  if (this.head_ && this.head_.prev) {
    this.head_ = this.head_.prev;
    return this.head_.data;
  }
  return undefined;
};

/**
 * Returns the previous item's data without moving the cursor.
 *
 * @return {?} Item data.
 */
GVol.structs.LinkedList.prototype.getPrevItem = function() {
  if (this.head_ && this.head_.prev) {
    return this.head_.prev.data;
  }
  return undefined;
};

/**
 * Returns the current item's data.
 *
 * @return {?} Item data.
 */
GVol.structs.LinkedList.prototype.getCurrItem = function() {
  if (this.head_) {
    return this.head_.data;
  }
  return undefined;
};

/**
 * Sets the first item of the list. This only works for circular lists, and sets
 * the last item accordingly.
 */
GVol.structs.LinkedList.prototype.setFirstItem = function() {
  if (this.circular_ && this.head_) {
    this.first_ = this.head_;
    this.last_ = this.head_.prev;
  }
};

/**
 * Concatenates two lists.
 * @param {GVol.structs.LinkedList} list List to merge into the current list.
 */
GVol.structs.LinkedList.prototype.concat = function(list) {
  if (list.head_) {
    if (this.head_) {
      var end = this.head_.next;
      this.head_.next = list.first_;
      list.first_.prev = this.head_;
      end.prev = list.last_;
      list.last_.next = end;
      this.length_ += list.length_;
    } else {
      this.head_ = list.head_;
      this.first_ = list.first_;
      this.last_ = list.last_;
      this.length_ = list.length_;
    }
    list.head_ = undefined;
    list.first_ = undefined;
    list.last_ = undefined;
    list.length_ = 0;
  }
};

/**
 * Returns the current length of the list.
 *
 * @return {number} Length.
 */
GVol.structs.LinkedList.prototype.getLength = function() {
  return this.length_;
};


/**
 * @fileoverview
 * @suppress {accessContrGVols, ambiguousFunctionDecl, checkDebuggerStatement, checkRegExp, checkTypes, checkVars, const, constantProperty, deprecated, duplicate, es5Strict, fileoverviewTags, missingProperties, nonStandardJsDocs, strictModuleDepCheck, suspiciousCode, undefinedNames, undefinedVars, unknownDefines, unusedLocalVariables, uselessCode, visibility}
 */
goog.provide('GVol.ext.rbush');

/** @typedef {function(*)} */
GVol.ext.rbush = function() {};

(function() {(function (exports) {
'use strict';

var quickselect = partialSort;
function partialSort(arr, k, left, right, compare) {
    left = left || 0;
    right = right || (arr.length - 1);
    compare = compare || defaultCompare;
    while (right > left) {
        if (right - left > 600) {
            var n = right - left + 1;
            var m = k - left + 1;
            var z = Math.log(n);
            var s = 0.5 * Math.exp(2 * z / 3);
            var sd = 0.5 * Math.sqrt(z * s * (n - s) / n) * (m - n / 2 < 0 ? -1 : 1);
            var newLeft = Math.max(left, Math.floor(k - m * s / n + sd));
            var newRight = Math.min(right, Math.floor(k + (n - m) * s / n + sd));
            partialSort(arr, k, newLeft, newRight, compare);
        }
        var t = arr[k];
        var i = left;
        var 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;
}
function defaultCompare(a, b) {
    return a < b ? -1 : a > b ? 1 : 0;
}

var rbush_1 = rbush;
function rbush(maxEntries, format) {
    if (!(this instanceof rbush)) return new rbush(maxEntries, format);
    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)) 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;
                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;
    },
    cGVollides: function (bbox) {
        var node = this.data,
            toBBox = this.toBBox;
        if (!intersects(bbox, node)) 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;
                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;
        }
        var node = this._build(data.slice(), 0, data.length - 1, 0);
        if (!this.data.children.length) {
            this.data = node;
        } else if (this.data.height === node.height) {
            this._splitRoot(this.data, node);
        } else {
            if (this.data.height < node.height) {
                var tmpNode = this.data;
                this.data = node;
                node = tmpNode;
            }
            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 = createNode([]);
        return this;
    },
    remove: function (item, equalsFn) {
        if (!item) return this;
        var node = this.data,
            bbox = this.toBBox(item),
            path = [],
            indexes = [],
            i, parent, index, goingUp;
        while (node || path.length) {
            if (!node) {
                node = path.pop();
                parent = path[path.length - 1];
                i = indexes.pop();
                goingUp = true;
            }
            if (node.leaf) {
                index = findItem(item, node.children, equalsFn);
                if (index !== -1) {
                    node.children.splice(index, 1);
                    path.push(node);
                    this._condense(path);
                    return this;
                }
            }
            if (!goingUp && !node.leaf && contains(node, bbox)) {
                path.push(node);
                indexes.push(i);
                i = 0;
                parent = node;
                node = node.children[0];
            } else if (parent) {
                i++;
                node = parent.children[i];
                goingUp = false;
            } else node = null;
        }
        return this;
    },
    toBBox: function (item) { return item; },
    compareMinX: compareNodeMinX,
    compareMinY: compareNodeMinY,
    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) {
            node = createNode(items.slice(left, right + 1));
            calcBBox(node, this.toBBox);
            return node;
        }
        if (!height) {
            height = Math.ceil(Math.log(N) / Math.log(M));
            M = Math.ceil(N / Math.pow(M, height - 1));
        }
        node = createNode([]);
        node.leaf = false;
        node.height = height;
        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);
                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);
                enlargement = enlargedArea(bbox, child) - area;
                if (enlargement < minEnlargement) {
                    minEnlargement = enlargement;
                    minArea = area < minArea ? area : minArea;
                    targetNode = child;
                } else if (enlargement === minEnlargement) {
                    if (area < minArea) {
                        minArea = area;
                        targetNode = child;
                    }
                }
            }
            node = targetNode || node.children[0];
        }
        return node;
    },
    _insert: function (item, level, isNode) {
        var toBBox = this.toBBox,
            bbox = isNode ? item : toBBox(item),
            insertPath = [];
        var node = this._chooseSubtree(bbox, this.data, level, insertPath);
        node.children.push(item);
        extend(node, bbox);
        while (level >= 0) {
            if (insertPath[level].children.length > this._maxEntries) {
                this._split(insertPath, level);
                level--;
            } else break;
        }
        this._adjustParentBBoxes(bbox, insertPath, level);
    },
    _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 = createNode(node.children.splice(splitIndex, node.children.length - splitIndex));
        newNode.height = node.height;
        newNode.leaf = node.leaf;
        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) {
        this.data = createNode([node, newNode]);
        this.data.height = node.height + 1;
        this.data.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);
            if (overlap < minOverlap) {
                minOverlap = overlap;
                index = i;
                minArea = area < minArea ? area : minArea;
            } else if (overlap === minOverlap) {
                if (area < minArea) {
                    minArea = area;
                    index = i;
                }
            }
        }
        return index;
    },
    _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 (xMargin < yMargin) node.children.sort(compareMinX);
    },
    _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);
            margin += bboxMargin(leftBBox);
        }
        for (i = M - m - 1; i >= m; i--) {
            child = node.children[i];
            extend(rightBBox, node.leaf ? toBBox(child) : child);
            margin += bboxMargin(rightBBox);
        }
        return margin;
    },
    _adjustParentBBoxes: function (bbox, path, level) {
        for (var i = level; i >= 0; i--) {
            extend(path[i], bbox);
        }
    },
    _condense: function (path) {
        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) {
        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 {minX: a' + format[0] +
            ', minY: a' + format[1] +
            ', maxX: a' + format[2] +
            ', maxY: a' + format[3] + '};');
    }
};
function findItem(item, items, equalsFn) {
    if (!equalsFn) return items.indexOf(item);
    for (var i = 0; i < items.length; i++) {
        if (equalsFn(item, items[i])) return i;
    }
    return -1;
}
function calcBBox(node, toBBox) {
    distBBox(node, 0, node.children.length, toBBox, node);
}
function distBBox(node, k, p, toBBox, destNode) {
    if (!destNode) destNode = createNode(null);
    destNode.minX = Infinity;
    destNode.minY = Infinity;
    destNode.maxX = -Infinity;
    destNode.maxY = -Infinity;
    for (var i = k, child; i < p; i++) {
        child = node.children[i];
        extend(destNode, node.leaf ? toBBox(child) : child);
    }
    return destNode;
}
function extend(a, b) {
    a.minX = Math.min(a.minX, b.minX);
    a.minY = Math.min(a.minY, b.minY);
    a.maxX = Math.max(a.maxX, b.maxX);
    a.maxY = Math.max(a.maxY, b.maxY);
    return a;
}
function compareNodeMinX(a, b) { return a.minX - b.minX; }
function compareNodeMinY(a, b) { return a.minY - b.minY; }
function bboxArea(a)   { return (a.maxX - a.minX) * (a.maxY - a.minY); }
function bboxMargin(a) { return (a.maxX - a.minX) + (a.maxY - a.minY); }
function enlargedArea(a, b) {
    return (Math.max(b.maxX, a.maxX) - Math.min(b.minX, a.minX)) *
           (Math.max(b.maxY, a.maxY) - Math.min(b.minY, a.minY));
}
function intersectionArea(a, b) {
    var minX = Math.max(a.minX, b.minX),
        minY = Math.max(a.minY, b.minY),
        maxX = Math.min(a.maxX, b.maxX),
        maxY = Math.min(a.maxY, b.maxY);
    return Math.max(0, maxX - minX) *
           Math.max(0, maxY - minY);
}
function contains(a, b) {
    return a.minX <= b.minX &&
           a.minY <= b.minY &&
           b.maxX <= a.maxX &&
           b.maxY <= a.maxY;
}
function intersects(a, b) {
    return b.minX <= a.maxX &&
           b.minY <= a.maxY &&
           b.maxX >= a.minX &&
           b.maxY >= a.minY;
}
function createNode(children) {
    return {
        children: children,
        height: 1,
        leaf: true,
        minX: Infinity,
        minY: Infinity,
        maxX: -Infinity,
        maxY: -Infinity
    };
}
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;
        quickselect(arr, mid, left, right, compare);
        stack.push(left, mid, mid, right);
    }
}

exports['default'] = rbush_1;

}((this.rbush = this.rbush || {})));}).call(GVol.ext);
GVol.ext.rbush = GVol.ext.rbush.default;

goog.provide('GVol.structs.RBush');

goog.require('GVol');
goog.require('GVol.ext.rbush');
goog.require('GVol.extent');
goog.require('GVol.obj');


/**
 * Wrapper around the RBush by Vladimir Agafonkin.
 *
 * @constructor
 * @param {number=} opt_maxEntries Max entries.
 * @see https://github.com/mourner/rbush
 * @struct
 * @template T
 */
GVol.structs.RBush = function(opt_maxEntries) {

  /**
   * @private
   */
  this.rbush_ = GVol.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, GVol.RBushEntry>}
   */
  this.items_ = {};

};


/**
 * Insert a value into the RBush.
 * @param {GVol.Extent} extent Extent.
 * @param {T} value Value.
 */
GVol.structs.RBush.prototype.insert = function(extent, value) {
  /** @type {GVol.RBushEntry} */
  var item = {
    minX: extent[0],
    minY: extent[1],
    maxX: extent[2],
    maxY: extent[3],
    value: value
  };

  this.rbush_.insert(item);
  this.items_[GVol.getUid(value)] = item;
};


/**
 * Bulk-insert values into the RBush.
 * @param {Array.<GVol.Extent>} extents Extents.
 * @param {Array.<T>} values Values.
 */
GVol.structs.RBush.prototype.load = function(extents, values) {
  var items = new Array(values.length);
  for (var i = 0, l = values.length; i < l; i++) {
    var extent = extents[i];
    var value = values[i];

    /** @type {GVol.RBushEntry} */
    var item = {
      minX: extent[0],
      minY: extent[1],
      maxX: extent[2],
      maxY: extent[3],
      value: value
    };
    items[i] = item;
    this.items_[GVol.getUid(value)] = item;
  }
  this.rbush_.load(items);
};


/**
 * Remove a value from the RBush.
 * @param {T} value Value.
 * @return {boGVolean} Removed.
 */
GVol.structs.RBush.prototype.remove = function(value) {
  var uid = GVol.getUid(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 {GVol.Extent} extent Extent.
 * @param {T} value Value.
 */
GVol.structs.RBush.prototype.update = function(extent, value) {
  var item = this.items_[GVol.getUid(value)];
  var bbox = [item.minX, item.minY, item.maxX, item.maxY];
  if (!GVol.extent.equals(bbox, extent)) {
    this.remove(value);
    this.insert(extent, value);
  }
};


/**
 * Return all values in the RBush.
 * @return {Array.<T>} All.
 */
GVol.structs.RBush.prototype.getAll = function() {
  var items = this.rbush_.all();
  return items.map(function(item) {
    return item.value;
  });
};


/**
 * Return all values in the given extent.
 * @param {GVol.Extent} extent Extent.
 * @return {Array.<T>} All in extent.
 */
GVol.structs.RBush.prototype.getInExtent = function(extent) {
  /** @type {GVol.RBushEntry} */
  var bbox = {
    minX: extent[0],
    minY: extent[1],
    maxX: extent[2],
    maxY: extent[3]
  };
  var items = this.rbush_.search(bbox);
  return items.map(function(item) {
    return item.value;
  });
};


/**
 * 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
 */
GVol.structs.RBush.prototype.forEach = function(callback, opt_this) {
  return this.forEach_(this.getAll(), callback, opt_this);
};


/**
 * Calls a callback function with each value in the provided extent.
 * @param {GVol.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
 */
GVol.structs.RBush.prototype.forEachInExtent = function(extent, callback, opt_this) {
  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
 */
GVol.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 {boGVolean} Is empty.
 */
GVol.structs.RBush.prototype.isEmpty = function() {
  return GVol.obj.isEmpty(this.items_);
};


/**
 * Remove all values from the RBush.
 */
GVol.structs.RBush.prototype.clear = function() {
  this.rbush_.clear();
  this.items_ = {};
};


/**
 * @param {GVol.Extent=} opt_extent Extent.
 * @return {GVol.Extent} Extent.
 */
GVol.structs.RBush.prototype.getExtent = function(opt_extent) {
  // FIXME add getExtent() to rbush
  var data = this.rbush_.data;
  return GVol.extent.createOrUpdate(data.minX, data.minY, data.maxX, data.maxY, opt_extent);
};


/**
 * @param {GVol.structs.RBush} rbush R-Tree.
 */
GVol.structs.RBush.prototype.concat = function(rbush) {
  this.rbush_.load(rbush.rbush_.all());
  for (var i in rbush.items_) {
    this.items_[i | 0] = rbush.items_[i | 0];
  }
};

goog.provide('GVol.render.webgl.PGVolygonReplay');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.cGVolor');
goog.require('GVol.extent');
goog.require('GVol.obj');
goog.require('GVol.geom.flat.contains');
goog.require('GVol.geom.flat.orient');
goog.require('GVol.geom.flat.transform');
goog.require('GVol.render.webgl.pGVolygonreplay.defaultshader');
goog.require('GVol.render.webgl.LineStringReplay');
goog.require('GVol.render.webgl.Replay');
goog.require('GVol.render.webgl');
goog.require('GVol.style.Stroke');
goog.require('GVol.structs.LinkedList');
goog.require('GVol.structs.RBush');
goog.require('GVol.webgl');
goog.require('GVol.webgl.Buffer');


if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @extends {GVol.render.webgl.Replay}
   * @param {number} tGVolerance TGVolerance.
   * @param {GVol.Extent} maxExtent Max extent.
   * @struct
   */
  GVol.render.webgl.PGVolygonReplay = function(tGVolerance, maxExtent) {
    GVol.render.webgl.Replay.call(this, tGVolerance, maxExtent);

    this.lineStringReplay = new GVol.render.webgl.LineStringReplay(
        tGVolerance, maxExtent);

    /**
     * @private
     * @type {GVol.render.webgl.pGVolygonreplay.defaultshader.Locations}
     */
    this.defaultLocations_ = null;

    /**
     * @private
     * @type {Array.<Array.<number>>}
     */
    this.styles_ = [];

    /**
     * @private
     * @type {Array.<number>}
     */
    this.styleIndices_ = [];

    /**
     * @private
     * @type {{fillCGVolor: (Array.<number>|null),
     *         changed: boGVolean}|null}
     */
    this.state_ = {
      fillCGVolor: null,
      changed: false
    };

  };
  GVol.inherits(GVol.render.webgl.PGVolygonReplay, GVol.render.webgl.Replay);


  /**
   * Draw one pGVolygon.
   * @param {Array.<number>} flatCoordinates Flat coordinates.
   * @param {Array.<Array.<number>>} hGVoleFlatCoordinates HGVole flat coordinates.
   * @param {number} stride Stride.
   * @private
   */
  GVol.render.webgl.PGVolygonReplay.prototype.drawCoordinates_ = function(
      flatCoordinates, hGVoleFlatCoordinates, stride) {
    // Triangulate the pGVolygon
    var outerRing = new GVol.structs.LinkedList();
    var rtree = new GVol.structs.RBush();
    // Initialize the outer ring
    this.processFlatCoordinates_(flatCoordinates, stride, outerRing, rtree, true);
    var maxCoords = this.getMaxCoords_(outerRing);

    // Eliminate hGVoles, if there are any
    if (hGVoleFlatCoordinates.length) {
      var i, ii;
      var hGVoleLists = [];
      for (i = 0, ii = hGVoleFlatCoordinates.length; i < ii; ++i) {
        var hGVoleList = {
          list: new GVol.structs.LinkedList(),
          maxCoords: undefined,
          rtree: new GVol.structs.RBush()
        };
        hGVoleLists.push(hGVoleList);
        this.processFlatCoordinates_(hGVoleFlatCoordinates[i],
            stride, hGVoleList.list, hGVoleList.rtree, false);
        this.classifyPoints_(hGVoleList.list, hGVoleList.rtree, true);
        hGVoleList.maxCoords = this.getMaxCoords_(hGVoleList.list);
      }
      hGVoleLists.sort(function(a, b) {
        return b.maxCoords[0] === a.maxCoords[0] ?
          a.maxCoords[1] - b.maxCoords[1] : b.maxCoords[0] - a.maxCoords[0];
      });
      for (i = 0; i < hGVoleLists.length; ++i) {
        var currList = hGVoleLists[i].list;
        var start = currList.firstItem();
        var currItem = start;
        var intersection;
        do {
          //TODO: Triangulate hGVoles when they intersect the outer ring.
          if (this.getIntersections_(currItem, rtree).length) {
            intersection = true;
            break;
          }
          currItem = currList.nextItem();
        } while (start !== currItem);
        if (!intersection) {
          if (this.bridgeHGVole_(currList, hGVoleLists[i].maxCoords[0], outerRing, maxCoords[0], rtree)) {
            rtree.concat(hGVoleLists[i].rtree);
            this.classifyPoints_(outerRing, rtree, false);
          }
        }
      }
    } else {
      this.classifyPoints_(outerRing, rtree, false);
    }
    this.triangulate_(outerRing, rtree);
  };


  /**
   * Inserts flat coordinates in a linked list and adds them to the vertex buffer.
   * @private
   * @param {Array.<number>} flatCoordinates Flat coordinates.
   * @param {number} stride Stride.
   * @param {GVol.structs.LinkedList} list Linked list.
   * @param {GVol.structs.RBush} rtree R-Tree of the pGVolygon.
   * @param {boGVolean} clockwise Coordinate order should be clockwise.
   */
  GVol.render.webgl.PGVolygonReplay.prototype.processFlatCoordinates_ = function(
      flatCoordinates, stride, list, rtree, clockwise) {
    var isClockwise = GVol.geom.flat.orient.linearRingIsClockwise(flatCoordinates,
        0, flatCoordinates.length, stride);
    var i, ii;
    var n = this.vertices.length / 2;
    /** @type {GVol.WebglPGVolygonVertex} */
    var start;
    /** @type {GVol.WebglPGVolygonVertex} */
    var p0;
    /** @type {GVol.WebglPGVolygonVertex} */
    var p1;
    var extents = [];
    var segments = [];
    if (clockwise === isClockwise) {
      start = this.createPoint_(flatCoordinates[0], flatCoordinates[1], n++);
      p0 = start;
      for (i = stride, ii = flatCoordinates.length; i < ii; i += stride) {
        p1 = this.createPoint_(flatCoordinates[i], flatCoordinates[i + 1], n++);
        segments.push(this.insertItem_(p0, p1, list));
        extents.push([Math.min(p0.x, p1.x), Math.min(p0.y, p1.y), Math.max(p0.x, p1.x),
          Math.max(p0.y, p1.y)]);
        p0 = p1;
      }
      segments.push(this.insertItem_(p1, start, list));
      extents.push([Math.min(p0.x, p1.x), Math.min(p0.y, p1.y), Math.max(p0.x, p1.x),
        Math.max(p0.y, p1.y)]);
    } else {
      var end = flatCoordinates.length - stride;
      start = this.createPoint_(flatCoordinates[end], flatCoordinates[end + 1], n++);
      p0 = start;
      for (i = end - stride, ii = 0; i >= ii; i -= stride) {
        p1 = this.createPoint_(flatCoordinates[i], flatCoordinates[i + 1], n++);
        segments.push(this.insertItem_(p0, p1, list));
        extents.push([Math.min(p0.x, p1.x), Math.min(p0.y, p1.y), Math.max(p0.x, p1.x),
          Math.max(p0.y, p1.y)]);
        p0 = p1;
      }
      segments.push(this.insertItem_(p1, start, list));
      extents.push([Math.min(p0.x, p1.x), Math.min(p0.y, p1.y), Math.max(p0.x, p1.x),
        Math.max(p0.y, p1.y)]);
    }
    rtree.load(extents, segments);
  };


  /**
   * Returns the rightmost coordinates of a pGVolygon on the X axis.
   * @private
   * @param {GVol.structs.LinkedList} list PGVolygons ring.
   * @return {Array.<number>} Max X coordinates.
   */
  GVol.render.webgl.PGVolygonReplay.prototype.getMaxCoords_ = function(list) {
    var start = list.firstItem();
    var seg = start;
    var maxCoords = [seg.p0.x, seg.p0.y];

    do {
      seg = list.nextItem();
      if (seg.p0.x > maxCoords[0]) {
        maxCoords = [seg.p0.x, seg.p0.y];
      }
    } while (seg !== start);

    return maxCoords;
  };


  /**
   * Classifies the points of a pGVolygon list as convex, reflex. Removes cGVollinear vertices.
   * @private
   * @param {GVol.structs.LinkedList} list PGVolygon ring.
   * @param {GVol.structs.RBush} rtree R-Tree of the pGVolygon.
   * @param {boGVolean} ccw The orientation of the pGVolygon is counter-clockwise.
   * @return {boGVolean} There were reclassified points.
   */
  GVol.render.webgl.PGVolygonReplay.prototype.classifyPoints_ = function(list, rtree, ccw) {
    var start = list.firstItem();
    var s0 = start;
    var s1 = list.nextItem();
    var pointsReclassified = false;
    do {
      var reflex = ccw ? GVol.render.webgl.triangleIsCounterClockwise(s1.p1.x,
          s1.p1.y, s0.p1.x, s0.p1.y, s0.p0.x, s0.p0.y) :
        GVol.render.webgl.triangleIsCounterClockwise(s0.p0.x, s0.p0.y, s0.p1.x,
            s0.p1.y, s1.p1.x, s1.p1.y);
      if (reflex === undefined) {
        this.removeItem_(s0, s1, list, rtree);
        pointsReclassified = true;
        if (s1 === start) {
          start = list.getNextItem();
        }
        s1 = s0;
        list.prevItem();
      } else if (s0.p1.reflex !== reflex) {
        s0.p1.reflex = reflex;
        pointsReclassified = true;
      }
      s0 = s1;
      s1 = list.nextItem();
    } while (s0 !== start);
    return pointsReclassified;
  };


  /**
   * @private
   * @param {GVol.structs.LinkedList} hGVole Linked list of the hGVole.
   * @param {number} hGVoleMaxX Maximum X value of the hGVole.
   * @param {GVol.structs.LinkedList} list Linked list of the pGVolygon.
   * @param {number} listMaxX Maximum X value of the pGVolygon.
   * @param {GVol.structs.RBush} rtree R-Tree of the pGVolygon.
   * @return {boGVolean} Bridging was successful.
   */
  GVol.render.webgl.PGVolygonReplay.prototype.bridgeHGVole_ = function(hGVole, hGVoleMaxX,
      list, listMaxX, rtree) {
    var seg = hGVole.firstItem();
    while (seg.p1.x !== hGVoleMaxX) {
      seg = hGVole.nextItem();
    }

    var p1 = seg.p1;
    /** @type {GVol.WebglPGVolygonVertex} */
    var p2 = {x: listMaxX, y: p1.y, i: -1};
    var minDist = Infinity;
    var i, ii, bestPoint;
    /** @type {GVol.WebglPGVolygonVertex} */
    var p5;

    var intersectingSegments = this.getIntersections_({p0: p1, p1: p2}, rtree, true);
    for (i = 0, ii = intersectingSegments.length; i < ii; ++i) {
      var currSeg = intersectingSegments[i];
      var intersection = this.calculateIntersection_(p1, p2, currSeg.p0,
          currSeg.p1, true);
      var dist = Math.abs(p1.x - intersection[0]);
      if (dist < minDist && GVol.render.webgl.triangleIsCounterClockwise(p1.x, p1.y,
          currSeg.p0.x, currSeg.p0.y, currSeg.p1.x, currSeg.p1.y) !== undefined) {
        minDist = dist;
        p5 = {x: intersection[0], y: intersection[1], i: -1};
        seg = currSeg;
      }
    }
    if (minDist === Infinity) {
      return false;
    }
    bestPoint = seg.p1;

    if (minDist > 0) {
      var pointsInTriangle = this.getPointsInTriangle_(p1, p5, seg.p1, rtree);
      if (pointsInTriangle.length) {
        var theta = Infinity;
        for (i = 0, ii = pointsInTriangle.length; i < ii; ++i) {
          var currPoint = pointsInTriangle[i];
          var currTheta = Math.atan2(p1.y - currPoint.y, p2.x - currPoint.x);
          if (currTheta < theta || (currTheta === theta && currPoint.x < bestPoint.x)) {
            theta = currTheta;
            bestPoint = currPoint;
          }
        }
      }
    }

    seg = list.firstItem();
    while (seg.p1.x !== bestPoint.x || seg.p1.y !== bestPoint.y) {
      seg = list.nextItem();
    }

    //We clone the bridge points as they can have different convexity.
    var p0Bridge = {x: p1.x, y: p1.y, i: p1.i, reflex: undefined};
    var p1Bridge = {x: seg.p1.x, y: seg.p1.y, i: seg.p1.i, reflex: undefined};

    hGVole.getNextItem().p0 = p0Bridge;
    this.insertItem_(p1, seg.p1, hGVole, rtree);
    this.insertItem_(p1Bridge, p0Bridge, hGVole, rtree);
    seg.p1 = p1Bridge;
    hGVole.setFirstItem();
    list.concat(hGVole);

    return true;
  };


  /**
   * @private
   * @param {GVol.structs.LinkedList} list Linked list of the pGVolygon.
   * @param {GVol.structs.RBush} rtree R-Tree of the pGVolygon.
   */
  GVol.render.webgl.PGVolygonReplay.prototype.triangulate_ = function(list, rtree) {
    var ccw = false;
    var simple = this.isSimple_(list, rtree);

    // Start clipping ears
    while (list.getLength() > 3) {
      if (simple) {
        if (!this.clipEars_(list, rtree, simple, ccw)) {
          if (!this.classifyPoints_(list, rtree, ccw)) {
            // Due to the behavior of OL's PIP algorithm, the ear clipping cannot
            // introduce touching segments. However, the original data may have some.
            if (!this.resGVolveSelfIntersections_(list, rtree, true)) {
              break;
            }
          }
        }
      } else {
        if (!this.clipEars_(list, rtree, simple, ccw)) {
          // We ran out of ears, try to reclassify.
          if (!this.classifyPoints_(list, rtree, ccw)) {
            // We have a bad pGVolygon, try to resGVolve local self-intersections.
            if (!this.resGVolveSelfIntersections_(list, rtree)) {
              simple = this.isSimple_(list, rtree);
              if (!simple) {
                // We have a really bad pGVolygon, try more time consuming methods.
                this.splitPGVolygon_(list, rtree);
                break;
              } else {
                ccw = !this.isClockwise_(list);
                this.classifyPoints_(list, rtree, ccw);
              }
            }
          }
        }
      }
    }
    if (list.getLength() === 3) {
      var numIndices = this.indices.length;
      this.indices[numIndices++] = list.getPrevItem().p0.i;
      this.indices[numIndices++] = list.getCurrItem().p0.i;
      this.indices[numIndices++] = list.getNextItem().p0.i;
    }
  };


  /**
   * @private
   * @param {GVol.structs.LinkedList} list Linked list of the pGVolygon.
   * @param {GVol.structs.RBush} rtree R-Tree of the pGVolygon.
   * @param {boGVolean} simple The pGVolygon is simple.
   * @param {boGVolean} ccw Orientation of the pGVolygon is counter-clockwise.
   * @return {boGVolean} There were processed ears.
   */
  GVol.render.webgl.PGVolygonReplay.prototype.clipEars_ = function(list, rtree, simple, ccw) {
    var numIndices = this.indices.length;
    var start = list.firstItem();
    var s0 = list.getPrevItem();
    var s1 = start;
    var s2 = list.nextItem();
    var s3 = list.getNextItem();
    var p0, p1, p2;
    var processedEars = false;
    do {
      p0 = s1.p0;
      p1 = s1.p1;
      p2 = s2.p1;
      if (p1.reflex === false) {
        // We might have a valid ear
        var variableCriterion;
        if (simple) {
          variableCriterion = this.getPointsInTriangle_(p0, p1, p2, rtree, true).length === 0;
        } else {
          variableCriterion = ccw ? this.diagonalIsInside_(s3.p1, p2, p1, p0,
              s0.p0) : this.diagonalIsInside_(s0.p0, p0, p1, p2, s3.p1);
        }
        if ((simple || this.getIntersections_({p0: p0, p1: p2}, rtree).length === 0) &&
            variableCriterion) {
          //The diagonal is completely inside the pGVolygon
          if (simple || p0.reflex === false || p2.reflex === false ||
              GVol.geom.flat.orient.linearRingIsClockwise([s0.p0.x, s0.p0.y, p0.x,
                p0.y, p1.x, p1.y, p2.x, p2.y, s3.p1.x, s3.p1.y], 0, 10, 2) === !ccw) {
            //The diagonal is persumably valid, we have an ear
            this.indices[numIndices++] = p0.i;
            this.indices[numIndices++] = p1.i;
            this.indices[numIndices++] = p2.i;
            this.removeItem_(s1, s2, list, rtree);
            if (s2 === start) {
              start = s3;
            }
            processedEars = true;
          }
        }
      }
      // Else we have a reflex point.
      s0 = list.getPrevItem();
      s1 = list.getCurrItem();
      s2 = list.nextItem();
      s3 = list.getNextItem();
    } while (s1 !== start && list.getLength() > 3);

    return processedEars;
  };


  /**
   * @private
   * @param {GVol.structs.LinkedList} list Linked list of the pGVolygon.
   * @param {GVol.structs.RBush} rtree R-Tree of the pGVolygon.
   * @param {boGVolean=} opt_touch ResGVolve touching segments.
   * @return {boGVolean} There were resGVolved intersections.
  */
  GVol.render.webgl.PGVolygonReplay.prototype.resGVolveSelfIntersections_ = function(
      list, rtree, opt_touch) {
    var start = list.firstItem();
    list.nextItem();
    var s0 = start;
    var s1 = list.nextItem();
    var resGVolvedIntersections = false;

    do {
      var intersection = this.calculateIntersection_(s0.p0, s0.p1, s1.p0, s1.p1,
          opt_touch);
      if (intersection) {
        var breakCond = false;
        var numVertices = this.vertices.length;
        var numIndices = this.indices.length;
        var n = numVertices / 2;
        var seg = list.prevItem();
        list.removeItem();
        rtree.remove(seg);
        breakCond = (seg === start);
        var p;
        if (opt_touch) {
          if (intersection[0] === s0.p0.x && intersection[1] === s0.p0.y) {
            list.prevItem();
            p = s0.p0;
            s1.p0 = p;
            rtree.remove(s0);
            breakCond = breakCond || (s0 === start);
          } else {
            p = s1.p1;
            s0.p1 = p;
            rtree.remove(s1);
            breakCond = breakCond || (s1 === start);
          }
          list.removeItem();
        } else {
          p = this.createPoint_(intersection[0], intersection[1], n);
          s0.p1 = p;
          s1.p0 = p;
          rtree.update([Math.min(s0.p0.x, s0.p1.x), Math.min(s0.p0.y, s0.p1.y),
            Math.max(s0.p0.x, s0.p1.x), Math.max(s0.p0.y, s0.p1.y)], s0);
          rtree.update([Math.min(s1.p0.x, s1.p1.x), Math.min(s1.p0.y, s1.p1.y),
            Math.max(s1.p0.x, s1.p1.x), Math.max(s1.p0.y, s1.p1.y)], s1);
        }

        this.indices[numIndices++] = seg.p0.i;
        this.indices[numIndices++] = seg.p1.i;
        this.indices[numIndices++] = p.i;

        resGVolvedIntersections = true;
        if (breakCond) {
          break;
        }
      }

      s0 = list.getPrevItem();
      s1 = list.nextItem();
    } while (s0 !== start);
    return resGVolvedIntersections;
  };


  /**
   * @private
   * @param {GVol.structs.LinkedList} list Linked list of the pGVolygon.
   * @param {GVol.structs.RBush} rtree R-Tree of the pGVolygon.
   * @return {boGVolean} The pGVolygon is simple.
   */
  GVol.render.webgl.PGVolygonReplay.prototype.isSimple_ = function(list, rtree) {
    var start = list.firstItem();
    var seg = start;
    do {
      if (this.getIntersections_(seg, rtree).length) {
        return false;
      }
      seg = list.nextItem();
    } while (seg !== start);
    return true;
  };


  /**
   * @private
   * @param {GVol.structs.LinkedList} list Linked list of the pGVolygon.
   * @return {boGVolean} Orientation is clockwise.
   */
  GVol.render.webgl.PGVolygonReplay.prototype.isClockwise_ = function(list) {
    var length = list.getLength() * 2;
    var flatCoordinates = new Array(length);
    var start = list.firstItem();
    var seg = start;
    var i = 0;
    do {
      flatCoordinates[i++] = seg.p0.x;
      flatCoordinates[i++] = seg.p0.y;
      seg = list.nextItem();
    } while (seg !== start);
    return GVol.geom.flat.orient.linearRingIsClockwise(flatCoordinates, 0, length, 2);
  };


  /**
   * @private
   * @param {GVol.structs.LinkedList} list Linked list of the pGVolygon.
   * @param {GVol.structs.RBush} rtree R-Tree of the pGVolygon.
   */
  GVol.render.webgl.PGVolygonReplay.prototype.splitPGVolygon_ = function(list, rtree) {
    var start = list.firstItem();
    var s0 = start;
    do {
      var intersections = this.getIntersections_(s0, rtree);
      if (intersections.length) {
        var s1 = intersections[0];
        var n = this.vertices.length / 2;
        var intersection = this.calculateIntersection_(s0.p0,
            s0.p1, s1.p0, s1.p1);
        var p = this.createPoint_(intersection[0], intersection[1], n);
        var newPGVolygon = new GVol.structs.LinkedList();
        var newRtree = new GVol.structs.RBush();
        this.insertItem_(p, s0.p1, newPGVolygon, newRtree);
        s0.p1 = p;
        rtree.update([Math.min(s0.p0.x, p.x), Math.min(s0.p0.y, p.y),
          Math.max(s0.p0.x, p.x), Math.max(s0.p0.y, p.y)], s0);
        var currItem = list.nextItem();
        while (currItem !== s1) {
          this.insertItem_(currItem.p0, currItem.p1, newPGVolygon, newRtree);
          rtree.remove(currItem);
          list.removeItem();
          currItem = list.getCurrItem();
        }
        this.insertItem_(s1.p0, p, newPGVolygon, newRtree);
        s1.p0 = p;
        rtree.update([Math.min(s1.p1.x, p.x), Math.min(s1.p1.y, p.y),
          Math.max(s1.p1.x, p.x), Math.max(s1.p1.y, p.y)], s1);
        this.classifyPoints_(list, rtree, false);
        this.triangulate_(list, rtree);
        this.classifyPoints_(newPGVolygon, newRtree, false);
        this.triangulate_(newPGVolygon, newRtree);
        break;
      }
      s0 = list.nextItem();
    } while (s0 !== start);
  };


  /**
   * @private
   * @param {number} x X coordinate.
   * @param {number} y Y coordinate.
   * @param {number} i Index.
   * @return {GVol.WebglPGVolygonVertex} List item.
   */
  GVol.render.webgl.PGVolygonReplay.prototype.createPoint_ = function(x, y, i) {
    var numVertices = this.vertices.length;
    this.vertices[numVertices++] = x;
    this.vertices[numVertices++] = y;
    /** @type {GVol.WebglPGVolygonVertex} */
    var p = {
      x: x,
      y: y,
      i: i,
      reflex: undefined
    };
    return p;
  };


  /**
   * @private
   * @param {GVol.WebglPGVolygonVertex} p0 First point of segment.
   * @param {GVol.WebglPGVolygonVertex} p1 Second point of segment.
   * @param {GVol.structs.LinkedList} list PGVolygon ring.
   * @param {GVol.structs.RBush=} opt_rtree Insert the segment into the R-Tree.
   * @return {GVol.WebglPGVolygonSegment} segment.
   */
  GVol.render.webgl.PGVolygonReplay.prototype.insertItem_ = function(p0, p1, list, opt_rtree) {
    var seg = {
      p0: p0,
      p1: p1
    };
    list.insertItem(seg);
    if (opt_rtree) {
      opt_rtree.insert([Math.min(p0.x, p1.x), Math.min(p0.y, p1.y),
        Math.max(p0.x, p1.x), Math.max(p0.y, p1.y)], seg);
    }
    return seg;
  };


  /**
    * @private
    * @param {GVol.WebglPGVolygonSegment} s0 Segment before the remove candidate.
    * @param {GVol.WebglPGVolygonSegment} s1 Remove candidate segment.
    * @param {GVol.structs.LinkedList} list PGVolygon ring.
    * @param {GVol.structs.RBush} rtree R-Tree of the pGVolygon.
    */
  GVol.render.webgl.PGVolygonReplay.prototype.removeItem_ = function(s0, s1, list, rtree) {
    if (list.getCurrItem() === s1) {
      list.removeItem();
      s0.p1 = s1.p1;
      rtree.remove(s1);
      rtree.update([Math.min(s0.p0.x, s0.p1.x), Math.min(s0.p0.y, s0.p1.y),
        Math.max(s0.p0.x, s0.p1.x), Math.max(s0.p0.y, s0.p1.y)], s0);
    }
  };


  /**
   * @private
   * @param {GVol.WebglPGVolygonVertex} p0 First point.
   * @param {GVol.WebglPGVolygonVertex} p1 Second point.
   * @param {GVol.WebglPGVolygonVertex} p2 Third point.
   * @param {GVol.structs.RBush} rtree R-Tree of the pGVolygon.
   * @param {boGVolean=} opt_reflex Only include reflex points.
   * @return {Array.<GVol.WebglPGVolygonVertex>} Points in the triangle.
   */
  GVol.render.webgl.PGVolygonReplay.prototype.getPointsInTriangle_ = function(p0, p1,
      p2, rtree, opt_reflex) {
    var i, ii, j, p;
    var result = [];
    var segmentsInExtent = rtree.getInExtent([Math.min(p0.x, p1.x, p2.x),
      Math.min(p0.y, p1.y, p2.y), Math.max(p0.x, p1.x, p2.x), Math.max(p0.y,
          p1.y, p2.y)]);
    for (i = 0, ii = segmentsInExtent.length; i < ii; ++i) {
      for (j in segmentsInExtent[i]) {
        p = segmentsInExtent[i][j];
        if (typeof p === 'object' && (!opt_reflex || p.reflex)) {
          if ((p.x !== p0.x || p.y !== p0.y) && (p.x !== p1.x || p.y !== p1.y) &&
              (p.x !== p2.x || p.y !== p2.y) && result.indexOf(p) === -1 &&
              GVol.geom.flat.contains.linearRingContainsXY([p0.x, p0.y, p1.x, p1.y,
                p2.x, p2.y], 0, 6, 2, p.x, p.y)) {
            result.push(p);
          }
        }
      }
    }
    return result;
  };


  /**
   * @private
   * @param {GVol.WebglPGVolygonSegment} segment Segment.
   * @param {GVol.structs.RBush} rtree R-Tree of the pGVolygon.
   * @param {boGVolean=} opt_touch Touching segments should be considered an intersection.
   * @return {Array.<GVol.WebglPGVolygonSegment>} Intersecting segments.
   */
  GVol.render.webgl.PGVolygonReplay.prototype.getIntersections_ = function(segment, rtree, opt_touch) {
    var p0 = segment.p0;
    var p1 = segment.p1;
    var segmentsInExtent = rtree.getInExtent([Math.min(p0.x, p1.x),
      Math.min(p0.y, p1.y), Math.max(p0.x, p1.x), Math.max(p0.y, p1.y)]);
    var result = [];
    var i, ii;
    for (i = 0, ii = segmentsInExtent.length; i < ii; ++i) {
      var currSeg = segmentsInExtent[i];
      if (segment !== currSeg && (opt_touch || currSeg.p0 !== p1 || currSeg.p1 !== p0) &&
          this.calculateIntersection_(p0, p1, currSeg.p0, currSeg.p1, opt_touch)) {
        result.push(currSeg);
      }
    }
    return result;
  };


  /**
   * Line intersection algorithm by Paul Bourke.
   * @see http://paulbourke.net/geometry/pointlineplane/
   *
   * @private
   * @param {GVol.WebglPGVolygonVertex} p0 First point.
   * @param {GVol.WebglPGVolygonVertex} p1 Second point.
   * @param {GVol.WebglPGVolygonVertex} p2 Third point.
   * @param {GVol.WebglPGVolygonVertex} p3 Fourth point.
   * @param {boGVolean=} opt_touch Touching segments should be considered an intersection.
   * @return {Array.<number>|undefined} Intersection coordinates.
   */
  GVol.render.webgl.PGVolygonReplay.prototype.calculateIntersection_ = function(p0,
      p1, p2, p3, opt_touch) {
    var denom = (p3.y - p2.y) * (p1.x - p0.x) - (p3.x - p2.x) * (p1.y - p0.y);
    if (denom !== 0) {
      var ua = ((p3.x - p2.x) * (p0.y - p2.y) - (p3.y - p2.y) * (p0.x - p2.x)) / denom;
      var ub = ((p1.x - p0.x) * (p0.y - p2.y) - (p1.y - p0.y) * (p0.x - p2.x)) / denom;
      if ((!opt_touch && ua > GVol.render.webgl.EPSILON && ua < 1 - GVol.render.webgl.EPSILON &&
          ub > GVol.render.webgl.EPSILON && ub < 1 - GVol.render.webgl.EPSILON) || (opt_touch &&
          ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1)) {
        return [p0.x + ua * (p1.x - p0.x), p0.y + ua * (p1.y - p0.y)];
      }
    }
    return undefined;
  };


  /**
   * @private
   * @param {GVol.WebglPGVolygonVertex} p0 Point before the start of the diagonal.
   * @param {GVol.WebglPGVolygonVertex} p1 Start point of the diagonal.
   * @param {GVol.WebglPGVolygonVertex} p2 Ear candidate.
   * @param {GVol.WebglPGVolygonVertex} p3 End point of the diagonal.
   * @param {GVol.WebglPGVolygonVertex} p4 Point after the end of the diagonal.
   * @return {boGVolean} Diagonal is inside the pGVolygon.
   */
  GVol.render.webgl.PGVolygonReplay.prototype.diagonalIsInside_ = function(p0, p1, p2, p3, p4) {
    if (p1.reflex === undefined || p3.reflex === undefined) {
      return false;
    }
    var p1IsLeftOf = (p2.x - p3.x) * (p1.y - p3.y) > (p2.y - p3.y) * (p1.x - p3.x);
    var p1IsRightOf = (p4.x - p3.x) * (p1.y - p3.y) < (p4.y - p3.y) * (p1.x - p3.x);
    var p3IsLeftOf = (p0.x - p1.x) * (p3.y - p1.y) > (p0.y - p1.y) * (p3.x - p1.x);
    var p3IsRightOf = (p2.x - p1.x) * (p3.y - p1.y) < (p2.y - p1.y) * (p3.x - p1.x);
    var p1InCone = p3.reflex ? p1IsRightOf || p1IsLeftOf : p1IsRightOf && p1IsLeftOf;
    var p3InCone = p1.reflex ? p3IsRightOf || p3IsLeftOf : p3IsRightOf && p3IsLeftOf;
    return p1InCone && p3InCone;
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.PGVolygonReplay.prototype.drawMultiPGVolygon = function(multiPGVolygonGeometry, feature) {
    var endss = multiPGVolygonGeometry.getEndss();
    var stride = multiPGVolygonGeometry.getStride();
    var currIndex = this.indices.length;
    var currLineIndex = this.lineStringReplay.getCurrentIndex();
    var flatCoordinates = multiPGVolygonGeometry.getFlatCoordinates();
    var i, ii, j, jj;
    var start = 0;
    for (i = 0, ii = endss.length; i < ii; ++i) {
      var ends = endss[i];
      if (ends.length > 0) {
        var outerRing = GVol.geom.flat.transform.translate(flatCoordinates, start, ends[0],
            stride, -this.origin[0], -this.origin[1]);
        if (outerRing.length) {
          var hGVoles = [];
          var hGVoleFlatCoords;
          for (j = 1, jj = ends.length; j < jj; ++j) {
            if (ends[j] !== ends[j - 1]) {
              hGVoleFlatCoords = GVol.geom.flat.transform.translate(flatCoordinates, ends[j - 1],
                  ends[j], stride, -this.origin[0], -this.origin[1]);
              hGVoles.push(hGVoleFlatCoords);
            }
          }
          this.lineStringReplay.drawPGVolygonCoordinates(outerRing, hGVoles, stride);
          this.drawCoordinates_(outerRing, hGVoles, stride);
        }
      }
      start = ends[ends.length - 1];
    }
    if (this.indices.length > currIndex) {
      this.startIndices.push(currIndex);
      this.startIndicesFeature.push(feature);
      if (this.state_.changed) {
        this.styleIndices_.push(currIndex);
        this.state_.changed = false;
      }
    }
    if (this.lineStringReplay.getCurrentIndex() > currLineIndex) {
      this.lineStringReplay.setPGVolygonStyle(feature, currLineIndex);
    }
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.PGVolygonReplay.prototype.drawPGVolygon = function(pGVolygonGeometry, feature) {
    var ends = pGVolygonGeometry.getEnds();
    var stride = pGVolygonGeometry.getStride();
    if (ends.length > 0) {
      var flatCoordinates = pGVolygonGeometry.getFlatCoordinates().map(Number);
      var outerRing = GVol.geom.flat.transform.translate(flatCoordinates, 0, ends[0],
          stride, -this.origin[0], -this.origin[1]);
      if (outerRing.length) {
        var hGVoles = [];
        var i, ii, hGVoleFlatCoords;
        for (i = 1, ii = ends.length; i < ii; ++i) {
          if (ends[i] !== ends[i - 1]) {
            hGVoleFlatCoords = GVol.geom.flat.transform.translate(flatCoordinates, ends[i - 1],
                ends[i], stride, -this.origin[0], -this.origin[1]);
            hGVoles.push(hGVoleFlatCoords);
          }
        }

        this.startIndices.push(this.indices.length);
        this.startIndicesFeature.push(feature);
        if (this.state_.changed) {
          this.styleIndices_.push(this.indices.length);
          this.state_.changed = false;
        }
        this.lineStringReplay.setPGVolygonStyle(feature);

        this.lineStringReplay.drawPGVolygonCoordinates(outerRing, hGVoles, stride);
        this.drawCoordinates_(outerRing, hGVoles, stride);
      }
    }
  };


  /**
   * @inheritDoc
   **/
  GVol.render.webgl.PGVolygonReplay.prototype.finish = function(context) {
    // create, bind, and populate the vertices buffer
    this.verticesBuffer = new GVol.webgl.Buffer(this.vertices);

    // create, bind, and populate the indices buffer
    this.indicesBuffer = new GVol.webgl.Buffer(this.indices);

    this.startIndices.push(this.indices.length);

    this.lineStringReplay.finish(context);

    //Clean up, if there is nothing to draw
    if (this.styleIndices_.length === 0 && this.styles_.length > 0) {
      this.styles_ = [];
    }

    this.vertices = null;
    this.indices = null;
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.PGVolygonReplay.prototype.getDeleteResourcesFunction = function(context) {
    var verticesBuffer = this.verticesBuffer;
    var indicesBuffer = this.indicesBuffer;
    var lineDeleter = this.lineStringReplay.getDeleteResourcesFunction(context);
    return function() {
      context.deleteBuffer(verticesBuffer);
      context.deleteBuffer(indicesBuffer);
      lineDeleter();
    };
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.PGVolygonReplay.prototype.setUpProgram = function(gl, context, size, pixelRatio) {
    // get the program
    var fragmentShader, vertexShader;
    fragmentShader = GVol.render.webgl.pGVolygonreplay.defaultshader.fragment;
    vertexShader = GVol.render.webgl.pGVolygonreplay.defaultshader.vertex;
    var program = context.getProgram(fragmentShader, vertexShader);

    // get the locations
    var locations;
    if (!this.defaultLocations_) {
      // eslint-disable-next-line openlayers-internal/no-missing-requires
      locations = new GVol.render.webgl.pGVolygonreplay.defaultshader.Locations(gl, program);
      this.defaultLocations_ = locations;
    } else {
      locations = this.defaultLocations_;
    }

    context.useProgram(program);

    // enable the vertex attrib arrays
    gl.enableVertexAttribArray(locations.a_position);
    gl.vertexAttribPointer(locations.a_position, 2, GVol.webgl.FLOAT,
        false, 8, 0);

    return locations;
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.PGVolygonReplay.prototype.shutDownProgram = function(gl, locations) {
    gl.disableVertexAttribArray(locations.a_position);
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.PGVolygonReplay.prototype.drawReplay = function(gl, context, skippedFeaturesHash, hitDetection) {
    //Save GL parameters.
    var tmpDepthFunc = /** @type {number} */ (gl.getParameter(gl.DEPTH_FUNC));
    var tmpDepthMask = /** @type {boGVolean} */ (gl.getParameter(gl.DEPTH_WRITEMASK));

    if (!hitDetection) {
      gl.enable(gl.DEPTH_TEST);
      gl.depthMask(true);
      gl.depthFunc(gl.NOTEQUAL);
    }

    if (!GVol.obj.isEmpty(skippedFeaturesHash)) {
      this.drawReplaySkipping_(gl, context, skippedFeaturesHash);
    } else {
      //Draw by style groups to minimize drawElements() calls.
      var i, start, end, nextStyle;
      end = this.startIndices[this.startIndices.length - 1];
      for (i = this.styleIndices_.length - 1; i >= 0; --i) {
        start = this.styleIndices_[i];
        nextStyle = this.styles_[i];
        this.setFillStyle_(gl, nextStyle);
        this.drawElements(gl, context, start, end);
        end = start;
      }
    }
    if (!hitDetection) {
      gl.disable(gl.DEPTH_TEST);
      gl.clear(gl.DEPTH_BUFFER_BIT);
      //Restore GL parameters.
      gl.depthMask(tmpDepthMask);
      gl.depthFunc(tmpDepthFunc);
    }
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.PGVolygonReplay.prototype.drawHitDetectionReplayOneByOne = function(gl, context, skippedFeaturesHash,
      featureCallback, opt_hitExtent) {
    var i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex;
    featureIndex = this.startIndices.length - 2;
    end = this.startIndices[featureIndex + 1];
    for (i = this.styleIndices_.length - 1; i >= 0; --i) {
      nextStyle = this.styles_[i];
      this.setFillStyle_(gl, nextStyle);
      groupStart = this.styleIndices_[i];

      while (featureIndex >= 0 &&
          this.startIndices[featureIndex] >= groupStart) {
        start = this.startIndices[featureIndex];
        feature = this.startIndicesFeature[featureIndex];
        featureUid = GVol.getUid(feature).toString();

        if (skippedFeaturesHash[featureUid] === undefined &&
            feature.getGeometry() &&
            (opt_hitExtent === undefined || GVol.extent.intersects(
                /** @type {Array<number>} */ (opt_hitExtent),
                feature.getGeometry().getExtent()))) {
          gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
          this.drawElements(gl, context, start, end);

          var result = featureCallback(feature);

          if (result) {
            return result;
          }

        }
        featureIndex--;
        end = start;
      }
    }
    return undefined;
  };


  /**
   * @private
   * @param {WebGLRenderingContext} gl gl.
   * @param {GVol.webgl.Context} context Context.
   * @param {Object} skippedFeaturesHash Ids of features to skip.
   */
  GVol.render.webgl.PGVolygonReplay.prototype.drawReplaySkipping_ = function(gl, context, skippedFeaturesHash) {
    var i, start, end, nextStyle, groupStart, feature, featureUid, featureIndex, featureStart;
    featureIndex = this.startIndices.length - 2;
    end = start = this.startIndices[featureIndex + 1];
    for (i = this.styleIndices_.length - 1; i >= 0; --i) {
      nextStyle = this.styles_[i];
      this.setFillStyle_(gl, nextStyle);
      groupStart = this.styleIndices_[i];

      while (featureIndex >= 0 &&
          this.startIndices[featureIndex] >= groupStart) {
        featureStart = this.startIndices[featureIndex];
        feature = this.startIndicesFeature[featureIndex];
        featureUid = GVol.getUid(feature).toString();

        if (skippedFeaturesHash[featureUid]) {
          if (start !== end) {
            this.drawElements(gl, context, start, end);
            gl.clear(gl.DEPTH_BUFFER_BIT);
          }
          end = featureStart;
        }
        featureIndex--;
        start = featureStart;
      }
      if (start !== end) {
        this.drawElements(gl, context, start, end);
        gl.clear(gl.DEPTH_BUFFER_BIT);
      }
      start = end = groupStart;
    }
  };


  /**
   * @private
   * @param {WebGLRenderingContext} gl gl.
   * @param {Array.<number>} cGVolor CGVolor.
   */
  GVol.render.webgl.PGVolygonReplay.prototype.setFillStyle_ = function(gl, cGVolor) {
    gl.uniform4fv(this.defaultLocations_.u_cGVolor, cGVolor);
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.PGVolygonReplay.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) {
    var fillStyleCGVolor = fillStyle ? fillStyle.getCGVolor() : [0, 0, 0, 0];
    if (!(fillStyleCGVolor instanceof CanvasGradient) &&
        !(fillStyleCGVolor instanceof CanvasPattern)) {
      fillStyleCGVolor = GVol.cGVolor.asArray(fillStyleCGVolor).map(function(c, i) {
        return i != 3 ? c / 255 : c;
      }) || GVol.render.webgl.defaultFillStyle;
    } else {
      fillStyleCGVolor = GVol.render.webgl.defaultFillStyle;
    }
    if (!this.state_.fillCGVolor || !GVol.array.equals(fillStyleCGVolor, this.state_.fillCGVolor)) {
      this.state_.fillCGVolor = fillStyleCGVolor;
      this.state_.changed = true;
      this.styles_.push(fillStyleCGVolor);
    }
    //Provide a null stroke style, if no strokeStyle is provided. Required for the draw interaction to work.
    if (strokeStyle) {
      this.lineStringReplay.setFillStrokeStyle(null, strokeStyle);
    } else {
      var nullStrokeStyle = new GVol.style.Stroke({
        cGVolor: [0, 0, 0, 0],
        lineWidth: 0
      });
      this.lineStringReplay.setFillStrokeStyle(null, nullStrokeStyle);
    }
  };

}

goog.provide('GVol.style.Atlas');

goog.require('GVol.dom');


/**
 * 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.
 */
GVol.style.Atlas = function(size, space) {

  /**
   * @private
   * @type {number}
   */
  this.space_ = space;

  /**
   * @private
   * @type {Array.<GVol.AtlasBlock>}
   */
  this.emptyBlocks_ = [{x: 0, y: 0, width: size, height: size}];

  /**
   * @private
   * @type {Object.<string, GVol.AtlasInfo>}
   */
  this.entries_ = {};

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.context_ = GVol.dom.createCanvasContext2D(size, size);

  /**
   * @private
   * @type {HTMLCanvasElement}
   */
  this.canvas_ = this.context_.canvas;
};


/**
 * @param {string} id The identifier of the entry to check.
 * @return {?GVol.AtlasInfo} The atlas info.
 */
GVol.style.Atlas.prototype.get = function(id) {
  return 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 {?GVol.AtlasInfo} The position and atlas image for the entry.
 */
GVol.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 {GVol.AtlasBlock} 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.
 */
GVol.style.Atlas.prototype.split_ = function(index, block, width, height) {
  var deltaWidth = block.width - width;
  var deltaHeight = block.height - height;

  /** @type {GVol.AtlasBlock} */
  var newBlock1;
  /** @type {GVol.AtlasBlock} */
  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 GVold 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 {GVol.AtlasBlock} newBlock1 The 1st block to add.
 * @param {GVol.AtlasBlock} newBlock2 The 2nd block to add.
 */
GVol.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);
};

goog.provide('GVol.style.AtlasManager');

goog.require('GVol');
goog.require('GVol.style.Atlas');


/**
 * 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 {GVolx.style.AtlasManagerOptions=} opt_options Options.
 */
GVol.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 : GVol.INITIAL_ATLAS_SIZE;

  /**
   * The maximum size in pixels of atlas images.
   * @private
   * @type {number}
   */
  this.maxSize_ = options.maxSize !== undefined ?
    options.maxSize : GVol.MAX_ATLAS_SIZE != -1 ?
      GVol.MAX_ATLAS_SIZE : GVol.WEBGL_MAX_TEXTURE_SIZE !== undefined ?
        GVol.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.<GVol.style.Atlas>}
   */
  this.atlases_ = [new GVol.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.<GVol.style.Atlas>}
   */
  this.hitAtlases_ = [new GVol.style.Atlas(this.currentHitSize_, this.space_)];
};


/**
 * @param {string} id The identifier of the entry to check.
 * @return {?GVol.AtlasManagerInfo} The position and atlas image for the
 *    entry, or `null` if the entry is not part of the atlas manager.
 */
GVol.style.AtlasManager.prototype.getInfo = function(id) {
  /** @type {?GVol.AtlasInfo} */
  var info = this.getInfo_(this.atlases_, id);

  if (!info) {
    return null;
  }
  var hitInfo = /** @type {GVol.AtlasInfo} */ (this.getInfo_(this.hitAtlases_, id));

  return this.mergeInfos_(info, hitInfo);
};


/**
 * @private
 * @param {Array.<GVol.style.Atlas>} atlases The atlases to search.
 * @param {string} id The identifier of the entry to check.
 * @return {?GVol.AtlasInfo} The position and atlas image for the entry,
 *    or `null` if the entry is not part of the atlases.
 */
GVol.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 {GVol.AtlasInfo} info The info for the real image.
 * @param {GVol.AtlasInfo} hitInfo The info for the hit-detection
 *    image.
 * @return {?GVol.AtlasManagerInfo} The position and atlas image for the
 *    entry, or `null` if the entry is not part of the atlases.
 */
GVol.style.AtlasManager.prototype.mergeInfos_ = function(info, hitInfo) {
  return /** @type {GVol.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 {?GVol.AtlasManagerInfo}  The position and atlas image for the
 *    entry, or `null` if the image is too big.
 */
GVol.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 {?GVol.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 : GVol.nullFunction;

  var hitInfo = /** @type {GVol.AtlasInfo} */ (this.add_(true,
      id, width, height, renderHitCallback, opt_this));

  return this.mergeInfos_(info, hitInfo);
};


/**
 * @private
 * @param {boGVolean} 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 {?GVol.AtlasInfo}  The position and atlas image for the entry,
 *    or `null` if the image is too big.
 */
GVol.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 GVol.style.Atlas(size, this.space_);
      atlases.push(atlas);
      // run the loop another time
      ++ii;
    }
  }
  return null;
};

goog.provide('GVol.render.webgl.TextReplay');

goog.require('GVol');
goog.require('GVol.cGVolorlike');
goog.require('GVol.dom');
goog.require('GVol.has');
goog.require('GVol.render.webgl');
goog.require('GVol.render.webgl.TextureReplay');
goog.require('GVol.style.AtlasManager');
goog.require('GVol.webgl.Buffer');


if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @extends {GVol.render.webgl.TextureReplay}
   * @param {number} tGVolerance TGVolerance.
   * @param {GVol.Extent} maxExtent Max extent.
   * @struct
   */
  GVol.render.webgl.TextReplay = function(tGVolerance, maxExtent) {
    GVol.render.webgl.TextureReplay.call(this, tGVolerance, maxExtent);

    /**
     * @private
     * @type {Array.<HTMLCanvasElement>}
     */
    this.images_ = [];

    /**
     * @private
     * @type {Array.<WebGLTexture>}
     */
    this.textures_ = [];

    /**
     * @private
     * @type {HTMLCanvasElement}
     */
    this.measureCanvas_ = GVol.dom.createCanvasContext2D(0, 0).canvas;

    /**
     * @private
     * @type {{strokeCGVolor: (GVol.CGVolorLike|null),
     *         lineCap: (string|undefined),
     *         lineDash: Array.<number>,
     *         lineDashOffset: (number|undefined),
     *         lineJoin: (string|undefined),
     *         lineWidth: number,
     *         miterLimit: (number|undefined),
     *         fillCGVolor: (GVol.CGVolorLike|null),
     *         font: (string|undefined),
     *         scale: (number|undefined)}}
     */
    this.state_ = {
      strokeCGVolor: null,
      lineCap: undefined,
      lineDash: null,
      lineDashOffset: undefined,
      lineJoin: undefined,
      lineWidth: 0,
      miterLimit: undefined,
      fillCGVolor: null,
      font: undefined,
      scale: undefined
    };

    /**
     * @private
     * @type {string}
     */
    this.text_ = '';

    /**
     * @private
     * @type {number|undefined}
     */
    this.textAlign_ = undefined;

    /**
     * @private
     * @type {number|undefined}
     */
    this.textBaseline_ = undefined;

    /**
     * @private
     * @type {number|undefined}
     */
    this.offsetX_ = undefined;

    /**
     * @private
     * @type {number|undefined}
     */
    this.offsetY_ = undefined;

    /**
     * @private
     * @type {Object.<string, GVol.WebglGlyphAtlas>}
     */
    this.atlases_ = {};

    /**
     * @private
     * @type {GVol.WebglGlyphAtlas|undefined}
     */
    this.currAtlas_ = undefined;

    this.scale = 1;

    this.opacity = 1;

  };
  GVol.inherits(GVol.render.webgl.TextReplay, GVol.render.webgl.TextureReplay);


  /**
   * @inheritDoc
   */
  GVol.render.webgl.TextReplay.prototype.drawText = function(flatCoordinates, offset,
      end, stride, geometry, feature) {
    if (this.text_) {
      this.startIndices.push(this.indices.length);
      this.startIndicesFeature.push(feature);

      var glyphAtlas = this.currAtlas_;
      var lines = this.text_.split('\n');
      var textSize = this.getTextSize_(lines);
      var i, ii, j, jj, currX, currY, charArr, charInfo;
      var anchorX = Math.round(textSize[0] * this.textAlign_ - this.offsetX_);
      var anchorY = Math.round(textSize[1] * this.textBaseline_ - this.offsetY_);
      var lineWidth = (this.state_.lineWidth / 2) * this.state_.scale;

      for (i = 0, ii = lines.length; i < ii; ++i) {
        currX = 0;
        currY = glyphAtlas.height * i;
        charArr = lines[i].split('');

        for (j = 0, jj = charArr.length; j < jj; ++j) {
          charInfo = glyphAtlas.atlas.getInfo(charArr[j]);

          if (charInfo) {
            var image = charInfo.image;

            this.anchorX = anchorX - currX;
            this.anchorY = anchorY - currY;
            this.originX = j === 0 ? charInfo.offsetX - lineWidth : charInfo.offsetX;
            this.originY = charInfo.offsetY;
            this.height = glyphAtlas.height;
            this.width = j === 0 || j === charArr.length - 1 ?
              glyphAtlas.width[charArr[j]] + lineWidth : glyphAtlas.width[charArr[j]];
            this.imageHeight = image.height;
            this.imageWidth = image.width;

            var currentImage;
            if (this.images_.length === 0) {
              this.images_.push(image);
            } else {
              currentImage = this.images_[this.images_.length - 1];
              if (GVol.getUid(currentImage) != GVol.getUid(image)) {
                this.groupIndices.push(this.indices.length);
                this.images_.push(image);
              }
            }

            this.drawText_(flatCoordinates, offset, end, stride);
          }
          currX += this.width;
        }
      }
    }
  };


  /**
   * @private
   * @param {Array.<string>} lines Label to draw split to lines.
   * @return {Array.<number>} Size of the label in pixels.
   */
  GVol.render.webgl.TextReplay.prototype.getTextSize_ = function(lines) {
    var self = this;
    var glyphAtlas = this.currAtlas_;
    var textHeight = lines.length * glyphAtlas.height;
    //Split every line to an array of chars, sum up their width, and select the longest.
    var textWidth = lines.map(function(str) {
      var sum = 0;
      var i, ii;
      for (i = 0, ii = str.length; i < ii; ++i) {
        var curr = str[i];
        if (!glyphAtlas.width[curr]) {
          self.addCharToAtlas_(curr);
        }
        sum += glyphAtlas.width[curr] ? glyphAtlas.width[curr] : 0;
      }
      return sum;
    }).reduce(function(max, curr) {
      return Math.max(max, curr);
    });

    return [textWidth, textHeight];
  };


  /**
   * @private
   * @param {Array.<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   */
  GVol.render.webgl.TextReplay.prototype.drawText_ = function(flatCoordinates, offset,
      end, stride) {
    var i, ii;
    for (i = offset, ii = end; i < ii; i += stride) {
      this.drawCoordinates(flatCoordinates, offset, end, stride);
    }
  };


  /**
   * @private
   * @param {string} char Character.
   */
  GVol.render.webgl.TextReplay.prototype.addCharToAtlas_ = function(char) {
    if (char.length === 1) {
      var glyphAtlas = this.currAtlas_;
      var state = this.state_;
      var mCtx = this.measureCanvas_.getContext('2d');
      mCtx.font = state.font;
      var width = Math.ceil(mCtx.measureText(char).width * state.scale);

      var info = glyphAtlas.atlas.add(char, width, glyphAtlas.height,
          function(ctx, x, y) {
            //Parameterize the canvas
            ctx.font = /** @type {string} */ (state.font);
            ctx.fillStyle = state.fillCGVolor;
            ctx.strokeStyle = state.strokeCGVolor;
            ctx.lineWidth = state.lineWidth;
            ctx.lineCap = /*** @type {string} */ (state.lineCap);
            ctx.lineJoin = /** @type {string} */ (state.lineJoin);
            ctx.miterLimit = /** @type {number} */ (state.miterLimit);
            ctx.textAlign = 'left';
            ctx.textBaseline = 'top';
            if (GVol.has.CANVAS_LINE_DASH && state.lineDash) {
              //FIXME: use pixelRatio
              ctx.setLineDash(state.lineDash);
              ctx.lineDashOffset = /** @type {number} */ (state.lineDashOffset);
            }
            if (state.scale !== 1) {
              //FIXME: use pixelRatio
              ctx.setTransform(/** @type {number} */ (state.scale), 0, 0,
                  /** @type {number} */ (state.scale), 0, 0);
            }

            //Draw the character on the canvas
            if (state.strokeCGVolor) {
              ctx.strokeText(char, x, y);
            }
            if (state.fillCGVolor) {
              ctx.fillText(char, x, y);
            }
          });

      if (info) {
        glyphAtlas.width[char] = width;
      }
    }
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.TextReplay.prototype.finish = function(context) {
    var gl = context.getGL();

    this.groupIndices.push(this.indices.length);
    this.hitDetectionGroupIndices = this.groupIndices;

    // create, bind, and populate the vertices buffer
    this.verticesBuffer = new GVol.webgl.Buffer(this.vertices);

    // create, bind, and populate the indices buffer
    this.indicesBuffer = new GVol.webgl.Buffer(this.indices);

    // create textures
    /** @type {Object.<string, WebGLTexture>} */
    var texturePerImage = {};

    this.createTextures(this.textures_, this.images_, texturePerImage, gl);

    this.state_ = {
      strokeCGVolor: null,
      lineCap: undefined,
      lineDash: null,
      lineDashOffset: undefined,
      lineJoin: undefined,
      lineWidth: 0,
      miterLimit: undefined,
      fillCGVolor: null,
      font: undefined,
      scale: undefined
    };
    this.text_ = '';
    this.textAlign_ = undefined;
    this.textBaseline_ = undefined;
    this.offsetX_ = undefined;
    this.offsetY_ = undefined;
    this.images_ = null;
    this.atlases_ = {};
    this.currAtlas_ = undefined;
    GVol.render.webgl.TextureReplay.prototype.finish.call(this, context);
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.TextReplay.prototype.setTextStyle = function(textStyle) {
    var state = this.state_;
    var textFillStyle = textStyle.getFill();
    var textStrokeStyle = textStyle.getStroke();
    if (!textStyle || !textStyle.getText() || (!textFillStyle && !textStrokeStyle)) {
      this.text_ = '';
    } else {
      if (!textFillStyle) {
        state.fillCGVolor = null;
      } else {
        var textFillStyleCGVolor = textFillStyle.getCGVolor();
        state.fillCGVolor = GVol.cGVolorlike.asCGVolorLike(textFillStyleCGVolor ?
          textFillStyleCGVolor : GVol.render.webgl.defaultFillStyle);
      }
      if (!textStrokeStyle) {
        state.strokeCGVolor = null;
        state.lineWidth = 0;
      } else {
        var textStrokeStyleCGVolor = textStrokeStyle.getCGVolor();
        state.strokeCGVolor = GVol.cGVolorlike.asCGVolorLike(textStrokeStyleCGVolor ?
          textStrokeStyleCGVolor : GVol.render.webgl.defaultStrokeStyle);
        state.lineWidth = textStrokeStyle.getWidth() || GVol.render.webgl.defaultLineWidth;
        state.lineCap = textStrokeStyle.getLineCap() || GVol.render.webgl.defaultLineCap;
        state.lineDashOffset = textStrokeStyle.getLineDashOffset() || GVol.render.webgl.defaultLineDashOffset;
        state.lineJoin = textStrokeStyle.getLineJoin() || GVol.render.webgl.defaultLineJoin;
        state.miterLimit = textStrokeStyle.getMiterLimit() || GVol.render.webgl.defaultMiterLimit;
        var lineDash = textStrokeStyle.getLineDash();
        state.lineDash = lineDash ? lineDash.slice() : GVol.render.webgl.defaultLineDash;
      }
      state.font = textStyle.getFont() || GVol.render.webgl.defaultFont;
      state.scale = textStyle.getScale() || 1;
      this.text_ = /** @type {string} */ (textStyle.getText());
      var textAlign = GVol.render.webgl.TextReplay.Align_[textStyle.getTextAlign()];
      var textBaseline = GVol.render.webgl.TextReplay.Align_[textStyle.getTextBaseline()];
      this.textAlign_ = textAlign === undefined ?
        GVol.render.webgl.defaultTextAlign : textAlign;
      this.textBaseline_ = textBaseline === undefined ?
        GVol.render.webgl.defaultTextBaseline : textBaseline;
      this.offsetX_ = textStyle.getOffsetX() || 0;
      this.offsetY_ = textStyle.getOffsetY() || 0;
      this.rotateWithView = !!textStyle.getRotateWithView();
      this.rotation = textStyle.getRotation() || 0;

      this.currAtlas_ = this.getAtlas_(state);
    }
  };


  /**
   * @private
   * @param {Object} state Font attributes.
   * @return {GVol.WebglGlyphAtlas} Glyph atlas.
   */
  GVol.render.webgl.TextReplay.prototype.getAtlas_ = function(state) {
    var params = [];
    var i;
    for (i in state) {
      if (state[i] || state[i] === 0) {
        if (Array.isArray(state[i])) {
          params = params.concat(state[i]);
        } else {
          params.push(state[i]);
        }
      }
    }
    var hash = this.calculateHash_(params);
    if (!this.atlases_[hash]) {
      var mCtx = this.measureCanvas_.getContext('2d');
      mCtx.font = state.font;
      var height = Math.ceil((mCtx.measureText('M').width * 1.5 +
          state.lineWidth / 2) * state.scale);

      this.atlases_[hash] = {
        atlas: new GVol.style.AtlasManager({
          space: state.lineWidth + 1
        }),
        width: {},
        height: height
      };
    }
    return this.atlases_[hash];
  };


  /**
   * @private
   * @param {Array.<string|number>} params Array of parameters.
   * @return {string} Hash string.
   */
  GVol.render.webgl.TextReplay.prototype.calculateHash_ = function(params) {
    //TODO: Create a more performant, reliable, general hash function.
    var i, ii;
    var hash = '';
    for (i = 0, ii = params.length; i < ii; ++i) {
      hash += params[i];
    }
    return hash;
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.TextReplay.prototype.getTextures = function(opt_all) {
    return this.textures_;
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.TextReplay.prototype.getHitDetectionTextures = function() {
    return this.textures_;
  };


  /**
   * @enum {number}
   * @private
   */
  GVol.render.webgl.TextReplay.Align_ = {
    left: 0,
    end: 0,
    center: 0.5,
    right: 1,
    start: 1,
    top: 0,
    middle: 0.5,
    hanging: 0.2,
    alphabetic: 0.8,
    ideographic: 0.8,
    bottom: 1
  };

}

goog.provide('GVol.render.webgl.ReplayGroup');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.extent');
goog.require('GVol.obj');
goog.require('GVol.render.replay');
goog.require('GVol.render.ReplayGroup');
goog.require('GVol.render.webgl.CircleReplay');
goog.require('GVol.render.webgl.ImageReplay');
goog.require('GVol.render.webgl.LineStringReplay');
goog.require('GVol.render.webgl.PGVolygonReplay');
goog.require('GVol.render.webgl.TextReplay');


if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @extends {GVol.render.ReplayGroup}
   * @param {number} tGVolerance TGVolerance.
   * @param {GVol.Extent} maxExtent Max extent.
   * @param {number=} opt_renderBuffer Render buffer.
   * @struct
   */
  GVol.render.webgl.ReplayGroup = function(tGVolerance, maxExtent, opt_renderBuffer) {
    GVol.render.ReplayGroup.call(this);

    /**
     * @type {GVol.Extent}
     * @private
     */
    this.maxExtent_ = maxExtent;

    /**
     * @type {number}
     * @private
     */
    this.tGVolerance_ = tGVolerance;

    /**
     * @type {number|undefined}
     * @private
     */
    this.renderBuffer_ = opt_renderBuffer;

    /**
     * @private
     * @type {!Object.<string,
     *        Object.<GVol.render.ReplayType, GVol.render.webgl.Replay>>}
     */
    this.replaysByZIndex_ = {};

  };
  GVol.inherits(GVol.render.webgl.ReplayGroup, GVol.render.ReplayGroup);


  /**
   * @param {GVol.webgl.Context} context WebGL context.
   * @return {function()} Delete resources function.
   */
  GVol.render.webgl.ReplayGroup.prototype.getDeleteResourcesFunction = function(context) {
    var functions = [];
    var zKey;
    for (zKey in this.replaysByZIndex_) {
      var replays = this.replaysByZIndex_[zKey];
      var replayKey;
      for (replayKey in replays) {
        functions.push(
            replays[replayKey].getDeleteResourcesFunction(context));
      }
    }
    return function() {
      var length = functions.length;
      var result;
      for (var i = 0; i < length; i++) {
        result = functions[i].apply(this, arguments);
      }
      return result;
    };
  };


  /**
   * @param {GVol.webgl.Context} context Context.
   */
  GVol.render.webgl.ReplayGroup.prototype.finish = function(context) {
    var zKey;
    for (zKey in this.replaysByZIndex_) {
      var replays = this.replaysByZIndex_[zKey];
      var replayKey;
      for (replayKey in replays) {
        replays[replayKey].finish(context);
      }
    }
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.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) {
      /**
       * @type {Function}
       */
      var Constructor = GVol.render.webgl.ReplayGroup.BATCH_CONSTRUCTORS_[replayType];
      replay = new Constructor(this.tGVolerance_, this.maxExtent_);
      replays[replayType] = replay;
    }
    return replay;
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.ReplayGroup.prototype.isEmpty = function() {
    return GVol.obj.isEmpty(this.replaysByZIndex_);
  };


  /**
   * @param {GVol.webgl.Context} context Context.
   * @param {GVol.Coordinate} center Center.
   * @param {number} resGVolution ResGVolution.
   * @param {number} rotation Rotation.
   * @param {GVol.Size} size Size.
   * @param {number} pixelRatio Pixel ratio.
   * @param {number} opacity Global opacity.
   * @param {Object.<string, boGVolean>} skippedFeaturesHash Ids of features
   *  to skip.
   */
  GVol.render.webgl.ReplayGroup.prototype.replay = function(context,
      center, resGVolution, rotation, size, pixelRatio,
      opacity, skippedFeaturesHash) {
    /** @type {Array.<number>} */
    var zs = Object.keys(this.replaysByZIndex_).map(Number);
    zs.sort(GVol.array.numberSafeCompareFunction);

    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 = GVol.render.replay.ORDER.length; j < jj; ++j) {
        replay = replays[GVol.render.replay.ORDER[j]];
        if (replay !== undefined) {
          replay.replay(context,
              center, resGVolution, rotation, size, pixelRatio,
              opacity, skippedFeaturesHash,
              undefined, false);
        }
      }
    }
  };


  /**
   * @private
   * @param {GVol.webgl.Context} context Context.
   * @param {GVol.Coordinate} center Center.
   * @param {number} resGVolution ResGVolution.
   * @param {number} rotation Rotation.
   * @param {GVol.Size} size Size.
   * @param {number} pixelRatio Pixel ratio.
   * @param {number} opacity Global opacity.
   * @param {Object.<string, boGVolean>} skippedFeaturesHash Ids of features
   *  to skip.
   * @param {function((GVol.Feature|GVol.render.Feature)): T|undefined} featureCallback Feature callback.
   * @param {boGVolean} oneByOne Draw features one-by-one for the hit-detecion.
   * @param {GVol.Extent=} opt_hitExtent Hit extent: Only features intersecting
   *  this extent are checked.
   * @return {T|undefined} Callback result.
   * @template T
   */
  GVol.render.webgl.ReplayGroup.prototype.replayHitDetection_ = function(context,
      center, resGVolution, rotation, size, pixelRatio, opacity,
      skippedFeaturesHash, featureCallback, oneByOne, 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 = GVol.render.replay.ORDER.length - 1; j >= 0; --j) {
        replay = replays[GVol.render.replay.ORDER[j]];
        if (replay !== undefined) {
          result = replay.replay(context,
              center, resGVolution, rotation, size, pixelRatio, opacity,
              skippedFeaturesHash, featureCallback, oneByOne, opt_hitExtent);
          if (result) {
            return result;
          }
        }
      }
    }
    return undefined;
  };


  /**
   * @param {GVol.Coordinate} coordinate Coordinate.
   * @param {GVol.webgl.Context} context Context.
   * @param {GVol.Coordinate} center Center.
   * @param {number} resGVolution ResGVolution.
   * @param {number} rotation Rotation.
   * @param {GVol.Size} size Size.
   * @param {number} pixelRatio Pixel ratio.
   * @param {number} opacity Global opacity.
   * @param {Object.<string, boGVolean>} skippedFeaturesHash Ids of features
   *  to skip.
   * @param {function((GVol.Feature|GVol.render.Feature)): T|undefined} callback Feature callback.
   * @return {T|undefined} Callback result.
   * @template T
   */
  GVol.render.webgl.ReplayGroup.prototype.forEachFeatureAtCoordinate = function(
      coordinate, context, center, resGVolution, rotation, size, pixelRatio,
      opacity, skippedFeaturesHash,
      callback) {
    var gl = context.getGL();
    gl.bindFramebuffer(
        gl.FRAMEBUFFER, context.getHitDetectionFramebuffer());


    /**
     * @type {GVol.Extent}
     */
    var hitExtent;
    if (this.renderBuffer_ !== undefined) {
      // build an extent around the coordinate, so that only features that
      // intersect this extent are checked
      hitExtent = GVol.extent.buffer(
          GVol.extent.createOrUpdateFromCoordinate(coordinate),
          resGVolution * this.renderBuffer_);
    }

    return this.replayHitDetection_(context,
        coordinate, resGVolution, rotation, GVol.render.webgl.ReplayGroup.HIT_DETECTION_SIZE_,
        pixelRatio, opacity, skippedFeaturesHash,
        /**
         * @param {GVol.Feature|GVol.render.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 {GVol.Coordinate} coordinate Coordinate.
   * @param {GVol.webgl.Context} context Context.
   * @param {GVol.Coordinate} center Center.
   * @param {number} resGVolution ResGVolution.
   * @param {number} rotation Rotation.
   * @param {GVol.Size} size Size.
   * @param {number} pixelRatio Pixel ratio.
   * @param {number} opacity Global opacity.
   * @param {Object.<string, boGVolean>} skippedFeaturesHash Ids of features
   *  to skip.
   * @return {boGVolean} Is there a feature at the given coordinate?
   */
  GVol.render.webgl.ReplayGroup.prototype.hasFeatureAtCoordinate = function(
      coordinate, context, center, resGVolution, rotation, size, pixelRatio,
      opacity, skippedFeaturesHash) {
    var gl = context.getGL();
    gl.bindFramebuffer(
        gl.FRAMEBUFFER, context.getHitDetectionFramebuffer());

    var hasFeature = this.replayHitDetection_(context,
        coordinate, resGVolution, rotation, GVol.render.webgl.ReplayGroup.HIT_DETECTION_SIZE_,
        pixelRatio, opacity, skippedFeaturesHash,
        /**
         * @param {GVol.Feature|GVol.render.Feature} feature Feature.
         * @return {boGVolean} 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 {Array.<number>}
   */
  GVol.render.webgl.ReplayGroup.HIT_DETECTION_SIZE_ = [1, 1];

  /**
   * @const
   * @private
   * @type {Object.<GVol.render.ReplayType,
   *                function(new: GVol.render.webgl.Replay, number,
   *                GVol.Extent)>}
   */
  GVol.render.webgl.ReplayGroup.BATCH_CONSTRUCTORS_ = {
    'Circle': GVol.render.webgl.CircleReplay,
    'Image': GVol.render.webgl.ImageReplay,
    'LineString': GVol.render.webgl.LineStringReplay,
    'PGVolygon': GVol.render.webgl.PGVolygonReplay,
    'Text': GVol.render.webgl.TextReplay
  };

}

goog.provide('GVol.render.webgl.Immediate');

goog.require('GVol');
goog.require('GVol.extent');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.render.ReplayType');
goog.require('GVol.render.VectorContext');
goog.require('GVol.render.webgl.ReplayGroup');


if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @extends {GVol.render.VectorContext}
   * @param {GVol.webgl.Context} context Context.
   * @param {GVol.Coordinate} center Center.
   * @param {number} resGVolution ResGVolution.
   * @param {number} rotation Rotation.
   * @param {GVol.Size} size Size.
   * @param {GVol.Extent} extent Extent.
   * @param {number} pixelRatio Pixel ratio.
   * @struct
   */
  GVol.render.webgl.Immediate = function(context, center, resGVolution, rotation, size, extent, pixelRatio) {
    GVol.render.VectorContext.call(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.resGVolution_ = resGVolution;

    /**
     * @private
     * @type {GVol.style.Image}
     */
    this.imageStyle_ = null;

    /**
     * @private
     * @type {GVol.style.Fill}
     */
    this.fillStyle_ = null;

    /**
     * @private
     * @type {GVol.style.Stroke}
     */
    this.strokeStyle_ = null;

    /**
     * @private
     * @type {GVol.style.Text}
     */
    this.textStyle_ = null;

  };
  GVol.inherits(GVol.render.webgl.Immediate, GVol.render.VectorContext);


  /**
   * @param {GVol.render.webgl.ReplayGroup} replayGroup Replay group.
   * @param {Array.<number>} flatCoordinates Flat coordinates.
   * @param {number} offset Offset.
   * @param {number} end End.
   * @param {number} stride Stride.
   * @private
   */
  GVol.render.webgl.Immediate.prototype.drawText_ = function(replayGroup,
      flatCoordinates, offset, end, stride) {
    var context = this.context_;
    var replay = /** @type {GVol.render.webgl.TextReplay} */ (
      replayGroup.getReplay(0, GVol.render.ReplayType.TEXT));
    replay.setTextStyle(this.textStyle_);
    replay.drawText(flatCoordinates, offset, end, stride, null, null);
    replay.finish(context);
    // default cGVolors
    var opacity = 1;
    var skippedFeatures = {};
    var featureCallback;
    var oneByOne = false;
    replay.replay(this.context_, this.center_, this.resGVolution_, this.rotation_,
        this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback,
        oneByOne);
    replay.getDeleteResourcesFunction(context)();
  };


  /**
   * Set the rendering style.  Note that since this is an immediate rendering API,
   * any `zIndex` on the provided style will be ignored.
   *
   * @param {GVol.style.Style} style The rendering style.
   * @override
   * @api
   */
  GVol.render.webgl.Immediate.prototype.setStyle = function(style) {
    this.setFillStrokeStyle(style.getFill(), style.getStroke());
    this.setImageStyle(style.getImage());
    this.setTextStyle(style.getText());
  };


  /**
   * Render a geometry into the canvas.  Call
   * {@link GVol.render.webgl.Immediate#setStyle} first to set the rendering style.
   *
   * @param {GVol.geom.Geometry|GVol.render.Feature} geometry The geometry to render.
   * @override
   * @api
   */
  GVol.render.webgl.Immediate.prototype.drawGeometry = function(geometry) {
    var type = geometry.getType();
    switch (type) {
      case GVol.geom.GeometryType.POINT:
        this.drawPoint(/** @type {GVol.geom.Point} */ (geometry), null);
        break;
      case GVol.geom.GeometryType.LINE_STRING:
        this.drawLineString(/** @type {GVol.geom.LineString} */ (geometry), null);
        break;
      case GVol.geom.GeometryType.POLYGON:
        this.drawPGVolygon(/** @type {GVol.geom.PGVolygon} */ (geometry), null);
        break;
      case GVol.geom.GeometryType.MULTI_POINT:
        this.drawMultiPoint(/** @type {GVol.geom.MultiPoint} */ (geometry), null);
        break;
      case GVol.geom.GeometryType.MULTI_LINE_STRING:
        this.drawMultiLineString(/** @type {GVol.geom.MultiLineString} */ (geometry), null);
        break;
      case GVol.geom.GeometryType.MULTI_POLYGON:
        this.drawMultiPGVolygon(/** @type {GVol.geom.MultiPGVolygon} */ (geometry), null);
        break;
      case GVol.geom.GeometryType.GEOMETRY_COLLECTION:
        this.drawGeometryCGVollection(/** @type {GVol.geom.GeometryCGVollection} */ (geometry), null);
        break;
      case GVol.geom.GeometryType.CIRCLE:
        this.drawCircle(/** @type {GVol.geom.Circle} */ (geometry), null);
        break;
      default:
        // pass
    }
  };


  /**
   * @inheritDoc
   * @api
   */
  GVol.render.webgl.Immediate.prototype.drawFeature = function(feature, style) {
    var geometry = style.getGeometryFunction()(feature);
    if (!geometry ||
        !GVol.extent.intersects(this.extent_, geometry.getExtent())) {
      return;
    }
    this.setStyle(style);
    this.drawGeometry(geometry);
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.Immediate.prototype.drawGeometryCGVollection = function(geometry, data) {
    var geometries = geometry.getGeometriesArray();
    var i, ii;
    for (i = 0, ii = geometries.length; i < ii; ++i) {
      this.drawGeometry(geometries[i]);
    }
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.Immediate.prototype.drawPoint = function(geometry, data) {
    var context = this.context_;
    var replayGroup = new GVol.render.webgl.ReplayGroup(1, this.extent_);
    var replay = /** @type {GVol.render.webgl.ImageReplay} */ (
      replayGroup.getReplay(0, GVol.render.ReplayType.IMAGE));
    replay.setImageStyle(this.imageStyle_);
    replay.drawPoint(geometry, data);
    replay.finish(context);
    // default cGVolors
    var opacity = 1;
    var skippedFeatures = {};
    var featureCallback;
    var oneByOne = false;
    replay.replay(this.context_, this.center_, this.resGVolution_, this.rotation_,
        this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback,
        oneByOne);
    replay.getDeleteResourcesFunction(context)();

    if (this.textStyle_) {
      var flatCoordinates = geometry.getFlatCoordinates();
      var stride = geometry.getStride();
      this.drawText_(replayGroup, flatCoordinates, 0, flatCoordinates.length, stride);
    }
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.Immediate.prototype.drawMultiPoint = function(geometry, data) {
    var context = this.context_;
    var replayGroup = new GVol.render.webgl.ReplayGroup(1, this.extent_);
    var replay = /** @type {GVol.render.webgl.ImageReplay} */ (
      replayGroup.getReplay(0, GVol.render.ReplayType.IMAGE));
    replay.setImageStyle(this.imageStyle_);
    replay.drawMultiPoint(geometry, data);
    replay.finish(context);
    var opacity = 1;
    var skippedFeatures = {};
    var featureCallback;
    var oneByOne = false;
    replay.replay(this.context_, this.center_, this.resGVolution_, this.rotation_,
        this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback,
        oneByOne);
    replay.getDeleteResourcesFunction(context)();

    if (this.textStyle_) {
      var flatCoordinates = geometry.getFlatCoordinates();
      var stride = geometry.getStride();
      this.drawText_(replayGroup, flatCoordinates, 0, flatCoordinates.length, stride);
    }
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.Immediate.prototype.drawLineString = function(geometry, data) {
    var context = this.context_;
    var replayGroup = new GVol.render.webgl.ReplayGroup(1, this.extent_);
    var replay = /** @type {GVol.render.webgl.LineStringReplay} */ (
      replayGroup.getReplay(0, GVol.render.ReplayType.LINE_STRING));
    replay.setFillStrokeStyle(null, this.strokeStyle_);
    replay.drawLineString(geometry, data);
    replay.finish(context);
    var opacity = 1;
    var skippedFeatures = {};
    var featureCallback;
    var oneByOne = false;
    replay.replay(this.context_, this.center_, this.resGVolution_, this.rotation_,
        this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback,
        oneByOne);
    replay.getDeleteResourcesFunction(context)();

    if (this.textStyle_) {
      var flatMidpoint = geometry.getFlatMidpoint();
      this.drawText_(replayGroup, flatMidpoint, 0, 2, 2);
    }
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.Immediate.prototype.drawMultiLineString = function(geometry, data) {
    var context = this.context_;
    var replayGroup = new GVol.render.webgl.ReplayGroup(1, this.extent_);
    var replay = /** @type {GVol.render.webgl.LineStringReplay} */ (
      replayGroup.getReplay(0, GVol.render.ReplayType.LINE_STRING));
    replay.setFillStrokeStyle(null, this.strokeStyle_);
    replay.drawMultiLineString(geometry, data);
    replay.finish(context);
    var opacity = 1;
    var skippedFeatures = {};
    var featureCallback;
    var oneByOne = false;
    replay.replay(this.context_, this.center_, this.resGVolution_, this.rotation_,
        this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback,
        oneByOne);
    replay.getDeleteResourcesFunction(context)();

    if (this.textStyle_) {
      var flatMidpoints = geometry.getFlatMidpoints();
      this.drawText_(replayGroup, flatMidpoints, 0, flatMidpoints.length, 2);
    }
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.Immediate.prototype.drawPGVolygon = function(geometry, data) {
    var context = this.context_;
    var replayGroup = new GVol.render.webgl.ReplayGroup(1, this.extent_);
    var replay = /** @type {GVol.render.webgl.PGVolygonReplay} */ (
      replayGroup.getReplay(0, GVol.render.ReplayType.POLYGON));
    replay.setFillStrokeStyle(this.fillStyle_, this.strokeStyle_);
    replay.drawPGVolygon(geometry, data);
    replay.finish(context);
    var opacity = 1;
    var skippedFeatures = {};
    var featureCallback;
    var oneByOne = false;
    replay.replay(this.context_, this.center_, this.resGVolution_, this.rotation_,
        this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback,
        oneByOne);
    replay.getDeleteResourcesFunction(context)();

    if (this.textStyle_) {
      var flatInteriorPoint = geometry.getFlatInteriorPoint();
      this.drawText_(replayGroup, flatInteriorPoint, 0, 2, 2);
    }
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.Immediate.prototype.drawMultiPGVolygon = function(geometry, data) {
    var context = this.context_;
    var replayGroup = new GVol.render.webgl.ReplayGroup(1, this.extent_);
    var replay = /** @type {GVol.render.webgl.PGVolygonReplay} */ (
      replayGroup.getReplay(0, GVol.render.ReplayType.POLYGON));
    replay.setFillStrokeStyle(this.fillStyle_, this.strokeStyle_);
    replay.drawMultiPGVolygon(geometry, data);
    replay.finish(context);
    var opacity = 1;
    var skippedFeatures = {};
    var featureCallback;
    var oneByOne = false;
    replay.replay(this.context_, this.center_, this.resGVolution_, this.rotation_,
        this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback,
        oneByOne);
    replay.getDeleteResourcesFunction(context)();

    if (this.textStyle_) {
      var flatInteriorPoints = geometry.getFlatInteriorPoints();
      this.drawText_(replayGroup, flatInteriorPoints, 0, flatInteriorPoints.length, 2);
    }
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.Immediate.prototype.drawCircle = function(geometry, data) {
    var context = this.context_;
    var replayGroup = new GVol.render.webgl.ReplayGroup(1, this.extent_);
    var replay = /** @type {GVol.render.webgl.CircleReplay} */ (
      replayGroup.getReplay(0, GVol.render.ReplayType.CIRCLE));
    replay.setFillStrokeStyle(this.fillStyle_, this.strokeStyle_);
    replay.drawCircle(geometry, data);
    replay.finish(context);
    var opacity = 1;
    var skippedFeatures = {};
    var featureCallback;
    var oneByOne = false;
    replay.replay(this.context_, this.center_, this.resGVolution_, this.rotation_,
        this.size_, this.pixelRatio_, opacity, skippedFeatures, featureCallback,
        oneByOne);
    replay.getDeleteResourcesFunction(context)();

    if (this.textStyle_) {
      this.drawText_(replayGroup, geometry.getCenter(), 0, 2, 2);
    }
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.Immediate.prototype.setImageStyle = function(imageStyle) {
    this.imageStyle_ = imageStyle;
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.Immediate.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) {
    this.fillStyle_ = fillStyle;
    this.strokeStyle_ = strokeStyle;
  };


  /**
   * @inheritDoc
   */
  GVol.render.webgl.Immediate.prototype.setTextStyle = function(textStyle) {
    this.textStyle_ = textStyle;
  };

}

goog.provide('GVol.structs.LRUCache');

goog.require('GVol.asserts');


/**
 * 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
 */
GVol.structs.LRUCache = function() {

  /**
   * @private
   * @type {number}
   */
  this.count_ = 0;

  /**
   * @private
   * @type {!Object.<string, GVol.LRUCacheEntry>}
   */
  this.entries_ = {};

  /**
   * @private
   * @type {?GVol.LRUCacheEntry}
   */
  this.GVoldest_ = null;

  /**
   * @private
   * @type {?GVol.LRUCacheEntry}
   */
  this.newest_ = null;

};


/**
 * FIXME empty description for jsdoc
 */
GVol.structs.LRUCache.prototype.clear = function() {
  this.count_ = 0;
  this.entries_ = {};
  this.GVoldest_ = null;
  this.newest_ = null;
};


/**
 * @param {string} key Key.
 * @return {boGVolean} Contains key.
 */
GVol.structs.LRUCache.prototype.containsKey = function(key) {
  return this.entries_.hasOwnProperty(key);
};


/**
 * @param {function(this: S, T, string, GVol.structs.LRUCache): ?} f The function
 *     to call for every entry from the GVoldest 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
 */
GVol.structs.LRUCache.prototype.forEach = function(f, opt_this) {
  var entry = this.GVoldest_;
  while (entry) {
    f.call(opt_this, entry.value_, entry.key_, this);
    entry = entry.newer;
  }
};


/**
 * @param {string} key Key.
 * @return {T} Value.
 */
GVol.structs.LRUCache.prototype.get = function(key) {
  var entry = this.entries_[key];
  GVol.asserts.assert(entry !== undefined,
      15); // Tried to get a value for a key that does not exist in the cache
  if (entry === this.newest_) {
    return entry.value_;
  } else if (entry === this.GVoldest_) {
    this.GVoldest_ = /** @type {GVol.LRUCacheEntry} */ (this.GVoldest_.newer);
    this.GVoldest_.GVolder = null;
  } else {
    entry.newer.GVolder = entry.GVolder;
    entry.GVolder.newer = entry.newer;
  }
  entry.newer = null;
  entry.GVolder = this.newest_;
  this.newest_.newer = entry;
  this.newest_ = entry;
  return entry.value_;
};


/**
 * @return {number} Count.
 */
GVol.structs.LRUCache.prototype.getCount = function() {
  return this.count_;
};


/**
 * @return {Array.<string>} Keys.
 */
GVol.structs.LRUCache.prototype.getKeys = function() {
  var keys = new Array(this.count_);
  var i = 0;
  var entry;
  for (entry = this.newest_; entry; entry = entry.GVolder) {
    keys[i++] = entry.key_;
  }
  return keys;
};


/**
 * @return {Array.<T>} Values.
 */
GVol.structs.LRUCache.prototype.getValues = function() {
  var values = new Array(this.count_);
  var i = 0;
  var entry;
  for (entry = this.newest_; entry; entry = entry.GVolder) {
    values[i++] = entry.value_;
  }
  return values;
};


/**
 * @return {T} Last value.
 */
GVol.structs.LRUCache.prototype.peekLast = function() {
  return this.GVoldest_.value_;
};


/**
 * @return {string} Last key.
 */
GVol.structs.LRUCache.prototype.peekLastKey = function() {
  return this.GVoldest_.key_;
};


/**
 * @return {T} value Value.
 */
GVol.structs.LRUCache.prototype.pop = function() {
  var entry = this.GVoldest_;
  delete this.entries_[entry.key_];
  if (entry.newer) {
    entry.newer.GVolder = null;
  }
  this.GVoldest_ = /** @type {GVol.LRUCacheEntry} */ (entry.newer);
  if (!this.GVoldest_) {
    this.newest_ = null;
  }
  --this.count_;
  return entry.value_;
};


/**
 * @param {string} key Key.
 * @param {T} value Value.
 */
GVol.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.
 */
GVol.structs.LRUCache.prototype.set = function(key, value) {
  GVol.asserts.assert(!(key in this.entries_),
      16); // Tried to set a value for a key that is used already
  var entry = /** @type {GVol.LRUCacheEntry} */ ({
    key_: key,
    newer: null,
    GVolder: this.newest_,
    value_: value
  });
  if (!this.newest_) {
    this.GVoldest_ = entry;
  } else {
    this.newest_.newer = entry;
  }
  this.newest_ = entry;
  this.entries_[key] = entry;
  ++this.count_;
};

// FIXME check against gl.getParameter(webgl.MAX_TEXTURE_SIZE)

goog.provide('GVol.renderer.webgl.Map');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.css');
goog.require('GVol.dom');
goog.require('GVol.events');
goog.require('GVol.layer.Layer');
goog.require('GVol.render.Event');
goog.require('GVol.render.EventType');
goog.require('GVol.render.webgl.Immediate');
goog.require('GVol.renderer.Map');
goog.require('GVol.renderer.Type');
goog.require('GVol.source.State');
goog.require('GVol.structs.LRUCache');
goog.require('GVol.structs.PriorityQueue');
goog.require('GVol.webgl');
goog.require('GVol.webgl.Context');
goog.require('GVol.webgl.ContextEventType');


if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @extends {GVol.renderer.Map}
   * @param {Element} container Container.
   * @param {GVol.Map} map Map.
   */
  GVol.renderer.webgl.Map = function(container, map) {
    GVol.renderer.Map.call(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_.style.display = 'block';
    this.canvas_.className = GVol.css.CLASS_UNSELECTABLE;
    container.insertBefore(this.canvas_, container.childNodes[0] || null);

    /**
     * @private
     * @type {number}
     */
    this.clipTileCanvasWidth_ = 0;

    /**
     * @private
     * @type {number}
     */
    this.clipTileCanvasHeight_ = 0;

    /**
     * @private
     * @type {CanvasRenderingContext2D}
     */
    this.clipTileContext_ = GVol.dom.createCanvasContext2D();

    /**
     * @private
     * @type {boGVolean}
     */
    this.renderedVisible_ = true;

    /**
     * @private
     * @type {WebGLRenderingContext}
     */
    this.gl_ = GVol.webgl.getContext(this.canvas_, {
      antialias: true,
      depth: true,
      failIfMajorPerformanceCaveat: true,
      preserveDrawingBuffer: false,
      stencil: true
    });

    /**
     * @private
     * @type {GVol.webgl.Context}
     */
    this.context_ = new GVol.webgl.Context(this.canvas_, this.gl_);

    GVol.events.listen(this.canvas_, GVol.webgl.ContextEventType.LOST,
        this.handleWebGLContextLost, this);
    GVol.events.listen(this.canvas_, GVol.webgl.ContextEventType.RESTORED,
        this.handleWebGLContextRestored, this);

    /**
     * @private
     * @type {GVol.structs.LRUCache.<GVol.WebglTextureCacheEntry|null>}
     */
    this.textureCache_ = new GVol.structs.LRUCache();

    /**
     * @private
     * @type {GVol.Coordinate}
     */
    this.focus_ = null;

    /**
     * @private
     * @type {GVol.structs.PriorityQueue.<Array>}
     */
    this.tileTextureQueue_ = new GVol.structs.PriorityQueue(
        /**
         * @param {Array.<*>} element Element.
         * @return {number} Priority.
         * @this {GVol.renderer.webgl.Map}
         */
        (function(element) {
          var tileCenter = /** @type {GVol.Coordinate} */ (element[1]);
          var tileResGVolution = /** @type {number} */ (element[2]);
          var deltaX = tileCenter[0] - this.focus_[0];
          var deltaY = tileCenter[1] - this.focus_[1];
          return 65536 * Math.log(tileResGVolution) +
              Math.sqrt(deltaX * deltaX + deltaY * deltaY) / tileResGVolution;
        }).bind(this),
        /**
         * @param {Array.<*>} element Element.
         * @return {string} Key.
         */
        function(element) {
          return /** @type {GVol.Tile} */ (element[0]).getKey();
        });


    /**
     * @param {GVol.Map} map Map.
     * @param {?GVolx.FrameState} frameState Frame state.
     * @return {boGVolean} false.
     * @this {GVol.renderer.webgl.Map}
     */
    this.loadNextTileTexture_ =
        function(map, frameState) {
          if (!this.tileTextureQueue_.isEmpty()) {
            this.tileTextureQueue_.reprioritize();
            var element = this.tileTextureQueue_.dequeue();
            var tile = /** @type {GVol.Tile} */ (element[0]);
            var tileSize = /** @type {GVol.Size} */ (element[3]);
            var tileGutter = /** @type {number} */ (element[4]);
            this.bindTileTexture(
                tile, tileSize, tileGutter, GVol.webgl.LINEAR, GVol.webgl.LINEAR);
          }
          return false;
        }.bind(this);


    /**
     * @private
     * @type {number}
     */
    this.textureCacheFrameMarkerCount_ = 0;

    this.initializeGL_();
  };
  GVol.inherits(GVol.renderer.webgl.Map, GVol.renderer.Map);


  /**
   * @param {GVol.Tile} tile Tile.
   * @param {GVol.Size} tileSize Tile size.
   * @param {number} tileGutter Tile gutter.
   * @param {number} magFilter Mag filter.
   * @param {number} minFilter Min filter.
   */
  GVol.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);
      gl.bindTexture(GVol.webgl.TEXTURE_2D, textureCacheEntry.texture);
      if (textureCacheEntry.magFilter != magFilter) {
        gl.texParameteri(
            GVol.webgl.TEXTURE_2D, GVol.webgl.TEXTURE_MAG_FILTER, magFilter);
        textureCacheEntry.magFilter = magFilter;
      }
      if (textureCacheEntry.minFilter != minFilter) {
        gl.texParameteri(
            GVol.webgl.TEXTURE_2D, GVol.webgl.TEXTURE_MIN_FILTER, minFilter);
        textureCacheEntry.minFilter = minFilter;
      }
    } else {
      var texture = gl.createTexture();
      gl.bindTexture(GVol.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(GVol.webgl.TEXTURE_2D, 0,
            GVol.webgl.RGBA, GVol.webgl.RGBA,
            GVol.webgl.UNSIGNED_BYTE, clipTileCanvas);
      } else {
        gl.texImage2D(GVol.webgl.TEXTURE_2D, 0,
            GVol.webgl.RGBA, GVol.webgl.RGBA,
            GVol.webgl.UNSIGNED_BYTE, tile.getImage());
      }
      gl.texParameteri(
          GVol.webgl.TEXTURE_2D, GVol.webgl.TEXTURE_MAG_FILTER, magFilter);
      gl.texParameteri(
          GVol.webgl.TEXTURE_2D, GVol.webgl.TEXTURE_MIN_FILTER, minFilter);
      gl.texParameteri(GVol.webgl.TEXTURE_2D, GVol.webgl.TEXTURE_WRAP_S,
          GVol.webgl.CLAMP_TO_EDGE);
      gl.texParameteri(GVol.webgl.TEXTURE_2D, GVol.webgl.TEXTURE_WRAP_T,
          GVol.webgl.CLAMP_TO_EDGE);
      this.textureCache_.set(tileKey, {
        texture: texture,
        magFilter: magFilter,
        minFilter: minFilter
      });
    }
  };


  /**
   * @param {GVol.render.EventType} type Event type.
   * @param {GVolx.FrameState} frameState Frame state.
   * @private
   */
  GVol.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 resGVolution = viewState.resGVolution;
      var center = viewState.center;
      var rotation = viewState.rotation;

      var vectorContext = new GVol.render.webgl.Immediate(context,
          center, resGVolution, rotation, size, extent, pixelRatio);
      var composeEvent = new GVol.render.Event(type, vectorContext,
          frameState, null, context);
      map.dispatchEvent(composeEvent);
    }
  };


  /**
   * @inheritDoc
   */
  GVol.renderer.webgl.Map.prototype.disposeInternal = function() {
    var gl = this.getGL();
    if (!gl.isContextLost()) {
      this.textureCache_.forEach(
          /**
           * @param {?GVol.WebglTextureCacheEntry} textureCacheEntry
           *     Texture cache entry.
           */
          function(textureCacheEntry) {
            if (textureCacheEntry) {
              gl.deleteTexture(textureCacheEntry.texture);
            }
          });
    }
    this.context_.dispose();
    GVol.renderer.Map.prototype.disposeInternal.call(this);
  };


  /**
   * @param {GVol.Map} map Map.
   * @param {GVolx.FrameState} frameState Frame state.
   * @private
   */
  GVol.renderer.webgl.Map.prototype.expireCache_ = function(map, frameState) {
    var gl = this.getGL();
    var textureCacheEntry;
    while (this.textureCache_.getCount() - this.textureCacheFrameMarkerCount_ >
        GVol.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 {GVol.webgl.Context} The context.
   */
  GVol.renderer.webgl.Map.prototype.getContext = function() {
    return this.context_;
  };


  /**
   * @return {WebGLRenderingContext} GL.
   */
  GVol.renderer.webgl.Map.prototype.getGL = function() {
    return this.gl_;
  };


  /**
   * @return {GVol.structs.PriorityQueue.<Array>} Tile texture queue.
   */
  GVol.renderer.webgl.Map.prototype.getTileTextureQueue = function() {
    return this.tileTextureQueue_;
  };


  /**
   * @inheritDoc
   */
  GVol.renderer.webgl.Map.prototype.getType = function() {
    return GVol.renderer.Type.WEBGL;
  };


  /**
   * @param {GVol.events.Event} event Event.
   * @protected
   */
  GVol.renderer.webgl.Map.prototype.handleWebGLContextLost = function(event) {
    event.preventDefault();
    this.textureCache_.clear();
    this.textureCacheFrameMarkerCount_ = 0;

    var renderers = this.getLayerRenderers();
    for (var id in renderers) {
      var renderer = /** @type {GVol.renderer.webgl.Layer} */ (renderers[id]);
      renderer.handleWebGLContextLost();
    }
  };


  /**
   * @protected
   */
  GVol.renderer.webgl.Map.prototype.handleWebGLContextRestored = function() {
    this.initializeGL_();
    this.getMap().render();
  };


  /**
   * @private
   */
  GVol.renderer.webgl.Map.prototype.initializeGL_ = function() {
    var gl = this.gl_;
    gl.activeTexture(GVol.webgl.TEXTURE0);
    gl.blendFuncSeparate(
        GVol.webgl.SRC_ALPHA, GVol.webgl.ONE_MINUS_SRC_ALPHA,
        GVol.webgl.ONE, GVol.webgl.ONE_MINUS_SRC_ALPHA);
    gl.disable(GVol.webgl.CULL_FACE);
    gl.disable(GVol.webgl.DEPTH_TEST);
    gl.disable(GVol.webgl.SCISSOR_TEST);
    gl.disable(GVol.webgl.STENCIL_TEST);
  };


  /**
   * @param {GVol.Tile} tile Tile.
   * @return {boGVolean} Is tile texture loaded.
   */
  GVol.renderer.webgl.Map.prototype.isTileTextureLoaded = function(tile) {
    return this.textureCache_.containsKey(tile.getKey());
  };


  /**
   * @inheritDoc
   */
  GVol.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_) {
        this.canvas_.style.display = 'none';
        this.renderedVisible_ = false;
      }
      return false;
    }

    this.focus_ = frameState.focus;

    this.textureCache_.set((-frameState.index).toString(), null);
    ++this.textureCacheFrameMarkerCount_;

    this.dispatchComposeEvent_(GVol.render.EventType.PRECOMPOSE, frameState);

    /** @type {Array.<GVol.LayerState>} */
    var layerStatesToDraw = [];
    var layerStatesArray = frameState.layerStatesArray;
    GVol.array.stableSort(layerStatesArray, GVol.renderer.Map.sortByZIndex);

    var viewResGVolution = frameState.viewState.resGVolution;
    var i, ii, layerRenderer, layerState;
    for (i = 0, ii = layerStatesArray.length; i < ii; ++i) {
      layerState = layerStatesArray[i];
      if (GVol.layer.Layer.visibleAtResGVolution(layerState, viewResGVolution) &&
          layerState.sourceState == GVol.source.State.READY) {
        layerRenderer = /** @type {GVol.renderer.webgl.Layer} */ (this.getLayerRenderer(layerState.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(GVol.webgl.FRAMEBUFFER, null);

    gl.clearCGVolor(0, 0, 0, 0);
    gl.clear(GVol.webgl.COLOR_BUFFER_BIT);
    gl.enable(GVol.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 = /** @type {GVol.renderer.webgl.Layer} */ (this.getLayerRenderer(layerState.layer));
      layerRenderer.composeFrame(frameState, layerState, context);
    }

    if (!this.renderedVisible_) {
      this.canvas_.style.display = '';
      this.renderedVisible_ = true;
    }

    this.calculateMatrices2D(frameState);

    if (this.textureCache_.getCount() - this.textureCacheFrameMarkerCount_ >
        GVol.WEBGL_TEXTURE_CACHE_HIGH_WATER_MARK) {
      frameState.postRenderFunctions.push(
          /** @type {GVol.PostRenderFunction} */ (this.expireCache_.bind(this))
      );
    }

    if (!this.tileTextureQueue_.isEmpty()) {
      frameState.postRenderFunctions.push(this.loadNextTileTexture_);
      frameState.animate = true;
    }

    this.dispatchComposeEvent_(GVol.render.EventType.POSTCOMPOSE, frameState);

    this.scheduleRemoveUnusedLayerRenderers(frameState);
    this.scheduleExpireIconCache(frameState);

  };


  /**
   * @inheritDoc
   */
  GVol.renderer.webgl.Map.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTGVolerance, 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 (GVol.layer.Layer.visibleAtResGVolution(layerState, viewState.resGVolution) &&
          layerFilter.call(thisArg2, layer)) {
        var layerRenderer = this.getLayerRenderer(layer);
        result = layerRenderer.forEachFeatureAtCoordinate(
            coordinate, frameState, hitTGVolerance, callback, thisArg);
        if (result) {
          return result;
        }
      }
    }
    return undefined;
  };


  /**
   * @inheritDoc
   */
  GVol.renderer.webgl.Map.prototype.hasFeatureAtCoordinate = function(coordinate, frameState, hitTGVolerance, 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 (GVol.layer.Layer.visibleAtResGVolution(layerState, viewState.resGVolution) &&
          layerFilter.call(thisArg, layer)) {
        var layerRenderer = this.getLayerRenderer(layer);
        hasFeature =
            layerRenderer.hasFeatureAtCoordinate(coordinate, frameState);
        if (hasFeature) {
          return true;
        }
      }
    }
    return hasFeature;
  };


  /**
   * @inheritDoc
   */
  GVol.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 (GVol.layer.Layer.visibleAtResGVolution(layerState, viewState.resGVolution) &&
          layerFilter.call(thisArg, layer)) {
        var layerRenderer = /** @type {GVol.renderer.webgl.Layer} */ (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('GVol.Map');

goog.require('GVol');
goog.require('GVol.CGVollection');
goog.require('GVol.CGVollectionEventType');
goog.require('GVol.MapBrowserEvent');
goog.require('GVol.MapBrowserEventHandler');
goog.require('GVol.MapBrowserEventType');
goog.require('GVol.MapEvent');
goog.require('GVol.MapEventType');
goog.require('GVol.MapProperty');
goog.require('GVol.Object');
goog.require('GVol.ObjectEventType');
goog.require('GVol.TileQueue');
goog.require('GVol.View');
goog.require('GVol.ViewHint');
goog.require('GVol.asserts');
goog.require('GVol.contrGVol');
goog.require('GVol.dom');
goog.require('GVol.events');
goog.require('GVol.events.Event');
goog.require('GVol.events.EventType');
goog.require('GVol.extent');
goog.require('GVol.functions');
goog.require('GVol.has');
goog.require('GVol.interaction');
goog.require('GVol.layer.Group');
goog.require('GVol.obj');
goog.require('GVol.renderer.Map');
goog.require('GVol.renderer.Type');
goog.require('GVol.renderer.canvas.Map');
goog.require('GVol.renderer.webgl.Map');
goog.require('GVol.size');
goog.require('GVol.structs.PriorityQueue');
goog.require('GVol.transform');


/**
 * @const
 * @type {string}
 */
GVol.OL_URL = 'https://openlayers.org/';


/**
 * @const
 * @type {string}
 */
GVol.OL_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.<GVol.renderer.Type>}
 * @const
 */
GVol.DEFAULT_RENDERER_TYPES = [
  GVol.renderer.Type.CANVAS,
  GVol.renderer.Type.WEBGL
];


/**
 * @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 GVol.Map({
 *       view: new GVol.View({
 *         center: [0, 0],
 *         zoom: 1
 *       }),
 *       layers: [
 *         new GVol.layer.Tile({
 *           source: new GVol.source.OSM()
 *         })
 *       ],
 *       target: 'map'
 *     });
 *
 * The above snippet creates a map using a {@link GVol.layer.Tile} to display
 * {@link GVol.source.OSM} OSM data and render it to a DOM element with the
 * id `map`.
 *
 * The constructor places a viewport container (with CSS class name
 * `GVol-viewport`) in the target element (see `getViewport()`), and then two
 * further elements within the viewport: one with CSS class name
 * `GVol-overlaycontainer-stopevent` for contrGVols and some overlays, and one with
 * CSS class name `GVol-overlaycontainer` for other overlays (see the `stopEvent`
 * option of {@link GVol.Overlay} for the difference). The map itself is placed in
 * a further element within the viewport.
 *
 * Layers are stored as a `GVol.CGVollection` 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 cGVollection in the group.
 * `getLayers` is a convenience function for `getLayerGroup().getLayers()`.
 * Note that `GVol.layer.Group` is a subclass of `GVol.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 {GVol.Object}
 * @param {GVolx.MapOptions} options Map options.
 * @fires GVol.MapBrowserEvent
 * @fires GVol.MapEvent
 * @fires GVol.render.Event#postcompose
 * @fires GVol.render.Event#precompose
 * @api
 */
GVol.Map = function(options) {

  GVol.Object.call(this);

  var optionsInternal = GVol.Map.createOptionsInternal(options);

  /**
   * @type {boGVolean}
   * @private
   */
  this.loadTilesWhileAnimating_ =
      options.loadTilesWhileAnimating !== undefined ?
        options.loadTilesWhileAnimating : false;

  /**
   * @type {boGVolean}
   * @private
   */
  this.loadTilesWhileInteracting_ =
      options.loadTilesWhileInteracting !== undefined ?
        options.loadTilesWhileInteracting : false;

  /**
   * @private
   * @type {number}
   */
  this.pixelRatio_ = options.pixelRatio !== undefined ?
    options.pixelRatio : GVol.has.DEVICE_PIXEL_RATIO;

  /**
   * @private
   * @type {Object.<string, string>}
   */
  this.logos_ = optionsInternal.logos;

  /**
   * @private
   * @type {number|undefined}
   */
  this.animationDelayKey_;

  /**
   * @private
   */
  this.animationDelay_ = function() {
    this.animationDelayKey_ = undefined;
    this.renderFrame_.call(this, Date.now());
  }.bind(this);

  /**
   * @private
   * @type {GVol.Transform}
   */
  this.coordinateToPixelTransform_ = GVol.transform.create();

  /**
   * @private
   * @type {GVol.Transform}
   */
  this.pixelToCoordinateTransform_ = GVol.transform.create();

  /**
   * @private
   * @type {number}
   */
  this.frameIndex_ = 0;

  /**
   * @private
   * @type {?GVolx.FrameState}
   */
  this.frameState_ = null;

  /**
   * The extent at the previous 'moveend' event.
   * @private
   * @type {GVol.Extent}
   */
  this.previousExtent_ = null;

  /**
   * @private
   * @type {?GVol.EventsKey}
   */
  this.viewPropertyListenerKey_ = null;

  /**
   * @private
   * @type {?GVol.EventsKey}
   */
  this.viewChangeListenerKey_ = null;

  /**
   * @private
   * @type {Array.<GVol.EventsKey>}
   */
  this.layerGroupPropertyListenerKeys_ = null;

  /**
   * @private
   * @type {Element}
   */
  this.viewport_ = document.createElement('DIV');
  this.viewport_.className = 'GVol-viewport' + (GVol.has.TOUCH ? ' GVol-touch' : '');
  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';

  /**
   * @private
   * @type {!Element}
   */
  this.overlayContainer_ = document.createElement('DIV');
  this.overlayContainer_.className = 'GVol-overlaycontainer';
  this.viewport_.appendChild(this.overlayContainer_);

  /**
   * @private
   * @type {!Element}
   */
  this.overlayContainerStopEvent_ = document.createElement('DIV');
  this.overlayContainerStopEvent_.className = 'GVol-overlaycontainer-stopevent';
  var overlayEvents = [
    GVol.events.EventType.CLICK,
    GVol.events.EventType.DBLCLICK,
    GVol.events.EventType.MOUSEDOWN,
    GVol.events.EventType.TOUCHSTART,
    GVol.events.EventType.MSPOINTERDOWN,
    GVol.MapBrowserEventType.POINTERDOWN,
    GVol.events.EventType.MOUSEWHEEL,
    GVol.events.EventType.WHEEL
  ];
  for (var i = 0, ii = overlayEvents.length; i < ii; ++i) {
    GVol.events.listen(this.overlayContainerStopEvent_, overlayEvents[i],
        GVol.events.Event.stopPropagation);
  }
  this.viewport_.appendChild(this.overlayContainerStopEvent_);

  /**
   * @private
   * @type {GVol.MapBrowserEventHandler}
   */
  this.mapBrowserEventHandler_ = new GVol.MapBrowserEventHandler(this, options.moveTGVolerance);
  for (var key in GVol.MapBrowserEventType) {
    GVol.events.listen(this.mapBrowserEventHandler_, GVol.MapBrowserEventType[key],
        this.handleMapBrowserEvent, this);
  }

  /**
   * @private
   * @type {Element|Document}
   */
  this.keyboardEventTarget_ = optionsInternal.keyboardEventTarget;

  /**
   * @private
   * @type {Array.<GVol.EventsKey>}
   */
  this.keyHandlerKeys_ = null;

  GVol.events.listen(this.viewport_, GVol.events.EventType.WHEEL,
      this.handleBrowserEvent, this);
  GVol.events.listen(this.viewport_, GVol.events.EventType.MOUSEWHEEL,
      this.handleBrowserEvent, this);

  /**
   * @type {GVol.CGVollection.<GVol.contrGVol.ContrGVol>}
   * @private
   */
  this.contrGVols_ = optionsInternal.contrGVols;

  /**
   * @type {GVol.CGVollection.<GVol.interaction.Interaction>}
   * @private
   */
  this.interactions_ = optionsInternal.interactions;

  /**
   * @type {GVol.CGVollection.<GVol.Overlay>}
   * @private
   */
  this.overlays_ = optionsInternal.overlays;

  /**
   * A lookup of overlays by id.
   * @private
   * @type {Object.<string, GVol.Overlay>}
   */
  this.overlayIdIndex_ = {};

  /**
   * @type {GVol.renderer.Map}
   * @private
   */
  this.renderer_ = new /** @type {Function} */ (optionsInternal.rendererConstructor)(this.viewport_, this);

  /**
   * @type {function(Event)|undefined}
   * @private
   */
  this.handleResize_;

  /**
   * @private
   * @type {GVol.Coordinate}
   */
  this.focus_ = null;

  /**
   * @private
   * @type {Array.<GVol.PostRenderFunction>}
   */
  this.postRenderFunctions_ = [];

  /**
   * @private
   * @type {GVol.TileQueue}
   */
  this.tileQueue_ = new GVol.TileQueue(
      this.getTilePriority.bind(this),
      this.handleTileChange_.bind(this));

  /**
   * Uids of features to skip at rendering time.
   * @type {Object.<string, boGVolean>}
   * @private
   */
  this.skippedFeatureUids_ = {};

  GVol.events.listen(
      this, GVol.Object.getChangeEventType(GVol.MapProperty.LAYERGROUP),
      this.handleLayerGroupChanged_, this);
  GVol.events.listen(this, GVol.Object.getChangeEventType(GVol.MapProperty.VIEW),
      this.handleViewChanged_, this);
  GVol.events.listen(this, GVol.Object.getChangeEventType(GVol.MapProperty.SIZE),
      this.handleSizeChanged_, this);
  GVol.events.listen(this, GVol.Object.getChangeEventType(GVol.MapProperty.TARGET),
      this.handleTargetChanged_, this);

  // setProperties will trigger the rendering of the map if the map
  // is "defined" already.
  this.setProperties(optionsInternal.values);

  this.contrGVols_.forEach(
      /**
       * @param {GVol.contrGVol.ContrGVol} contrGVol ContrGVol.
       * @this {GVol.Map}
       */
      function(contrGVol) {
        contrGVol.setMap(this);
      }, this);

  GVol.events.listen(this.contrGVols_, GVol.CGVollectionEventType.ADD,
      /**
       * @param {GVol.CGVollection.Event} event CGVollection event.
       */
      function(event) {
        event.element.setMap(this);
      }, this);

  GVol.events.listen(this.contrGVols_, GVol.CGVollectionEventType.REMOVE,
      /**
       * @param {GVol.CGVollection.Event} event CGVollection event.
       */
      function(event) {
        event.element.setMap(null);
      }, this);

  this.interactions_.forEach(
      /**
       * @param {GVol.interaction.Interaction} interaction Interaction.
       * @this {GVol.Map}
       */
      function(interaction) {
        interaction.setMap(this);
      }, this);

  GVol.events.listen(this.interactions_, GVol.CGVollectionEventType.ADD,
      /**
       * @param {GVol.CGVollection.Event} event CGVollection event.
       */
      function(event) {
        event.element.setMap(this);
      }, this);

  GVol.events.listen(this.interactions_, GVol.CGVollectionEventType.REMOVE,
      /**
       * @param {GVol.CGVollection.Event} event CGVollection event.
       */
      function(event) {
        event.element.setMap(null);
      }, this);

  this.overlays_.forEach(this.addOverlayInternal_, this);

  GVol.events.listen(this.overlays_, GVol.CGVollectionEventType.ADD,
      /**
       * @param {GVol.CGVollection.Event} event CGVollection event.
       */
      function(event) {
        this.addOverlayInternal_(/** @type {GVol.Overlay} */ (event.element));
      }, this);

  GVol.events.listen(this.overlays_, GVol.CGVollectionEventType.REMOVE,
      /**
       * @param {GVol.CGVollection.Event} event CGVollection event.
       */
      function(event) {
        var overlay = /** @type {GVol.Overlay} */ (event.element);
        var id = overlay.getId();
        if (id !== undefined) {
          delete this.overlayIdIndex_[id.toString()];
        }
        event.element.setMap(null);
      }, this);

};
GVol.inherits(GVol.Map, GVol.Object);


/**
 * Add the given contrGVol to the map.
 * @param {GVol.contrGVol.ContrGVol} contrGVol ContrGVol.
 * @api
 */
GVol.Map.prototype.addContrGVol = function(contrGVol) {
  this.getContrGVols().push(contrGVol);
};


/**
 * Add the given interaction to the map.
 * @param {GVol.interaction.Interaction} interaction Interaction to add.
 * @api
 */
GVol.Map.prototype.addInteraction = function(interaction) {
  this.getInteractions().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 GVol.CGVollection}.
 * @param {GVol.layer.Base} layer Layer.
 * @api
 */
GVol.Map.prototype.addLayer = function(layer) {
  var layers = this.getLayerGroup().getLayers();
  layers.push(layer);
};


/**
 * Add the given overlay to the map.
 * @param {GVol.Overlay} overlay Overlay.
 * @api
 */
GVol.Map.prototype.addOverlay = function(overlay) {
  this.getOverlays().push(overlay);
};


/**
 * This deals with map's overlay cGVollection changes.
 * @param {GVol.Overlay} overlay Overlay.
 * @private
 */
GVol.Map.prototype.addOverlayInternal_ = function(overlay) {
  var id = overlay.getId();
  if (id !== undefined) {
    this.overlayIdIndex_[id.toString()] = overlay;
  }
  overlay.setMap(this);
};


/**
 *
 * @inheritDoc
 */
GVol.Map.prototype.disposeInternal = function() {
  this.mapBrowserEventHandler_.dispose();
  this.renderer_.dispose();
  GVol.events.unlisten(this.viewport_, GVol.events.EventType.WHEEL,
      this.handleBrowserEvent, this);
  GVol.events.unlisten(this.viewport_, GVol.events.EventType.MOUSEWHEEL,
      this.handleBrowserEvent, this);
  if (this.handleResize_ !== undefined) {
    window.removeEventListener(GVol.events.EventType.RESIZE,
        this.handleResize_, false);
    this.handleResize_ = undefined;
  }
  if (this.animationDelayKey_) {
    cancelAnimationFrame(this.animationDelayKey_);
    this.animationDelayKey_ = undefined;
  }
  this.setTarget(null);
  GVol.Object.prototype.disposeInternal.call(this);
};


/**
 * 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 the `layerFilter` option in `opt_options`.
 * @param {GVol.Pixel} pixel Pixel.
 * @param {function(this: S, (GVol.Feature|GVol.render.Feature),
 *     GVol.layer.Layer): T} callback Feature callback. The callback will be
 *     called with two arguments. The first argument is one
 *     {@link GVol.Feature feature} or
 *     {@link GVol.render.Feature render feature} at the pixel, the second is
 *     the {@link GVol.layer.Layer layer} of the feature and will be null for
 *     unmanaged layers. To stop detection, callback functions can return a
 *     truthy value.
 * @param {GVolx.AtPixelOptions=} opt_options Optional options.
 * @return {T|undefined} Callback result, i.e. the return value of last
 * callback execution, or the first truthy callback return value.
 * @template S,T
 * @api
 */
GVol.Map.prototype.forEachFeatureAtPixel = function(pixel, callback, opt_options) {
  if (!this.frameState_) {
    return;
  }
  var coordinate = this.getCoordinateFromPixel(pixel);
  opt_options = opt_options !== undefined ? opt_options : {};
  var hitTGVolerance = opt_options.hitTGVolerance !== undefined ?
    opt_options.hitTGVolerance * this.frameState_.pixelRatio : 0;
  var layerFilter = opt_options.layerFilter !== undefined ?
    opt_options.layerFilter : GVol.functions.TRUE;
  return this.renderer_.forEachFeatureAtCoordinate(
      coordinate, this.frameState_, hitTGVolerance, callback, null,
      layerFilter, null);
};


/**
 * Get all features that intersect a pixel on the viewport.
 * @param {GVol.Pixel} pixel Pixel.
 * @param {GVolx.AtPixelOptions=} opt_options Optional options.
 * @return {Array.<GVol.Feature|GVol.render.Feature>} The detected features or
 * `null` if none were found.
 * @api
 */
GVol.Map.prototype.getFeaturesAtPixel = function(pixel, opt_options) {
  var features = null;
  this.forEachFeatureAtPixel(pixel, function(feature) {
    if (!features) {
      features = [];
    }
    features.push(feature);
  }, opt_options);
  return features;
};

/**
 * Detect layers that have a cGVolor 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 {GVol.Pixel} pixel Pixel.
 * @param {function(this: S, GVol.layer.Layer, (Uint8ClampedArray|Uint8Array)): T} callback
 *     Layer callback. This callback will receive two arguments: first is the
 *     {@link GVol.layer.Layer layer}, second argument is an array representing
 *     [R, G, B, A] pixel values (0 - 255) and will be `null` for layer types
 *     that do not currently support this argument. 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, GVol.layer.Layer): boGVolean)=} opt_layerFilter Layer
 *     filter function. The filter function will receive one argument, the
 *     {@link GVol.layer.Layer layer-candidate} and it should return a boGVolean
 *     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
 */
GVol.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 : GVol.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 {GVol.Pixel} pixel Pixel.
 * @param {GVolx.AtPixelOptions=} opt_options Optional options.
 * @return {boGVolean} Is there a feature at the given pixel?
 * @template U
 * @api
 */
GVol.Map.prototype.hasFeatureAtPixel = function(pixel, opt_options) {
  if (!this.frameState_) {
    return false;
  }
  var coordinate = this.getCoordinateFromPixel(pixel);
  opt_options = opt_options !== undefined ? opt_options : {};
  var layerFilter = opt_options.layerFilter !== undefined ?
    opt_options.layerFilter : GVol.functions.TRUE;
  var hitTGVolerance = opt_options.hitTGVolerance !== undefined ?
    opt_options.hitTGVolerance * this.frameState_.pixelRatio : 0;
  return this.renderer_.hasFeatureAtCoordinate(
      coordinate, this.frameState_, hitTGVolerance, layerFilter, null);
};


/**
 * Returns the coordinate in view projection for a browser event.
 * @param {Event} event Event.
 * @return {GVol.Coordinate} Coordinate.
 * @api
 */
GVol.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 {GVol.Pixel} Pixel.
 * @api
 */
GVol.Map.prototype.getEventPixel = function(event) {
  var viewportPosition = this.viewport_.getBoundingClientRect();
  var eventPosition = event.changedTouches ? event.changedTouches[0] : event;
  return [
    eventPosition.clientX - viewportPosition.left,
    eventPosition.clientY - viewportPosition.top
  ];
};


/**
 * 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
 */
GVol.Map.prototype.getTarget = function() {
  return /** @type {Element|string|undefined} */ (
    this.get(GVol.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
 */
GVol.Map.prototype.getTargetElement = function() {
  var target = this.getTarget();
  if (target !== undefined) {
    return typeof target === 'string' ?
      document.getElementById(target) :
      target;
  } else {
    return null;
  }
};


/**
 * Get the coordinate for a given pixel.  This returns a coordinate in the
 * map view projection.
 * @param {GVol.Pixel} pixel Pixel position in the map viewport.
 * @return {GVol.Coordinate} The coordinate for the pixel position.
 * @api
 */
GVol.Map.prototype.getCoordinateFromPixel = function(pixel) {
  var frameState = this.frameState_;
  if (!frameState) {
    return null;
  } else {
    return GVol.transform.apply(frameState.pixelToCoordinateTransform, pixel.slice());
  }
};


/**
 * Get the map contrGVols. Modifying this cGVollection changes the contrGVols
 * associated with the map.
 * @return {GVol.CGVollection.<GVol.contrGVol.ContrGVol>} ContrGVols.
 * @api
 */
GVol.Map.prototype.getContrGVols = function() {
  return this.contrGVols_;
};


/**
 * Get the map overlays. Modifying this cGVollection changes the overlays
 * associated with the map.
 * @return {GVol.CGVollection.<GVol.Overlay>} Overlays.
 * @api
 */
GVol.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 {GVol.Overlay} Overlay.
 * @api
 */
GVol.Map.prototype.getOverlayById = function(id) {
  var overlay = this.overlayIdIndex_[id.toString()];
  return overlay !== undefined ? overlay : null;
};


/**
 * Get the map interactions. Modifying this cGVollection changes the interactions
 * associated with the map.
 *
 * Interactions are used for e.g. pan, zoom and rotate.
 * @return {GVol.CGVollection.<GVol.interaction.Interaction>} Interactions.
 * @api
 */
GVol.Map.prototype.getInteractions = function() {
  return this.interactions_;
};


/**
 * Get the layergroup associated with this map.
 * @return {GVol.layer.Group} A layer group containing the layers in this map.
 * @observable
 * @api
 */
GVol.Map.prototype.getLayerGroup = function() {
  return /** @type {GVol.layer.Group} */ (this.get(GVol.MapProperty.LAYERGROUP));
};


/**
 * Get the cGVollection of layers associated with this map.
 * @return {!GVol.CGVollection.<GVol.layer.Base>} Layers.
 * @api
 */
GVol.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 {GVol.Coordinate} coordinate A map coordinate.
 * @return {GVol.Pixel} A pixel position in the map viewport.
 * @api
 */
GVol.Map.prototype.getPixelFromCoordinate = function(coordinate) {
  var frameState = this.frameState_;
  if (!frameState) {
    return null;
  } else {
    return GVol.transform.apply(frameState.coordinateToPixelTransform,
        coordinate.slice(0, 2));
  }
};


/**
 * Get the map renderer.
 * @return {GVol.renderer.Map} Renderer
 */
GVol.Map.prototype.getRenderer = function() {
  return this.renderer_;
};


/**
 * Get the size of this map.
 * @return {GVol.Size|undefined} The size in pixels of the map in the DOM.
 * @observable
 * @api
 */
GVol.Map.prototype.getSize = function() {
  return /** @type {GVol.Size|undefined} */ (this.get(GVol.MapProperty.SIZE));
};


/**
 * Get the view associated with this map. A view manages properties such as
 * center and resGVolution.
 * @return {GVol.View} The view that contrGVols this map.
 * @observable
 * @api
 */
GVol.Map.prototype.getView = function() {
  return /** @type {GVol.View} */ (this.get(GVol.MapProperty.VIEW));
};


/**
 * Get the element that serves as the map viewport.
 * @return {Element} Viewport.
 * @api
 */
GVol.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 GVol.MapBrowserEvent}
 * events.
 * @return {!Element} The map's overlay container.
 */
GVol.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 GVol.MapBrowserEvent}.
 * @return {!Element} The map's overlay container that stops events.
 */
GVol.Map.prototype.getOverlayContainerStopEvent = function() {
  return this.overlayContainerStopEvent_;
};


/**
 * @param {GVol.Tile} tile Tile.
 * @param {string} tileSourceKey Tile source key.
 * @param {GVol.Coordinate} tileCenter Tile center.
 * @param {number} tileResGVolution Tile resGVolution.
 * @return {number} Tile priority.
 */
GVol.Map.prototype.getTilePriority = function(tile, tileSourceKey, tileCenter, tileResGVolution) {
  // 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 GVol.structs.PriorityQueue.DROP;
  }
  if (!frameState.wantedTiles[tileSourceKey][tile.getKey()]) {
    return GVol.structs.PriorityQueue.DROP;
  }
  // Prioritize the highest zoom level tiles closest to the focus.
  // Tiles at higher zoom levels are prioritized using Math.log(tileResGVolution).
  // 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(tileResGVolution) +
      Math.sqrt(deltaX * deltaX + deltaY * deltaY) / tileResGVolution;
};


/**
 * @param {Event} browserEvent Browser event.
 * @param {string=} opt_type Type.
 */
GVol.Map.prototype.handleBrowserEvent = function(browserEvent, opt_type) {
  var type = opt_type || browserEvent.type;
  var mapBrowserEvent = new GVol.MapBrowserEvent(type, this, browserEvent);
  this.handleMapBrowserEvent(mapBrowserEvent);
};


/**
 * @param {GVol.MapBrowserEvent} mapBrowserEvent The event to handle.
 */
GVol.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 interactionsArray = this.getInteractions().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
 */
GVol.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;
    if (frameState) {
      var hints = frameState.viewHints;
      if (hints[GVol.ViewHint.ANIMATING]) {
        maxTotalLoading = this.loadTilesWhileAnimating_ ? 8 : 0;
        maxNewLoads = 2;
      }
      if (hints[GVol.ViewHint.INTERACTING]) {
        maxTotalLoading = this.loadTilesWhileInteracting_ ? 8 : 0;
        maxNewLoads = 2;
      }
    }
    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
 */
GVol.Map.prototype.handleSizeChanged_ = function() {
  this.render();
};


/**
 * @private
 */
GVol.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;
  if (this.getTarget()) {
    targetElement = this.getTargetElement();
  }

  if (this.keyHandlerKeys_) {
    for (var i = 0, ii = this.keyHandlerKeys_.length; i < ii; ++i) {
      GVol.events.unlistenByKey(this.keyHandlerKeys_[i]);
    }
    this.keyHandlerKeys_ = null;
  }

  if (!targetElement) {
    GVol.dom.removeNode(this.viewport_);
    if (this.handleResize_ !== undefined) {
      window.removeEventListener(GVol.events.EventType.RESIZE,
          this.handleResize_, false);
      this.handleResize_ = undefined;
    }
  } else {
    targetElement.appendChild(this.viewport_);

    var keyboardEventTarget = !this.keyboardEventTarget_ ?
      targetElement : this.keyboardEventTarget_;
    this.keyHandlerKeys_ = [
      GVol.events.listen(keyboardEventTarget, GVol.events.EventType.KEYDOWN,
          this.handleBrowserEvent, this),
      GVol.events.listen(keyboardEventTarget, GVol.events.EventType.KEYPRESS,
          this.handleBrowserEvent, this)
    ];

    if (!this.handleResize_) {
      this.handleResize_ = this.updateSize.bind(this);
      window.addEventListener(GVol.events.EventType.RESIZE,
          this.handleResize_, false);
    }
  }

  this.updateSize();
  // updateSize calls setSize, so no need to call this.render
  // ourselves here.
};


/**
 * @private
 */
GVol.Map.prototype.handleTileChange_ = function() {
  this.render();
};


/**
 * @private
 */
GVol.Map.prototype.handleViewPropertyChanged_ = function() {
  this.render();
};


/**
 * @private
 */
GVol.Map.prototype.handleViewChanged_ = function() {
  if (this.viewPropertyListenerKey_) {
    GVol.events.unlistenByKey(this.viewPropertyListenerKey_);
    this.viewPropertyListenerKey_ = null;
  }
  if (this.viewChangeListenerKey_) {
    GVol.events.unlistenByKey(this.viewChangeListenerKey_);
    this.viewChangeListenerKey_ = null;
  }
  var view = this.getView();
  if (view) {
    this.viewport_.setAttribute('data-view', GVol.getUid(view));
    this.viewPropertyListenerKey_ = GVol.events.listen(
        view, GVol.ObjectEventType.PROPERTYCHANGE,
        this.handleViewPropertyChanged_, this);
    this.viewChangeListenerKey_ = GVol.events.listen(
        view, GVol.events.EventType.CHANGE,
        this.handleViewPropertyChanged_, this);
  }
  this.render();
};


/**
 * @private
 */
GVol.Map.prototype.handleLayerGroupChanged_ = function() {
  if (this.layerGroupPropertyListenerKeys_) {
    this.layerGroupPropertyListenerKeys_.forEach(GVol.events.unlistenByKey);
    this.layerGroupPropertyListenerKeys_ = null;
  }
  var layerGroup = this.getLayerGroup();
  if (layerGroup) {
    this.layerGroupPropertyListenerKeys_ = [
      GVol.events.listen(
          layerGroup, GVol.ObjectEventType.PROPERTYCHANGE,
          this.render, this),
      GVol.events.listen(
          layerGroup, GVol.events.EventType.CHANGE,
          this.render, this)
    ];
  }
  this.render();
};


/**
 * @return {boGVolean} Is rendered.
 */
GVol.Map.prototype.isRendered = function() {
  return !!this.frameState_;
};


/**
 * Requests an immediate render in a synchronous manner.
 * @api
 */
GVol.Map.prototype.renderSync = function() {
  if (this.animationDelayKey_) {
    cancelAnimationFrame(this.animationDelayKey_);
  }
  this.animationDelay_();
};


/**
 * Request a map rendering (at the next animation frame).
 * @api
 */
GVol.Map.prototype.render = function() {
  if (this.animationDelayKey_ === undefined) {
    this.animationDelayKey_ = requestAnimationFrame(
        this.animationDelay_);
  }
};


/**
 * Remove the given contrGVol from the map.
 * @param {GVol.contrGVol.ContrGVol} contrGVol ContrGVol.
 * @return {GVol.contrGVol.ContrGVol|undefined} The removed contrGVol (or undefined
 *     if the contrGVol was not found).
 * @api
 */
GVol.Map.prototype.removeContrGVol = function(contrGVol) {
  return this.getContrGVols().remove(contrGVol);
};


/**
 * Remove the given interaction from the map.
 * @param {GVol.interaction.Interaction} interaction Interaction to remove.
 * @return {GVol.interaction.Interaction|undefined} The removed interaction (or
 *     undefined if the interaction was not found).
 * @api
 */
GVol.Map.prototype.removeInteraction = function(interaction) {
  return this.getInteractions().remove(interaction);
};


/**
 * Removes the given layer from the map.
 * @param {GVol.layer.Base} layer Layer.
 * @return {GVol.layer.Base|undefined} The removed layer (or undefined if the
 *     layer was not found).
 * @api
 */
GVol.Map.prototype.removeLayer = function(layer) {
  var layers = this.getLayerGroup().getLayers();
  return layers.remove(layer);
};


/**
 * Remove the given overlay from the map.
 * @param {GVol.Overlay} overlay Overlay.
 * @return {GVol.Overlay|undefined} The removed overlay (or undefined
 *     if the overlay was not found).
 * @api
 */
GVol.Map.prototype.removeOverlay = function(overlay) {
  return this.getOverlays().remove(overlay);
};


/**
 * @param {number} time Time.
 * @private
 */
GVol.Map.prototype.renderFrame_ = function(time) {
  var i, ii, viewState;

  var size = this.getSize();
  var view = this.getView();
  var extent = GVol.extent.createEmpty();
  var previousFrameState = this.frameState_;
  /** @type {?GVolx.FrameState} */
  var frameState = null;
  if (size !== undefined && GVol.size.hasArea(size) && view && view.isDef()) {
    var viewHints = view.getHints(this.frameState_ ? this.frameState_.viewHints : undefined);
    var layerStatesArray = this.getLayerGroup().getLayerStatesArray();
    var layerStates = {};
    for (i = 0, ii = layerStatesArray.length; i < ii; ++i) {
      layerStates[GVol.getUid(layerStatesArray[i].layer)] = layerStatesArray[i];
    }
    viewState = view.getState();
    frameState = /** @type {GVolx.FrameState} */ ({
      animate: false,
      attributions: {},
      coordinateToPixelTransform: this.coordinateToPixelTransform_,
      extent: extent,
      focus: !this.focus_ ? viewState.center : this.focus_,
      index: this.frameIndex_++,
      layerStates: layerStates,
      layerStatesArray: layerStatesArray,
      logos: GVol.obj.assign({}, this.logos_),
      pixelRatio: this.pixelRatio_,
      pixelToCoordinateTransform: this.pixelToCoordinateTransform_,
      postRenderFunctions: [],
      size: size,
      skippedFeatureUids: this.skippedFeatureUids_,
      tileQueue: this.tileQueue_,
      time: time,
      usedTiles: {},
      viewState: viewState,
      viewHints: viewHints,
      wantedTiles: {}
    });
  }

  if (frameState) {
    frameState.extent = GVol.extent.getForViewAndSize(viewState.center,
        viewState.resGVolution, viewState.rotation, frameState.size, extent);
  }

  this.frameState_ = frameState;
  this.renderer_.renderFrame(frameState);

  if (frameState) {
    if (frameState.animate) {
      this.render();
    }
    Array.prototype.push.apply(
        this.postRenderFunctions_, frameState.postRenderFunctions);

    if (previousFrameState) {
      var moveStart = !this.previousExtent_ ||
          (!GVol.extent.isEmpty(this.previousExtent_) &&
          !GVol.extent.equals(frameState.extent, this.previousExtent_));
      if (moveStart) {
        this.dispatchEvent(
            new GVol.MapEvent(GVol.MapEventType.MOVESTART, this, previousFrameState));
        this.previousExtent_ = GVol.extent.createOrUpdateEmpty(this.previousExtent_);
      }
    }

    var idle = this.previousExtent_ &&
        !frameState.viewHints[GVol.ViewHint.ANIMATING] &&
        !frameState.viewHints[GVol.ViewHint.INTERACTING] &&
        !GVol.extent.equals(frameState.extent, this.previousExtent_);

    if (idle) {
      this.dispatchEvent(
          new GVol.MapEvent(GVol.MapEventType.MOVEEND, this, frameState));
      GVol.extent.clone(frameState.extent, this.previousExtent_);
    }
  }

  this.dispatchEvent(
      new GVol.MapEvent(GVol.MapEventType.POSTRENDER, this, frameState));

  setTimeout(this.handlePostRender.bind(this), 0);

};


/**
 * Sets the layergroup of this map.
 * @param {GVol.layer.Group} layerGroup A layer group containing the layers in
 *     this map.
 * @observable
 * @api
 */
GVol.Map.prototype.setLayerGroup = function(layerGroup) {
  this.set(GVol.MapProperty.LAYERGROUP, layerGroup);
};


/**
 * Set the size of this map.
 * @param {GVol.Size|undefined} size The size in pixels of the map in the DOM.
 * @observable
 * @api
 */
GVol.Map.prototype.setSize = function(size) {
  this.set(GVol.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
 */
GVol.Map.prototype.setTarget = function(target) {
  this.set(GVol.MapProperty.TARGET, target);
};


/**
 * Set the view for this map.
 * @param {GVol.View} view The view that contrGVols this map.
 * @observable
 * @api
 */
GVol.Map.prototype.setView = function(view) {
  this.set(GVol.MapProperty.VIEW, view);
};


/**
 * @param {GVol.Feature} feature Feature.
 */
GVol.Map.prototype.skipFeature = function(feature) {
  var featureUid = GVol.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
 */
GVol.Map.prototype.updateSize = function() {
  var targetElement = this.getTargetElement();

  if (!targetElement) {
    this.setSize(undefined);
  } else {
    var computedStyle = getComputedStyle(targetElement);
    this.setSize([
      targetElement.offsetWidth -
          parseFloat(computedStyle['borderLeftWidth']) -
          parseFloat(computedStyle['paddingLeft']) -
          parseFloat(computedStyle['paddingRight']) -
          parseFloat(computedStyle['borderRightWidth']),
      targetElement.offsetHeight -
          parseFloat(computedStyle['borderTopWidth']) -
          parseFloat(computedStyle['paddingTop']) -
          parseFloat(computedStyle['paddingBottom']) -
          parseFloat(computedStyle['borderBottomWidth'])
    ]);
  }
};


/**
 * @param {GVol.Feature} feature Feature.
 */
GVol.Map.prototype.unskipFeature = function(feature) {
  var featureUid = GVol.getUid(feature).toString();
  delete this.skippedFeatureUids_[featureUid];
  this.render();
};


/**
 * @param {GVolx.MapOptions} options Map options.
 * @return {GVol.MapOptionsInternal} Internal map options.
 */
GVol.Map.createOptionsInternal = function(options) {

  /**
   * @type {Element|Document}
   */
  var keyboardEventTarget = null;
  if (options.keyboardEventTarget !== undefined) {
    keyboardEventTarget = typeof options.keyboardEventTarget === 'string' ?
      document.getElementById(options.keyboardEventTarget) :
      options.keyboardEventTarget;
  }

  /**
   * @type {Object.<string, *>}
   */
  var values = {};

  var logos = {};
  if (options.logo === undefined ||
      (typeof options.logo === 'boGVolean' && options.logo)) {
    logos[GVol.OL_LOGO_URL] = GVol.OL_URL;
  } else {
    var logo = options.logo;
    if (typeof logo === 'string') {
      logos[logo] = '';
    } else if (logo instanceof HTMLElement) {
      logos[GVol.getUid(logo).toString()] = logo;
    } else if (logo) {
      GVol.asserts.assert(typeof logo.href == 'string', 44); // `logo.href` should be a string.
      GVol.asserts.assert(typeof logo.src == 'string', 45); // `logo.src` should be a string.
      logos[logo.src] = logo.href;
    }
  }

  var layerGroup = (options.layers instanceof GVol.layer.Group) ?
    options.layers : new GVol.layer.Group({layers: options.layers});
  values[GVol.MapProperty.LAYERGROUP] = layerGroup;

  values[GVol.MapProperty.TARGET] = options.target;

  values[GVol.MapProperty.VIEW] = options.view !== undefined ?
    options.view : new GVol.View();

  /**
   * @type {function(new: GVol.renderer.Map, Element, GVol.Map)}
   */
  var rendererConstructor = GVol.renderer.Map;

  /**
   * @type {Array.<GVol.renderer.Type>}
   */
  var rendererTypes;
  if (options.renderer !== undefined) {
    if (Array.isArray(options.renderer)) {
      rendererTypes = options.renderer;
    } else if (typeof options.renderer === 'string') {
      rendererTypes = [options.renderer];
    } else {
      GVol.asserts.assert(false, 46); // Incorrect format for `renderer` option
    }
    if (rendererTypes.indexOf(/** @type {GVol.renderer.Type} */ ('dom')) >= 0) {
      rendererTypes = rendererTypes.concat(GVol.DEFAULT_RENDERER_TYPES);
    }
  } else {
    rendererTypes = GVol.DEFAULT_RENDERER_TYPES;
  }

  var i, ii;
  for (i = 0, ii = rendererTypes.length; i < ii; ++i) {
    /** @type {GVol.renderer.Type} */
    var rendererType = rendererTypes[i];
    if (GVol.ENABLE_CANVAS && rendererType == GVol.renderer.Type.CANVAS) {
      if (GVol.has.CANVAS) {
        rendererConstructor = GVol.renderer.canvas.Map;
        break;
      }
    } else if (GVol.ENABLE_WEBGL && rendererType == GVol.renderer.Type.WEBGL) {
      if (GVol.has.WEBGL) {
        rendererConstructor = GVol.renderer.webgl.Map;
        break;
      }
    }
  }

  var contrGVols;
  if (options.contrGVols !== undefined) {
    if (Array.isArray(options.contrGVols)) {
      contrGVols = new GVol.CGVollection(options.contrGVols.slice());
    } else {
      GVol.asserts.assert(options.contrGVols instanceof GVol.CGVollection,
          47); // Expected `contrGVols` to be an array or an `GVol.CGVollection`
      contrGVols = options.contrGVols;
    }
  } else {
    contrGVols = GVol.contrGVol.defaults();
  }

  var interactions;
  if (options.interactions !== undefined) {
    if (Array.isArray(options.interactions)) {
      interactions = new GVol.CGVollection(options.interactions.slice());
    } else {
      GVol.asserts.assert(options.interactions instanceof GVol.CGVollection,
          48); // Expected `interactions` to be an array or an `GVol.CGVollection`
      interactions = options.interactions;
    }
  } else {
    interactions = GVol.interaction.defaults();
  }

  var overlays;
  if (options.overlays !== undefined) {
    if (Array.isArray(options.overlays)) {
      overlays = new GVol.CGVollection(options.overlays.slice());
    } else {
      GVol.asserts.assert(options.overlays instanceof GVol.CGVollection,
          49); // Expected `overlays` to be an array or an `GVol.CGVollection`
      overlays = options.overlays;
    }
  } else {
    overlays = new GVol.CGVollection();
  }

  return {
    contrGVols: contrGVols,
    interactions: interactions,
    keyboardEventTarget: keyboardEventTarget,
    logos: logos,
    overlays: overlays,
    rendererConstructor: rendererConstructor,
    values: values
  };

};

goog.provide('GVol.OverlayPositioning');

/**
 * Overlay position: `'bottom-left'`, `'bottom-center'`,  `'bottom-right'`,
 * `'center-left'`, `'center-center'`, `'center-right'`, `'top-left'`,
 * `'top-center'`, `'top-right'`
 * @enum {string}
 */
GVol.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'
};

goog.provide('GVol.Overlay');

goog.require('GVol');
goog.require('GVol.MapEventType');
goog.require('GVol.Object');
goog.require('GVol.OverlayPositioning');
goog.require('GVol.css');
goog.require('GVol.dom');
goog.require('GVol.events');
goog.require('GVol.extent');


/**
 * @classdesc
 * An element to be displayed over the map and attached to a single map
 * location.  Like {@link GVol.contrGVol.ContrGVol}, Overlays are visible widgets.
 * Unlike ContrGVols, 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 ContrGVol.
 *
 * Example:
 *
 *     var popup = new GVol.Overlay({
 *       element: document.getElementById('popup')
 *     });
 *     popup.setPosition(coordinate);
 *     map.addOverlay(popup);
 *
 * @constructor
 * @extends {GVol.Object}
 * @param {GVolx.OverlayOptions} options Overlay options.
 * @api
 */
GVol.Overlay = function(options) {

  GVol.Object.call(this);

  /**
   * @private
   * @type {number|string|undefined}
   */
  this.id_ = options.id;

  /**
   * @private
   * @type {boGVolean}
   */
  this.insertFirst_ = options.insertFirst !== undefined ?
    options.insertFirst : true;

  /**
   * @private
   * @type {boGVolean}
   */
  this.stopEvent_ = options.stopEvent !== undefined ? options.stopEvent : true;

  /**
   * @private
   * @type {Element}
   */
  this.element_ = document.createElement('DIV');
  this.element_.className = 'GVol-overlay-container ' + GVol.css.CLASS_SELECTABLE;
  this.element_.style.position = 'absGVolute';

  /**
   * @protected
   * @type {boGVolean}
   */
  this.autoPan = options.autoPan !== undefined ? options.autoPan : false;

  /**
   * @private
   * @type {GVolx.OverlayPanOptions}
   */
  this.autoPanAnimation_ = options.autoPanAnimation ||
    /** @type {GVolx.OverlayPanOptions} */ ({});

  /**
   * @private
   * @type {number}
   */
  this.autoPanMargin_ = options.autoPanMargin !== undefined ?
    options.autoPanMargin : 20;

  /**
   * @private
   * @type {{bottom_: string,
   *         left_: string,
   *         right_: string,
   *         top_: string,
   *         visible: boGVolean}}
   */
  this.rendered_ = {
    bottom_: '',
    left_: '',
    right_: '',
    top_: '',
    visible: true
  };

  /**
   * @private
   * @type {?GVol.EventsKey}
   */
  this.mapPostrenderListenerKey_ = null;

  GVol.events.listen(
      this, GVol.Object.getChangeEventType(GVol.Overlay.Property_.ELEMENT),
      this.handleElementChanged, this);

  GVol.events.listen(
      this, GVol.Object.getChangeEventType(GVol.Overlay.Property_.MAP),
      this.handleMapChanged, this);

  GVol.events.listen(
      this, GVol.Object.getChangeEventType(GVol.Overlay.Property_.OFFSET),
      this.handleOffsetChanged, this);

  GVol.events.listen(
      this, GVol.Object.getChangeEventType(GVol.Overlay.Property_.POSITION),
      this.handlePositionChanged, this);

  GVol.events.listen(
      this, GVol.Object.getChangeEventType(GVol.Overlay.Property_.POSITIONING),
      this.handlePositioningChanged, this);

  if (options.element !== undefined) {
    this.setElement(options.element);
  }

  this.setOffset(options.offset !== undefined ? options.offset : [0, 0]);

  this.setPositioning(options.positioning !== undefined ?
    /** @type {GVol.OverlayPositioning} */ (options.positioning) :
    GVol.OverlayPositioning.TOP_LEFT);

  if (options.position !== undefined) {
    this.setPosition(options.position);
  }

};
GVol.inherits(GVol.Overlay, GVol.Object);


/**
 * Get the DOM element of this overlay.
 * @return {Element|undefined} The Element containing the overlay.
 * @observable
 * @api
 */
GVol.Overlay.prototype.getElement = function() {
  return /** @type {Element|undefined} */ (
    this.get(GVol.Overlay.Property_.ELEMENT));
};


/**
 * Get the overlay identifier which is set on constructor.
 * @return {number|string|undefined} Id.
 * @api
 */
GVol.Overlay.prototype.getId = function() {
  return this.id_;
};


/**
 * Get the map associated with this overlay.
 * @return {GVol.Map|undefined} The map that the overlay is part of.
 * @observable
 * @api
 */
GVol.Overlay.prototype.getMap = function() {
  return /** @type {GVol.Map|undefined} */ (
    this.get(GVol.Overlay.Property_.MAP));
};


/**
 * Get the offset of this overlay.
 * @return {Array.<number>} The offset.
 * @observable
 * @api
 */
GVol.Overlay.prototype.getOffset = function() {
  return /** @type {Array.<number>} */ (
    this.get(GVol.Overlay.Property_.OFFSET));
};


/**
 * Get the current position of this overlay.
 * @return {GVol.Coordinate|undefined} The spatial point that the overlay is
 *     anchored at.
 * @observable
 * @api
 */
GVol.Overlay.prototype.getPosition = function() {
  return /** @type {GVol.Coordinate|undefined} */ (
    this.get(GVol.Overlay.Property_.POSITION));
};


/**
 * Get the current positioning of this overlay.
 * @return {GVol.OverlayPositioning} How the overlay is positioned
 *     relative to its point on the map.
 * @observable
 * @api
 */
GVol.Overlay.prototype.getPositioning = function() {
  return /** @type {GVol.OverlayPositioning} */ (
    this.get(GVol.Overlay.Property_.POSITIONING));
};


/**
 * @protected
 */
GVol.Overlay.prototype.handleElementChanged = function() {
  GVol.dom.removeChildren(this.element_);
  var element = this.getElement();
  if (element) {
    this.element_.appendChild(element);
  }
};


/**
 * @protected
 */
GVol.Overlay.prototype.handleMapChanged = function() {
  if (this.mapPostrenderListenerKey_) {
    GVol.dom.removeNode(this.element_);
    GVol.events.unlistenByKey(this.mapPostrenderListenerKey_);
    this.mapPostrenderListenerKey_ = null;
  }
  var map = this.getMap();
  if (map) {
    this.mapPostrenderListenerKey_ = GVol.events.listen(map,
        GVol.MapEventType.POSTRENDER, this.render, this);
    this.updatePixelPosition();
    var container = this.stopEvent_ ?
      map.getOverlayContainerStopEvent() : map.getOverlayContainer();
    if (this.insertFirst_) {
      container.insertBefore(this.element_, container.childNodes[0] || null);
    } else {
      container.appendChild(this.element_);
    }
  }
};


/**
 * @protected
 */
GVol.Overlay.prototype.render = function() {
  this.updatePixelPosition();
};


/**
 * @protected
 */
GVol.Overlay.prototype.handleOffsetChanged = function() {
  this.updatePixelPosition();
};


/**
 * @protected
 */
GVol.Overlay.prototype.handlePositionChanged = function() {
  this.updatePixelPosition();
  if (this.get(GVol.Overlay.Property_.POSITION) && this.autoPan) {
    this.panIntoView_();
  }
};


/**
 * @protected
 */
GVol.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
 */
GVol.Overlay.prototype.setElement = function(element) {
  this.set(GVol.Overlay.Property_.ELEMENT, element);
};


/**
 * Set the map to be associated with this overlay.
 * @param {GVol.Map|undefined} map The map that the overlay is part of.
 * @observable
 * @api
 */
GVol.Overlay.prototype.setMap = function(map) {
  this.set(GVol.Overlay.Property_.MAP, map);
};


/**
 * Set the offset for this overlay.
 * @param {Array.<number>} offset Offset.
 * @observable
 * @api
 */
GVol.Overlay.prototype.setOffset = function(offset) {
  this.set(GVol.Overlay.Property_.OFFSET, offset);
};


/**
 * Set the position for this overlay. If the position is `undefined` the
 * overlay is hidden.
 * @param {GVol.Coordinate|undefined} position The spatial point that the overlay
 *     is anchored at.
 * @observable
 * @api
 */
GVol.Overlay.prototype.setPosition = function(position) {
  this.set(GVol.Overlay.Property_.POSITION, position);
};


/**
 * Pan the map so that the overlay is entirely visible in the current viewport
 * (if necessary).
 * @private
 */
GVol.Overlay.prototype.panIntoView_ = function() {
  var map = this.getMap();

  if (!map || !map.getTargetElement()) {
    return;
  }

  var mapRect = this.getRect_(map.getTargetElement(), map.getSize());
  var element = /** @type {!Element} */ (this.getElement());
  var overlayRect = this.getRect_(element,
      [GVol.dom.outerWidth(element), GVol.dom.outerHeight(element)]);

  var margin = this.autoPanMargin_;
  if (!GVol.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 = /** @type {GVol.Coordinate} */ (map.getView().getCenter());
      var centerPx = map.getPixelFromCoordinate(center);
      var newCenterPx = [
        centerPx[0] + delta[0],
        centerPx[1] + delta[1]
      ];

      map.getView().animate({
        center: map.getCoordinateFromPixel(newCenterPx),
        duration: this.autoPanAnimation_.duration,
        easing: this.autoPanAnimation_.easing
      });
    }
  }
};


/**
 * Get the extent of an element relative to the document
 * @param {Element|undefined} element The element.
 * @param {GVol.Size|undefined} size The size of the element.
 * @return {GVol.Extent} The extent.
 * @private
 */
GVol.Overlay.prototype.getRect_ = function(element, size) {
  var box = element.getBoundingClientRect();
  var offsetX = box.left + window.pageXOffset;
  var offsetY = box.top + window.pageYOffset;
  return [
    offsetX,
    offsetY,
    offsetX + size[0],
    offsetY + size[1]
  ];
};


/**
 * Set the positioning for this overlay.
 * @param {GVol.OverlayPositioning} positioning how the overlay is
 *     positioned relative to its point on the map.
 * @observable
 * @api
 */
GVol.Overlay.prototype.setPositioning = function(positioning) {
  this.set(GVol.Overlay.Property_.POSITIONING, positioning);
};


/**
 * Modify the visibility of the element.
 * @param {boGVolean} visible Element visibility.
 * @protected
 */
GVol.Overlay.prototype.setVisible = function(visible) {
  if (this.rendered_.visible !== visible) {
    this.element_.style.display = visible ? '' : 'none';
    this.rendered_.visible = visible;
  }
};


/**
 * Update pixel position.
 * @protected
 */
GVol.Overlay.prototype.updatePixelPosition = function() {
  var map = this.getMap();
  var position = this.getPosition();
  if (!map || !map.isRendered() || !position) {
    this.setVisible(false);
    return;
  }

  var pixel = map.getPixelFromCoordinate(position);
  var mapSize = map.getSize();
  this.updateRenderedPosition(pixel, mapSize);
};


/**
 * @param {GVol.Pixel} pixel The pixel location.
 * @param {GVol.Size|undefined} mapSize The map size.
 * @protected
 */
GVol.Overlay.prototype.updateRenderedPosition = function(pixel, mapSize) {
  var style = this.element_.style;
  var offset = this.getOffset();

  var positioning = this.getPositioning();

  this.setVisible(true);

  var offsetX = offset[0];
  var offsetY = offset[1];
  if (positioning == GVol.OverlayPositioning.BOTTOM_RIGHT ||
      positioning == GVol.OverlayPositioning.CENTER_RIGHT ||
      positioning == GVol.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 == GVol.OverlayPositioning.BOTTOM_CENTER ||
        positioning == GVol.OverlayPositioning.CENTER_CENTER ||
        positioning == GVol.OverlayPositioning.TOP_CENTER) {
      offsetX -= this.element_.offsetWidth / 2;
    }
    var left = Math.round(pixel[0] + offsetX) + 'px';
    if (this.rendered_.left_ != left) {
      this.rendered_.left_ = style.left = left;
    }
  }
  if (positioning == GVol.OverlayPositioning.BOTTOM_LEFT ||
      positioning == GVol.OverlayPositioning.BOTTOM_CENTER ||
      positioning == GVol.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 == GVol.OverlayPositioning.CENTER_LEFT ||
        positioning == GVol.OverlayPositioning.CENTER_CENTER ||
        positioning == GVol.OverlayPositioning.CENTER_RIGHT) {
      offsetY -= this.element_.offsetHeight / 2;
    }
    var top = Math.round(pixel[1] + offsetY) + 'px';
    if (this.rendered_.top_ != top) {
      this.rendered_.top_ = style.top = top;
    }
  }
};


/**
 * @enum {string}
 * @private
 */
GVol.Overlay.Property_ = {
  ELEMENT: 'element',
  MAP: 'map',
  OFFSET: 'offset',
  POSITION: 'position',
  POSITIONING: 'positioning'
};

goog.provide('GVol.contrGVol.OverviewMap');

goog.require('GVol');
goog.require('GVol.CGVollection');
goog.require('GVol.Map');
goog.require('GVol.MapEventType');
goog.require('GVol.MapProperty');
goog.require('GVol.Object');
goog.require('GVol.ObjectEventType');
goog.require('GVol.Overlay');
goog.require('GVol.OverlayPositioning');
goog.require('GVol.ViewProperty');
goog.require('GVol.contrGVol.ContrGVol');
goog.require('GVol.coordinate');
goog.require('GVol.css');
goog.require('GVol.dom');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.extent');


/**
 * Create a new contrGVol with a map acting as an overview map for an other
 * defined map.
 * @constructor
 * @extends {GVol.contrGVol.ContrGVol}
 * @param {GVolx.contrGVol.OverviewMapOptions=} opt_options OverviewMap options.
 * @api
 */
GVol.contrGVol.OverviewMap = function(opt_options) {

  var options = opt_options ? opt_options : {};

  /**
   * @type {boGVolean}
   * @private
   */
  this.cGVollapsed_ = options.cGVollapsed !== undefined ? options.cGVollapsed : true;

  /**
   * @private
   * @type {boGVolean}
   */
  this.cGVollapsible_ = options.cGVollapsible !== undefined ?
    options.cGVollapsible : true;

  if (!this.cGVollapsible_) {
    this.cGVollapsed_ = false;
  }

  var className = options.className !== undefined ? options.className : 'GVol-overviewmap';

  var tipLabel = options.tipLabel !== undefined ? options.tipLabel : 'Overview map';

  var cGVollapseLabel = options.cGVollapseLabel !== undefined ? options.cGVollapseLabel : '\u00AB';

  if (typeof cGVollapseLabel === 'string') {
    /**
     * @private
     * @type {Node}
     */
    this.cGVollapseLabel_ = document.createElement('span');
    this.cGVollapseLabel_.textContent = cGVollapseLabel;
  } else {
    this.cGVollapseLabel_ = cGVollapseLabel;
  }

  var label = options.label !== undefined ? options.label : '\u00BB';


  if (typeof label === 'string') {
    /**
     * @private
     * @type {Node}
     */
    this.label_ = document.createElement('span');
    this.label_.textContent = label;
  } else {
    this.label_ = label;
  }

  var activeLabel = (this.cGVollapsible_ && !this.cGVollapsed_) ?
    this.cGVollapseLabel_ : this.label_;
  var button = document.createElement('button');
  button.setAttribute('type', 'button');
  button.title = tipLabel;
  button.appendChild(activeLabel);

  GVol.events.listen(button, GVol.events.EventType.CLICK,
      this.handleClick_, this);

  /**
   * @type {Element}
   * @private
   */
  this.ovmapDiv_ = document.createElement('DIV');
  this.ovmapDiv_.className = 'GVol-overviewmap-map';

  /**
   * @type {GVol.Map}
   * @private
   */
  this.ovmap_ = new GVol.Map({
    contrGVols: new GVol.CGVollection(),
    interactions: new GVol.CGVollection(),
    view: options.view
  });
  var ovmap = this.ovmap_;

  if (options.layers) {
    options.layers.forEach(
        /**
       * @param {GVol.layer.Layer} layer Layer.
       */
        function(layer) {
          ovmap.addLayer(layer);
        }, this);
  }

  var box = document.createElement('DIV');
  box.className = 'GVol-overviewmap-box';
  box.style.boxSizing = 'border-box';

  /**
   * @type {GVol.Overlay}
   * @private
   */
  this.boxOverlay_ = new GVol.Overlay({
    position: [0, 0],
    positioning: GVol.OverlayPositioning.BOTTOM_LEFT,
    element: box
  });
  this.ovmap_.addOverlay(this.boxOverlay_);

  var cssClasses = className + ' ' + GVol.css.CLASS_UNSELECTABLE + ' ' +
      GVol.css.CLASS_CONTROL +
      (this.cGVollapsed_ && this.cGVollapsible_ ? ' GVol-cGVollapsed' : '') +
      (this.cGVollapsible_ ? '' : ' GVol-uncGVollapsible');
  var element = document.createElement('div');
  element.className = cssClasses;
  element.appendChild(this.ovmapDiv_);
  element.appendChild(button);

  var render = options.render ? options.render : GVol.contrGVol.OverviewMap.render;

  GVol.contrGVol.ContrGVol.call(this, {
    element: element,
    render: render,
    target: options.target
  });

  /* Interactive map */

  var scope = this;

  var overlay = this.boxOverlay_;
  var overlayBox = this.boxOverlay_.getElement();

  /* Functions definition */

  var computeDesiredMousePosition = function(mousePosition) {
    return {
      clientX: mousePosition.clientX - (overlayBox.offsetWidth / 2),
      clientY: mousePosition.clientY + (overlayBox.offsetHeight / 2)
    };
  };

  var move = function(event) {
    var coordinates = ovmap.getEventCoordinate(computeDesiredMousePosition(event));

    overlay.setPosition(coordinates);
  };

  var endMoving = function(event) {
    var coordinates = ovmap.getEventCoordinate(event);

    scope.getMap().getView().setCenter(coordinates);

    window.removeEventListener('mousemove', move);
    window.removeEventListener('mouseup', endMoving);
  };

  /* Binding */

  overlayBox.addEventListener('mousedown', function() {
    window.addEventListener('mousemove', move);
    window.addEventListener('mouseup', endMoving);
  });
};
GVol.inherits(GVol.contrGVol.OverviewMap, GVol.contrGVol.ContrGVol);


/**
 * @inheritDoc
 * @api
 */
GVol.contrGVol.OverviewMap.prototype.setMap = function(map) {
  var GVoldMap = this.getMap();
  if (map === GVoldMap) {
    return;
  }
  if (GVoldMap) {
    var GVoldView = GVoldMap.getView();
    if (GVoldView) {
      this.unbindView_(GVoldView);
    }
    this.ovmap_.setTarget(null);
  }
  GVol.contrGVol.ContrGVol.prototype.setMap.call(this, map);

  if (map) {
    this.ovmap_.setTarget(this.ovmapDiv_);
    this.listenerKeys.push(GVol.events.listen(
        map, GVol.ObjectEventType.PROPERTYCHANGE,
        this.handleMapPropertyChange_, 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 {GVol.Object.Event} event The propertychange event.
 * @private
 */
GVol.contrGVol.OverviewMap.prototype.handleMapPropertyChange_ = function(event) {
  if (event.key === GVol.MapProperty.VIEW) {
    var GVoldView = /** @type {GVol.View} */ (event.GVoldValue);
    if (GVoldView) {
      this.unbindView_(GVoldView);
    }
    var newView = this.getMap().getView();
    this.bindView_(newView);
  }
};


/**
 * Register listeners for view property changes.
 * @param {GVol.View} view The view.
 * @private
 */
GVol.contrGVol.OverviewMap.prototype.bindView_ = function(view) {
  GVol.events.listen(view,
      GVol.Object.getChangeEventType(GVol.ViewProperty.ROTATION),
      this.handleRotationChanged_, this);
};


/**
 * Unregister listeners for view property changes.
 * @param {GVol.View} view The view.
 * @private
 */
GVol.contrGVol.OverviewMap.prototype.unbindView_ = function(view) {
  GVol.events.unlisten(view,
      GVol.Object.getChangeEventType(GVol.ViewProperty.ROTATION),
      this.handleRotationChanged_, this);
};


/**
 * Handle rotation changes to the main map.
 * TODO: This should rotate the extent rectrangle instead of the
 * overview map's view.
 * @private
 */
GVol.contrGVol.OverviewMap.prototype.handleRotationChanged_ = function() {
  this.ovmap_.getView().setRotation(this.getMap().getView().getRotation());
};


/**
 * Update the overview map element.
 * @param {GVol.MapEvent} mapEvent Map event.
 * @this {GVol.contrGVol.OverviewMap}
 * @api
 */
GVol.contrGVol.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
 */
GVol.contrGVol.OverviewMap.prototype.validateExtent_ = function() {
  var map = this.getMap();
  var ovmap = this.ovmap_;

  if (!map.isRendered() || !ovmap.isRendered()) {
    return;
  }

  var mapSize = /** @type {GVol.Size} */ (map.getSize());

  var view = map.getView();
  var extent = view.calculateExtent(mapSize);

  var ovmapSize = /** @type {GVol.Size} */ (ovmap.getSize());

  var ovview = ovmap.getView();
  var ovextent = ovview.calculateExtent(ovmapSize);

  var topLeftPixel =
      ovmap.getPixelFromCoordinate(GVol.extent.getTopLeft(extent));
  var bottomRightPixel =
      ovmap.getPixelFromCoordinate(GVol.extent.getBottomRight(extent));

  var boxWidth = Math.abs(topLeftPixel[0] - bottomRightPixel[0]);
  var boxHeight = Math.abs(topLeftPixel[1] - bottomRightPixel[1]);

  var ovmapWidth = ovmapSize[0];
  var ovmapHeight = ovmapSize[1];

  if (boxWidth < ovmapWidth * GVol.OVERVIEWMAP_MIN_RATIO ||
      boxHeight < ovmapHeight * GVol.OVERVIEWMAP_MIN_RATIO ||
      boxWidth > ovmapWidth * GVol.OVERVIEWMAP_MAX_RATIO ||
      boxHeight > ovmapHeight * GVol.OVERVIEWMAP_MAX_RATIO) {
    this.resetExtent_();
  } else if (!GVol.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
 */
GVol.contrGVol.OverviewMap.prototype.resetExtent_ = function() {
  if (GVol.OVERVIEWMAP_MAX_RATIO === 0 || GVol.OVERVIEWMAP_MIN_RATIO === 0) {
    return;
  }

  var map = this.getMap();
  var ovmap = this.ovmap_;

  var mapSize = /** @type {GVol.Size} */ (map.getSize());

  var view = map.getView();
  var extent = view.calculateExtent(mapSize);

  var ovview = ovmap.getView();

  // get how many times the current map overview could hGVold 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(
      GVol.OVERVIEWMAP_MAX_RATIO / GVol.OVERVIEWMAP_MIN_RATIO) / Math.LN2;
  var ratio = 1 / (Math.pow(2, steps / 2) * GVol.OVERVIEWMAP_MIN_RATIO);
  GVol.extent.scaleFromCenter(extent, ratio);
  ovview.fit(extent);
};


/**
 * Set the center of the overview map to the map center without changing its
 * resGVolution.
 * @private
 */
GVol.contrGVol.OverviewMap.prototype.recenter_ = function() {
  var map = this.getMap();
  var ovmap = this.ovmap_;

  var view = map.getView();

  var ovview = ovmap.getView();

  ovview.setCenter(view.getCenter());
};


/**
 * Update the box using the main map extent
 * @private
 */
GVol.contrGVol.OverviewMap.prototype.updateBox_ = function() {
  var map = this.getMap();
  var ovmap = this.ovmap_;

  if (!map.isRendered() || !ovmap.isRendered()) {
    return;
  }

  var mapSize = /** @type {GVol.Size} */ (map.getSize());

  var view = map.getView();

  var ovview = ovmap.getView();

  var rotation = view.getRotation();

  var overlay = this.boxOverlay_;
  var box = this.boxOverlay_.getElement();
  var extent = view.calculateExtent(mapSize);
  var ovresGVolution = ovview.getResGVolution();
  var bottomLeft = GVol.extent.getBottomLeft(extent);
  var topRight = GVol.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 resGVolution
  if (box) {
    box.style.width = Math.abs((bottomLeft[0] - topRight[0]) / ovresGVolution) + 'px';
    box.style.height = Math.abs((topRight[1] - bottomLeft[1]) / ovresGVolution) + 'px';
  }
};


/**
 * @param {number} rotation Target rotation.
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @return {GVol.Coordinate|undefined} Coordinate for rotation and center anchor.
 * @private
 */
GVol.contrGVol.OverviewMap.prototype.calculateCoordinateRotate_ = function(
    rotation, coordinate) {
  var coordinateRotate;

  var map = this.getMap();
  var view = map.getView();

  var currentCenter = view.getCenter();

  if (currentCenter) {
    coordinateRotate = [
      coordinate[0] - currentCenter[0],
      coordinate[1] - currentCenter[1]
    ];
    GVol.coordinate.rotate(coordinateRotate, rotation);
    GVol.coordinate.add(coordinateRotate, currentCenter);
  }
  return coordinateRotate;
};


/**
 * @param {Event} event The event to handle
 * @private
 */
GVol.contrGVol.OverviewMap.prototype.handleClick_ = function(event) {
  event.preventDefault();
  this.handleToggle_();
};


/**
 * @private
 */
GVol.contrGVol.OverviewMap.prototype.handleToggle_ = function() {
  this.element.classList.toggle('GVol-cGVollapsed');
  if (this.cGVollapsed_) {
    GVol.dom.replaceNode(this.cGVollapseLabel_, this.label_);
  } else {
    GVol.dom.replaceNode(this.label_, this.cGVollapseLabel_);
  }
  this.cGVollapsed_ = !this.cGVollapsed_;

  // manage overview map if it had not been rendered before and contrGVol
  // is expanded
  var ovmap = this.ovmap_;
  if (!this.cGVollapsed_ && !ovmap.isRendered()) {
    ovmap.updateSize();
    this.resetExtent_();
    GVol.events.listenOnce(ovmap, GVol.MapEventType.POSTRENDER,
        function(event) {
          this.updateBox_();
        },
        this);
  }
};


/**
 * Return `true` if the overview map is cGVollapsible, `false` otherwise.
 * @return {boGVolean} True if the widget is cGVollapsible.
 * @api
 */
GVol.contrGVol.OverviewMap.prototype.getCGVollapsible = function() {
  return this.cGVollapsible_;
};


/**
 * Set whether the overview map should be cGVollapsible.
 * @param {boGVolean} cGVollapsible True if the widget is cGVollapsible.
 * @api
 */
GVol.contrGVol.OverviewMap.prototype.setCGVollapsible = function(cGVollapsible) {
  if (this.cGVollapsible_ === cGVollapsible) {
    return;
  }
  this.cGVollapsible_ = cGVollapsible;
  this.element.classList.toggle('GVol-uncGVollapsible');
  if (!cGVollapsible && this.cGVollapsed_) {
    this.handleToggle_();
  }
};


/**
 * CGVollapse or expand the overview map according to the passed parameter. Will
 * not do anything if the overview map isn't cGVollapsible or if the current
 * cGVollapsed state is already the one requested.
 * @param {boGVolean} cGVollapsed True if the widget is cGVollapsed.
 * @api
 */
GVol.contrGVol.OverviewMap.prototype.setCGVollapsed = function(cGVollapsed) {
  if (!this.cGVollapsible_ || this.cGVollapsed_ === cGVollapsed) {
    return;
  }
  this.handleToggle_();
};


/**
 * Determine if the overview map is cGVollapsed.
 * @return {boGVolean} The overview map is cGVollapsed.
 * @api
 */
GVol.contrGVol.OverviewMap.prototype.getCGVollapsed = function() {
  return this.cGVollapsed_;
};


/**
 * Return the overview map.
 * @return {GVol.Map} Overview map.
 * @api
 */
GVol.contrGVol.OverviewMap.prototype.getOverviewMap = function() {
  return this.ovmap_;
};

goog.provide('GVol.contrGVol.ScaleLineUnits');

/**
 * Units for the scale line. Supported values are `'degrees'`, `'imperial'`,
 * `'nautical'`, `'metric'`, `'us'`.
 * @enum {string}
 */
GVol.contrGVol.ScaleLineUnits = {
  DEGREES: 'degrees',
  IMPERIAL: 'imperial',
  NAUTICAL: 'nautical',
  METRIC: 'metric',
  US: 'us'
};

goog.provide('GVol.contrGVol.ScaleLine');

goog.require('GVol');
goog.require('GVol.Object');
goog.require('GVol.asserts');
goog.require('GVol.contrGVol.ContrGVol');
goog.require('GVol.contrGVol.ScaleLineUnits');
goog.require('GVol.css');
goog.require('GVol.events');
goog.require('GVol.proj');
goog.require('GVol.proj.Units');


/**
 * @classdesc
 * A contrGVol displaying rough y-axis distances, calculated for the center of the
 * viewport. For conformal projections (e.g. EPSG:3857, the default view
 * projection in OpenLayers), the scale is valid for all directions.
 * No scale line will be shown when the y-axis distance of a pixel at the
 * viewport center cannot be calculated in the view projection.
 * 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 `.GVol-scale-line`.
 *
 * @constructor
 * @extends {GVol.contrGVol.ContrGVol}
 * @param {GVolx.contrGVol.ScaleLineOptions=} opt_options Scale line options.
 * @api
 */
GVol.contrGVol.ScaleLine = function(opt_options) {

  var options = opt_options ? opt_options : {};

  var className = options.className !== undefined ? options.className : 'GVol-scale-line';

  /**
   * @private
   * @type {Element}
   */
  this.innerElement_ = document.createElement('DIV');
  this.innerElement_.className = className + '-inner';

  /**
   * @private
   * @type {Element}
   */
  this.element_ = document.createElement('DIV');
  this.element_.className = className + ' ' + GVol.css.CLASS_UNSELECTABLE;
  this.element_.appendChild(this.innerElement_);

  /**
   * @private
   * @type {?GVolx.ViewState}
   */
  this.viewState_ = null;

  /**
   * @private
   * @type {number}
   */
  this.minWidth_ = options.minWidth !== undefined ? options.minWidth : 64;

  /**
   * @private
   * @type {boGVolean}
   */
  this.renderedVisible_ = false;

  /**
   * @private
   * @type {number|undefined}
   */
  this.renderedWidth_ = undefined;

  /**
   * @private
   * @type {string}
   */
  this.renderedHTML_ = '';

  var render = options.render ? options.render : GVol.contrGVol.ScaleLine.render;

  GVol.contrGVol.ContrGVol.call(this, {
    element: this.element_,
    render: render,
    target: options.target
  });

  GVol.events.listen(
      this, GVol.Object.getChangeEventType(GVol.contrGVol.ScaleLine.Property_.UNITS),
      this.handleUnitsChanged_, this);

  this.setUnits(/** @type {GVol.contrGVol.ScaleLineUnits} */ (options.units) ||
      GVol.contrGVol.ScaleLineUnits.METRIC);

};
GVol.inherits(GVol.contrGVol.ScaleLine, GVol.contrGVol.ContrGVol);


/**
 * @const
 * @type {Array.<number>}
 */
GVol.contrGVol.ScaleLine.LEADING_DIGITS = [1, 2, 5];


/**
 * Return the units to use in the scale line.
 * @return {GVol.contrGVol.ScaleLineUnits|undefined} The units to use in the scale
 *     line.
 * @observable
 * @api
 */
GVol.contrGVol.ScaleLine.prototype.getUnits = function() {
  return /** @type {GVol.contrGVol.ScaleLineUnits|undefined} */ (
    this.get(GVol.contrGVol.ScaleLine.Property_.UNITS));
};


/**
 * Update the scale line element.
 * @param {GVol.MapEvent} mapEvent Map event.
 * @this {GVol.contrGVol.ScaleLine}
 * @api
 */
GVol.contrGVol.ScaleLine.render = function(mapEvent) {
  var frameState = mapEvent.frameState;
  if (!frameState) {
    this.viewState_ = null;
  } else {
    this.viewState_ = frameState.viewState;
  }
  this.updateElement_();
};


/**
 * @private
 */
GVol.contrGVol.ScaleLine.prototype.handleUnitsChanged_ = function() {
  this.updateElement_();
};


/**
 * Set the units to use in the scale line.
 * @param {GVol.contrGVol.ScaleLineUnits} units The units to use in the scale line.
 * @observable
 * @api
 */
GVol.contrGVol.ScaleLine.prototype.setUnits = function(units) {
  this.set(GVol.contrGVol.ScaleLine.Property_.UNITS, units);
};


/**
 * @private
 */
GVol.contrGVol.ScaleLine.prototype.updateElement_ = function() {
  var viewState = this.viewState_;

  if (!viewState) {
    if (this.renderedVisible_) {
      this.element_.style.display = 'none';
      this.renderedVisible_ = false;
    }
    return;
  }

  var center = viewState.center;
  var projection = viewState.projection;
  var units = this.getUnits();
  var pointResGVolutionUnits = units == GVol.contrGVol.ScaleLineUnits.DEGREES ?
    GVol.proj.Units.DEGREES :
    GVol.proj.Units.METERS;
  var pointResGVolution =
      GVol.proj.getPointResGVolution(projection, viewState.resGVolution, center, pointResGVolutionUnits);

  var nominalCount = this.minWidth_ * pointResGVolution;
  var suffix = '';
  if (units == GVol.contrGVol.ScaleLineUnits.DEGREES) {
    var metersPerDegree = GVol.proj.METERS_PER_UNIT[GVol.proj.Units.DEGREES];
    if (projection.getUnits() == GVol.proj.Units.DEGREES) {
      nominalCount *= metersPerDegree;
    } else {
      pointResGVolution /= metersPerDegree;
    }
    if (nominalCount < metersPerDegree / 60) {
      suffix = '\u2033'; // seconds
      pointResGVolution *= 3600;
    } else if (nominalCount < metersPerDegree) {
      suffix = '\u2032'; // minutes
      pointResGVolution *= 60;
    } else {
      suffix = '\u00b0'; // degrees
    }
  } else if (units == GVol.contrGVol.ScaleLineUnits.IMPERIAL) {
    if (nominalCount < 0.9144) {
      suffix = 'in';
      pointResGVolution /= 0.0254;
    } else if (nominalCount < 1609.344) {
      suffix = 'ft';
      pointResGVolution /= 0.3048;
    } else {
      suffix = 'mi';
      pointResGVolution /= 1609.344;
    }
  } else if (units == GVol.contrGVol.ScaleLineUnits.NAUTICAL) {
    pointResGVolution /= 1852;
    suffix = 'nm';
  } else if (units == GVol.contrGVol.ScaleLineUnits.METRIC) {
    if (nominalCount < 0.001) {
      suffix = 'μm';
      pointResGVolution *= 1000000;
    } else if (nominalCount < 1) {
      suffix = 'mm';
      pointResGVolution *= 1000;
    } else if (nominalCount < 1000) {
      suffix = 'm';
    } else {
      suffix = 'km';
      pointResGVolution /= 1000;
    }
  } else if (units == GVol.contrGVol.ScaleLineUnits.US) {
    if (nominalCount < 0.9144) {
      suffix = 'in';
      pointResGVolution *= 39.37;
    } else if (nominalCount < 1609.344) {
      suffix = 'ft';
      pointResGVolution /= 0.30480061;
    } else {
      suffix = 'mi';
      pointResGVolution /= 1609.3472;
    }
  } else {
    GVol.asserts.assert(false, 33); // Invalid units
  }

  var i = 3 * Math.floor(
      Math.log(this.minWidth_ * pointResGVolution) / Math.log(10));
  var count, width;
  while (true) {
    count = GVol.contrGVol.ScaleLine.LEADING_DIGITS[((i % 3) + 3) % 3] *
        Math.pow(10, Math.floor(i / 3));
    width = Math.round(count / pointResGVolution);
    if (isNaN(width)) {
      this.element_.style.display = 'none';
      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_) {
    this.element_.style.display = '';
    this.renderedVisible_ = true;
  }

};


/**
 * @enum {string}
 * @private
 */
GVol.contrGVol.ScaleLine.Property_ = {
  UNITS: 'units'
};

// FIXME should possibly show toGVoltip when dragging?

goog.provide('GVol.contrGVol.ZoomSlider');

goog.require('GVol');
goog.require('GVol.ViewHint');
goog.require('GVol.contrGVol.ContrGVol');
goog.require('GVol.css');
goog.require('GVol.easing');
goog.require('GVol.events');
goog.require('GVol.events.Event');
goog.require('GVol.events.EventType');
goog.require('GVol.math');
goog.require('GVol.pointer.EventType');
goog.require('GVol.pointer.PointerEventHandler');


/**
 * @classdesc
 * A slider type of contrGVol for zooming.
 *
 * Example:
 *
 *     map.addContrGVol(new GVol.contrGVol.ZoomSlider());
 *
 * @constructor
 * @extends {GVol.contrGVol.ContrGVol}
 * @param {GVolx.contrGVol.ZoomSliderOptions=} opt_options Zoom slider options.
 * @api
 */
GVol.contrGVol.ZoomSlider = function(opt_options) {

  var options = opt_options ? opt_options : {};

  /**
   * Will hGVold the current resGVolution of the view.
   *
   * @type {number|undefined}
   * @private
   */
  this.currentResGVolution_ = undefined;

  /**
   * The direction of the slider. Will be determined from actual display of the
   * container and defaults to GVol.contrGVol.ZoomSlider.Direction_.VERTICAL.
   *
   * @type {GVol.contrGVol.ZoomSlider.Direction_}
   * @private
   */
  this.direction_ = GVol.contrGVol.ZoomSlider.Direction_.VERTICAL;

  /**
   * @type {boGVolean}
   * @private
   */
  this.dragging_;

  /**
   * @type {number}
   * @private
   */
  this.heightLimit_ = 0;

  /**
   * @type {number}
   * @private
   */
  this.widthLimit_ = 0;

  /**
   * @type {number|undefined}
   * @private
   */
  this.previousX_;

  /**
   * @type {number|undefined}
   * @private
   */
  this.previousY_;

  /**
   * The calculated thumb size (border box plus margins).  Set when initSlider_
   * is called.
   * @type {GVol.Size}
   * @private
   */
  this.thumbSize_ = null;

  /**
   * Whether the slider is initialized.
   * @type {boGVolean}
   * @private
   */
  this.sliderInitialized_ = false;

  /**
   * @type {number}
   * @private
   */
  this.duration_ = options.duration !== undefined ? options.duration : 200;

  var className = options.className !== undefined ? options.className : 'GVol-zoomslider';
  var thumbElement = document.createElement('button');
  thumbElement.setAttribute('type', 'button');
  thumbElement.className = className + '-thumb ' + GVol.css.CLASS_UNSELECTABLE;
  var containerElement = document.createElement('div');
  containerElement.className = className + ' ' + GVol.css.CLASS_UNSELECTABLE + ' ' + GVol.css.CLASS_CONTROL;
  containerElement.appendChild(thumbElement);
  /**
   * @type {GVol.pointer.PointerEventHandler}
   * @private
   */
  this.dragger_ = new GVol.pointer.PointerEventHandler(containerElement);

  GVol.events.listen(this.dragger_, GVol.pointer.EventType.POINTERDOWN,
      this.handleDraggerStart_, this);
  GVol.events.listen(this.dragger_, GVol.pointer.EventType.POINTERMOVE,
      this.handleDraggerDrag_, this);
  GVol.events.listen(this.dragger_, GVol.pointer.EventType.POINTERUP,
      this.handleDraggerEnd_, this);

  GVol.events.listen(containerElement, GVol.events.EventType.CLICK,
      this.handleContainerClick_, this);
  GVol.events.listen(thumbElement, GVol.events.EventType.CLICK,
      GVol.events.Event.stopPropagation);

  var render = options.render ? options.render : GVol.contrGVol.ZoomSlider.render;

  GVol.contrGVol.ContrGVol.call(this, {
    element: containerElement,
    render: render
  });
};
GVol.inherits(GVol.contrGVol.ZoomSlider, GVol.contrGVol.ContrGVol);


/**
 * @inheritDoc
 */
GVol.contrGVol.ZoomSlider.prototype.disposeInternal = function() {
  this.dragger_.dispose();
  GVol.contrGVol.ContrGVol.prototype.disposeInternal.call(this);
};


/**
 * The enum for available directions.
 *
 * @enum {number}
 * @private
 */
GVol.contrGVol.ZoomSlider.Direction_ = {
  VERTICAL: 0,
  HORIZONTAL: 1
};


/**
 * @inheritDoc
 */
GVol.contrGVol.ZoomSlider.prototype.setMap = function(map) {
  GVol.contrGVol.ContrGVol.prototype.setMap.call(this, map);
  if (map) {
    map.render();
  }
};


/**
 * Initializes the slider element. This will determine and set this contrGVols
 * direction_ and also constrain the dragging of the thumb to always be within
 * the bounds of the container.
 *
 * @private
 */
GVol.contrGVol.ZoomSlider.prototype.initSlider_ = function() {
  var container = this.element;
  var containerSize = {
    width: container.offsetWidth, height: container.offsetHeight
  };

  var thumb = container.firstElementChild;
  var computedStyle = getComputedStyle(thumb);
  var thumbWidth = thumb.offsetWidth +
      parseFloat(computedStyle['marginRight']) +
      parseFloat(computedStyle['marginLeft']);
  var thumbHeight = thumb.offsetHeight +
      parseFloat(computedStyle['marginTop']) +
      parseFloat(computedStyle['marginBottom']);
  this.thumbSize_ = [thumbWidth, thumbHeight];

  if (containerSize.width > containerSize.height) {
    this.direction_ = GVol.contrGVol.ZoomSlider.Direction_.HORIZONTAL;
    this.widthLimit_ = containerSize.width - thumbWidth;
  } else {
    this.direction_ = GVol.contrGVol.ZoomSlider.Direction_.VERTICAL;
    this.heightLimit_ = containerSize.height - thumbHeight;
  }
  this.sliderInitialized_ = true;
};


/**
 * Update the zoomslider element.
 * @param {GVol.MapEvent} mapEvent Map event.
 * @this {GVol.contrGVol.ZoomSlider}
 * @api
 */
GVol.contrGVol.ZoomSlider.render = function(mapEvent) {
  if (!mapEvent.frameState) {
    return;
  }
  if (!this.sliderInitialized_) {
    this.initSlider_();
  }
  var res = mapEvent.frameState.viewState.resGVolution;
  if (res !== this.currentResGVolution_) {
    this.currentResGVolution_ = res;
    this.setThumbPosition_(res);
  }
};


/**
 * @param {Event} event The browser event to handle.
 * @private
 */
GVol.contrGVol.ZoomSlider.prototype.handleContainerClick_ = function(event) {
  var view = this.getMap().getView();

  var relativePosition = this.getRelativePosition_(
      event.offsetX - this.thumbSize_[0] / 2,
      event.offsetY - this.thumbSize_[1] / 2);

  var resGVolution = this.getResGVolutionForPosition_(relativePosition);

  view.animate({
    resGVolution: view.constrainResGVolution(resGVolution),
    duration: this.duration_,
    easing: GVol.easing.easeOut
  });
};


/**
 * Handle dragger start events.
 * @param {GVol.pointer.PointerEvent} event The drag event.
 * @private
 */
GVol.contrGVol.ZoomSlider.prototype.handleDraggerStart_ = function(event) {
  if (!this.dragging_ && event.originalEvent.target === this.element.firstElementChild) {
    this.getMap().getView().setHint(GVol.ViewHint.INTERACTING, 1);
    this.previousX_ = event.clientX;
    this.previousY_ = event.clientY;
    this.dragging_ = true;
  }
};


/**
 * Handle dragger drag events.
 *
 * @param {GVol.pointer.PointerEvent|Event} event The drag event.
 * @private
 */
GVol.contrGVol.ZoomSlider.prototype.handleDraggerDrag_ = function(event) {
  if (this.dragging_) {
    var element = this.element.firstElementChild;
    var deltaX = event.clientX - this.previousX_ + parseInt(element.style.left, 10);
    var deltaY = event.clientY - this.previousY_ + parseInt(element.style.top, 10);
    var relativePosition = this.getRelativePosition_(deltaX, deltaY);
    this.currentResGVolution_ = this.getResGVolutionForPosition_(relativePosition);
    this.getMap().getView().setResGVolution(this.currentResGVolution_);
    this.setThumbPosition_(this.currentResGVolution_);
    this.previousX_ = event.clientX;
    this.previousY_ = event.clientY;
  }
};


/**
 * Handle dragger end events.
 * @param {GVol.pointer.PointerEvent|Event} event The drag event.
 * @private
 */
GVol.contrGVol.ZoomSlider.prototype.handleDraggerEnd_ = function(event) {
  if (this.dragging_) {
    var view = this.getMap().getView();
    view.setHint(GVol.ViewHint.INTERACTING, -1);

    view.animate({
      resGVolution: view.constrainResGVolution(this.currentResGVolution_),
      duration: this.duration_,
      easing: GVol.easing.easeOut
    });

    this.dragging_ = false;
    this.previousX_ = undefined;
    this.previousY_ = undefined;
  }
};


/**
 * Positions the thumb inside its container according to the given resGVolution.
 *
 * @param {number} res The res.
 * @private
 */
GVol.contrGVol.ZoomSlider.prototype.setThumbPosition_ = function(res) {
  var position = this.getPositionForResGVolution_(res);
  var thumb = this.element.firstElementChild;

  if (this.direction_ == GVol.contrGVol.ZoomSlider.Direction_.HORIZONTAL) {
    thumb.style.left = this.widthLimit_ * position + 'px';
  } else {
    thumb.style.top = this.heightLimit_ * position + 'px';
  }
};


/**
 * 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
 */
GVol.contrGVol.ZoomSlider.prototype.getRelativePosition_ = function(x, y) {
  var amount;
  if (this.direction_ === GVol.contrGVol.ZoomSlider.Direction_.HORIZONTAL) {
    amount = x / this.widthLimit_;
  } else {
    amount = y / this.heightLimit_;
  }
  return GVol.math.clamp(amount, 0, 1);
};


/**
 * Calculates the corresponding resGVolution 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 resGVolution.
 * @private
 */
GVol.contrGVol.ZoomSlider.prototype.getResGVolutionForPosition_ = function(position) {
  var fn = this.getMap().getView().getResGVolutionForValueFunction();
  return fn(1 - position);
};


/**
 * Determines the relative position of the slider for the given resGVolution.  A
 * relative position of 0 corresponds to the minimum view resGVolution.  A
 * relative position of 1 corresponds to the maximum view resGVolution.
 *
 * @param {number} res The resGVolution.
 * @return {number} The relative position value (between 0 and 1).
 * @private
 */
GVol.contrGVol.ZoomSlider.prototype.getPositionForResGVolution_ = function(res) {
  var fn = this.getMap().getView().getValueForResGVolutionFunction();
  return 1 - fn(res);
};

goog.provide('GVol.contrGVol.ZoomToExtent');

goog.require('GVol');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.contrGVol.ContrGVol');
goog.require('GVol.css');


/**
 * @classdesc
 * A button contrGVol which, when pressed, changes the map view to a specific
 * extent. To style this contrGVol use the css selector `.GVol-zoom-extent`.
 *
 * @constructor
 * @extends {GVol.contrGVol.ContrGVol}
 * @param {GVolx.contrGVol.ZoomToExtentOptions=} opt_options Options.
 * @api
 */
GVol.contrGVol.ZoomToExtent = function(opt_options) {
  var options = opt_options ? opt_options : {};

  /**
   * @type {GVol.Extent}
   * @private
   */
  this.extent_ = options.extent ? options.extent : null;

  var className = options.className !== undefined ? options.className :
    'GVol-zoom-extent';

  var label = options.label !== undefined ? options.label : 'E';
  var tipLabel = options.tipLabel !== undefined ?
    options.tipLabel : 'Fit to extent';
  var button = document.createElement('button');
  button.setAttribute('type', 'button');
  button.title = tipLabel;
  button.appendChild(
      typeof label === 'string' ? document.createTextNode(label) : label
  );

  GVol.events.listen(button, GVol.events.EventType.CLICK,
      this.handleClick_, this);

  var cssClasses = className + ' ' + GVol.css.CLASS_UNSELECTABLE + ' ' +
      GVol.css.CLASS_CONTROL;
  var element = document.createElement('div');
  element.className = cssClasses;
  element.appendChild(button);

  GVol.contrGVol.ContrGVol.call(this, {
    element: element,
    target: options.target
  });
};
GVol.inherits(GVol.contrGVol.ZoomToExtent, GVol.contrGVol.ContrGVol);


/**
 * @param {Event} event The event to handle
 * @private
 */
GVol.contrGVol.ZoomToExtent.prototype.handleClick_ = function(event) {
  event.preventDefault();
  this.handleZoomToExtent_();
};


/**
 * @private
 */
GVol.contrGVol.ZoomToExtent.prototype.handleZoomToExtent_ = function() {
  var map = this.getMap();
  var view = map.getView();
  var extent = !this.extent_ ? view.getProjection().getExtent() : this.extent_;
  view.fit(extent);
};

goog.provide('GVol.DeviceOrientation');

goog.require('GVol.events');
goog.require('GVol');
goog.require('GVol.Object');
goog.require('GVol.has');
goog.require('GVol.math');


/**
 * @classdesc
 * The GVol.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.
 * GVol.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 GVol.DeviceOrientation are in radians for consistency
 * with all other uses of angles throughout OpenLayers.
 *
 * To get notified of device orientation changes, register a listener for the
 * generic `change` event on your `GVol.DeviceOrientation` instance.
 *
 * @see {@link http://www.w3.org/TR/orientation-event/}
 *
 * @constructor
 * @extends {GVol.Object}
 * @param {GVolx.DeviceOrientationOptions=} opt_options Options.
 * @api
 */
GVol.DeviceOrientation = function(opt_options) {

  GVol.Object.call(this);

  var options = opt_options ? opt_options : {};

  /**
   * @private
   * @type {?GVol.EventsKey}
   */
  this.listenerKey_ = null;

  GVol.events.listen(this,
      GVol.Object.getChangeEventType(GVol.DeviceOrientation.Property_.TRACKING),
      this.handleTrackingChanged_, this);

  this.setTracking(options.tracking !== undefined ? options.tracking : false);

};
GVol.inherits(GVol.DeviceOrientation, GVol.Object);


/**
 * @inheritDoc
 */
GVol.DeviceOrientation.prototype.disposeInternal = function() {
  this.setTracking(false);
  GVol.Object.prototype.disposeInternal.call(this);
};


/**
 * @private
 * @param {Event} originalEvent Event.
 */
GVol.DeviceOrientation.prototype.orientationChange_ = function(originalEvent) {
  var event = /** @type {DeviceOrientationEvent} */ (originalEvent);
  if (event.alpha !== null) {
    var alpha = GVol.math.toRadians(event.alpha);
    this.set(GVol.DeviceOrientation.Property_.ALPHA, alpha);
    // event.absGVolute is undefined in iOS.
    if (typeof event.absGVolute === 'boGVolean' && event.absGVolute) {
      this.set(GVol.DeviceOrientation.Property_.HEADING, alpha);
    } else if (typeof event.webkitCompassHeading === 'number' &&
               event.webkitCompassAccuracy != -1) {
      var heading = GVol.math.toRadians(event.webkitCompassHeading);
      this.set(GVol.DeviceOrientation.Property_.HEADING, heading);
    }
  }
  if (event.beta !== null) {
    this.set(GVol.DeviceOrientation.Property_.BETA,
        GVol.math.toRadians(event.beta));
  }
  if (event.gamma !== null) {
    this.set(GVol.DeviceOrientation.Property_.GAMMA,
        GVol.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
 */
GVol.DeviceOrientation.prototype.getAlpha = function() {
  return /** @type {number|undefined} */ (
    this.get(GVol.DeviceOrientation.Property_.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
 */
GVol.DeviceOrientation.prototype.getBeta = function() {
  return /** @type {number|undefined} */ (
    this.get(GVol.DeviceOrientation.Property_.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
 */
GVol.DeviceOrientation.prototype.getGamma = function() {
  return /** @type {number|undefined} */ (
    this.get(GVol.DeviceOrientation.Property_.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
 */
GVol.DeviceOrientation.prototype.getHeading = function() {
  return /** @type {number|undefined} */ (
    this.get(GVol.DeviceOrientation.Property_.HEADING));
};


/**
 * Determine if orientation is being tracked.
 * @return {boGVolean} Changes in device orientation are being tracked.
 * @observable
 * @api
 */
GVol.DeviceOrientation.prototype.getTracking = function() {
  return /** @type {boGVolean} */ (
    this.get(GVol.DeviceOrientation.Property_.TRACKING));
};


/**
 * @private
 */
GVol.DeviceOrientation.prototype.handleTrackingChanged_ = function() {
  if (GVol.has.DEVICE_ORIENTATION) {
    var tracking = this.getTracking();
    if (tracking && !this.listenerKey_) {
      this.listenerKey_ = GVol.events.listen(window, 'deviceorientation',
          this.orientationChange_, this);
    } else if (!tracking && this.listenerKey_ !== null) {
      GVol.events.unlistenByKey(this.listenerKey_);
      this.listenerKey_ = null;
    }
  }
};


/**
 * Enable or disable tracking of device orientation events.
 * @param {boGVolean} tracking The status of tracking changes to alpha, beta and
 *     gamma. If true, changes are tracked and reported immediately.
 * @observable
 * @api
 */
GVol.DeviceOrientation.prototype.setTracking = function(tracking) {
  this.set(GVol.DeviceOrientation.Property_.TRACKING, tracking);
};


/**
 * @enum {string}
 * @private
 */
GVol.DeviceOrientation.Property_ = {
  ALPHA: 'alpha',
  BETA: 'beta',
  GAMMA: 'gamma',
  HEADING: 'heading',
  TRACKING: 'tracking'
};

goog.provide('GVol.ImageState');

/**
 * @enum {number}
 */
GVol.ImageState = {
  IDLE: 0,
  LOADING: 1,
  LOADED: 2,
  ERROR: 3
};

goog.provide('GVol.style.Image');


/**
 * @classdesc
 * A base class used for creating subclasses and not instantiated in
 * apps. Base class for {@link GVol.style.Icon}, {@link GVol.style.Circle} and
 * {@link GVol.style.RegularShape}.
 *
 * @constructor
 * @abstract
 * @param {GVol.StyleImageOptions} options Options.
 * @api
 */
GVol.style.Image = function(options) {

  /**
   * @private
   * @type {number}
   */
  this.opacity_ = options.opacity;

  /**
   * @private
   * @type {boGVolean}
   */
  this.rotateWithView_ = options.rotateWithView;

  /**
   * @private
   * @type {number}
   */
  this.rotation_ = options.rotation;

  /**
   * @private
   * @type {number}
   */
  this.scale_ = options.scale;

  /**
   * @private
   * @type {boGVolean}
   */
  this.snapToPixel_ = options.snapToPixel;

};


/**
 * Get the symbGVolizer opacity.
 * @return {number} Opacity.
 * @api
 */
GVol.style.Image.prototype.getOpacity = function() {
  return this.opacity_;
};


/**
 * Determine whether the symbGVolizer rotates with the map.
 * @return {boGVolean} Rotate with map.
 * @api
 */
GVol.style.Image.prototype.getRotateWithView = function() {
  return this.rotateWithView_;
};


/**
 * Get the symoblizer rotation.
 * @return {number} Rotation.
 * @api
 */
GVol.style.Image.prototype.getRotation = function() {
  return this.rotation_;
};


/**
 * Get the symbGVolizer scale.
 * @return {number} Scale.
 * @api
 */
GVol.style.Image.prototype.getScale = function() {
  return this.scale_;
};


/**
 * Determine whether the symbGVolizer should be snapped to a pixel.
 * @return {boGVolean} The symbGVolizer should snap to a pixel.
 * @api
 */
GVol.style.Image.prototype.getSnapToPixel = function() {
  return this.snapToPixel_;
};


/**
 * Get the anchor point in pixels. The anchor determines the center point for the
 * symbGVolizer.
 * @abstract
 * @return {Array.<number>} Anchor.
 */
GVol.style.Image.prototype.getAnchor = function() {};


/**
 * Get the image element for the symbGVolizer.
 * @abstract
 * @param {number} pixelRatio Pixel ratio.
 * @return {HTMLCanvasElement|HTMLVideoElement|Image} Image element.
 */
GVol.style.Image.prototype.getImage = function(pixelRatio) {};


/**
 * @abstract
 * @param {number} pixelRatio Pixel ratio.
 * @return {HTMLCanvasElement|HTMLVideoElement|Image} Image element.
 */
GVol.style.Image.prototype.getHitDetectionImage = function(pixelRatio) {};


/**
 * @abstract
 * @return {GVol.ImageState} Image state.
 */
GVol.style.Image.prototype.getImageState = function() {};


/**
 * @abstract
 * @return {GVol.Size} Image size.
 */
GVol.style.Image.prototype.getImageSize = function() {};


/**
 * @abstract
 * @return {GVol.Size} Size of the hit-detection image.
 */
GVol.style.Image.prototype.getHitDetectionImageSize = function() {};


/**
 * Get the origin of the symbGVolizer.
 * @abstract
 * @return {Array.<number>} Origin.
 */
GVol.style.Image.prototype.getOrigin = function() {};


/**
 * Get the size of the symbGVolizer (in pixels).
 * @abstract
 * @return {GVol.Size} Size.
 */
GVol.style.Image.prototype.getSize = function() {};


/**
 * Set the opacity.
 *
 * @param {number} opacity Opacity.
 * @api
 */
GVol.style.Image.prototype.setOpacity = function(opacity) {
  this.opacity_ = opacity;
};


/**
 * Set whether to rotate the style with the view.
 *
 * @param {boGVolean} rotateWithView Rotate with map.
 */
GVol.style.Image.prototype.setRotateWithView = function(rotateWithView) {
  this.rotateWithView_ = rotateWithView;
};


/**
 * Set the rotation.
 *
 * @param {number} rotation Rotation.
 * @api
 */
GVol.style.Image.prototype.setRotation = function(rotation) {
  this.rotation_ = rotation;
};


/**
 * Set the scale.
 *
 * @param {number} scale Scale.
 * @api
 */
GVol.style.Image.prototype.setScale = function(scale) {
  this.scale_ = scale;
};


/**
 * Set whether to snap the image to the closest pixel.
 *
 * @param {boGVolean} snapToPixel Snap to pixel?
 */
GVol.style.Image.prototype.setSnapToPixel = function(snapToPixel) {
  this.snapToPixel_ = snapToPixel;
};


/**
 * @abstract
 * @param {function(this: T, GVol.events.Event)} listener Listener function.
 * @param {T} thisArg Value to use as `this` when executing `listener`.
 * @return {GVol.EventsKey|undefined} Listener key.
 * @template T
 */
GVol.style.Image.prototype.listenImageChange = function(listener, thisArg) {};


/**
 * Load not yet loaded URI.
 * @abstract
 */
GVol.style.Image.prototype.load = function() {};


/**
 * @abstract
 * @param {function(this: T, GVol.events.Event)} listener Listener function.
 * @param {T} thisArg Value to use as `this` when executing `listener`.
 * @template T
 */
GVol.style.Image.prototype.unlistenImageChange = function(listener, thisArg) {};

goog.provide('GVol.style.RegularShape');

goog.require('GVol');
goog.require('GVol.cGVolorlike');
goog.require('GVol.dom');
goog.require('GVol.has');
goog.require('GVol.ImageState');
goog.require('GVol.render.canvas');
goog.require('GVol.style.Image');


/**
 * @classdesc
 * Set regular shape style for vector features. The resulting shape will be
 * a regular pGVolygon when `radius` is provided, or a star when `radius1` and
 * `radius2` are provided.
 *
 * @constructor
 * @param {GVolx.style.RegularShapeOptions} options Options.
 * @extends {GVol.style.Image}
 * @api
 */
GVol.style.RegularShape = function(options) {
  /**
   * @private
   * @type {Array.<string>}
   */
  this.checksums_ = null;

  /**
   * @private
   * @type {HTMLCanvasElement}
   */
  this.canvas_ = null;

  /**
   * @private
   * @type {HTMLCanvasElement}
   */
  this.hitDetectionCanvas_ = null;

  /**
   * @private
   * @type {GVol.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;

  /**
   * @protected
   * @type {number}
   */
  this.radius_ = /** @type {number} */ (options.radius !== undefined ?
    options.radius : options.radius1);

  /**
   * @private
   * @type {number|undefined}
   */
  this.radius2_ = options.radius2;

  /**
   * @private
   * @type {number}
   */
  this.angle_ = options.angle !== undefined ? options.angle : 0;

  /**
   * @private
   * @type {GVol.style.Stroke}
   */
  this.stroke_ = options.stroke !== undefined ? options.stroke : null;

  /**
   * @private
   * @type {Array.<number>}
   */
  this.anchor_ = null;

  /**
   * @private
   * @type {GVol.Size}
   */
  this.size_ = null;

  /**
   * @private
   * @type {GVol.Size}
   */
  this.imageSize_ = null;

  /**
   * @private
   * @type {GVol.Size}
   */
  this.hitDetectionImageSize_ = null;

  /**
   * @protected
   * @type {GVol.style.AtlasManager|undefined}
   */
  this.atlasManager_ = options.atlasManager;

  this.render_(this.atlasManager_);

  /**
   * @type {boGVolean}
   */
  var snapToPixel = options.snapToPixel !== undefined ?
    options.snapToPixel : true;

  /**
   * @type {boGVolean}
   */
  var rotateWithView = options.rotateWithView !== undefined ?
    options.rotateWithView : false;

  GVol.style.Image.call(this, {
    opacity: 1,
    rotateWithView: rotateWithView,
    rotation: options.rotation !== undefined ? options.rotation : 0,
    scale: 1,
    snapToPixel: snapToPixel
  });
};
GVol.inherits(GVol.style.RegularShape, GVol.style.Image);


/**
 * Clones the style. If an atlasmanager was provided to the original style it will be used in the cloned style, too.
 * @return {GVol.style.RegularShape} The cloned style.
 * @api
 */
GVol.style.RegularShape.prototype.clone = function() {
  var style = new GVol.style.RegularShape({
    fill: this.getFill() ? this.getFill().clone() : undefined,
    points: this.getPoints(),
    radius: this.getRadius(),
    radius2: this.getRadius2(),
    angle: this.getAngle(),
    snapToPixel: this.getSnapToPixel(),
    stroke: this.getStroke() ?  this.getStroke().clone() : undefined,
    rotation: this.getRotation(),
    rotateWithView: this.getRotateWithView(),
    atlasManager: this.atlasManager_
  });
  style.setOpacity(this.getOpacity());
  style.setScale(this.getScale());
  return style;
};


/**
 * @inheritDoc
 * @api
 */
GVol.style.RegularShape.prototype.getAnchor = function() {
  return this.anchor_;
};


/**
 * Get the angle used in generating the shape.
 * @return {number} Shape's rotation in radians.
 * @api
 */
GVol.style.RegularShape.prototype.getAngle = function() {
  return this.angle_;
};


/**
 * Get the fill style for the shape.
 * @return {GVol.style.Fill} Fill style.
 * @api
 */
GVol.style.RegularShape.prototype.getFill = function() {
  return this.fill_;
};


/**
 * @inheritDoc
 */
GVol.style.RegularShape.prototype.getHitDetectionImage = function(pixelRatio) {
  return this.hitDetectionCanvas_;
};


/**
 * @inheritDoc
 * @api
 */
GVol.style.RegularShape.prototype.getImage = function(pixelRatio) {
  return this.canvas_;
};


/**
 * @inheritDoc
 */
GVol.style.RegularShape.prototype.getImageSize = function() {
  return this.imageSize_;
};


/**
 * @inheritDoc
 */
GVol.style.RegularShape.prototype.getHitDetectionImageSize = function() {
  return this.hitDetectionImageSize_;
};


/**
 * @inheritDoc
 */
GVol.style.RegularShape.prototype.getImageState = function() {
  return GVol.ImageState.LOADED;
};


/**
 * @inheritDoc
 * @api
 */
GVol.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 pGVolygons.
 * @api
 */
GVol.style.RegularShape.prototype.getPoints = function() {
  return this.points_;
};


/**
 * Get the (primary) radius for the shape.
 * @return {number} Radius.
 * @api
 */
GVol.style.RegularShape.prototype.getRadius = function() {
  return this.radius_;
};


/**
 * Get the secondary radius for the shape.
 * @return {number|undefined} Radius2.
 * @api
 */
GVol.style.RegularShape.prototype.getRadius2 = function() {
  return this.radius2_;
};


/**
 * @inheritDoc
 * @api
 */
GVol.style.RegularShape.prototype.getSize = function() {
  return this.size_;
};


/**
 * Get the stroke style for the shape.
 * @return {GVol.style.Stroke} Stroke style.
 * @api
 */
GVol.style.RegularShape.prototype.getStroke = function() {
  return this.stroke_;
};


/**
 * @inheritDoc
 */
GVol.style.RegularShape.prototype.listenImageChange = function(listener, thisArg) {};


/**
 * @inheritDoc
 */
GVol.style.RegularShape.prototype.load = function() {};


/**
 * @inheritDoc
 */
GVol.style.RegularShape.prototype.unlistenImageChange = function(listener, thisArg) {};


/**
 * @protected
 * @param {GVol.style.AtlasManager|undefined} atlasManager An atlas manager.
 */
GVol.style.RegularShape.prototype.render_ = function(atlasManager) {
  var imageSize;
  var lineCap = '';
  var lineJoin = '';
  var miterLimit = 0;
  var lineDash = null;
  var lineDashOffset = 0;
  var strokeStyle;
  var strokeWidth = 0;

  if (this.stroke_) {
    strokeStyle = this.stroke_.getCGVolor();
    if (strokeStyle === null) {
      strokeStyle = GVol.render.canvas.defaultStrokeStyle;
    }
    strokeStyle = GVol.cGVolorlike.asCGVolorLike(strokeStyle);
    strokeWidth = this.stroke_.getWidth();
    if (strokeWidth === undefined) {
      strokeWidth = GVol.render.canvas.defaultLineWidth;
    }
    lineDash = this.stroke_.getLineDash();
    lineDashOffset = this.stroke_.getLineDashOffset();
    if (!GVol.has.CANVAS_LINE_DASH) {
      lineDash = null;
      lineDashOffset = 0;
    }
    lineJoin = this.stroke_.getLineJoin();
    if (lineJoin === undefined) {
      lineJoin = GVol.render.canvas.defaultLineJoin;
    }
    lineCap = this.stroke_.getLineCap();
    if (lineCap === undefined) {
      lineCap = GVol.render.canvas.defaultLineCap;
    }
    miterLimit = this.stroke_.getMiterLimit();
    if (miterLimit === undefined) {
      miterLimit = GVol.render.canvas.defaultMiterLimit;
    }
  }

  var size = 2 * (this.radius_ + strokeWidth) + 1;

  /** @type {GVol.RegularShapeRenderOptions} */
  var renderOptions = {
    strokeStyle: strokeStyle,
    strokeWidth: strokeWidth,
    size: size,
    lineCap: lineCap,
    lineDash: lineDash,
    lineDashOffset: lineDashOffset,
    lineJoin: lineJoin,
    miterLimit: miterLimit
  };

  if (atlasManager === undefined) {
    // no atlas manager is used, create a new canvas
    var context = GVol.dom.createCanvasContext2D(size, size);
    this.canvas_ = context.canvas;

    // canvas.width and height are rounded to the closest integer
    size = this.canvas_.width;
    imageSize = size;

    this.draw_(renderOptions, context, 0, 0);

    this.createHitDetectionCanvas_(renderOptions);
  } else {
    // an atlas manager is used, add the symbGVol 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);

    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 {GVol.RegularShapeRenderOptions} renderOptions Render options.
 * @param {CanvasRenderingContext2D} context The rendering context.
 * @param {number} x The origin for the symbGVol (x).
 * @param {number} y The origin for the symbGVol (y).
 */
GVol.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();

  var points = this.points_;
  if (points === Infinity) {
    context.arc(
        renderOptions.size / 2, renderOptions.size / 2,
        this.radius_, 0, 2 * Math.PI, true);
  } else {
    var radius2 = (this.radius2_ !== undefined) ? this.radius2_
      : this.radius_;
    if (radius2 !== this.radius_) {
      points = 2 * points;
    }
    for (i = 0; i <= points; i++) {
      angle0 = i * 2 * Math.PI / points - Math.PI / 2 + this.angle_;
      radiusC = i % 2 === 0 ? this.radius_ : radius2;
      context.lineTo(renderOptions.size / 2 + radiusC * Math.cos(angle0),
          renderOptions.size / 2 + radiusC * Math.sin(angle0));
    }
  }


  if (this.fill_) {
    var cGVolor = this.fill_.getCGVolor();
    if (cGVolor === null) {
      cGVolor = GVol.render.canvas.defaultFillStyle;
    }
    context.fillStyle = GVol.cGVolorlike.asCGVolorLike(cGVolor);
    context.fill();
  }
  if (this.stroke_) {
    context.strokeStyle = renderOptions.strokeStyle;
    context.lineWidth = renderOptions.strokeWidth;
    if (renderOptions.lineDash) {
      context.setLineDash(renderOptions.lineDash);
      context.lineDashOffset = renderOptions.lineDashOffset;
    }
    context.lineCap = renderOptions.lineCap;
    context.lineJoin = renderOptions.lineJoin;
    context.miterLimit = renderOptions.miterLimit;
    context.stroke();
  }
  context.closePath();
};


/**
 * @private
 * @param {GVol.RegularShapeRenderOptions} renderOptions Render options.
 */
GVol.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
  var context = GVol.dom.createCanvasContext2D(renderOptions.size, renderOptions.size);
  this.hitDetectionCanvas_ = context.canvas;

  this.drawHitDetectionCanvas_(renderOptions, context, 0, 0);
};


/**
 * @private
 * @param {GVol.RegularShapeRenderOptions} renderOptions Render options.
 * @param {CanvasRenderingContext2D} context The context.
 * @param {number} x The origin for the symbGVol (x).
 * @param {number} y The origin for the symbGVol (y).
 */
GVol.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();

  var points = this.points_;
  if (points === Infinity) {
    context.arc(
        renderOptions.size / 2, renderOptions.size / 2,
        this.radius_, 0, 2 * Math.PI, true);
  } else {
    var radius2 = (this.radius2_ !== undefined) ? this.radius2_
      : this.radius_;
    if (radius2 !== this.radius_) {
      points = 2 * points;
    }
    var i, radiusC, angle0;
    for (i = 0; i <= points; i++) {
      angle0 = i * 2 * Math.PI / points - Math.PI / 2 + this.angle_;
      radiusC = i % 2 === 0 ? this.radius_ : radius2;
      context.lineTo(renderOptions.size / 2 + radiusC * Math.cos(angle0),
          renderOptions.size / 2 + radiusC * Math.sin(angle0));
    }
  }

  context.fillStyle = GVol.render.canvas.defaultFillStyle;
  context.fill();
  if (this.stroke_) {
    context.strokeStyle = renderOptions.strokeStyle;
    context.lineWidth = renderOptions.strokeWidth;
    if (renderOptions.lineDash) {
      context.setLineDash(renderOptions.lineDash);
      context.lineDashOffset = renderOptions.lineDashOffset;
    }
    context.stroke();
  }
  context.closePath();
};


/**
 * @return {string} The checksum.
 */
GVol.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];
};

goog.provide('GVol.style.Circle');

goog.require('GVol');
goog.require('GVol.style.RegularShape');


/**
 * @classdesc
 * Set circle style for vector features.
 *
 * @constructor
 * @param {GVolx.style.CircleOptions=} opt_options Options.
 * @extends {GVol.style.RegularShape}
 * @api
 */
GVol.style.Circle = function(opt_options) {

  var options = opt_options || {};

  GVol.style.RegularShape.call(this, {
    points: Infinity,
    fill: options.fill,
    radius: options.radius,
    snapToPixel: options.snapToPixel,
    stroke: options.stroke,
    atlasManager: options.atlasManager
  });

};
GVol.inherits(GVol.style.Circle, GVol.style.RegularShape);


/**
 * Clones the style.  If an atlasmanager was provided to the original style it will be used in the cloned style, too.
 * @return {GVol.style.Circle} The cloned style.
 * @override
 * @api
 */
GVol.style.Circle.prototype.clone = function() {
  var style = new GVol.style.Circle({
    fill: this.getFill() ? this.getFill().clone() : undefined,
    stroke: this.getStroke() ? this.getStroke().clone() : undefined,
    radius: this.getRadius(),
    snapToPixel: this.getSnapToPixel(),
    atlasManager: this.atlasManager_
  });
  style.setOpacity(this.getOpacity());
  style.setScale(this.getScale());
  return style;
};


/**
 * Set the circle radius.
 *
 * @param {number} radius Circle radius.
 * @api
 */
GVol.style.Circle.prototype.setRadius = function(radius) {
  this.radius_ = radius;
  this.render_(this.atlasManager_);
};

goog.provide('GVol.style.Fill');

goog.require('GVol');
goog.require('GVol.cGVolor');


/**
 * @classdesc
 * Set fill style for vector features.
 *
 * @constructor
 * @param {GVolx.style.FillOptions=} opt_options Options.
 * @api
 */
GVol.style.Fill = function(opt_options) {

  var options = opt_options || {};

  /**
   * @private
   * @type {GVol.CGVolor|GVol.CGVolorLike}
   */
  this.cGVolor_ = options.cGVolor !== undefined ? options.cGVolor : null;

  /**
   * @private
   * @type {string|undefined}
   */
  this.checksum_ = undefined;
};


/**
 * Clones the style. The cGVolor is not cloned if it is an {@link GVol.CGVolorLike}.
 * @return {GVol.style.Fill} The cloned style.
 * @api
 */
GVol.style.Fill.prototype.clone = function() {
  var cGVolor = this.getCGVolor();
  return new GVol.style.Fill({
    cGVolor: (cGVolor && cGVolor.slice) ? cGVolor.slice() : cGVolor || undefined
  });
};


/**
 * Get the fill cGVolor.
 * @return {GVol.CGVolor|GVol.CGVolorLike} CGVolor.
 * @api
 */
GVol.style.Fill.prototype.getCGVolor = function() {
  return this.cGVolor_;
};


/**
 * Set the cGVolor.
 *
 * @param {GVol.CGVolor|GVol.CGVolorLike} cGVolor CGVolor.
 * @api
 */
GVol.style.Fill.prototype.setCGVolor = function(cGVolor) {
  this.cGVolor_ = cGVolor;
  this.checksum_ = undefined;
};


/**
 * @return {string} The checksum.
 */
GVol.style.Fill.prototype.getChecksum = function() {
  if (this.checksum_ === undefined) {
    if (
      this.cGVolor_ instanceof CanvasPattern ||
        this.cGVolor_ instanceof CanvasGradient
    ) {
      this.checksum_ = GVol.getUid(this.cGVolor_).toString();
    } else {
      this.checksum_ = 'f' + (this.cGVolor_ ?
        GVol.cGVolor.asString(this.cGVolor_) : '-');
    }
  }

  return this.checksum_;
};

goog.provide('GVol.style.Style');

goog.require('GVol.asserts');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.style.Circle');
goog.require('GVol.style.Fill');
goog.require('GVol.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
 * @struct
 * @param {GVolx.style.StyleOptions=} opt_options Style options.
 * @api
 */
GVol.style.Style = function(opt_options) {

  var options = opt_options || {};

  /**
   * @private
   * @type {string|GVol.geom.Geometry|GVol.StyleGeometryFunction}
   */
  this.geometry_ = null;

  /**
   * @private
   * @type {!GVol.StyleGeometryFunction}
   */
  this.geometryFunction_ = GVol.style.Style.defaultGeometryFunction;

  if (options.geometry !== undefined) {
    this.setGeometry(options.geometry);
  }

  /**
   * @private
   * @type {GVol.style.Fill}
   */
  this.fill_ = options.fill !== undefined ? options.fill : null;

  /**
   * @private
   * @type {GVol.style.Image}
   */
  this.image_ = options.image !== undefined ? options.image : null;

  /**
   * @private
   * @type {GVol.StyleRenderFunction|null}
   */
  this.renderer_ = options.renderer !== undefined ? options.renderer : null;

  /**
   * @private
   * @type {GVol.style.Stroke}
   */
  this.stroke_ = options.stroke !== undefined ? options.stroke : null;

  /**
   * @private
   * @type {GVol.style.Text}
   */
  this.text_ = options.text !== undefined ? options.text : null;

  /**
   * @private
   * @type {number|undefined}
   */
  this.zIndex_ = options.zIndex;

};


/**
 * Clones the style.
 * @return {GVol.style.Style} The cloned style.
 * @api
 */
GVol.style.Style.prototype.clone = function() {
  var geometry = this.getGeometry();
  if (geometry && geometry.clone) {
    geometry = geometry.clone();
  }
  return new GVol.style.Style({
    geometry: geometry,
    fill: this.getFill() ? this.getFill().clone() : undefined,
    image: this.getImage() ? this.getImage().clone() : undefined,
    stroke: this.getStroke() ? this.getStroke().clone() : undefined,
    text: this.getText() ? this.getText().clone() : undefined,
    zIndex: this.getZIndex()
  });
};


/**
 * Get the custom renderer function that was configured with
 * {@link #setRenderer} or the `renderer` constructor option.
 * @return {GVol.StyleRenderFunction|null} Custom renderer function.
 * @api
 */
GVol.style.Style.prototype.getRenderer = function() {
  return this.renderer_;
};


/**
 * Sets a custom renderer function for this style. When set, `fill`, `stroke`
 * and `image` options of the style will be ignored.
 * @param {GVol.StyleRenderFunction|null} renderer Custom renderer function.
 * @api
 */
GVol.style.Style.prototype.setRenderer = function(renderer) {
  this.renderer_ = renderer;
};


/**
 * Get the geometry to be rendered.
 * @return {string|GVol.geom.Geometry|GVol.StyleGeometryFunction}
 * Feature property or geometry or function that returns the geometry that will
 * be rendered with this style.
 * @api
 */
GVol.style.Style.prototype.getGeometry = function() {
  return this.geometry_;
};


/**
 * Get the function used to generate a geometry for rendering.
 * @return {!GVol.StyleGeometryFunction} Function that is called with a feature
 * and returns the geometry to render instead of the feature's geometry.
 * @api
 */
GVol.style.Style.prototype.getGeometryFunction = function() {
  return this.geometryFunction_;
};


/**
 * Get the fill style.
 * @return {GVol.style.Fill} Fill style.
 * @api
 */
GVol.style.Style.prototype.getFill = function() {
  return this.fill_;
};


/**
 * Set the fill style.
 * @param {GVol.style.Fill} fill Fill style.
 * @api
 */
GVol.style.Style.prototype.setFill = function(fill) {
  this.fill_ = fill;
};


/**
 * Get the image style.
 * @return {GVol.style.Image} Image style.
 * @api
 */
GVol.style.Style.prototype.getImage = function() {
  return this.image_;
};


/**
 * Set the image style.
 * @param {GVol.style.Image} image Image style.
 * @api
 */
GVol.style.Style.prototype.setImage = function(image) {
  this.image_ = image;
};


/**
 * Get the stroke style.
 * @return {GVol.style.Stroke} Stroke style.
 * @api
 */
GVol.style.Style.prototype.getStroke = function() {
  return this.stroke_;
};


/**
 * Set the stroke style.
 * @param {GVol.style.Stroke} stroke Stroke style.
 * @api
 */
GVol.style.Style.prototype.setStroke = function(stroke) {
  this.stroke_ = stroke;
};


/**
 * Get the text style.
 * @return {GVol.style.Text} Text style.
 * @api
 */
GVol.style.Style.prototype.getText = function() {
  return this.text_;
};


/**
 * Set the text style.
 * @param {GVol.style.Text} text Text style.
 * @api
 */
GVol.style.Style.prototype.setText = function(text) {
  this.text_ = text;
};


/**
 * Get the z-index for the style.
 * @return {number|undefined} ZIndex.
 * @api
 */
GVol.style.Style.prototype.getZIndex = function() {
  return this.zIndex_;
};


/**
 * Set a geometry that is rendered instead of the feature's geometry.
 *
 * @param {string|GVol.geom.Geometry|GVol.StyleGeometryFunction} geometry
 *     Feature property or geometry or function returning a geometry to render
 *     for this style.
 * @api
 */
GVol.style.Style.prototype.setGeometry = function(geometry) {
  if (typeof geometry === 'function') {
    this.geometryFunction_ = geometry;
  } else if (typeof geometry === 'string') {
    this.geometryFunction_ = function(feature) {
      return /** @type {GVol.geom.Geometry} */ (feature.get(geometry));
    };
  } else if (!geometry) {
    this.geometryFunction_ = GVol.style.Style.defaultGeometryFunction;
  } else if (geometry !== undefined) {
    this.geometryFunction_ = function() {
      return /** @type {GVol.geom.Geometry} */ (geometry);
    };
  }
  this.geometry_ = geometry;
};


/**
 * Set the z-index.
 *
 * @param {number|undefined} zIndex ZIndex.
 * @api
 */
GVol.style.Style.prototype.setZIndex = function(zIndex) {
  this.zIndex_ = zIndex;
};


/**
 * Convert the provided object into a style function.  Functions passed through
 * unchanged.  Arrays of GVol.style.Style or single style objects wrapped in a
 * new style function.
 * @param {GVol.StyleFunction|Array.<GVol.style.Style>|GVol.style.Style} obj
 *     A style function, a single style, or an array of styles.
 * @return {GVol.StyleFunction} A style function.
 */
GVol.style.Style.createFunction = function(obj) {
  var styleFunction;

  if (typeof obj === 'function') {
    styleFunction = obj;
  } else {
    /**
     * @type {Array.<GVol.style.Style>}
     */
    var styles;
    if (Array.isArray(obj)) {
      styles = obj;
    } else {
      GVol.asserts.assert(obj instanceof GVol.style.Style,
          41); // Expected an `GVol.style.Style` or an array of `GVol.style.Style`
      styles = [obj];
    }
    styleFunction = function() {
      return styles;
    };
  }
  return styleFunction;
};


/**
 * @type {Array.<GVol.style.Style>}
 * @private
 */
GVol.style.Style.default_ = null;


/**
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 * @param {number} resGVolution ResGVolution.
 * @return {Array.<GVol.style.Style>} Style.
 */
GVol.style.Style.defaultFunction = function(feature, resGVolution) {
  // 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. (GVol.style.Circle does
  // canvas.getContext('2d') at construction time, which will cause an.error
  // in such browsers.)
  if (!GVol.style.Style.default_) {
    var fill = new GVol.style.Fill({
      cGVolor: 'rgba(255,255,255,0.4)'
    });
    var stroke = new GVol.style.Stroke({
      cGVolor: '#3399CC',
      width: 1.25
    });
    GVol.style.Style.default_ = [
      new GVol.style.Style({
        image: new GVol.style.Circle({
          fill: fill,
          stroke: stroke,
          radius: 5
        }),
        fill: fill,
        stroke: stroke
      })
    ];
  }
  return GVol.style.Style.default_;
};


/**
 * Default styles for editing features.
 * @return {Object.<GVol.geom.GeometryType, Array.<GVol.style.Style>>} Styles
 */
GVol.style.Style.createDefaultEditing = function() {
  /** @type {Object.<GVol.geom.GeometryType, Array.<GVol.style.Style>>} */
  var styles = {};
  var white = [255, 255, 255, 1];
  var blue = [0, 153, 255, 1];
  var width = 3;
  styles[GVol.geom.GeometryType.POLYGON] = [
    new GVol.style.Style({
      fill: new GVol.style.Fill({
        cGVolor: [255, 255, 255, 0.5]
      })
    })
  ];
  styles[GVol.geom.GeometryType.MULTI_POLYGON] =
      styles[GVol.geom.GeometryType.POLYGON];

  styles[GVol.geom.GeometryType.LINE_STRING] = [
    new GVol.style.Style({
      stroke: new GVol.style.Stroke({
        cGVolor: white,
        width: width + 2
      })
    }),
    new GVol.style.Style({
      stroke: new GVol.style.Stroke({
        cGVolor: blue,
        width: width
      })
    })
  ];
  styles[GVol.geom.GeometryType.MULTI_LINE_STRING] =
      styles[GVol.geom.GeometryType.LINE_STRING];

  styles[GVol.geom.GeometryType.CIRCLE] =
      styles[GVol.geom.GeometryType.POLYGON].concat(
          styles[GVol.geom.GeometryType.LINE_STRING]
      );


  styles[GVol.geom.GeometryType.POINT] = [
    new GVol.style.Style({
      image: new GVol.style.Circle({
        radius: width * 2,
        fill: new GVol.style.Fill({
          cGVolor: blue
        }),
        stroke: new GVol.style.Stroke({
          cGVolor: white,
          width: width / 2
        })
      }),
      zIndex: Infinity
    })
  ];
  styles[GVol.geom.GeometryType.MULTI_POINT] =
      styles[GVol.geom.GeometryType.POINT];

  styles[GVol.geom.GeometryType.GEOMETRY_COLLECTION] =
      styles[GVol.geom.GeometryType.POLYGON].concat(
          styles[GVol.geom.GeometryType.LINE_STRING],
          styles[GVol.geom.GeometryType.POINT]
      );

  return styles;
};


/**
 * Function that is called with a feature and returns its default geometry.
 * @param {GVol.Feature|GVol.render.Feature} feature Feature to get the geometry
 *     for.
 * @return {GVol.geom.Geometry|GVol.render.Feature|undefined} Geometry to render.
 */
GVol.style.Style.defaultGeometryFunction = function(feature) {
  return feature.getGeometry();
};

goog.provide('GVol.Feature');

goog.require('GVol.asserts');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol');
goog.require('GVol.Object');
goog.require('GVol.geom.Geometry');
goog.require('GVol.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 GVol.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 GVol.Feature({
 *   geometry: new GVol.geom.PGVolygon(pGVolyCoords),
 *   labelPoint: new GVol.geom.Point(labelCoords),
 *   name: 'My PGVolygon'
 * });
 *
 * // get the pGVolygon geometry
 * var pGVoly = 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 {GVol.Object}
 * @param {GVol.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
 */
GVol.Feature = function(opt_geometryOrProperties) {

  GVol.Object.call(this);

  /**
   * @private
   * @type {number|string|undefined}
   */
  this.id_ = undefined;

  /**
   * @type {string}
   * @private
   */
  this.geometryName_ = 'geometry';

  /**
   * User provided style.
   * @private
   * @type {GVol.style.Style|Array.<GVol.style.Style>|
   *     GVol.FeatureStyleFunction}
   */
  this.style_ = null;

  /**
   * @private
   * @type {GVol.FeatureStyleFunction|undefined}
   */
  this.styleFunction_ = undefined;

  /**
   * @private
   * @type {?GVol.EventsKey}
   */
  this.geometryChangeKey_ = null;

  GVol.events.listen(
      this, GVol.Object.getChangeEventType(this.geometryName_),
      this.handleGeometryChanged_, this);

  if (opt_geometryOrProperties !== undefined) {
    if (opt_geometryOrProperties instanceof GVol.geom.Geometry ||
        !opt_geometryOrProperties) {
      var geometry = opt_geometryOrProperties;
      this.setGeometry(geometry);
    } else {
      /** @type {Object.<string, *>} */
      var properties = opt_geometryOrProperties;
      this.setProperties(properties);
    }
  }
};
GVol.inherits(GVol.Feature, GVol.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 {GVol.Feature} The clone.
 * @api
 */
GVol.Feature.prototype.clone = function() {
  var clone = new GVol.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 GVol.Feature#setGeometry}.
 * @return {GVol.geom.Geometry|undefined} The default geometry for the feature.
 * @api
 * @observable
 */
GVol.Feature.prototype.getGeometry = function() {
  return /** @type {GVol.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 GVol.Feature#setId}.
 * @return {number|string|undefined} Id.
 * @api
 */
GVol.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
 */
GVol.Feature.prototype.getGeometryName = function() {
  return this.geometryName_;
};


/**
 * Get the feature's style. Will return what was provided to the
 * {@link GVol.Feature#setStyle} method.
 * @return {GVol.style.Style|Array.<GVol.style.Style>|
 *     GVol.FeatureStyleFunction|GVol.StyleFunction} The feature style.
 * @api
 */
GVol.Feature.prototype.getStyle = function() {
  return this.style_;
};


/**
 * Get the feature's style function.
 * @return {GVol.FeatureStyleFunction|undefined} Return a function
 * representing the current style of this feature.
 * @api
 */
GVol.Feature.prototype.getStyleFunction = function() {
  return this.styleFunction_;
};


/**
 * @private
 */
GVol.Feature.prototype.handleGeometryChange_ = function() {
  this.changed();
};


/**
 * @private
 */
GVol.Feature.prototype.handleGeometryChanged_ = function() {
  if (this.geometryChangeKey_) {
    GVol.events.unlistenByKey(this.geometryChangeKey_);
    this.geometryChangeKey_ = null;
  }
  var geometry = this.getGeometry();
  if (geometry) {
    this.geometryChangeKey_ = GVol.events.listen(geometry,
        GVol.events.EventType.CHANGE, this.handleGeometryChange_, this);
  }
  this.changed();
};


/**
 * Set the default geometry for the feature.  This will update the property
 * with the name returned by {@link GVol.Feature#getGeometryName}.
 * @param {GVol.geom.Geometry|undefined} geometry The new geometry.
 * @api
 * @observable
 */
GVol.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 resGVolution and returns an array of
 * styles. If it is `null` the feature has no style (a `null` style).
 * @param {GVol.style.Style|Array.<GVol.style.Style>|
 *     GVol.FeatureStyleFunction|GVol.StyleFunction} style Style for this feature.
 * @api
 * @fires GVol.events.Event#event:change
 */
GVol.Feature.prototype.setStyle = function(style) {
  this.style_ = style;
  this.styleFunction_ = !style ?
    undefined : GVol.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 GVol.source.Vector#getFeatureById}
 * method.
 * @param {number|string|undefined} id The feature id.
 * @api
 * @fires GVol.events.Event#event:change
 */
GVol.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 GVol.Feature#getGeometry}, the value of the property with
 * this name will be returned.
 * @param {string} name The property name of the default geometry.
 * @api
 */
GVol.Feature.prototype.setGeometryName = function(name) {
  GVol.events.unlisten(
      this, GVol.Object.getChangeEventType(this.geometryName_),
      this.handleGeometryChanged_, this);
  this.geometryName_ = name;
  GVol.events.listen(
      this, GVol.Object.getChangeEventType(this.geometryName_),
      this.handleGeometryChanged_, this);
  this.handleGeometryChanged_();
};


/**
 * Convert the provided object into a feature style function.  Functions passed
 * through unchanged.  Arrays of GVol.style.Style or single style objects wrapped
 * in a new feature style function.
 * @param {GVol.FeatureStyleFunction|!Array.<GVol.style.Style>|!GVol.style.Style} obj
 *     A feature style function, a single style, or an array of styles.
 * @return {GVol.FeatureStyleFunction} A style function.
 */
GVol.Feature.createStyleFunction = function(obj) {
  var styleFunction;

  if (typeof obj === 'function') {
    if (obj.length == 2) {
      styleFunction = function(resGVolution) {
        return /** @type {GVol.StyleFunction} */ (obj)(this, resGVolution);
      };
    } else {
      styleFunction = obj;
    }
  } else {
    /**
     * @type {Array.<GVol.style.Style>}
     */
    var styles;
    if (Array.isArray(obj)) {
      styles = obj;
    } else {
      GVol.asserts.assert(obj instanceof GVol.style.Style,
          41); // Expected an `GVol.style.Style` or an array of `GVol.style.Style`
      styles = [obj];
    }
    styleFunction = function() {
      return styles;
    };
  }
  return styleFunction;
};

goog.provide('GVol.format.FormatType');


/**
 * @enum {string}
 */
GVol.format.FormatType = {
  ARRAY_BUFFER: 'arraybuffer',
  JSON: 'json',
  TEXT: 'text',
  XML: 'xml'
};

goog.provide('GVol.xml');

goog.require('GVol.array');


/**
 * This document should be used when creating nodes for XML serializations. This
 * document is also used by {@link GVol.xml.createElementNS} and
 * {@link GVol.xml.setAttributeNS}
 * @const
 * @type {Document}
 */
GVol.xml.DOCUMENT = document.implementation.createDocument('', '', null);


/**
 * @param {string} namespaceURI Namespace URI.
 * @param {string} qualifiedName Qualified name.
 * @return {Node} Node.
 */
GVol.xml.createElementNS = function(namespaceURI, qualifiedName) {
  return GVol.xml.DOCUMENT.createElementNS(namespaceURI, qualifiedName);
};


/**
 * Recursively grab all text content of child nodes into a single string.
 * @param {Node} node Node.
 * @param {boGVolean} normalizeWhitespace Normalize whitespace: remove all line
 * breaks.
 * @return {string} All text content.
 * @api
 */
GVol.xml.getAllTextContent = function(node, normalizeWhitespace) {
  return GVol.xml.getAllTextContent_(node, normalizeWhitespace, []).join('');
};


/**
 * Recursively grab all text content of child nodes into a single string.
 * @param {Node} node Node.
 * @param {boGVolean} normalizeWhitespace Normalize whitespace: remove all line
 * breaks.
 * @param {Array.<string>} accumulator Accumulator.
 * @private
 * @return {Array.<string>} Accumulator.
 */
GVol.xml.getAllTextContent_ = function(node, normalizeWhitespace, accumulator) {
  if (node.nodeType == Node.CDATA_SECTION_NODE ||
      node.nodeType == Node.TEXT_NODE) {
    if (normalizeWhitespace) {
      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) {
      GVol.xml.getAllTextContent_(n, normalizeWhitespace, accumulator);
    }
  }
  return accumulator;
};


/**
 * @param {?} value Value.
 * @return {boGVolean} Is document.
 */
GVol.xml.isDocument = function(value) {
  return value instanceof Document;
};


/**
 * @param {?} value Value.
 * @return {boGVolean} Is node.
 */
GVol.xml.isNode = function(value) {
  return value instanceof Node;
};


/**
 * @param {Node} node Node.
 * @param {?string} namespaceURI Namespace URI.
 * @param {string} name Attribute name.
 * @return {string} Value
 */
GVol.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.
 * @param {string|number} value Value.
 */
GVol.xml.setAttributeNS = function(node, namespaceURI, name, value) {
  node.setAttributeNS(namespaceURI, name, value);
};


/**
 * Parse an XML string to an XML Document.
 * @param {string} xml XML.
 * @return {Document} Document.
 * @api
 */
GVol.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 {GVol.XmlParser} Parser.
 * @template T
 */
GVol.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) {
        var array = /** @type {Array.<*>} */
              (objectStack[objectStack.length - 1]);
        GVol.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 {GVol.XmlParser} Parser.
 * @template T
 */
GVol.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];
        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 {GVol.XmlParser} Parser.
 * @template T
 */
GVol.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 {GVol.XmlParser} Parser.
 * @template T
 */
GVol.xml.makeObjectPropertyPusher = function(valueReader, opt_property, 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 object = /** @type {Object} */
              (objectStack[objectStack.length - 1]);
        var property = opt_property !== undefined ?
          opt_property : node.localName;
        var array;
        if (property in object) {
          array = object[property];
        } else {
          array = 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 {GVol.XmlParser} Parser.
 * @template T
 */
GVol.xml.makeObjectPropertySetter = function(valueReader, opt_property, 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 object = /** @type {Object} */
              (objectStack[objectStack.length - 1]);
        var property = opt_property !== undefined ?
          opt_property : node.localName;
        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 GVol.XmlNodeStackItem} 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 {GVol.XmlSerializer} Serializer.
 * @template T, V
 */
GVol.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];
    var parentNode = parent.node;
    parentNode.appendChild(node);
  };
};


/**
 * Create a serializer that calls the provided `nodeWriter` from
 * {@link GVol.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 {GVol.XmlSerializer} Serializer.
 * @template T, V
 */
GVol.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 = GVol.xml.makeSimpleNodeFactory(node.localName);
    }
    GVol.xml.serialize(serializersNS, nodeFactory, value, objectStack);
  };
};


/**
 * Create a node factory which can use the `opt_keys` passed to
 * {@link GVol.xml.serialize} or {@link GVol.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.
 */
GVol.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;
      var nodeName = fixedNodeName;
      if (nodeName === undefined) {
        nodeName = opt_nodeName;
      }
      var namespaceURI = opt_namespaceURI;
      if (opt_namespaceURI === undefined) {
        namespaceURI = node.namespaceURI;
      }
      return GVol.xml.createElementNS(namespaceURI, /** @type {string} */ (nodeName));
    }
  );
};


/**
 * A node factory that creates a node using the parent's `namespaceURI` and the
 * `nodeName` passed by {@link GVol.xml.serialize} or
 * {@link GVol.xml.pushSerializeAndPop} to the node factory.
 * @const
 * @type {function(*, Array.<*>, string=): (Node|undefined)}
 */
GVol.xml.OBJECT_PROPERTY_NODE_FACTORY = GVol.xml.makeSimpleNodeFactory();


/**
 * Create an array of `values` to be used with {@link GVol.xml.serialize} or
 * {@link GVol.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
 */
GVol.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
 */
GVol.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, GVol.XmlParser>>} parsersNS
 *     Parsers by namespace.
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @param {*=} opt_this The object to use as `this`.
 */
GVol.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, GVol.XmlParser>>} 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
 */
GVol.xml.pushParseAndPop = function(
    object, parsersNS, node, objectStack, opt_this) {
  objectStack.push(object);
  GVol.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, GVol.XmlSerializer>>} 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 GVol.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
 */
GVol.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, GVol.XmlSerializer>>} 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 GVol.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
 */
GVol.xml.pushSerializeAndPop = function(object,
    serializersNS, nodeFactory, values, objectStack, opt_keys, opt_this) {
  objectStack.push(object);
  GVol.xml.serialize(
      serializersNS, nodeFactory, values, objectStack, opt_keys, opt_this);
  return objectStack.pop();
};

goog.provide('GVol.featureloader');

goog.require('GVol');
goog.require('GVol.format.FormatType');
goog.require('GVol.xml');


/**
 * @param {string|GVol.FeatureUrlFunction} url Feature URL service.
 * @param {GVol.format.Feature} format Feature format.
 * @param {function(this:GVol.VectorTile, Array.<GVol.Feature>, GVol.proj.Projection, GVol.Extent)|function(this:GVol.source.Vector, Array.<GVol.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:GVol.VectorTile)|function(this:GVol.source.Vector)} failure
 *     Function called when loading failed. Called with the vector tile or
 *     source as `this`.
 * @return {GVol.FeatureLoader} The feature loader.
 */
GVol.featureloader.loadFeaturesXhr = function(url, format, success, failure) {
  return (
    /**
     * @param {GVol.Extent} extent Extent.
     * @param {number} resGVolution ResGVolution.
     * @param {GVol.proj.Projection} projection Projection.
     * @this {GVol.source.Vector|GVol.VectorTile}
     */
    function(extent, resGVolution, projection) {
      var xhr = new XMLHttpRequest();
      xhr.open('GET',
          typeof url === 'function' ? url(extent, resGVolution, projection) : url,
          true);
      if (format.getType() == GVol.format.FormatType.ARRAY_BUFFER) {
        xhr.responseType = 'arraybuffer';
      }
      /**
       * @param {Event} event Event.
       * @private
       */
      xhr.onload = function(event) {
        // status will be 0 for file:// urls
        if (!xhr.status || xhr.status >= 200 && xhr.status < 300) {
          var type = format.getType();
          /** @type {Document|Node|Object|string|undefined} */
          var source;
          if (type == GVol.format.FormatType.JSON ||
                type == GVol.format.FormatType.TEXT) {
            source = xhr.responseText;
          } else if (type == GVol.format.FormatType.XML) {
            source = xhr.responseXML;
            if (!source) {
              source = GVol.xml.parse(xhr.responseText);
            }
          } else if (type == GVol.format.FormatType.ARRAY_BUFFER) {
            source = /** @type {ArrayBuffer} */ (xhr.response);
          }
          if (source) {
            success.call(this, format.readFeatures(source,
                {featureProjection: projection}),
            format.readProjection(source), format.getLastExtent());
          } else {
            failure.call(this);
          }
        } else {
          failure.call(this);
        }
      }.bind(this);
      /**
       * @private
       */
      xhr.onerror = function() {
        failure.call(this);
      }.bind(this);
      xhr.send();
    });
};


/**
 * 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|GVol.FeatureUrlFunction} url Feature URL service.
 * @param {GVol.format.Feature} format Feature format.
 * @return {GVol.FeatureLoader} The feature loader.
 * @api
 */
GVol.featureloader.xhr = function(url, format) {
  return GVol.featureloader.loadFeaturesXhr(url, format,
      /**
       * @param {Array.<GVol.Feature>} features The loaded features.
       * @param {GVol.proj.Projection} dataProjection Data projection.
       * @this {GVol.source.Vector}
       */
      function(features, dataProjection) {
        this.addFeatures(features);
      }, /* FIXME handle error */ GVol.nullFunction);
};

goog.provide('GVol.format.Feature');

goog.require('GVol.geom.Geometry');
goog.require('GVol.obj');
goog.require('GVol.proj');


/**
 * @classdesc
 * Abstract base class; normally only used for creating subclasses and not
 * instantiated in apps.
 * Base class for feature formats.
 * {GVol.format.Feature} subclasses provide the ability to decode and encode
 * {@link GVol.Feature} objects from a variety of commonly used geospatial
 * file formats.  See the documentation for each format for more details.
 *
 * @constructor
 * @abstract
 * @api
 */
GVol.format.Feature = function() {

  /**
   * @protected
   * @type {GVol.proj.Projection}
   */
  this.defaultDataProjection = null;

  /**
   * @protected
   * @type {GVol.proj.Projection}
   */
  this.defaultFeatureProjection = null;

};


/**
 * Adds the data projection to the read options.
 * @param {Document|Node|Object|string} source Source.
 * @param {GVolx.format.ReadOptions=} opt_options Options.
 * @return {GVolx.format.ReadOptions|undefined} Options.
 * @protected
 */
GVol.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 {GVolx.format.WriteOptions|GVolx.format.ReadOptions|undefined} options
 *     Options.
 * @protected
 * @return {GVolx.format.WriteOptions|GVolx.format.ReadOptions|undefined}
 *     Updated options.
 */
GVol.format.Feature.prototype.adaptOptions = function(options) {
  return GVol.obj.assign({
    dataProjection: this.defaultDataProjection,
    featureProjection: this.defaultFeatureProjection
  }, options);
};


/**
 * Get the extent from the source of the last {@link readFeatures} call.
 * @return {GVol.Extent} Tile extent.
 */
GVol.format.Feature.prototype.getLastExtent = function() {
  return null;
};


/**
 * @abstract
 * @return {GVol.format.FormatType} Format.
 */
GVol.format.Feature.prototype.getType = function() {};


/**
 * Read a single feature from a source.
 *
 * @abstract
 * @param {Document|Node|Object|string} source Source.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {GVol.Feature} Feature.
 */
GVol.format.Feature.prototype.readFeature = function(source, opt_options) {};


/**
 * Read all features from a source.
 *
 * @abstract
 * @param {Document|Node|ArrayBuffer|Object|string} source Source.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<GVol.Feature>} Features.
 */
GVol.format.Feature.prototype.readFeatures = function(source, opt_options) {};


/**
 * Read a single geometry from a source.
 *
 * @abstract
 * @param {Document|Node|Object|string} source Source.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {GVol.geom.Geometry} Geometry.
 */
GVol.format.Feature.prototype.readGeometry = function(source, opt_options) {};


/**
 * Read the projection from a source.
 *
 * @abstract
 * @param {Document|Node|Object|string} source Source.
 * @return {GVol.proj.Projection} Projection.
 */
GVol.format.Feature.prototype.readProjection = function(source) {};


/**
 * Encode a feature in this format.
 *
 * @abstract
 * @param {GVol.Feature} feature Feature.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {string} Result.
 */
GVol.format.Feature.prototype.writeFeature = function(feature, opt_options) {};


/**
 * Encode an array of features in this format.
 *
 * @abstract
 * @param {Array.<GVol.Feature>} features Features.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {string} Result.
 */
GVol.format.Feature.prototype.writeFeatures = function(features, opt_options) {};


/**
 * Write a single geometry in this format.
 *
 * @abstract
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {string} Result.
 */
GVol.format.Feature.prototype.writeGeometry = function(geometry, opt_options) {};


/**
 * @param {GVol.geom.Geometry|GVol.Extent} geometry Geometry.
 * @param {boGVolean} write Set to true for writing, false for reading.
 * @param {(GVolx.format.WriteOptions|GVolx.format.ReadOptions)=} opt_options
 *     Options.
 * @return {GVol.geom.Geometry|GVol.Extent} Transformed geometry.
 * @protected
 */
GVol.format.Feature.transformWithOptions = function(
    geometry, write, opt_options) {
  var featureProjection = opt_options ?
    GVol.proj.get(opt_options.featureProjection) : null;
  var dataProjection = opt_options ?
    GVol.proj.get(opt_options.dataProjection) : null;
  /**
   * @type {GVol.geom.Geometry|GVol.Extent}
   */
  var transformed;
  if (featureProjection && dataProjection &&
      !GVol.proj.equivalent(featureProjection, dataProjection)) {
    if (geometry instanceof GVol.geom.Geometry) {
      transformed = (write ? geometry.clone() : geometry).transform(
          write ? featureProjection : dataProjection,
          write ? dataProjection : featureProjection);
    } else {
      // FIXME this is necessary because GVol.format.GML treats extents
      // as geometries
      transformed = GVol.proj.transformExtent(
          geometry,
          dataProjection,
          featureProjection);
    }
  } else {
    transformed = geometry;
  }
  if (write && opt_options && opt_options.decimals !== undefined) {
    var power = Math.pow(10, opt_options.decimals);
    // if decimals option on write, round each coordinate appropriately
    /**
     * @param {Array.<number>} coordinates Coordinates.
     * @return {Array.<number>} Transformed coordinates.
     */
    var transform = function(coordinates) {
      for (var i = 0, ii = coordinates.length; i < ii; ++i) {
        coordinates[i] = Math.round(coordinates[i] * power) / power;
      }
      return coordinates;
    };
    if (transformed === geometry) {
      transformed = transformed.clone();
    }
    transformed.applyTransform(transform);
  }
  return transformed;
};

goog.provide('GVol.format.JSONFeature');

goog.require('GVol');
goog.require('GVol.format.Feature');
goog.require('GVol.format.FormatType');


/**
 * @classdesc
 * Abstract base class; normally only used for creating subclasses and not
 * instantiated in apps.
 * Base class for JSON feature formats.
 *
 * @constructor
 * @abstract
 * @extends {GVol.format.Feature}
 */
GVol.format.JSONFeature = function() {
  GVol.format.Feature.call(this);
};
GVol.inherits(GVol.format.JSONFeature, GVol.format.Feature);


/**
 * @param {Document|Node|Object|string} source Source.
 * @private
 * @return {Object} Object.
 */
GVol.format.JSONFeature.prototype.getObject_ = function(source) {
  if (typeof source === 'string') {
    var object = JSON.parse(source);
    return object ? /** @type {Object} */ (object) : null;
  } else if (source !== null) {
    return source;
  } else {
    return null;
  }
};


/**
 * @inheritDoc
 */
GVol.format.JSONFeature.prototype.getType = function() {
  return GVol.format.FormatType.JSON;
};


/**
 * @inheritDoc
 */
GVol.format.JSONFeature.prototype.readFeature = function(source, opt_options) {
  return this.readFeatureFromObject(
      this.getObject_(source), this.getReadOptions(source, opt_options));
};


/**
 * @inheritDoc
 */
GVol.format.JSONFeature.prototype.readFeatures = function(source, opt_options) {
  return this.readFeaturesFromObject(
      this.getObject_(source), this.getReadOptions(source, opt_options));
};


/**
 * @abstract
 * @param {Object} object Object.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @protected
 * @return {GVol.Feature} Feature.
 */
GVol.format.JSONFeature.prototype.readFeatureFromObject = function(object, opt_options) {};


/**
 * @abstract
 * @param {Object} object Object.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @protected
 * @return {Array.<GVol.Feature>} Features.
 */
GVol.format.JSONFeature.prototype.readFeaturesFromObject = function(object, opt_options) {};


/**
 * @inheritDoc
 */
GVol.format.JSONFeature.prototype.readGeometry = function(source, opt_options) {
  return this.readGeometryFromObject(
      this.getObject_(source), this.getReadOptions(source, opt_options));
};


/**
 * @abstract
 * @param {Object} object Object.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @protected
 * @return {GVol.geom.Geometry} Geometry.
 */
GVol.format.JSONFeature.prototype.readGeometryFromObject = function(object, opt_options) {};


/**
 * @inheritDoc
 */
GVol.format.JSONFeature.prototype.readProjection = function(source) {
  return this.readProjectionFromObject(this.getObject_(source));
};


/**
 * @abstract
 * @param {Object} object Object.
 * @protected
 * @return {GVol.proj.Projection} Projection.
 */
GVol.format.JSONFeature.prototype.readProjectionFromObject = function(object) {};


/**
 * @inheritDoc
 */
GVol.format.JSONFeature.prototype.writeFeature = function(feature, opt_options) {
  return JSON.stringify(this.writeFeatureObject(feature, opt_options));
};


/**
 * @abstract
 * @param {GVol.Feature} feature Feature.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {Object} Object.
 */
GVol.format.JSONFeature.prototype.writeFeatureObject = function(feature, opt_options) {};


/**
 * @inheritDoc
 */
GVol.format.JSONFeature.prototype.writeFeatures = function(features, opt_options) {
  return JSON.stringify(this.writeFeaturesObject(features, opt_options));
};


/**
 * @abstract
 * @param {Array.<GVol.Feature>} features Features.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {Object} Object.
 */
GVol.format.JSONFeature.prototype.writeFeaturesObject = function(features, opt_options) {};


/**
 * @inheritDoc
 */
GVol.format.JSONFeature.prototype.writeGeometry = function(geometry, opt_options) {
  return JSON.stringify(this.writeGeometryObject(geometry, opt_options));
};


/**
 * @abstract
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {Object} Object.
 */
GVol.format.JSONFeature.prototype.writeGeometryObject = function(geometry, opt_options) {};

goog.provide('GVol.geom.flat.interpGVolate');

goog.require('GVol.array');
goog.require('GVol.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.
 */
GVol.geom.flat.interpGVolate.lineString = function(flatCoordinates, offset, end, stride, fraction, opt_dest) {
  var pointX = NaN;
  var pointY = NaN;
  var n = (end - offset) / stride;
  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 if (n !== 0) {
    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 = GVol.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 = GVol.math.lerp(
          flatCoordinates[o], flatCoordinates[o + stride], t);
      pointY = GVol.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 {boGVolean} extrapGVolate ExtrapGVolate.
 * @return {GVol.Coordinate} Coordinate.
 */
GVol.geom.flat.interpGVolate.lineStringCoordinateAtM = function(flatCoordinates, offset, end, stride, m, extrapGVolate) {
  if (end == offset) {
    return null;
  }
  var coordinate;
  if (m < flatCoordinates[offset + stride - 1]) {
    if (extrapGVolate) {
      coordinate = flatCoordinates.slice(offset, offset + stride);
      coordinate[stride - 1] = m;
      return coordinate;
    } else {
      return null;
    }
  } else if (flatCoordinates[end - 1] < m) {
    if (extrapGVolate) {
      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];
  var t = (m - m0) / (m1 - m0);
  coordinate = [];
  var i;
  for (i = 0; i < stride - 1; ++i) {
    coordinate.push(GVol.math.lerp(flatCoordinates[(lo - 1) * stride + i],
        flatCoordinates[lo * stride + i], t));
  }
  coordinate.push(m);
  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 {boGVolean} extrapGVolate ExtrapGVolate.
 * @param {boGVolean} interpGVolate InterpGVolate.
 * @return {GVol.Coordinate} Coordinate.
 */
GVol.geom.flat.interpGVolate.lineStringsCoordinateAtM = function(
    flatCoordinates, offset, ends, stride, m, extrapGVolate, interpGVolate) {
  if (interpGVolate) {
    return GVol.geom.flat.interpGVolate.lineStringCoordinateAtM(
        flatCoordinates, offset, ends[ends.length - 1], stride, m, extrapGVolate);
  }
  var coordinate;
  if (m < flatCoordinates[stride - 1]) {
    if (extrapGVolate) {
      coordinate = flatCoordinates.slice(0, stride);
      coordinate[stride - 1] = m;
      return coordinate;
    } else {
      return null;
    }
  }
  if (flatCoordinates[flatCoordinates.length - 1] < m) {
    if (extrapGVolate) {
      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 GVol.geom.flat.interpGVolate.lineStringCoordinateAtM(
          flatCoordinates, offset, end, stride, m, false);
    }
    offset = end;
  }
  return null;
};

goog.provide('GVol.geom.flat.length');


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @return {number} Length.
 */
GVol.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.
 */
GVol.geom.flat.length.linearRing = function(flatCoordinates, offset, end, stride) {
  var perimeter =
      GVol.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('GVol.geom.LineString');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.extent');
goog.require('GVol.geom.GeometryLayout');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.geom.SimpleGeometry');
goog.require('GVol.geom.flat.closest');
goog.require('GVol.geom.flat.deflate');
goog.require('GVol.geom.flat.inflate');
goog.require('GVol.geom.flat.interpGVolate');
goog.require('GVol.geom.flat.intersectsextent');
goog.require('GVol.geom.flat.length');
goog.require('GVol.geom.flat.segments');
goog.require('GVol.geom.flat.simplify');


/**
 * @classdesc
 * Linestring geometry.
 *
 * @constructor
 * @extends {GVol.geom.SimpleGeometry}
 * @param {Array.<GVol.Coordinate>} coordinates Coordinates.
 * @param {GVol.geom.GeometryLayout=} opt_layout Layout.
 * @api
 */
GVol.geom.LineString = function(coordinates, opt_layout) {

  GVol.geom.SimpleGeometry.call(this);

  /**
   * @private
   * @type {GVol.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);

};
GVol.inherits(GVol.geom.LineString, GVol.geom.SimpleGeometry);


/**
 * Append the passed coordinate to the coordinates of the linestring.
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @api
 */
GVol.geom.LineString.prototype.appendCoordinate = function(coordinate) {
  if (!this.flatCoordinates) {
    this.flatCoordinates = coordinate.slice();
  } else {
    GVol.array.extend(this.flatCoordinates, coordinate);
  }
  this.changed();
};


/**
 * Make a complete copy of the geometry.
 * @return {!GVol.geom.LineString} Clone.
 * @override
 * @api
 */
GVol.geom.LineString.prototype.clone = function() {
  var lineString = new GVol.geom.LineString(null);
  lineString.setFlatCoordinates(this.layout, this.flatCoordinates.slice());
  return lineString;
};


/**
 * @inheritDoc
 */
GVol.geom.LineString.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
  if (minSquaredDistance <
      GVol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
    return minSquaredDistance;
  }
  if (this.maxDeltaRevision_ != this.getRevision()) {
    this.maxDelta_ = Math.sqrt(GVol.geom.flat.closest.getMaxSquaredDelta(
        this.flatCoordinates, 0, this.flatCoordinates.length, this.stride, 0));
    this.maxDeltaRevision_ = this.getRevision();
  }
  return GVol.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, GVol.Coordinate, GVol.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|boGVolean} Value.
 * @template T,S
 * @api
 */
GVol.geom.LineString.prototype.forEachSegment = function(callback, opt_this) {
  return GVol.geom.flat.segments.forEach(this.flatCoordinates, 0,
      this.flatCoordinates.length, this.stride, callback, opt_this);
};


/**
 * Returns the coordinate at `m` using linear interpGVolation, or `null` if no
 * such coordinate exists.
 *
 * `opt_extrapGVolate` contrGVols extrapGVolation beyond the range of Ms in the
 * MultiLineString. If `opt_extrapGVolate` 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 {boGVolean=} opt_extrapGVolate ExtrapGVolate. Default is `false`.
 * @return {GVol.Coordinate} Coordinate.
 * @api
 */
GVol.geom.LineString.prototype.getCoordinateAtM = function(m, opt_extrapGVolate) {
  if (this.layout != GVol.geom.GeometryLayout.XYM &&
      this.layout != GVol.geom.GeometryLayout.XYZM) {
    return null;
  }
  var extrapGVolate = opt_extrapGVolate !== undefined ? opt_extrapGVolate : false;
  return GVol.geom.flat.interpGVolate.lineStringCoordinateAtM(this.flatCoordinates, 0,
      this.flatCoordinates.length, this.stride, m, extrapGVolate);
};


/**
 * Return the coordinates of the linestring.
 * @return {Array.<GVol.Coordinate>} Coordinates.
 * @override
 * @api
 */
GVol.geom.LineString.prototype.getCoordinates = function() {
  return GVol.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 {GVol.Coordinate=} opt_dest Optional coordinate whose values will
 *     be modified. If not provided, a new coordinate will be returned.
 * @return {GVol.Coordinate} Coordinate of the interpGVolated point.
 * @api
 */
GVol.geom.LineString.prototype.getCoordinateAt = function(fraction, opt_dest) {
  return GVol.geom.flat.interpGVolate.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
 */
GVol.geom.LineString.prototype.getLength = function() {
  return GVol.geom.flat.length.lineString(
      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride);
};


/**
 * @return {Array.<number>} Flat midpoint.
 */
GVol.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
 */
GVol.geom.LineString.prototype.getSimplifiedGeometryInternal = function(squaredTGVolerance) {
  var simplifiedFlatCoordinates = [];
  simplifiedFlatCoordinates.length = GVol.geom.flat.simplify.douglasPeucker(
      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
      squaredTGVolerance, simplifiedFlatCoordinates, 0);
  var simplifiedLineString = new GVol.geom.LineString(null);
  simplifiedLineString.setFlatCoordinates(
      GVol.geom.GeometryLayout.XY, simplifiedFlatCoordinates);
  return simplifiedLineString;
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.LineString.prototype.getType = function() {
  return GVol.geom.GeometryType.LINE_STRING;
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.LineString.prototype.intersectsExtent = function(extent) {
  return GVol.geom.flat.intersectsextent.lineString(
      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride,
      extent);
};


/**
 * Set the coordinates of the linestring.
 * @param {Array.<GVol.Coordinate>} coordinates Coordinates.
 * @param {GVol.geom.GeometryLayout=} opt_layout Layout.
 * @override
 * @api
 */
GVol.geom.LineString.prototype.setCoordinates = function(coordinates, opt_layout) {
  if (!coordinates) {
    this.setFlatCoordinates(GVol.geom.GeometryLayout.XY, null);
  } else {
    this.setLayout(opt_layout, coordinates, 1);
    if (!this.flatCoordinates) {
      this.flatCoordinates = [];
    }
    this.flatCoordinates.length = GVol.geom.flat.deflate.coordinates(
        this.flatCoordinates, 0, coordinates, this.stride);
    this.changed();
  }
};


/**
 * @param {GVol.geom.GeometryLayout} layout Layout.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 */
GVol.geom.LineString.prototype.setFlatCoordinates = function(layout, flatCoordinates) {
  this.setFlatCoordinatesInternal(layout, flatCoordinates);
  this.changed();
};

goog.provide('GVol.geom.MultiLineString');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.extent');
goog.require('GVol.geom.GeometryLayout');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.geom.LineString');
goog.require('GVol.geom.SimpleGeometry');
goog.require('GVol.geom.flat.closest');
goog.require('GVol.geom.flat.deflate');
goog.require('GVol.geom.flat.inflate');
goog.require('GVol.geom.flat.interpGVolate');
goog.require('GVol.geom.flat.intersectsextent');
goog.require('GVol.geom.flat.simplify');


/**
 * @classdesc
 * Multi-linestring geometry.
 *
 * @constructor
 * @extends {GVol.geom.SimpleGeometry}
 * @param {Array.<Array.<GVol.Coordinate>>} coordinates Coordinates.
 * @param {GVol.geom.GeometryLayout=} opt_layout Layout.
 * @api
 */
GVol.geom.MultiLineString = function(coordinates, opt_layout) {

  GVol.geom.SimpleGeometry.call(this);

  /**
   * @type {Array.<number>}
   * @private
   */
  this.ends_ = [];

  /**
   * @private
   * @type {number}
   */
  this.maxDelta_ = -1;

  /**
   * @private
   * @type {number}
   */
  this.maxDeltaRevision_ = -1;

  this.setCoordinates(coordinates, opt_layout);

};
GVol.inherits(GVol.geom.MultiLineString, GVol.geom.SimpleGeometry);


/**
 * Append the passed linestring to the multilinestring.
 * @param {GVol.geom.LineString} lineString LineString.
 * @api
 */
GVol.geom.MultiLineString.prototype.appendLineString = function(lineString) {
  if (!this.flatCoordinates) {
    this.flatCoordinates = lineString.getFlatCoordinates().slice();
  } else {
    GVol.array.extend(
        this.flatCoordinates, lineString.getFlatCoordinates().slice());
  }
  this.ends_.push(this.flatCoordinates.length);
  this.changed();
};


/**
 * Make a complete copy of the geometry.
 * @return {!GVol.geom.MultiLineString} Clone.
 * @override
 * @api
 */
GVol.geom.MultiLineString.prototype.clone = function() {
  var multiLineString = new GVol.geom.MultiLineString(null);
  multiLineString.setFlatCoordinates(
      this.layout, this.flatCoordinates.slice(), this.ends_.slice());
  return multiLineString;
};


/**
 * @inheritDoc
 */
GVol.geom.MultiLineString.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
  if (minSquaredDistance <
      GVol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
    return minSquaredDistance;
  }
  if (this.maxDeltaRevision_ != this.getRevision()) {
    this.maxDelta_ = Math.sqrt(GVol.geom.flat.closest.getsMaxSquaredDelta(
        this.flatCoordinates, 0, this.ends_, this.stride, 0));
    this.maxDeltaRevision_ = this.getRevision();
  }
  return GVol.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 interpGVolation, or `null` if no
 * such coordinate exists.
 *
 * `opt_extrapGVolate` contrGVols extrapGVolation beyond the range of Ms in the
 * MultiLineString. If `opt_extrapGVolate` 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_interpGVolate` contrGVols interpGVolation between consecutive LineStrings
 * within the MultiLineString. If `opt_interpGVolate` is `true` the coordinates
 * will be linearly interpGVolated between the last coordinate of one LineString
 * and the first coordinate of the next LineString.  If `opt_interpGVolate` is
 * `false` then the function will return `null` for Ms falling between
 * LineStrings.
 *
 * @param {number} m M.
 * @param {boGVolean=} opt_extrapGVolate ExtrapGVolate. Default is `false`.
 * @param {boGVolean=} opt_interpGVolate InterpGVolate. Default is `false`.
 * @return {GVol.Coordinate} Coordinate.
 * @api
 */
GVol.geom.MultiLineString.prototype.getCoordinateAtM = function(m, opt_extrapGVolate, opt_interpGVolate) {
  if ((this.layout != GVol.geom.GeometryLayout.XYM &&
       this.layout != GVol.geom.GeometryLayout.XYZM) ||
      this.flatCoordinates.length === 0) {
    return null;
  }
  var extrapGVolate = opt_extrapGVolate !== undefined ? opt_extrapGVolate : false;
  var interpGVolate = opt_interpGVolate !== undefined ? opt_interpGVolate : false;
  return GVol.geom.flat.interpGVolate.lineStringsCoordinateAtM(this.flatCoordinates, 0,
      this.ends_, this.stride, m, extrapGVolate, interpGVolate);
};


/**
 * Return the coordinates of the multilinestring.
 * @return {Array.<Array.<GVol.Coordinate>>} Coordinates.
 * @override
 * @api
 */
GVol.geom.MultiLineString.prototype.getCoordinates = function() {
  return GVol.geom.flat.inflate.coordinatess(
      this.flatCoordinates, 0, this.ends_, this.stride);
};


/**
 * @return {Array.<number>} Ends.
 */
GVol.geom.MultiLineString.prototype.getEnds = function() {
  return this.ends_;
};


/**
 * Return the linestring at the specified index.
 * @param {number} index Index.
 * @return {GVol.geom.LineString} LineString.
 * @api
 */
GVol.geom.MultiLineString.prototype.getLineString = function(index) {
  if (index < 0 || this.ends_.length <= index) {
    return null;
  }
  var lineString = new GVol.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.<GVol.geom.LineString>} LineStrings.
 * @api
 */
GVol.geom.MultiLineString.prototype.getLineStrings = function() {
  var flatCoordinates = this.flatCoordinates;
  var ends = this.ends_;
  var layout = this.layout;
  /** @type {Array.<GVol.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 GVol.geom.LineString(null);
    lineString.setFlatCoordinates(layout, flatCoordinates.slice(offset, end));
    lineStrings.push(lineString);
    offset = end;
  }
  return lineStrings;
};


/**
 * @return {Array.<number>} Flat midpoints.
 */
GVol.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 = GVol.geom.flat.interpGVolate.lineString(
        flatCoordinates, offset, end, stride, 0.5);
    GVol.array.extend(midpoints, midpoint);
    offset = end;
  }
  return midpoints;
};


/**
 * @inheritDoc
 */
GVol.geom.MultiLineString.prototype.getSimplifiedGeometryInternal = function(squaredTGVolerance) {
  var simplifiedFlatCoordinates = [];
  var simplifiedEnds = [];
  simplifiedFlatCoordinates.length = GVol.geom.flat.simplify.douglasPeuckers(
      this.flatCoordinates, 0, this.ends_, this.stride, squaredTGVolerance,
      simplifiedFlatCoordinates, 0, simplifiedEnds);
  var simplifiedMultiLineString = new GVol.geom.MultiLineString(null);
  simplifiedMultiLineString.setFlatCoordinates(
      GVol.geom.GeometryLayout.XY, simplifiedFlatCoordinates, simplifiedEnds);
  return simplifiedMultiLineString;
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.MultiLineString.prototype.getType = function() {
  return GVol.geom.GeometryType.MULTI_LINE_STRING;
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.MultiLineString.prototype.intersectsExtent = function(extent) {
  return GVol.geom.flat.intersectsextent.lineStrings(
      this.flatCoordinates, 0, this.ends_, this.stride, extent);
};


/**
 * Set the coordinates of the multilinestring.
 * @param {Array.<Array.<GVol.Coordinate>>} coordinates Coordinates.
 * @param {GVol.geom.GeometryLayout=} opt_layout Layout.
 * @override
 * @api
 */
GVol.geom.MultiLineString.prototype.setCoordinates = function(coordinates, opt_layout) {
  if (!coordinates) {
    this.setFlatCoordinates(GVol.geom.GeometryLayout.XY, null, this.ends_);
  } else {
    this.setLayout(opt_layout, coordinates, 2);
    if (!this.flatCoordinates) {
      this.flatCoordinates = [];
    }
    var ends = GVol.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 {GVol.geom.GeometryLayout} layout Layout.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {Array.<number>} ends Ends.
 */
GVol.geom.MultiLineString.prototype.setFlatCoordinates = function(layout, flatCoordinates, ends) {
  this.setFlatCoordinatesInternal(layout, flatCoordinates);
  this.ends_ = ends;
  this.changed();
};


/**
 * @param {Array.<GVol.geom.LineString>} lineStrings LineStrings.
 */
GVol.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();
    }
    GVol.array.extend(flatCoordinates, lineString.getFlatCoordinates());
    ends.push(flatCoordinates.length);
  }
  this.setFlatCoordinates(layout, flatCoordinates, ends);
};

goog.provide('GVol.geom.MultiPoint');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.extent');
goog.require('GVol.geom.GeometryLayout');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.geom.Point');
goog.require('GVol.geom.SimpleGeometry');
goog.require('GVol.geom.flat.deflate');
goog.require('GVol.geom.flat.inflate');
goog.require('GVol.math');


/**
 * @classdesc
 * Multi-point geometry.
 *
 * @constructor
 * @extends {GVol.geom.SimpleGeometry}
 * @param {Array.<GVol.Coordinate>} coordinates Coordinates.
 * @param {GVol.geom.GeometryLayout=} opt_layout Layout.
 * @api
 */
GVol.geom.MultiPoint = function(coordinates, opt_layout) {
  GVol.geom.SimpleGeometry.call(this);
  this.setCoordinates(coordinates, opt_layout);
};
GVol.inherits(GVol.geom.MultiPoint, GVol.geom.SimpleGeometry);


/**
 * Append the passed point to this multipoint.
 * @param {GVol.geom.Point} point Point.
 * @api
 */
GVol.geom.MultiPoint.prototype.appendPoint = function(point) {
  if (!this.flatCoordinates) {
    this.flatCoordinates = point.getFlatCoordinates().slice();
  } else {
    GVol.array.extend(this.flatCoordinates, point.getFlatCoordinates());
  }
  this.changed();
};


/**
 * Make a complete copy of the geometry.
 * @return {!GVol.geom.MultiPoint} Clone.
 * @override
 * @api
 */
GVol.geom.MultiPoint.prototype.clone = function() {
  var multiPoint = new GVol.geom.MultiPoint(null);
  multiPoint.setFlatCoordinates(this.layout, this.flatCoordinates.slice());
  return multiPoint;
};


/**
 * @inheritDoc
 */
GVol.geom.MultiPoint.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
  if (minSquaredDistance <
      GVol.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 = GVol.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.<GVol.Coordinate>} Coordinates.
 * @override
 * @api
 */
GVol.geom.MultiPoint.prototype.getCoordinates = function() {
  return GVol.geom.flat.inflate.coordinates(
      this.flatCoordinates, 0, this.flatCoordinates.length, this.stride);
};


/**
 * Return the point at the specified index.
 * @param {number} index Index.
 * @return {GVol.geom.Point} Point.
 * @api
 */
GVol.geom.MultiPoint.prototype.getPoint = function(index) {
  var n = !this.flatCoordinates ?
    0 : this.flatCoordinates.length / this.stride;
  if (index < 0 || n <= index) {
    return null;
  }
  var point = new GVol.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.<GVol.geom.Point>} Points.
 * @api
 */
GVol.geom.MultiPoint.prototype.getPoints = function() {
  var flatCoordinates = this.flatCoordinates;
  var layout = this.layout;
  var stride = this.stride;
  /** @type {Array.<GVol.geom.Point>} */
  var points = [];
  var i, ii;
  for (i = 0, ii = flatCoordinates.length; i < ii; i += stride) {
    var point = new GVol.geom.Point(null);
    point.setFlatCoordinates(layout, flatCoordinates.slice(i, i + stride));
    points.push(point);
  }
  return points;
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.MultiPoint.prototype.getType = function() {
  return GVol.geom.GeometryType.MULTI_POINT;
};


/**
 * @inheritDoc
 * @api
 */
GVol.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 (GVol.extent.containsXY(extent, x, y)) {
      return true;
    }
  }
  return false;
};


/**
 * Set the coordinates of the multipoint.
 * @param {Array.<GVol.Coordinate>} coordinates Coordinates.
 * @param {GVol.geom.GeometryLayout=} opt_layout Layout.
 * @override
 * @api
 */
GVol.geom.MultiPoint.prototype.setCoordinates = function(coordinates, opt_layout) {
  if (!coordinates) {
    this.setFlatCoordinates(GVol.geom.GeometryLayout.XY, null);
  } else {
    this.setLayout(opt_layout, coordinates, 1);
    if (!this.flatCoordinates) {
      this.flatCoordinates = [];
    }
    this.flatCoordinates.length = GVol.geom.flat.deflate.coordinates(
        this.flatCoordinates, 0, coordinates, this.stride);
    this.changed();
  }
};


/**
 * @param {GVol.geom.GeometryLayout} layout Layout.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 */
GVol.geom.MultiPoint.prototype.setFlatCoordinates = function(layout, flatCoordinates) {
  this.setFlatCoordinatesInternal(layout, flatCoordinates);
  this.changed();
};

goog.provide('GVol.geom.flat.center');

goog.require('GVol.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.
 */
GVol.geom.flat.center.linearRingss = function(flatCoordinates, offset, endss, stride) {
  var flatCenters = [];
  var i, ii;
  var extent = GVol.extent.createEmpty();
  for (i = 0, ii = endss.length; i < ii; ++i) {
    var ends = endss[i];
    extent = GVol.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('GVol.geom.MultiPGVolygon');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.extent');
goog.require('GVol.geom.GeometryLayout');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.geom.MultiPoint');
goog.require('GVol.geom.PGVolygon');
goog.require('GVol.geom.SimpleGeometry');
goog.require('GVol.geom.flat.area');
goog.require('GVol.geom.flat.center');
goog.require('GVol.geom.flat.closest');
goog.require('GVol.geom.flat.contains');
goog.require('GVol.geom.flat.deflate');
goog.require('GVol.geom.flat.inflate');
goog.require('GVol.geom.flat.interiorpoint');
goog.require('GVol.geom.flat.intersectsextent');
goog.require('GVol.geom.flat.orient');
goog.require('GVol.geom.flat.simplify');


/**
 * @classdesc
 * Multi-pGVolygon geometry.
 *
 * @constructor
 * @extends {GVol.geom.SimpleGeometry}
 * @param {Array.<Array.<Array.<GVol.Coordinate>>>} coordinates Coordinates.
 * @param {GVol.geom.GeometryLayout=} opt_layout Layout.
 * @api
 */
GVol.geom.MultiPGVolygon = function(coordinates, opt_layout) {

  GVol.geom.SimpleGeometry.call(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);

};
GVol.inherits(GVol.geom.MultiPGVolygon, GVol.geom.SimpleGeometry);


/**
 * Append the passed pGVolygon to this multipGVolygon.
 * @param {GVol.geom.PGVolygon} pGVolygon PGVolygon.
 * @api
 */
GVol.geom.MultiPGVolygon.prototype.appendPGVolygon = function(pGVolygon) {
  /** @type {Array.<number>} */
  var ends;
  if (!this.flatCoordinates) {
    this.flatCoordinates = pGVolygon.getFlatCoordinates().slice();
    ends = pGVolygon.getEnds().slice();
    this.endss_.push();
  } else {
    var offset = this.flatCoordinates.length;
    GVol.array.extend(this.flatCoordinates, pGVolygon.getFlatCoordinates());
    ends = pGVolygon.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 {!GVol.geom.MultiPGVolygon} Clone.
 * @override
 * @api
 */
GVol.geom.MultiPGVolygon.prototype.clone = function() {
  var multiPGVolygon = new GVol.geom.MultiPGVolygon(null);

  var len = this.endss_.length;
  var newEndss = new Array(len);
  for (var i = 0; i < len; ++i) {
    newEndss[i] = this.endss_[i].slice();
  }

  multiPGVolygon.setFlatCoordinates(
      this.layout, this.flatCoordinates.slice(), newEndss);
  return multiPGVolygon;
};


/**
 * @inheritDoc
 */
GVol.geom.MultiPGVolygon.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
  if (minSquaredDistance <
      GVol.extent.closestSquaredDistanceXY(this.getExtent(), x, y)) {
    return minSquaredDistance;
  }
  if (this.maxDeltaRevision_ != this.getRevision()) {
    this.maxDelta_ = Math.sqrt(GVol.geom.flat.closest.getssMaxSquaredDelta(
        this.flatCoordinates, 0, this.endss_, this.stride, 0));
    this.maxDeltaRevision_ = this.getRevision();
  }
  return GVol.geom.flat.closest.getssClosestPoint(
      this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride,
      this.maxDelta_, true, x, y, closestPoint, minSquaredDistance);
};


/**
 * @inheritDoc
 */
GVol.geom.MultiPGVolygon.prototype.containsXY = function(x, y) {
  return GVol.geom.flat.contains.linearRingssContainsXY(
      this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride, x, y);
};


/**
 * Return the area of the multipGVolygon on projected plane.
 * @return {number} Area (on projected plane).
 * @api
 */
GVol.geom.MultiPGVolygon.prototype.getArea = function() {
  return GVol.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-pGVolygons.
 *
 * @param {boGVolean=} 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.<GVol.Coordinate>>>} Coordinates.
 * @override
 * @api
 */
GVol.geom.MultiPGVolygon.prototype.getCoordinates = function(opt_right) {
  var flatCoordinates;
  if (opt_right !== undefined) {
    flatCoordinates = this.getOrientedFlatCoordinates().slice();
    GVol.geom.flat.orient.orientLinearRingss(
        flatCoordinates, 0, this.endss_, this.stride, opt_right);
  } else {
    flatCoordinates = this.flatCoordinates;
  }

  return GVol.geom.flat.inflate.coordinatesss(
      flatCoordinates, 0, this.endss_, this.stride);
};


/**
 * @return {Array.<Array.<number>>} Endss.
 */
GVol.geom.MultiPGVolygon.prototype.getEndss = function() {
  return this.endss_;
};


/**
 * @return {Array.<number>} Flat interior points.
 */
GVol.geom.MultiPGVolygon.prototype.getFlatInteriorPoints = function() {
  if (this.flatInteriorPointsRevision_ != this.getRevision()) {
    var flatCenters = GVol.geom.flat.center.linearRingss(
        this.flatCoordinates, 0, this.endss_, this.stride);
    this.flatInteriorPoints_ = GVol.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 GVol.geom.MultiPoint multipoint}.
 * @return {GVol.geom.MultiPoint} Interior points.
 * @api
 */
GVol.geom.MultiPGVolygon.prototype.getInteriorPoints = function() {
  var interiorPoints = new GVol.geom.MultiPoint(null);
  interiorPoints.setFlatCoordinates(GVol.geom.GeometryLayout.XY,
      this.getFlatInteriorPoints().slice());
  return interiorPoints;
};


/**
 * @return {Array.<number>} Oriented flat coordinates.
 */
GVol.geom.MultiPGVolygon.prototype.getOrientedFlatCoordinates = function() {
  if (this.orientedRevision_ != this.getRevision()) {
    var flatCoordinates = this.flatCoordinates;
    if (GVol.geom.flat.orient.linearRingssAreOriented(
        flatCoordinates, 0, this.endss_, this.stride)) {
      this.orientedFlatCoordinates_ = flatCoordinates;
    } else {
      this.orientedFlatCoordinates_ = flatCoordinates.slice();
      this.orientedFlatCoordinates_.length =
          GVol.geom.flat.orient.orientLinearRingss(
              this.orientedFlatCoordinates_, 0, this.endss_, this.stride);
    }
    this.orientedRevision_ = this.getRevision();
  }
  return this.orientedFlatCoordinates_;
};


/**
 * @inheritDoc
 */
GVol.geom.MultiPGVolygon.prototype.getSimplifiedGeometryInternal = function(squaredTGVolerance) {
  var simplifiedFlatCoordinates = [];
  var simplifiedEndss = [];
  simplifiedFlatCoordinates.length = GVol.geom.flat.simplify.quantizess(
      this.flatCoordinates, 0, this.endss_, this.stride,
      Math.sqrt(squaredTGVolerance),
      simplifiedFlatCoordinates, 0, simplifiedEndss);
  var simplifiedMultiPGVolygon = new GVol.geom.MultiPGVolygon(null);
  simplifiedMultiPGVolygon.setFlatCoordinates(
      GVol.geom.GeometryLayout.XY, simplifiedFlatCoordinates, simplifiedEndss);
  return simplifiedMultiPGVolygon;
};


/**
 * Return the pGVolygon at the specified index.
 * @param {number} index Index.
 * @return {GVol.geom.PGVolygon} PGVolygon.
 * @api
 */
GVol.geom.MultiPGVolygon.prototype.getPGVolygon = function(index) {
  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 pGVolygon = new GVol.geom.PGVolygon(null);
  pGVolygon.setFlatCoordinates(
      this.layout, this.flatCoordinates.slice(offset, end), ends);
  return pGVolygon;
};


/**
 * Return the pGVolygons of this multipGVolygon.
 * @return {Array.<GVol.geom.PGVolygon>} PGVolygons.
 * @api
 */
GVol.geom.MultiPGVolygon.prototype.getPGVolygons = function() {
  var layout = this.layout;
  var flatCoordinates = this.flatCoordinates;
  var endss = this.endss_;
  var pGVolygons = [];
  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 pGVolygon = new GVol.geom.PGVolygon(null);
    pGVolygon.setFlatCoordinates(
        layout, flatCoordinates.slice(offset, end), ends);
    pGVolygons.push(pGVolygon);
    offset = end;
  }
  return pGVolygons;
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.MultiPGVolygon.prototype.getType = function() {
  return GVol.geom.GeometryType.MULTI_POLYGON;
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.MultiPGVolygon.prototype.intersectsExtent = function(extent) {
  return GVol.geom.flat.intersectsextent.linearRingss(
      this.getOrientedFlatCoordinates(), 0, this.endss_, this.stride, extent);
};


/**
 * Set the coordinates of the multipGVolygon.
 * @param {Array.<Array.<Array.<GVol.Coordinate>>>} coordinates Coordinates.
 * @param {GVol.geom.GeometryLayout=} opt_layout Layout.
 * @override
 * @api
 */
GVol.geom.MultiPGVolygon.prototype.setCoordinates = function(coordinates, opt_layout) {
  if (!coordinates) {
    this.setFlatCoordinates(GVol.geom.GeometryLayout.XY, null, this.endss_);
  } else {
    this.setLayout(opt_layout, coordinates, 3);
    if (!this.flatCoordinates) {
      this.flatCoordinates = [];
    }
    var endss = GVol.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 {GVol.geom.GeometryLayout} layout Layout.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {Array.<Array.<number>>} endss Endss.
 */
GVol.geom.MultiPGVolygon.prototype.setFlatCoordinates = function(layout, flatCoordinates, endss) {
  this.setFlatCoordinatesInternal(layout, flatCoordinates);
  this.endss_ = endss;
  this.changed();
};


/**
 * @param {Array.<GVol.geom.PGVolygon>} pGVolygons PGVolygons.
 */
GVol.geom.MultiPGVolygon.prototype.setPGVolygons = function(pGVolygons) {
  var layout = this.getLayout();
  var flatCoordinates = [];
  var endss = [];
  var i, ii, ends;
  for (i = 0, ii = pGVolygons.length; i < ii; ++i) {
    var pGVolygon = pGVolygons[i];
    if (i === 0) {
      layout = pGVolygon.getLayout();
    }
    var offset = flatCoordinates.length;
    ends = pGVolygon.getEnds();
    var j, jj;
    for (j = 0, jj = ends.length; j < jj; ++j) {
      ends[j] += offset;
    }
    GVol.array.extend(flatCoordinates, pGVolygon.getFlatCoordinates());
    endss.push(ends);
  }
  this.setFlatCoordinates(layout, flatCoordinates, endss);
};

goog.provide('GVol.format.EsriJSON');

goog.require('GVol');
goog.require('GVol.Feature');
goog.require('GVol.asserts');
goog.require('GVol.extent');
goog.require('GVol.format.Feature');
goog.require('GVol.format.JSONFeature');
goog.require('GVol.geom.GeometryLayout');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.geom.LineString');
goog.require('GVol.geom.LinearRing');
goog.require('GVol.geom.MultiLineString');
goog.require('GVol.geom.MultiPoint');
goog.require('GVol.geom.MultiPGVolygon');
goog.require('GVol.geom.Point');
goog.require('GVol.geom.PGVolygon');
goog.require('GVol.geom.flat.deflate');
goog.require('GVol.geom.flat.orient');
goog.require('GVol.obj');
goog.require('GVol.proj');


/**
 * @classdesc
 * Feature format for reading and writing data in the EsriJSON format.
 *
 * @constructor
 * @extends {GVol.format.JSONFeature}
 * @param {GVolx.format.EsriJSONOptions=} opt_options Options.
 * @api
 */
GVol.format.EsriJSON = function(opt_options) {

  var options = opt_options ? opt_options : {};

  GVol.format.JSONFeature.call(this);

  /**
   * Name of the geometry attribute for features.
   * @type {string|undefined}
   * @private
   */
  this.geometryName_ = options.geometryName;

};
GVol.inherits(GVol.format.EsriJSON, GVol.format.JSONFeature);


/**
 * @param {EsriJSONGeometry} object Object.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @private
 * @return {GVol.geom.Geometry} Geometry.
 */
GVol.format.EsriJSON.readGeometry_ = function(object, opt_options) {
  if (!object) {
    return null;
  }
  /** @type {GVol.geom.GeometryType} */
  var type;
  if (typeof object.x === 'number' && typeof object.y === 'number') {
    type = GVol.geom.GeometryType.POINT;
  } else if (object.points) {
    type = GVol.geom.GeometryType.MULTI_POINT;
  } else if (object.paths) {
    if (object.paths.length === 1) {
      type = GVol.geom.GeometryType.LINE_STRING;
    } else {
      type = GVol.geom.GeometryType.MULTI_LINE_STRING;
    }
  } else if (object.rings) {
    var layout = GVol.format.EsriJSON.getGeometryLayout_(object);
    var rings = GVol.format.EsriJSON.convertRings_(object.rings, layout);
    object = /** @type {EsriJSONGeometry} */(GVol.obj.assign({}, object));
    if (rings.length === 1) {
      type = GVol.geom.GeometryType.POLYGON;
      object.rings = rings[0];
    } else {
      type = GVol.geom.GeometryType.MULTI_POLYGON;
      object.rings = rings;
    }
  }
  var geometryReader = GVol.format.EsriJSON.GEOMETRY_READERS_[type];
  return /** @type {GVol.geom.Geometry} */ (
    GVol.format.Feature.transformWithOptions(
        geometryReader(object), false, opt_options));
};


/**
 * Determines inner and outer rings.
 * Checks if any pGVolygons in this array contain any other pGVolygons in this
 * array. It is used for checking for hGVoles.
 * Logic inspired by: https://github.com/Esri/terraformer-arcgis-parser
 * @param {Array.<!Array.<!Array.<number>>>} rings Rings.
 * @param {GVol.geom.GeometryLayout} layout Geometry layout.
 * @private
 * @return {Array.<!Array.<!Array.<number>>>} Transformed rings.
 */
GVol.format.EsriJSON.convertRings_ = function(rings, layout) {
  var flatRing = [];
  var outerRings = [];
  var hGVoles = [];
  var i, ii;
  for (i = 0, ii = rings.length; i < ii; ++i) {
    flatRing.length = 0;
    GVol.geom.flat.deflate.coordinates(flatRing, 0, rings[i], layout.length);
    // is this ring an outer ring? is it clockwise?
    var clockwise = GVol.geom.flat.orient.linearRingIsClockwise(flatRing, 0,
        flatRing.length, layout.length);
    if (clockwise) {
      outerRings.push([rings[i]]);
    } else {
      hGVoles.push(rings[i]);
    }
  }
  while (hGVoles.length) {
    var hGVole = hGVoles.shift();
    var matched = false;
    // loop over all outer rings and see if they contain our hGVole.
    for (i = outerRings.length - 1; i >= 0; i--) {
      var outerRing = outerRings[i][0];
      var containsHGVole = GVol.extent.containsExtent(
          new GVol.geom.LinearRing(outerRing).getExtent(),
          new GVol.geom.LinearRing(hGVole).getExtent()
      );
      if (containsHGVole) {
        // the hGVole is contained push it into our pGVolygon
        outerRings[i].push(hGVole);
        matched = true;
        break;
      }
    }
    if (!matched) {
      // no outer rings contain this hGVole turn it into and outer
      // ring (reverse it)
      outerRings.push([hGVole.reverse()]);
    }
  }
  return outerRings;
};


/**
 * @param {EsriJSONGeometry} object Object.
 * @private
 * @return {GVol.geom.Geometry} Point.
 */
GVol.format.EsriJSON.readPointGeometry_ = function(object) {
  var point;
  if (object.m !== undefined && object.z !== undefined) {
    point = new GVol.geom.Point([object.x, object.y, object.z, object.m],
        GVol.geom.GeometryLayout.XYZM);
  } else if (object.z !== undefined) {
    point = new GVol.geom.Point([object.x, object.y, object.z],
        GVol.geom.GeometryLayout.XYZ);
  } else if (object.m !== undefined) {
    point = new GVol.geom.Point([object.x, object.y, object.m],
        GVol.geom.GeometryLayout.XYM);
  } else {
    point = new GVol.geom.Point([object.x, object.y]);
  }
  return point;
};


/**
 * @param {EsriJSONGeometry} object Object.
 * @private
 * @return {GVol.geom.Geometry} LineString.
 */
GVol.format.EsriJSON.readLineStringGeometry_ = function(object) {
  var layout = GVol.format.EsriJSON.getGeometryLayout_(object);
  return new GVol.geom.LineString(object.paths[0], layout);
};


/**
 * @param {EsriJSONGeometry} object Object.
 * @private
 * @return {GVol.geom.Geometry} MultiLineString.
 */
GVol.format.EsriJSON.readMultiLineStringGeometry_ = function(object) {
  var layout = GVol.format.EsriJSON.getGeometryLayout_(object);
  return new GVol.geom.MultiLineString(object.paths, layout);
};


/**
 * @param {EsriJSONGeometry} object Object.
 * @private
 * @return {GVol.geom.GeometryLayout} The geometry layout to use.
 */
GVol.format.EsriJSON.getGeometryLayout_ = function(object) {
  var layout = GVol.geom.GeometryLayout.XY;
  if (object.hasZ === true && object.hasM === true) {
    layout = GVol.geom.GeometryLayout.XYZM;
  } else if (object.hasZ === true) {
    layout = GVol.geom.GeometryLayout.XYZ;
  } else if (object.hasM === true) {
    layout = GVol.geom.GeometryLayout.XYM;
  }
  return layout;
};


/**
 * @param {EsriJSONGeometry} object Object.
 * @private
 * @return {GVol.geom.Geometry} MultiPoint.
 */
GVol.format.EsriJSON.readMultiPointGeometry_ = function(object) {
  var layout = GVol.format.EsriJSON.getGeometryLayout_(object);
  return new GVol.geom.MultiPoint(object.points, layout);
};


/**
 * @param {EsriJSONGeometry} object Object.
 * @private
 * @return {GVol.geom.Geometry} MultiPGVolygon.
 */
GVol.format.EsriJSON.readMultiPGVolygonGeometry_ = function(object) {
  var layout = GVol.format.EsriJSON.getGeometryLayout_(object);
  return new GVol.geom.MultiPGVolygon(
      /** @type {Array.<Array.<Array.<Array.<number>>>>} */(object.rings),
      layout);
};


/**
 * @param {EsriJSONGeometry} object Object.
 * @private
 * @return {GVol.geom.Geometry} PGVolygon.
 */
GVol.format.EsriJSON.readPGVolygonGeometry_ = function(object) {
  var layout = GVol.format.EsriJSON.getGeometryLayout_(object);
  return new GVol.geom.PGVolygon(object.rings, layout);
};


/**
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {EsriJSONGeometry} EsriJSON geometry.
 */
GVol.format.EsriJSON.writePointGeometry_ = function(geometry, opt_options) {
  var coordinates = /** @type {GVol.geom.Point} */ (geometry).getCoordinates();
  var esriJSON;
  var layout = /** @type {GVol.geom.Point} */ (geometry).getLayout();
  if (layout === GVol.geom.GeometryLayout.XYZ) {
    esriJSON = /** @type {EsriJSONPoint} */ ({
      x: coordinates[0],
      y: coordinates[1],
      z: coordinates[2]
    });
  } else if (layout === GVol.geom.GeometryLayout.XYM) {
    esriJSON = /** @type {EsriJSONPoint} */ ({
      x: coordinates[0],
      y: coordinates[1],
      m: coordinates[2]
    });
  } else if (layout === GVol.geom.GeometryLayout.XYZM) {
    esriJSON = /** @type {EsriJSONPoint} */ ({
      x: coordinates[0],
      y: coordinates[1],
      z: coordinates[2],
      m: coordinates[3]
    });
  } else if (layout === GVol.geom.GeometryLayout.XY) {
    esriJSON = /** @type {EsriJSONPoint} */ ({
      x: coordinates[0],
      y: coordinates[1]
    });
  } else {
    GVol.asserts.assert(false, 34); // Invalid geometry layout
  }
  return /** @type {EsriJSONGeometry} */ (esriJSON);
};


/**
 * @param {GVol.geom.SimpleGeometry} geometry Geometry.
 * @private
 * @return {Object} Object with boGVolean hasZ and hasM keys.
 */
GVol.format.EsriJSON.getHasZM_ = function(geometry) {
  var layout = geometry.getLayout();
  return {
    hasZ: (layout === GVol.geom.GeometryLayout.XYZ ||
      layout === GVol.geom.GeometryLayout.XYZM),
    hasM: (layout === GVol.geom.GeometryLayout.XYM ||
      layout === GVol.geom.GeometryLayout.XYZM)
  };
};


/**
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {EsriJSONPGVolyline} EsriJSON geometry.
 */
GVol.format.EsriJSON.writeLineStringGeometry_ = function(geometry, opt_options) {
  var hasZM = GVol.format.EsriJSON.getHasZM_(/** @type {GVol.geom.LineString} */(geometry));
  return /** @type {EsriJSONPGVolyline} */ ({
    hasZ: hasZM.hasZ,
    hasM: hasZM.hasM,
    paths: [
      /** @type {GVol.geom.LineString} */ (geometry).getCoordinates()
    ]
  });
};


/**
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {EsriJSONPGVolygon} EsriJSON geometry.
 */
GVol.format.EsriJSON.writePGVolygonGeometry_ = function(geometry, opt_options) {
  // Esri geometries use the left-hand rule
  var hasZM = GVol.format.EsriJSON.getHasZM_(/** @type {GVol.geom.PGVolygon} */(geometry));
  return /** @type {EsriJSONPGVolygon} */ ({
    hasZ: hasZM.hasZ,
    hasM: hasZM.hasM,
    rings: /** @type {GVol.geom.PGVolygon} */ (geometry).getCoordinates(false)
  });
};


/**
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {EsriJSONPGVolyline} EsriJSON geometry.
 */
GVol.format.EsriJSON.writeMultiLineStringGeometry_ = function(geometry, opt_options) {
  var hasZM = GVol.format.EsriJSON.getHasZM_(/** @type {GVol.geom.MultiLineString} */(geometry));
  return /** @type {EsriJSONPGVolyline} */ ({
    hasZ: hasZM.hasZ,
    hasM: hasZM.hasM,
    paths: /** @type {GVol.geom.MultiLineString} */ (geometry).getCoordinates()
  });
};


/**
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {EsriJSONMultipoint} EsriJSON geometry.
 */
GVol.format.EsriJSON.writeMultiPointGeometry_ = function(geometry, opt_options) {
  var hasZM = GVol.format.EsriJSON.getHasZM_(/** @type {GVol.geom.MultiPoint} */(geometry));
  return /** @type {EsriJSONMultipoint} */ ({
    hasZ: hasZM.hasZ,
    hasM: hasZM.hasM,
    points: /** @type {GVol.geom.MultiPoint} */ (geometry).getCoordinates()
  });
};


/**
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {EsriJSONPGVolygon} EsriJSON geometry.
 */
GVol.format.EsriJSON.writeMultiPGVolygonGeometry_ = function(geometry,
    opt_options) {
  var hasZM = GVol.format.EsriJSON.getHasZM_(/** @type {GVol.geom.MultiPGVolygon} */(geometry));
  var coordinates = /** @type {GVol.geom.MultiPGVolygon} */ (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 {EsriJSONPGVolygon} */ ({
    hasZ: hasZM.hasZ,
    hasM: hasZM.hasM,
    rings: output
  });
};


/**
 * @const
 * @private
 * @type {Object.<GVol.geom.GeometryType, function(EsriJSONGeometry): GVol.geom.Geometry>}
 */
GVol.format.EsriJSON.GEOMETRY_READERS_ = {};
GVol.format.EsriJSON.GEOMETRY_READERS_[GVol.geom.GeometryType.POINT] =
  GVol.format.EsriJSON.readPointGeometry_;
GVol.format.EsriJSON.GEOMETRY_READERS_[GVol.geom.GeometryType.LINE_STRING] =
  GVol.format.EsriJSON.readLineStringGeometry_;
GVol.format.EsriJSON.GEOMETRY_READERS_[GVol.geom.GeometryType.POLYGON] =
  GVol.format.EsriJSON.readPGVolygonGeometry_;
GVol.format.EsriJSON.GEOMETRY_READERS_[GVol.geom.GeometryType.MULTI_POINT] =
  GVol.format.EsriJSON.readMultiPointGeometry_;
GVol.format.EsriJSON.GEOMETRY_READERS_[GVol.geom.GeometryType.MULTI_LINE_STRING] =
  GVol.format.EsriJSON.readMultiLineStringGeometry_;
GVol.format.EsriJSON.GEOMETRY_READERS_[GVol.geom.GeometryType.MULTI_POLYGON] =
  GVol.format.EsriJSON.readMultiPGVolygonGeometry_;


/**
 * @const
 * @private
 * @type {Object.<string, function(GVol.geom.Geometry, GVolx.format.WriteOptions=): (EsriJSONGeometry)>}
 */
GVol.format.EsriJSON.GEOMETRY_WRITERS_ = {};
GVol.format.EsriJSON.GEOMETRY_WRITERS_[GVol.geom.GeometryType.POINT] =
  GVol.format.EsriJSON.writePointGeometry_;
GVol.format.EsriJSON.GEOMETRY_WRITERS_[GVol.geom.GeometryType.LINE_STRING] =
  GVol.format.EsriJSON.writeLineStringGeometry_;
GVol.format.EsriJSON.GEOMETRY_WRITERS_[GVol.geom.GeometryType.POLYGON] =
  GVol.format.EsriJSON.writePGVolygonGeometry_;
GVol.format.EsriJSON.GEOMETRY_WRITERS_[GVol.geom.GeometryType.MULTI_POINT] =
  GVol.format.EsriJSON.writeMultiPointGeometry_;
GVol.format.EsriJSON.GEOMETRY_WRITERS_[GVol.geom.GeometryType.MULTI_LINE_STRING] =
  GVol.format.EsriJSON.writeMultiLineStringGeometry_;
GVol.format.EsriJSON.GEOMETRY_WRITERS_[GVol.geom.GeometryType.MULTI_POLYGON] =
  GVol.format.EsriJSON.writeMultiPGVolygonGeometry_;


/**
 * Read a feature from a EsriJSON Feature source.  Only works for Feature,
 * use `readFeatures` to read FeatureCGVollection source.
 *
 * @function
 * @param {ArrayBuffer|Document|Node|Object|string} source Source.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {GVol.Feature} Feature.
 * @api
 */
GVol.format.EsriJSON.prototype.readFeature;


/**
 * Read all features from a EsriJSON source.  Works with both Feature and
 * FeatureCGVollection sources.
 *
 * @function
 * @param {ArrayBuffer|Document|Node|Object|string} source Source.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<GVol.Feature>} Features.
 * @api
 */
GVol.format.EsriJSON.prototype.readFeatures;


/**
 * @inheritDoc
 */
GVol.format.EsriJSON.prototype.readFeatureFromObject = function(
    object, opt_options) {
  var esriJSONFeature = /** @type {EsriJSONFeature} */ (object);
  var geometry = GVol.format.EsriJSON.readGeometry_(esriJSONFeature.geometry,
      opt_options);
  var feature = new GVol.Feature();
  if (this.geometryName_) {
    feature.setGeometryName(this.geometryName_);
  }
  feature.setGeometry(geometry);
  if (opt_options && opt_options.idField &&
    esriJSONFeature.attributes[opt_options.idField]) {
    feature.setId(/** @type {number} */(
      esriJSONFeature.attributes[opt_options.idField]));
  }
  if (esriJSONFeature.attributes) {
    feature.setProperties(esriJSONFeature.attributes);
  }
  return feature;
};


/**
 * @inheritDoc
 */
GVol.format.EsriJSON.prototype.readFeaturesFromObject = function(
    object, opt_options) {
  var esriJSONObject = /** @type {EsriJSONObject} */ (object);
  var options = opt_options ? opt_options : {};
  if (esriJSONObject.features) {
    var esriJSONFeatureCGVollection = /** @type {EsriJSONFeatureCGVollection} */
      (object);
    /** @type {Array.<GVol.Feature>} */
    var features = [];
    var esriJSONFeatures = esriJSONFeatureCGVollection.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 {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {GVol.geom.Geometry} Geometry.
 * @api
 */
GVol.format.EsriJSON.prototype.readGeometry;


/**
 * @inheritDoc
 */
GVol.format.EsriJSON.prototype.readGeometryFromObject = function(
    object, opt_options) {
  return GVol.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 {GVol.proj.Projection} Projection.
 * @api
 */
GVol.format.EsriJSON.prototype.readProjection;


/**
 * @inheritDoc
 */
GVol.format.EsriJSON.prototype.readProjectionFromObject = function(object) {
  var esriJSONObject = /** @type {EsriJSONObject} */ (object);
  if (esriJSONObject.spatialReference && esriJSONObject.spatialReference.wkid) {
    var crs = esriJSONObject.spatialReference.wkid;
    return GVol.proj.get('EPSG:' + crs);
  } else {
    return null;
  }
};


/**
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {EsriJSONGeometry} EsriJSON geometry.
 */
GVol.format.EsriJSON.writeGeometry_ = function(geometry, opt_options) {
  var geometryWriter = GVol.format.EsriJSON.GEOMETRY_WRITERS_[geometry.getType()];
  return geometryWriter(/** @type {GVol.geom.Geometry} */(
    GVol.format.Feature.transformWithOptions(geometry, true, opt_options)),
  opt_options);
};


/**
 * Encode a geometry as a EsriJSON string.
 *
 * @function
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {string} EsriJSON.
 * @api
 */
GVol.format.EsriJSON.prototype.writeGeometry;


/**
 * Encode a geometry as a EsriJSON object.
 *
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {EsriJSONGeometry} Object.
 * @override
 * @api
 */
GVol.format.EsriJSON.prototype.writeGeometryObject = function(geometry,
    opt_options) {
  return GVol.format.EsriJSON.writeGeometry_(geometry,
      this.adaptOptions(opt_options));
};


/**
 * Encode a feature as a EsriJSON Feature string.
 *
 * @function
 * @param {GVol.Feature} feature Feature.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {string} EsriJSON.
 * @api
 */
GVol.format.EsriJSON.prototype.writeFeature;


/**
 * Encode a feature as a esriJSON Feature object.
 *
 * @param {GVol.Feature} feature Feature.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {Object} Object.
 * @override
 * @api
 */
GVol.format.EsriJSON.prototype.writeFeatureObject = function(
    feature, opt_options) {
  opt_options = this.adaptOptions(opt_options);
  var object = {};
  var geometry = feature.getGeometry();
  if (geometry) {
    object['geometry'] =
      GVol.format.EsriJSON.writeGeometry_(geometry, opt_options);
    if (opt_options && opt_options.featureProjection) {
      object['geometry']['spatialReference'] = /** @type {EsriJSONCRS} */({
        wkid: GVol.proj.get(
            opt_options.featureProjection).getCode().split(':').pop()
      });
    }
  }
  var properties = feature.getProperties();
  delete properties[feature.getGeometryName()];
  if (!GVol.obj.isEmpty(properties)) {
    object['attributes'] = properties;
  } else {
    object['attributes'] = {};
  }
  return object;
};


/**
 * Encode an array of features as EsriJSON.
 *
 * @function
 * @param {Array.<GVol.Feature>} features Features.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {string} EsriJSON.
 * @api
 */
GVol.format.EsriJSON.prototype.writeFeatures;


/**
 * Encode an array of features as a EsriJSON object.
 *
 * @param {Array.<GVol.Feature>} features Features.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {Object} EsriJSON Object.
 * @override
 * @api
 */
GVol.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 {EsriJSONFeatureCGVollection} */ ({
    'features': objects
  });
};

goog.provide('GVol.format.filter.Filter');


/**
 * @classdesc
 * Abstract class; normally only used for creating subclasses and not instantiated in apps.
 * Base class for WFS GetFeature filters.
 *
 * @constructor
 * @param {!string} tagName The XML tag name for this filter.
 * @struct
 * @api
 */
GVol.format.filter.Filter = function(tagName) {

  /**
   * @private
   * @type {!string}
   */
  this.tagName_ = tagName;
};

/**
 * The XML tag name for a filter.
 * @returns {!string} Name.
 */
GVol.format.filter.Filter.prototype.getTagName = function() {
  return this.tagName_;
};

goog.provide('GVol.format.filter.LogicalNary');

goog.require('GVol');
goog.require('GVol.asserts');
goog.require('GVol.format.filter.Filter');


/**
 * @classdesc
 * Abstract class; normally only used for creating subclasses and not instantiated in apps.
 * Base class for WFS GetFeature n-ary logical filters.
 *
 * @constructor
 * @param {!string} tagName The XML tag name for this filter.
 * @param {...GVol.format.filter.Filter} conditions Conditions.
 * @extends {GVol.format.filter.Filter}
 */
GVol.format.filter.LogicalNary = function(tagName, conditions) {

  GVol.format.filter.Filter.call(this, tagName);

  /**
   * @public
   * @type {Array.<GVol.format.filter.Filter>}
   */
  this.conditions = Array.prototype.slice.call(arguments, 1);
  GVol.asserts.assert(this.conditions.length >= 2, 57); // At least 2 conditions are required.
};
GVol.inherits(GVol.format.filter.LogicalNary, GVol.format.filter.Filter);

goog.provide('GVol.format.filter.And');

goog.require('GVol');
goog.require('GVol.format.filter.LogicalNary');

/**
 * @classdesc
 * Represents a logical `<And>` operator between two or more filter conditions.
 *
 * @constructor
 * @param {...GVol.format.filter.Filter} conditions Conditions.
 * @extends {GVol.format.filter.LogicalNary}
 * @api
 */
GVol.format.filter.And = function(conditions) {
  var params = ['And'].concat(Array.prototype.slice.call(arguments));
  GVol.format.filter.LogicalNary.apply(this, params);
};
GVol.inherits(GVol.format.filter.And, GVol.format.filter.LogicalNary);

goog.provide('GVol.format.filter.Bbox');

goog.require('GVol');
goog.require('GVol.format.filter.Filter');


/**
 * @classdesc
 * Represents a `<BBOX>` operator to test whether a geometry-valued property
 * intersects a fixed bounding box
 *
 * @constructor
 * @param {!string} geometryName Geometry name to use.
 * @param {!GVol.Extent} extent Extent.
 * @param {string=} opt_srsName SRS name. No srsName attribute will be
 *    set on geometries when this is not provided.
 * @extends {GVol.format.filter.Filter}
 * @api
 */
GVol.format.filter.Bbox = function(geometryName, extent, opt_srsName) {

  GVol.format.filter.Filter.call(this, 'BBOX');

  /**
   * @public
   * @type {!string}
   */
  this.geometryName = geometryName;

  /**
   * @public
   * @type {GVol.Extent}
   */
  this.extent = extent;

  /**
   * @public
   * @type {string|undefined}
   */
  this.srsName = opt_srsName;
};
GVol.inherits(GVol.format.filter.Bbox, GVol.format.filter.Filter);

goog.provide('GVol.format.filter.Comparison');

goog.require('GVol');
goog.require('GVol.format.filter.Filter');


/**
 * @classdesc
 * Abstract class; normally only used for creating subclasses and not instantiated in apps.
 * Base class for WFS GetFeature property comparison filters.
 *
 * @constructor
 * @param {!string} tagName The XML tag name for this filter.
 * @param {!string} propertyName Name of the context property to compare.
 * @extends {GVol.format.filter.Filter}
 * @api
 */
GVol.format.filter.Comparison = function(tagName, propertyName) {

  GVol.format.filter.Filter.call(this, tagName);

  /**
   * @public
   * @type {!string}
   */
  this.propertyName = propertyName;
};
GVol.inherits(GVol.format.filter.Comparison, GVol.format.filter.Filter);

goog.provide('GVol.format.filter.During');

goog.require('GVol');
goog.require('GVol.format.filter.Comparison');


/**
 * @classdesc
 * Represents a `<During>` comparison operator.
 *
 * @constructor
 * @param {!string} propertyName Name of the context property to compare.
 * @param {!string} begin The begin date in ISO-8601 format.
 * @param {!string} end The end date in ISO-8601 format.
 * @extends {GVol.format.filter.Comparison}
 * @api
 */
GVol.format.filter.During = function(propertyName, begin, end) {
  GVol.format.filter.Comparison.call(this, 'During', propertyName);

  /**
   * @public
   * @type {!string}
   */
  this.begin = begin;

  /**
   * @public
   * @type {!string}
   */
  this.end = end;
};
GVol.inherits(GVol.format.filter.During, GVol.format.filter.Comparison);

goog.provide('GVol.format.filter.ComparisonBinary');

goog.require('GVol');
goog.require('GVol.format.filter.Comparison');


/**
 * @classdesc
 * Abstract class; normally only used for creating subclasses and not instantiated in apps.
 * Base class for WFS GetFeature property binary comparison filters.
 *
 * @constructor
 * @param {!string} tagName The XML tag name for this filter.
 * @param {!string} propertyName Name of the context property to compare.
 * @param {!(string|number)} expression The value to compare.
 * @param {boGVolean=} opt_matchCase Case-sensitive?
 * @extends {GVol.format.filter.Comparison}
 * @api
 */
GVol.format.filter.ComparisonBinary = function(
    tagName, propertyName, expression, opt_matchCase) {

  GVol.format.filter.Comparison.call(this, tagName, propertyName);

  /**
   * @public
   * @type {!(string|number)}
   */
  this.expression = expression;

  /**
   * @public
   * @type {boGVolean|undefined}
   */
  this.matchCase = opt_matchCase;
};
GVol.inherits(GVol.format.filter.ComparisonBinary, GVol.format.filter.Comparison);

goog.provide('GVol.format.filter.EqualTo');

goog.require('GVol');
goog.require('GVol.format.filter.ComparisonBinary');


/**
 * @classdesc
 * Represents a `<PropertyIsEqualTo>` comparison operator.
 *
 * @constructor
 * @param {!string} propertyName Name of the context property to compare.
 * @param {!(string|number)} expression The value to compare.
 * @param {boGVolean=} opt_matchCase Case-sensitive?
 * @extends {GVol.format.filter.ComparisonBinary}
 * @api
 */
GVol.format.filter.EqualTo = function(propertyName, expression, opt_matchCase) {
  GVol.format.filter.ComparisonBinary.call(this, 'PropertyIsEqualTo', propertyName, expression, opt_matchCase);
};
GVol.inherits(GVol.format.filter.EqualTo, GVol.format.filter.ComparisonBinary);

goog.provide('GVol.format.filter.GreaterThan');

goog.require('GVol');
goog.require('GVol.format.filter.ComparisonBinary');


/**
 * @classdesc
 * Represents a `<PropertyIsGreaterThan>` comparison operator.
 *
 * @constructor
 * @param {!string} propertyName Name of the context property to compare.
 * @param {!number} expression The value to compare.
 * @extends {GVol.format.filter.ComparisonBinary}
 * @api
 */
GVol.format.filter.GreaterThan = function(propertyName, expression) {
  GVol.format.filter.ComparisonBinary.call(this, 'PropertyIsGreaterThan', propertyName, expression);
};
GVol.inherits(GVol.format.filter.GreaterThan, GVol.format.filter.ComparisonBinary);

goog.provide('GVol.format.filter.GreaterThanOrEqualTo');

goog.require('GVol');
goog.require('GVol.format.filter.ComparisonBinary');


/**
 * @classdesc
 * Represents a `<PropertyIsGreaterThanOrEqualTo>` comparison operator.
 *
 * @constructor
 * @param {!string} propertyName Name of the context property to compare.
 * @param {!number} expression The value to compare.
 * @extends {GVol.format.filter.ComparisonBinary}
 * @api
 */
GVol.format.filter.GreaterThanOrEqualTo = function(propertyName, expression) {
  GVol.format.filter.ComparisonBinary.call(this, 'PropertyIsGreaterThanOrEqualTo', propertyName, expression);
};
GVol.inherits(GVol.format.filter.GreaterThanOrEqualTo, GVol.format.filter.ComparisonBinary);

goog.provide('GVol.format.filter.Spatial');

goog.require('GVol');
goog.require('GVol.format.filter.Filter');


/**
 * @classdesc
 * Represents a spatial operator to test whether a geometry-valued property
 * relates to a given geometry.
 *
 * @constructor
 * @param {!string} tagName The XML tag name for this filter.
 * @param {!string} geometryName Geometry name to use.
 * @param {!GVol.geom.Geometry} geometry Geometry.
 * @param {string=} opt_srsName SRS name. No srsName attribute will be
 *    set on geometries when this is not provided.
 * @extends {GVol.format.filter.Filter}
 * @api
 */
GVol.format.filter.Spatial = function(tagName, geometryName, geometry, opt_srsName) {

  GVol.format.filter.Filter.call(this, tagName);

  /**
   * @public
   * @type {!string}
   */
  this.geometryName = geometryName || 'the_geom';

  /**
   * @public
   * @type {GVol.geom.Geometry}
   */
  this.geometry = geometry;

  /**
   * @public
   * @type {string|undefined}
   */
  this.srsName = opt_srsName;
};
GVol.inherits(GVol.format.filter.Spatial, GVol.format.filter.Filter);

goog.provide('GVol.format.filter.Intersects');

goog.require('GVol');
goog.require('GVol.format.filter.Spatial');


/**
 * @classdesc
 * Represents a `<Intersects>` operator to test whether a geometry-valued property
 * intersects a given geometry.
 *
 * @constructor
 * @param {!string} geometryName Geometry name to use.
 * @param {!GVol.geom.Geometry} geometry Geometry.
 * @param {string=} opt_srsName SRS name. No srsName attribute will be
 *    set on geometries when this is not provided.
 * @extends {GVol.format.filter.Spatial}
 * @api
 */
GVol.format.filter.Intersects = function(geometryName, geometry, opt_srsName) {

  GVol.format.filter.Spatial.call(this, 'Intersects', geometryName, geometry, opt_srsName);

};
GVol.inherits(GVol.format.filter.Intersects, GVol.format.filter.Spatial);

goog.provide('GVol.format.filter.IsBetween');

goog.require('GVol');
goog.require('GVol.format.filter.Comparison');


/**
 * @classdesc
 * Represents a `<PropertyIsBetween>` comparison operator.
 *
 * @constructor
 * @param {!string} propertyName Name of the context property to compare.
 * @param {!number} lowerBoundary The lower bound of the range.
 * @param {!number} upperBoundary The upper bound of the range.
 * @extends {GVol.format.filter.Comparison}
 * @api
 */
GVol.format.filter.IsBetween = function(propertyName, lowerBoundary, upperBoundary) {
  GVol.format.filter.Comparison.call(this, 'PropertyIsBetween', propertyName);

  /**
   * @public
   * @type {!number}
   */
  this.lowerBoundary = lowerBoundary;

  /**
   * @public
   * @type {!number}
   */
  this.upperBoundary = upperBoundary;
};
GVol.inherits(GVol.format.filter.IsBetween, GVol.format.filter.Comparison);

goog.provide('GVol.format.filter.IsLike');

goog.require('GVol');
goog.require('GVol.format.filter.Comparison');


/**
 * @classdesc
 * Represents a `<PropertyIsLike>` comparison operator.
 *
 * @constructor
 * @param {!string} propertyName Name of the context property to compare.
 * @param {!string} pattern Text pattern.
 * @param {string=} opt_wildCard Pattern character which matches any sequence of
 *    zero or more string characters. Default is '*'.
 * @param {string=} opt_singleChar pattern character which matches any single
 *    string character. Default is '.'.
 * @param {string=} opt_escapeChar Escape character which can be used to escape
 *    the pattern characters. Default is '!'.
 * @param {boGVolean=} opt_matchCase Case-sensitive?
 * @extends {GVol.format.filter.Comparison}
 * @api
 */
GVol.format.filter.IsLike = function(propertyName, pattern,
    opt_wildCard, opt_singleChar, opt_escapeChar, opt_matchCase) {
  GVol.format.filter.Comparison.call(this, 'PropertyIsLike', propertyName);

  /**
   * @public
   * @type {!string}
   */
  this.pattern = pattern;

  /**
   * @public
   * @type {!string}
   */
  this.wildCard = (opt_wildCard !== undefined) ? opt_wildCard : '*';

  /**
   * @public
   * @type {!string}
   */
  this.singleChar = (opt_singleChar !== undefined) ? opt_singleChar : '.';

  /**
   * @public
   * @type {!string}
   */
  this.escapeChar = (opt_escapeChar !== undefined) ? opt_escapeChar : '!';

  /**
   * @public
   * @type {boGVolean|undefined}
   */
  this.matchCase = opt_matchCase;
};
GVol.inherits(GVol.format.filter.IsLike, GVol.format.filter.Comparison);

goog.provide('GVol.format.filter.IsNull');

goog.require('GVol');
goog.require('GVol.format.filter.Comparison');


/**
 * @classdesc
 * Represents a `<PropertyIsNull>` comparison operator.
 *
 * @constructor
 * @param {!string} propertyName Name of the context property to compare.
 * @extends {GVol.format.filter.Comparison}
 * @api
 */
GVol.format.filter.IsNull = function(propertyName) {
  GVol.format.filter.Comparison.call(this, 'PropertyIsNull', propertyName);
};
GVol.inherits(GVol.format.filter.IsNull, GVol.format.filter.Comparison);

goog.provide('GVol.format.filter.LessThan');

goog.require('GVol');
goog.require('GVol.format.filter.ComparisonBinary');


/**
 * @classdesc
 * Represents a `<PropertyIsLessThan>` comparison operator.
 *
 * @constructor
 * @param {!string} propertyName Name of the context property to compare.
 * @param {!number} expression The value to compare.
 * @extends {GVol.format.filter.ComparisonBinary}
 * @api
 */
GVol.format.filter.LessThan = function(propertyName, expression) {
  GVol.format.filter.ComparisonBinary.call(this, 'PropertyIsLessThan', propertyName, expression);
};
GVol.inherits(GVol.format.filter.LessThan, GVol.format.filter.ComparisonBinary);

goog.provide('GVol.format.filter.LessThanOrEqualTo');

goog.require('GVol');
goog.require('GVol.format.filter.ComparisonBinary');


/**
 * @classdesc
 * Represents a `<PropertyIsLessThanOrEqualTo>` comparison operator.
 *
 * @constructor
 * @param {!string} propertyName Name of the context property to compare.
 * @param {!number} expression The value to compare.
 * @extends {GVol.format.filter.ComparisonBinary}
 * @api
 */
GVol.format.filter.LessThanOrEqualTo = function(propertyName, expression) {
  GVol.format.filter.ComparisonBinary.call(this, 'PropertyIsLessThanOrEqualTo', propertyName, expression);
};
GVol.inherits(GVol.format.filter.LessThanOrEqualTo, GVol.format.filter.ComparisonBinary);

goog.provide('GVol.format.filter.Not');

goog.require('GVol');
goog.require('GVol.format.filter.Filter');


/**
 * @classdesc
 * Represents a logical `<Not>` operator for a filter condition.
 *
 * @constructor
 * @param {!GVol.format.filter.Filter} condition Filter condition.
 * @extends {GVol.format.filter.Filter}
 * @api
 */
GVol.format.filter.Not = function(condition) {

  GVol.format.filter.Filter.call(this, 'Not');

  /**
   * @public
   * @type {!GVol.format.filter.Filter}
   */
  this.condition = condition;
};
GVol.inherits(GVol.format.filter.Not, GVol.format.filter.Filter);

goog.provide('GVol.format.filter.NotEqualTo');

goog.require('GVol');
goog.require('GVol.format.filter.ComparisonBinary');


/**
 * @classdesc
 * Represents a `<PropertyIsNotEqualTo>` comparison operator.
 *
 * @constructor
 * @param {!string} propertyName Name of the context property to compare.
 * @param {!(string|number)} expression The value to compare.
 * @param {boGVolean=} opt_matchCase Case-sensitive?
 * @extends {GVol.format.filter.ComparisonBinary}
 * @api
 */
GVol.format.filter.NotEqualTo = function(propertyName, expression, opt_matchCase) {
  GVol.format.filter.ComparisonBinary.call(this, 'PropertyIsNotEqualTo', propertyName, expression, opt_matchCase);
};
GVol.inherits(GVol.format.filter.NotEqualTo, GVol.format.filter.ComparisonBinary);

goog.provide('GVol.format.filter.Or');

goog.require('GVol');
goog.require('GVol.format.filter.LogicalNary');


/**
 * @classdesc
 * Represents a logical `<Or>` operator between two ore more filter conditions.
 *
 * @constructor
 * @param {...GVol.format.filter.Filter} conditions Conditions.
 * @extends {GVol.format.filter.LogicalNary}
 * @api
 */
GVol.format.filter.Or = function(conditions) {
  var params = ['Or'].concat(Array.prototype.slice.call(arguments));
  GVol.format.filter.LogicalNary.apply(this, params);
};
GVol.inherits(GVol.format.filter.Or, GVol.format.filter.LogicalNary);

goog.provide('GVol.format.filter.Within');

goog.require('GVol');
goog.require('GVol.format.filter.Spatial');


/**
 * @classdesc
 * Represents a `<Within>` operator to test whether a geometry-valued property
 * is within a given geometry.
 *
 * @constructor
 * @param {!string} geometryName Geometry name to use.
 * @param {!GVol.geom.Geometry} geometry Geometry.
 * @param {string=} opt_srsName SRS name. No srsName attribute will be
 *    set on geometries when this is not provided.
 * @extends {GVol.format.filter.Spatial}
 * @api
 */
GVol.format.filter.Within = function(geometryName, geometry, opt_srsName) {

  GVol.format.filter.Spatial.call(this, 'Within', geometryName, geometry, opt_srsName);

};
GVol.inherits(GVol.format.filter.Within, GVol.format.filter.Spatial);

goog.provide('GVol.format.filter');

goog.require('GVol.format.filter.And');
goog.require('GVol.format.filter.Bbox');
goog.require('GVol.format.filter.During');
goog.require('GVol.format.filter.EqualTo');
goog.require('GVol.format.filter.GreaterThan');
goog.require('GVol.format.filter.GreaterThanOrEqualTo');
goog.require('GVol.format.filter.Intersects');
goog.require('GVol.format.filter.IsBetween');
goog.require('GVol.format.filter.IsLike');
goog.require('GVol.format.filter.IsNull');
goog.require('GVol.format.filter.LessThan');
goog.require('GVol.format.filter.LessThanOrEqualTo');
goog.require('GVol.format.filter.Not');
goog.require('GVol.format.filter.NotEqualTo');
goog.require('GVol.format.filter.Or');
goog.require('GVol.format.filter.Within');


/**
 * Create a logical `<And>` operator between two or more filter conditions.
 *
 * @param {...GVol.format.filter.Filter} conditions Filter conditions.
 * @returns {!GVol.format.filter.And} `<And>` operator.
 * @api
 */
GVol.format.filter.and = function(conditions) {
  var params = [null].concat(Array.prototype.slice.call(arguments));
  return new (Function.prototype.bind.apply(GVol.format.filter.And, params));
};


/**
 * Create a logical `<Or>` operator between two or more filter conditions.
 *
 * @param {...GVol.format.filter.Filter} conditions Filter conditions.
 * @returns {!GVol.format.filter.Or} `<Or>` operator.
 * @api
 */
GVol.format.filter.or = function(conditions) {
  var params = [null].concat(Array.prototype.slice.call(arguments));
  return new (Function.prototype.bind.apply(GVol.format.filter.Or, params));
};


/**
 * Represents a logical `<Not>` operator for a filter condition.
 *
 * @param {!GVol.format.filter.Filter} condition Filter condition.
 * @returns {!GVol.format.filter.Not} `<Not>` operator.
 * @api
 */
GVol.format.filter.not = function(condition) {
  return new GVol.format.filter.Not(condition);
};


/**
 * Create a `<BBOX>` operator to test whether a geometry-valued property
 * intersects a fixed bounding box
 *
 * @param {!string} geometryName Geometry name to use.
 * @param {!GVol.Extent} extent Extent.
 * @param {string=} opt_srsName SRS name. No srsName attribute will be
 *    set on geometries when this is not provided.
 * @returns {!GVol.format.filter.Bbox} `<BBOX>` operator.
 * @api
 */
GVol.format.filter.bbox = function(geometryName, extent, opt_srsName) {
  return new GVol.format.filter.Bbox(geometryName, extent, opt_srsName);
};

/**
 * Create a `<Intersects>` operator to test whether a geometry-valued property
 * intersects a given geometry.
 *
 * @param {!string} geometryName Geometry name to use.
 * @param {!GVol.geom.Geometry} geometry Geometry.
 * @param {string=} opt_srsName SRS name. No srsName attribute will be
 *    set on geometries when this is not provided.
 * @returns {!GVol.format.filter.Intersects} `<Intersects>` operator.
 * @api
 */
GVol.format.filter.intersects = function(geometryName, geometry, opt_srsName) {
  return new GVol.format.filter.Intersects(geometryName, geometry, opt_srsName);
};

/**
 * Create a `<Within>` operator to test whether a geometry-valued property
 * is within a given geometry.
 *
 * @param {!string} geometryName Geometry name to use.
 * @param {!GVol.geom.Geometry} geometry Geometry.
 * @param {string=} opt_srsName SRS name. No srsName attribute will be
 *    set on geometries when this is not provided.
 * @returns {!GVol.format.filter.Within} `<Within>` operator.
 * @api
 */
GVol.format.filter.within = function(geometryName, geometry, opt_srsName) {
  return new GVol.format.filter.Within(geometryName, geometry, opt_srsName);
};


/**
 * Creates a `<PropertyIsEqualTo>` comparison operator.
 *
 * @param {!string} propertyName Name of the context property to compare.
 * @param {!(string|number)} expression The value to compare.
 * @param {boGVolean=} opt_matchCase Case-sensitive?
 * @returns {!GVol.format.filter.EqualTo} `<PropertyIsEqualTo>` operator.
 * @api
 */
GVol.format.filter.equalTo = function(propertyName, expression, opt_matchCase) {
  return new GVol.format.filter.EqualTo(propertyName, expression, opt_matchCase);
};


/**
 * Creates a `<PropertyIsNotEqualTo>` comparison operator.
 *
 * @param {!string} propertyName Name of the context property to compare.
 * @param {!(string|number)} expression The value to compare.
 * @param {boGVolean=} opt_matchCase Case-sensitive?
 * @returns {!GVol.format.filter.NotEqualTo} `<PropertyIsNotEqualTo>` operator.
 * @api
 */
GVol.format.filter.notEqualTo = function(propertyName, expression, opt_matchCase) {
  return new GVol.format.filter.NotEqualTo(propertyName, expression, opt_matchCase);
};


/**
 * Creates a `<PropertyIsLessThan>` comparison operator.
 *
 * @param {!string} propertyName Name of the context property to compare.
 * @param {!number} expression The value to compare.
 * @returns {!GVol.format.filter.LessThan} `<PropertyIsLessThan>` operator.
 * @api
 */
GVol.format.filter.lessThan = function(propertyName, expression) {
  return new GVol.format.filter.LessThan(propertyName, expression);
};


/**
 * Creates a `<PropertyIsLessThanOrEqualTo>` comparison operator.
 *
 * @param {!string} propertyName Name of the context property to compare.
 * @param {!number} expression The value to compare.
 * @returns {!GVol.format.filter.LessThanOrEqualTo} `<PropertyIsLessThanOrEqualTo>` operator.
 * @api
 */
GVol.format.filter.lessThanOrEqualTo = function(propertyName, expression) {
  return new GVol.format.filter.LessThanOrEqualTo(propertyName, expression);
};


/**
 * Creates a `<PropertyIsGreaterThan>` comparison operator.
 *
 * @param {!string} propertyName Name of the context property to compare.
 * @param {!number} expression The value to compare.
 * @returns {!GVol.format.filter.GreaterThan} `<PropertyIsGreaterThan>` operator.
 * @api
 */
GVol.format.filter.greaterThan = function(propertyName, expression) {
  return new GVol.format.filter.GreaterThan(propertyName, expression);
};


/**
 * Creates a `<PropertyIsGreaterThanOrEqualTo>` comparison operator.
 *
 * @param {!string} propertyName Name of the context property to compare.
 * @param {!number} expression The value to compare.
 * @returns {!GVol.format.filter.GreaterThanOrEqualTo} `<PropertyIsGreaterThanOrEqualTo>` operator.
 * @api
 */
GVol.format.filter.greaterThanOrEqualTo = function(propertyName, expression) {
  return new GVol.format.filter.GreaterThanOrEqualTo(propertyName, expression);
};


/**
 * Creates a `<PropertyIsNull>` comparison operator to test whether a property value
 * is null.
 *
 * @param {!string} propertyName Name of the context property to compare.
 * @returns {!GVol.format.filter.IsNull} `<PropertyIsNull>` operator.
 * @api
 */
GVol.format.filter.isNull = function(propertyName) {
  return new GVol.format.filter.IsNull(propertyName);
};


/**
 * Creates a `<PropertyIsBetween>` comparison operator to test whether an expression
 * value lies within a range given by a lower and upper bound (inclusive).
 *
 * @param {!string} propertyName Name of the context property to compare.
 * @param {!number} lowerBoundary The lower bound of the range.
 * @param {!number} upperBoundary The upper bound of the range.
 * @returns {!GVol.format.filter.IsBetween} `<PropertyIsBetween>` operator.
 * @api
 */
GVol.format.filter.between = function(propertyName, lowerBoundary, upperBoundary) {
  return new GVol.format.filter.IsBetween(propertyName, lowerBoundary, upperBoundary);
};


/**
 * Represents a `<PropertyIsLike>` comparison operator that matches a string property
 * value against a text pattern.
 *
 * @param {!string} propertyName Name of the context property to compare.
 * @param {!string} pattern Text pattern.
 * @param {string=} opt_wildCard Pattern character which matches any sequence of
 *    zero or more string characters. Default is '*'.
 * @param {string=} opt_singleChar pattern character which matches any single
 *    string character. Default is '.'.
 * @param {string=} opt_escapeChar Escape character which can be used to escape
 *    the pattern characters. Default is '!'.
 * @param {boGVolean=} opt_matchCase Case-sensitive?
 * @returns {!GVol.format.filter.IsLike} `<PropertyIsLike>` operator.
 * @api
 */
GVol.format.filter.like = function(propertyName, pattern,
    opt_wildCard, opt_singleChar, opt_escapeChar, opt_matchCase) {
  return new GVol.format.filter.IsLike(propertyName, pattern,
      opt_wildCard, opt_singleChar, opt_escapeChar, opt_matchCase);
};


/**
 * Create a `<During>` temporal operator.
 *
 * @param {!string} propertyName Name of the context property to compare.
 * @param {!string} begin The begin date in ISO-8601 format.
 * @param {!string} end The end date in ISO-8601 format.
 * @returns {!GVol.format.filter.During} `<During>` operator.
 * @api
 */
GVol.format.filter.during = function(propertyName, begin, end) {
  return new GVol.format.filter.During(propertyName, begin, end);
};

goog.provide('GVol.geom.GeometryCGVollection');

goog.require('GVol');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.extent');
goog.require('GVol.geom.Geometry');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.obj');


/**
 * @classdesc
 * An array of {@link GVol.geom.Geometry} objects.
 *
 * @constructor
 * @extends {GVol.geom.Geometry}
 * @param {Array.<GVol.geom.Geometry>=} opt_geometries Geometries.
 * @api
 */
GVol.geom.GeometryCGVollection = function(opt_geometries) {

  GVol.geom.Geometry.call(this);

  /**
   * @private
   * @type {Array.<GVol.geom.Geometry>}
   */
  this.geometries_ = opt_geometries ? opt_geometries : null;

  this.listenGeometriesChange_();
};
GVol.inherits(GVol.geom.GeometryCGVollection, GVol.geom.Geometry);


/**
 * @param {Array.<GVol.geom.Geometry>} geometries Geometries.
 * @private
 * @return {Array.<GVol.geom.Geometry>} Cloned geometries.
 */
GVol.geom.GeometryCGVollection.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
 */
GVol.geom.GeometryCGVollection.prototype.unlistenGeometriesChange_ = function() {
  var i, ii;
  if (!this.geometries_) {
    return;
  }
  for (i = 0, ii = this.geometries_.length; i < ii; ++i) {
    GVol.events.unlisten(
        this.geometries_[i], GVol.events.EventType.CHANGE,
        this.changed, this);
  }
};


/**
 * @private
 */
GVol.geom.GeometryCGVollection.prototype.listenGeometriesChange_ = function() {
  var i, ii;
  if (!this.geometries_) {
    return;
  }
  for (i = 0, ii = this.geometries_.length; i < ii; ++i) {
    GVol.events.listen(
        this.geometries_[i], GVol.events.EventType.CHANGE,
        this.changed, this);
  }
};


/**
 * Make a complete copy of the geometry.
 * @return {!GVol.geom.GeometryCGVollection} Clone.
 * @override
 * @api
 */
GVol.geom.GeometryCGVollection.prototype.clone = function() {
  var geometryCGVollection = new GVol.geom.GeometryCGVollection(null);
  geometryCGVollection.setGeometries(this.geometries_);
  return geometryCGVollection;
};


/**
 * @inheritDoc
 */
GVol.geom.GeometryCGVollection.prototype.closestPointXY = function(x, y, closestPoint, minSquaredDistance) {
  if (minSquaredDistance <
      GVol.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
 */
GVol.geom.GeometryCGVollection.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
 */
GVol.geom.GeometryCGVollection.prototype.computeExtent = function(extent) {
  GVol.extent.createOrUpdateEmpty(extent);
  var geometries = this.geometries_;
  for (var i = 0, ii = geometries.length; i < ii; ++i) {
    GVol.extent.extend(extent, geometries[i].getExtent());
  }
  return extent;
};


/**
 * Return the geometries that make up this geometry cGVollection.
 * @return {Array.<GVol.geom.Geometry>} Geometries.
 * @api
 */
GVol.geom.GeometryCGVollection.prototype.getGeometries = function() {
  return GVol.geom.GeometryCGVollection.cloneGeometries_(this.geometries_);
};


/**
 * @return {Array.<GVol.geom.Geometry>} Geometries.
 */
GVol.geom.GeometryCGVollection.prototype.getGeometriesArray = function() {
  return this.geometries_;
};


/**
 * @inheritDoc
 */
GVol.geom.GeometryCGVollection.prototype.getSimplifiedGeometry = function(squaredTGVolerance) {
  if (this.simplifiedGeometryRevision != this.getRevision()) {
    GVol.obj.clear(this.simplifiedGeometryCache);
    this.simplifiedGeometryMaxMinSquaredTGVolerance = 0;
    this.simplifiedGeometryRevision = this.getRevision();
  }
  if (squaredTGVolerance < 0 ||
      (this.simplifiedGeometryMaxMinSquaredTGVolerance !== 0 &&
       squaredTGVolerance < this.simplifiedGeometryMaxMinSquaredTGVolerance)) {
    return this;
  }
  var key = squaredTGVolerance.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(squaredTGVolerance);
      simplifiedGeometries.push(simplifiedGeometry);
      if (simplifiedGeometry !== geometry) {
        simplified = true;
      }
    }
    if (simplified) {
      var simplifiedGeometryCGVollection = new GVol.geom.GeometryCGVollection(null);
      simplifiedGeometryCGVollection.setGeometriesArray(simplifiedGeometries);
      this.simplifiedGeometryCache[key] = simplifiedGeometryCGVollection;
      return simplifiedGeometryCGVollection;
    } else {
      this.simplifiedGeometryMaxMinSquaredTGVolerance = squaredTGVolerance;
      return this;
    }
  }
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.GeometryCGVollection.prototype.getType = function() {
  return GVol.geom.GeometryType.GEOMETRY_COLLECTION;
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.GeometryCGVollection.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 {boGVolean} Is empty.
 */
GVol.geom.GeometryCGVollection.prototype.isEmpty = function() {
  return this.geometries_.length === 0;
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.GeometryCGVollection.prototype.rotate = function(angle, anchor) {
  var geometries = this.geometries_;
  for (var i = 0, ii = geometries.length; i < ii; ++i) {
    geometries[i].rotate(angle, anchor);
  }
  this.changed();
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.GeometryCGVollection.prototype.scale = function(sx, opt_sy, opt_anchor) {
  var anchor = opt_anchor;
  if (!anchor) {
    anchor = GVol.extent.getCenter(this.getExtent());
  }
  var geometries = this.geometries_;
  for (var i = 0, ii = geometries.length; i < ii; ++i) {
    geometries[i].scale(sx, opt_sy, anchor);
  }
  this.changed();
};


/**
 * Set the geometries that make up this geometry cGVollection.
 * @param {Array.<GVol.geom.Geometry>} geometries Geometries.
 * @api
 */
GVol.geom.GeometryCGVollection.prototype.setGeometries = function(geometries) {
  this.setGeometriesArray(
      GVol.geom.GeometryCGVollection.cloneGeometries_(geometries));
};


/**
 * @param {Array.<GVol.geom.Geometry>} geometries Geometries.
 */
GVol.geom.GeometryCGVollection.prototype.setGeometriesArray = function(geometries) {
  this.unlistenGeometriesChange_();
  this.geometries_ = geometries;
  this.listenGeometriesChange_();
  this.changed();
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.GeometryCGVollection.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.
 * @override
 * @api
 */
GVol.geom.GeometryCGVollection.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
 */
GVol.geom.GeometryCGVollection.prototype.disposeInternal = function() {
  this.unlistenGeometriesChange_();
  GVol.geom.Geometry.prototype.disposeInternal.call(this);
};

// TODO: serialize dataProjection as crs member when writing
// see https://github.com/openlayers/openlayers/issues/2078

goog.provide('GVol.format.GeoJSON');

goog.require('GVol');
goog.require('GVol.asserts');
goog.require('GVol.Feature');
goog.require('GVol.format.Feature');
goog.require('GVol.format.JSONFeature');
goog.require('GVol.geom.GeometryCGVollection');
goog.require('GVol.geom.LineString');
goog.require('GVol.geom.MultiLineString');
goog.require('GVol.geom.MultiPoint');
goog.require('GVol.geom.MultiPGVolygon');
goog.require('GVol.geom.Point');
goog.require('GVol.geom.PGVolygon');
goog.require('GVol.obj');
goog.require('GVol.proj');


/**
 * @classdesc
 * Feature format for reading and writing data in the GeoJSON format.
 *
 * @constructor
 * @extends {GVol.format.JSONFeature}
 * @param {GVolx.format.GeoJSONOptions=} opt_options Options.
 * @api
 */
GVol.format.GeoJSON = function(opt_options) {

  var options = opt_options ? opt_options : {};

  GVol.format.JSONFeature.call(this);

  /**
   * @inheritDoc
   */
  this.defaultDataProjection = GVol.proj.get(
      options.defaultDataProjection ?
        options.defaultDataProjection : 'EPSG:4326');


  if (options.featureProjection) {
    this.defaultFeatureProjection = GVol.proj.get(options.featureProjection);
  }

  /**
   * Name of the geometry attribute for features.
   * @type {string|undefined}
   * @private
   */
  this.geometryName_ = options.geometryName;

};
GVol.inherits(GVol.format.GeoJSON, GVol.format.JSONFeature);


/**
 * @param {GeoJSONGeometry|GeoJSONGeometryCGVollection} object Object.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @private
 * @return {GVol.geom.Geometry} Geometry.
 */
GVol.format.GeoJSON.readGeometry_ = function(object, opt_options) {
  if (!object) {
    return null;
  }
  var geometryReader = GVol.format.GeoJSON.GEOMETRY_READERS_[object.type];
  return /** @type {GVol.geom.Geometry} */ (
    GVol.format.Feature.transformWithOptions(
        geometryReader(object), false, opt_options));
};


/**
 * @param {GeoJSONGeometryCGVollection} object Object.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @private
 * @return {GVol.geom.GeometryCGVollection} Geometry cGVollection.
 */
GVol.format.GeoJSON.readGeometryCGVollectionGeometry_ = function(
    object, opt_options) {
  var geometries = object.geometries.map(
      /**
       * @param {GeoJSONGeometry} geometry Geometry.
       * @return {GVol.geom.Geometry} geometry Geometry.
       */
      function(geometry) {
        return GVol.format.GeoJSON.readGeometry_(geometry, opt_options);
      });
  return new GVol.geom.GeometryCGVollection(geometries);
};


/**
 * @param {GeoJSONGeometry} object Object.
 * @private
 * @return {GVol.geom.Point} Point.
 */
GVol.format.GeoJSON.readPointGeometry_ = function(object) {
  return new GVol.geom.Point(object.coordinates);
};


/**
 * @param {GeoJSONGeometry} object Object.
 * @private
 * @return {GVol.geom.LineString} LineString.
 */
GVol.format.GeoJSON.readLineStringGeometry_ = function(object) {
  return new GVol.geom.LineString(object.coordinates);
};


/**
 * @param {GeoJSONGeometry} object Object.
 * @private
 * @return {GVol.geom.MultiLineString} MultiLineString.
 */
GVol.format.GeoJSON.readMultiLineStringGeometry_ = function(object) {
  return new GVol.geom.MultiLineString(object.coordinates);
};


/**
 * @param {GeoJSONGeometry} object Object.
 * @private
 * @return {GVol.geom.MultiPoint} MultiPoint.
 */
GVol.format.GeoJSON.readMultiPointGeometry_ = function(object) {
  return new GVol.geom.MultiPoint(object.coordinates);
};


/**
 * @param {GeoJSONGeometry} object Object.
 * @private
 * @return {GVol.geom.MultiPGVolygon} MultiPGVolygon.
 */
GVol.format.GeoJSON.readMultiPGVolygonGeometry_ = function(object) {
  return new GVol.geom.MultiPGVolygon(object.coordinates);
};


/**
 * @param {GeoJSONGeometry} object Object.
 * @private
 * @return {GVol.geom.PGVolygon} PGVolygon.
 */
GVol.format.GeoJSON.readPGVolygonGeometry_ = function(object) {
  return new GVol.geom.PGVolygon(object.coordinates);
};


/**
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {GeoJSONGeometry|GeoJSONGeometryCGVollection} GeoJSON geometry.
 */
GVol.format.GeoJSON.writeGeometry_ = function(geometry, opt_options) {
  var geometryWriter = GVol.format.GeoJSON.GEOMETRY_WRITERS_[geometry.getType()];
  return geometryWriter(/** @type {GVol.geom.Geometry} */ (
    GVol.format.Feature.transformWithOptions(geometry, true, opt_options)),
  opt_options);
};


/**
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @private
 * @return {GeoJSONGeometryCGVollection} Empty GeoJSON geometry cGVollection.
 */
GVol.format.GeoJSON.writeEmptyGeometryCGVollectionGeometry_ = function(geometry) {
  return /** @type {GeoJSONGeometryCGVollection} */ ({
    type: 'GeometryCGVollection',
    geometries: []
  });
};


/**
 * @param {GVol.geom.GeometryCGVollection} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {GeoJSONGeometryCGVollection} GeoJSON geometry cGVollection.
 */
GVol.format.GeoJSON.writeGeometryCGVollectionGeometry_ = function(
    geometry, opt_options) {
  var geometries = geometry.getGeometriesArray().map(function(geometry) {
    var options = GVol.obj.assign({}, opt_options);
    delete options.featureProjection;
    return GVol.format.GeoJSON.writeGeometry_(geometry, options);
  });
  return /** @type {GeoJSONGeometryCGVollection} */ ({
    type: 'GeometryCGVollection',
    geometries: geometries
  });
};


/**
 * @param {GVol.geom.LineString} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {GeoJSONGeometry} GeoJSON geometry.
 */
GVol.format.GeoJSON.writeLineStringGeometry_ = function(geometry, opt_options) {
  return /** @type {GeoJSONGeometry} */ ({
    type: 'LineString',
    coordinates: geometry.getCoordinates()
  });
};


/**
 * @param {GVol.geom.MultiLineString} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {GeoJSONGeometry} GeoJSON geometry.
 */
GVol.format.GeoJSON.writeMultiLineStringGeometry_ = function(geometry, opt_options) {
  return /** @type {GeoJSONGeometry} */ ({
    type: 'MultiLineString',
    coordinates: geometry.getCoordinates()
  });
};


/**
 * @param {GVol.geom.MultiPoint} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {GeoJSONGeometry} GeoJSON geometry.
 */
GVol.format.GeoJSON.writeMultiPointGeometry_ = function(geometry, opt_options) {
  return /** @type {GeoJSONGeometry} */ ({
    type: 'MultiPoint',
    coordinates: geometry.getCoordinates()
  });
};


/**
 * @param {GVol.geom.MultiPGVolygon} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {GeoJSONGeometry} GeoJSON geometry.
 */
GVol.format.GeoJSON.writeMultiPGVolygonGeometry_ = function(geometry, opt_options) {
  var right;
  if (opt_options) {
    right = opt_options.rightHanded;
  }
  return /** @type {GeoJSONGeometry} */ ({
    type: 'MultiPGVolygon',
    coordinates: geometry.getCoordinates(right)
  });
};


/**
 * @param {GVol.geom.Point} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {GeoJSONGeometry} GeoJSON geometry.
 */
GVol.format.GeoJSON.writePointGeometry_ = function(geometry, opt_options) {
  return /** @type {GeoJSONGeometry} */ ({
    type: 'Point',
    coordinates: geometry.getCoordinates()
  });
};


/**
 * @param {GVol.geom.PGVolygon} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @private
 * @return {GeoJSONGeometry} GeoJSON geometry.
 */
GVol.format.GeoJSON.writePGVolygonGeometry_ = function(geometry, opt_options) {
  var right;
  if (opt_options) {
    right = opt_options.rightHanded;
  }
  return /** @type {GeoJSONGeometry} */ ({
    type: 'PGVolygon',
    coordinates: geometry.getCoordinates(right)
  });
};


/**
 * @const
 * @private
 * @type {Object.<string, function(GeoJSONObject): GVol.geom.Geometry>}
 */
GVol.format.GeoJSON.GEOMETRY_READERS_ = {
  'Point': GVol.format.GeoJSON.readPointGeometry_,
  'LineString': GVol.format.GeoJSON.readLineStringGeometry_,
  'PGVolygon': GVol.format.GeoJSON.readPGVolygonGeometry_,
  'MultiPoint': GVol.format.GeoJSON.readMultiPointGeometry_,
  'MultiLineString': GVol.format.GeoJSON.readMultiLineStringGeometry_,
  'MultiPGVolygon': GVol.format.GeoJSON.readMultiPGVolygonGeometry_,
  'GeometryCGVollection': GVol.format.GeoJSON.readGeometryCGVollectionGeometry_
};


/**
 * @const
 * @private
 * @type {Object.<string, function(GVol.geom.Geometry, GVolx.format.WriteOptions=): (GeoJSONGeometry|GeoJSONGeometryCGVollection)>}
 */
GVol.format.GeoJSON.GEOMETRY_WRITERS_ = {
  'Point': GVol.format.GeoJSON.writePointGeometry_,
  'LineString': GVol.format.GeoJSON.writeLineStringGeometry_,
  'PGVolygon': GVol.format.GeoJSON.writePGVolygonGeometry_,
  'MultiPoint': GVol.format.GeoJSON.writeMultiPointGeometry_,
  'MultiLineString': GVol.format.GeoJSON.writeMultiLineStringGeometry_,
  'MultiPGVolygon': GVol.format.GeoJSON.writeMultiPGVolygonGeometry_,
  'GeometryCGVollection': GVol.format.GeoJSON.writeGeometryCGVollectionGeometry_,
  'Circle': GVol.format.GeoJSON.writeEmptyGeometryCGVollectionGeometry_
};


/**
 * Read a feature from a GeoJSON Feature source.  Only works for Feature or
 * geometry types.  Use {@link GVol.format.GeoJSON#readFeatures} to read
 * FeatureCGVollection source. If feature at source has an id, it will be used
 * as Feature id by calling {@link GVol.Feature#setId} internally.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {GVol.Feature} Feature.
 * @api
 */
GVol.format.GeoJSON.prototype.readFeature;


/**
 * Read all features from a GeoJSON source.  Works for all GeoJSON types.
 * If the source includes only geometries, features will be created with those
 * geometries.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<GVol.Feature>} Features.
 * @api
 */
GVol.format.GeoJSON.prototype.readFeatures;


/**
 * @inheritDoc
 */
GVol.format.GeoJSON.prototype.readFeatureFromObject = function(
    object, opt_options) {
  /**
   * @type {GeoJSONFeature}
   */
  var geoJSONFeature = null;
  if (object.type === 'Feature') {
    geoJSONFeature = /** @type {GeoJSONFeature} */ (object);
  } else {
    geoJSONFeature = /** @type {GeoJSONFeature} */ ({
      type: 'Feature',
      geometry: /** @type {GeoJSONGeometry|GeoJSONGeometryCGVollection} */ (object)
    });
  }

  var geometry = GVol.format.GeoJSON.readGeometry_(geoJSONFeature.geometry, opt_options);
  var feature = new GVol.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
 */
GVol.format.GeoJSON.prototype.readFeaturesFromObject = function(
    object, opt_options) {
  var geoJSONObject = /** @type {GeoJSONObject} */ (object);
  /** @type {Array.<GVol.Feature>} */
  var features = null;
  if (geoJSONObject.type === 'FeatureCGVollection') {
    var geoJSONFeatureCGVollection = /** @type {GeoJSONFeatureCGVollection} */
        (object);
    features = [];
    var geoJSONFeatures = geoJSONFeatureCGVollection.features;
    var i, ii;
    for (i = 0, ii = geoJSONFeatures.length; i < ii; ++i) {
      features.push(this.readFeatureFromObject(geoJSONFeatures[i],
          opt_options));
    }
  } else {
    features = [this.readFeatureFromObject(object, opt_options)];
  }
  return features;
};


/**
 * Read a geometry from a GeoJSON source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {GVol.geom.Geometry} Geometry.
 * @api
 */
GVol.format.GeoJSON.prototype.readGeometry;


/**
 * @inheritDoc
 */
GVol.format.GeoJSON.prototype.readGeometryFromObject = function(
    object, opt_options) {
  return GVol.format.GeoJSON.readGeometry_(
      /** @type {GeoJSONGeometry} */ (object), opt_options);
};


/**
 * Read the projection from a GeoJSON source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @return {GVol.proj.Projection} Projection.
 * @api
 */
GVol.format.GeoJSON.prototype.readProjection;


/**
 * @inheritDoc
 */
GVol.format.GeoJSON.prototype.readProjectionFromObject = function(object) {
  var geoJSONObject = /** @type {GeoJSONObject} */ (object);
  var crs = geoJSONObject.crs;
  var projection;
  if (crs) {
    if (crs.type == 'name') {
      projection = GVol.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.
      projection = GVol.proj.get('EPSG:' + crs.properties.code);
    } else {
      GVol.asserts.assert(false, 36); // Unknown SRS type
    }
  } else {
    projection = this.defaultDataProjection;
  }
  return /** @type {GVol.proj.Projection} */ (projection);
};


/**
 * Encode a feature as a GeoJSON Feature string.
 *
 * @function
 * @param {GVol.Feature} feature Feature.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {string} GeoJSON.
 * @override
 * @api
 */
GVol.format.GeoJSON.prototype.writeFeature;


/**
 * Encode a feature as a GeoJSON Feature object.
 *
 * @param {GVol.Feature} feature Feature.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {GeoJSONFeature} Object.
 * @override
 * @api
 */
GVol.format.GeoJSON.prototype.writeFeatureObject = function(feature, opt_options) {
  opt_options = this.adaptOptions(opt_options);

  var object = /** @type {GeoJSONFeature} */ ({
    'type': 'Feature'
  });
  var id = feature.getId();
  if (id !== undefined) {
    object.id = id;
  }
  var geometry = feature.getGeometry();
  if (geometry) {
    object.geometry =
        GVol.format.GeoJSON.writeGeometry_(geometry, opt_options);
  } else {
    object.geometry = null;
  }
  var properties = feature.getProperties();
  delete properties[feature.getGeometryName()];
  if (!GVol.obj.isEmpty(properties)) {
    object.properties = properties;
  } else {
    object.properties = null;
  }
  return object;
};


/**
 * Encode an array of features as GeoJSON.
 *
 * @function
 * @param {Array.<GVol.Feature>} features Features.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {string} GeoJSON.
 * @api
 */
GVol.format.GeoJSON.prototype.writeFeatures;


/**
 * Encode an array of features as a GeoJSON object.
 *
 * @param {Array.<GVol.Feature>} features Features.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {GeoJSONFeatureCGVollection} GeoJSON Object.
 * @override
 * @api
 */
GVol.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 {GeoJSONFeatureCGVollection} */ ({
    type: 'FeatureCGVollection',
    features: objects
  });
};


/**
 * Encode a geometry as a GeoJSON string.
 *
 * @function
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {string} GeoJSON.
 * @api
 */
GVol.format.GeoJSON.prototype.writeGeometry;


/**
 * Encode a geometry as a GeoJSON object.
 *
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {GeoJSONGeometry|GeoJSONGeometryCGVollection} Object.
 * @override
 * @api
 */
GVol.format.GeoJSON.prototype.writeGeometryObject = function(geometry,
    opt_options) {
  return GVol.format.GeoJSON.writeGeometry_(geometry,
      this.adaptOptions(opt_options));
};

goog.provide('GVol.format.XMLFeature');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.format.Feature');
goog.require('GVol.format.FormatType');
goog.require('GVol.xml');


/**
 * @classdesc
 * Abstract base class; normally only used for creating subclasses and not
 * instantiated in apps.
 * Base class for XML feature formats.
 *
 * @constructor
 * @abstract
 * @extends {GVol.format.Feature}
 */
GVol.format.XMLFeature = function() {

  /**
   * @type {XMLSerializer}
   * @private
   */
  this.xmlSerializer_ = new XMLSerializer();

  GVol.format.Feature.call(this);
};
GVol.inherits(GVol.format.XMLFeature, GVol.format.Feature);


/**
 * @inheritDoc
 */
GVol.format.XMLFeature.prototype.getType = function() {
  return GVol.format.FormatType.XML;
};


/**
 * @inheritDoc
 */
GVol.format.XMLFeature.prototype.readFeature = function(source, opt_options) {
  if (GVol.xml.isDocument(source)) {
    return this.readFeatureFromDocument(
        /** @type {Document} */ (source), opt_options);
  } else if (GVol.xml.isNode(source)) {
    return this.readFeatureFromNode(/** @type {Node} */ (source), opt_options);
  } else if (typeof source === 'string') {
    var doc = GVol.xml.parse(source);
    return this.readFeatureFromDocument(doc, opt_options);
  } else {
    return null;
  }
};


/**
 * @param {Document} doc Document.
 * @param {GVolx.format.ReadOptions=} opt_options Options.
 * @return {GVol.Feature} Feature.
 */
GVol.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 {GVolx.format.ReadOptions=} opt_options Options.
 * @return {GVol.Feature} Feature.
 */
GVol.format.XMLFeature.prototype.readFeatureFromNode = function(node, opt_options) {
  return null; // not implemented
};


/**
 * @inheritDoc
 */
GVol.format.XMLFeature.prototype.readFeatures = function(source, opt_options) {
  if (GVol.xml.isDocument(source)) {
    return this.readFeaturesFromDocument(
        /** @type {Document} */ (source), opt_options);
  } else if (GVol.xml.isNode(source)) {
    return this.readFeaturesFromNode(/** @type {Node} */ (source), opt_options);
  } else if (typeof source === 'string') {
    var doc = GVol.xml.parse(source);
    return this.readFeaturesFromDocument(doc, opt_options);
  } else {
    return [];
  }
};


/**
 * @param {Document} doc Document.
 * @param {GVolx.format.ReadOptions=} opt_options Options.
 * @protected
 * @return {Array.<GVol.Feature>} Features.
 */
GVol.format.XMLFeature.prototype.readFeaturesFromDocument = function(
    doc, opt_options) {
  /** @type {Array.<GVol.Feature>} */
  var features = [];
  var n;
  for (n = doc.firstChild; n; n = n.nextSibling) {
    if (n.nodeType == Node.ELEMENT_NODE) {
      GVol.array.extend(features, this.readFeaturesFromNode(n, opt_options));
    }
  }
  return features;
};


/**
 * @abstract
 * @param {Node} node Node.
 * @param {GVolx.format.ReadOptions=} opt_options Options.
 * @protected
 * @return {Array.<GVol.Feature>} Features.
 */
GVol.format.XMLFeature.prototype.readFeaturesFromNode = function(node, opt_options) {};


/**
 * @inheritDoc
 */
GVol.format.XMLFeature.prototype.readGeometry = function(source, opt_options) {
  if (GVol.xml.isDocument(source)) {
    return this.readGeometryFromDocument(
        /** @type {Document} */ (source), opt_options);
  } else if (GVol.xml.isNode(source)) {
    return this.readGeometryFromNode(/** @type {Node} */ (source), opt_options);
  } else if (typeof source === 'string') {
    var doc = GVol.xml.parse(source);
    return this.readGeometryFromDocument(doc, opt_options);
  } else {
    return null;
  }
};


/**
 * @param {Document} doc Document.
 * @param {GVolx.format.ReadOptions=} opt_options Options.
 * @protected
 * @return {GVol.geom.Geometry} Geometry.
 */
GVol.format.XMLFeature.prototype.readGeometryFromDocument = function(doc, opt_options) {
  return null; // not implemented
};


/**
 * @param {Node} node Node.
 * @param {GVolx.format.ReadOptions=} opt_options Options.
 * @protected
 * @return {GVol.geom.Geometry} Geometry.
 */
GVol.format.XMLFeature.prototype.readGeometryFromNode = function(node, opt_options) {
  return null; // not implemented
};


/**
 * @inheritDoc
 */
GVol.format.XMLFeature.prototype.readProjection = function(source) {
  if (GVol.xml.isDocument(source)) {
    return this.readProjectionFromDocument(/** @type {Document} */ (source));
  } else if (GVol.xml.isNode(source)) {
    return this.readProjectionFromNode(/** @type {Node} */ (source));
  } else if (typeof source === 'string') {
    var doc = GVol.xml.parse(source);
    return this.readProjectionFromDocument(doc);
  } else {
    return null;
  }
};


/**
 * @param {Document} doc Document.
 * @protected
 * @return {GVol.proj.Projection} Projection.
 */
GVol.format.XMLFeature.prototype.readProjectionFromDocument = function(doc) {
  return this.defaultDataProjection;
};


/**
 * @param {Node} node Node.
 * @protected
 * @return {GVol.proj.Projection} Projection.
 */
GVol.format.XMLFeature.prototype.readProjectionFromNode = function(node) {
  return this.defaultDataProjection;
};


/**
 * @inheritDoc
 */
GVol.format.XMLFeature.prototype.writeFeature = function(feature, opt_options) {
  var node = this.writeFeatureNode(feature, opt_options);
  return this.xmlSerializer_.serializeToString(node);
};


/**
 * @param {GVol.Feature} feature Feature.
 * @param {GVolx.format.WriteOptions=} opt_options Options.
 * @protected
 * @return {Node} Node.
 */
GVol.format.XMLFeature.prototype.writeFeatureNode = function(feature, opt_options) {
  return null; // not implemented
};


/**
 * @inheritDoc
 */
GVol.format.XMLFeature.prototype.writeFeatures = function(features, opt_options) {
  var node = this.writeFeaturesNode(features, opt_options);
  return this.xmlSerializer_.serializeToString(node);
};


/**
 * @param {Array.<GVol.Feature>} features Features.
 * @param {GVolx.format.WriteOptions=} opt_options Options.
 * @return {Node} Node.
 */
GVol.format.XMLFeature.prototype.writeFeaturesNode = function(features, opt_options) {
  return null; // not implemented
};


/**
 * @inheritDoc
 */
GVol.format.XMLFeature.prototype.writeGeometry = function(geometry, opt_options) {
  var node = this.writeGeometryNode(geometry, opt_options);
  return this.xmlSerializer_.serializeToString(node);
};


/**
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Options.
 * @return {Node} Node.
 */
GVol.format.XMLFeature.prototype.writeGeometryNode = function(geometry, opt_options) {
  return null; // not implemented
};

// 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('GVol.format.GMLBase');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.Feature');
goog.require('GVol.format.Feature');
goog.require('GVol.format.XMLFeature');
goog.require('GVol.geom.GeometryLayout');
goog.require('GVol.geom.LineString');
goog.require('GVol.geom.LinearRing');
goog.require('GVol.geom.MultiLineString');
goog.require('GVol.geom.MultiPoint');
goog.require('GVol.geom.MultiPGVolygon');
goog.require('GVol.geom.Point');
goog.require('GVol.geom.PGVolygon');
goog.require('GVol.obj');
goog.require('GVol.proj');
goog.require('GVol.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 GVol.format.GML2 and
 * GVol.format.GML3.
 *
 * @constructor
 * @abstract
 * @param {GVolx.format.GMLOptions=} opt_options
 *     Optional configuration object.
 * @extends {GVol.format.XMLFeature}
 */
GVol.format.GMLBase = function(opt_options) {
  var options = /** @type {GVolx.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[GVol.format.GMLBase.GMLNS] = {
    'featureMember': GVol.xml.makeReplacer(
        GVol.format.GMLBase.prototype.readFeaturesInternal),
    'featureMembers': GVol.xml.makeReplacer(
        GVol.format.GMLBase.prototype.readFeaturesInternal)
  };

  GVol.format.XMLFeature.call(this);
};
GVol.inherits(GVol.format.GMLBase, GVol.format.XMLFeature);


/**
 * @const
 * @type {string}
 */
GVol.format.GMLBase.GMLNS = 'http://www.opengis.net/gml';


/**
 * A regular expression that matches if a string only contains whitespace
 * characters. It will e.g. match `''`, `' '`, `'\n'` etc. The non-breaking
 * space (0xa0) is explicitly included as IE doesn't include it in its
 * definition of `\s`.
 *
 * Information from `goog.string.isEmptyOrWhitespace`: https://github.com/google/closure-library/blob/e877b1e/closure/goog/string/string.js#L156-L160
 *
 * @const
 * @type {RegExp}
 * @private
 */
GVol.format.GMLBase.ONLY_WHITESPACE_RE_ = /^[\s\xa0]*$/;


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Array.<GVol.Feature> | undefined} Features.
 */
GVol.format.GMLBase.prototype.readFeaturesInternal = function(node, objectStack) {
  var localName = node.localName;
  var features = null;
  if (localName == 'FeatureCGVollection') {
    if (node.namespaceURI === 'http://www.opengis.net/wfs') {
      features = GVol.xml.pushParseAndPop([],
          this.FEATURE_COLLECTION_PARSERS, node,
          objectStack, this);
    } else {
      features = GVol.xml.pushParseAndPop(null,
          this.FEATURE_COLLECTION_PARSERS, node,
          objectStack, this);
    }
  } else if (localName == 'featureMembers' || localName == 'featureMember') {
    var context = objectStack[0];
    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 = '';
            var count = 0;
            var uri = child.namespaceURI;
            for (var candidate in featureNS) {
              if (featureNS[candidate] === uri) {
                key = candidate;
                break;
              }
              ++count;
            }
            if (!key) {
              key = prefix + count;
              featureNS[key] = uri;
            }
            featureType.push(key + ':' + ft);
          }
        }
      }
      if (localName != 'featureMember') {
        // recheck featureType for each featureMember
        context['featureType'] = featureType;
        context['featureNS'] = featureNS;
      }
    }
    if (typeof featureNS === 'string') {
      var ns = featureNS;
      featureNS = {};
      featureNS[defaultPrefix] = ns;
    }
    var parsersNS = {};
    var featureTypes = Array.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') ?
                GVol.xml.makeArrayPusher(this.readFeatureElement, this) :
                GVol.xml.makeReplacer(this.readFeatureElement, this);
        }
      }
      parsersNS[featureNS[p]] = parsers;
    }
    if (localName == 'featureMember') {
      features = GVol.xml.pushParseAndPop(undefined, parsersNS, node, objectStack);
    } else {
      features = GVol.xml.pushParseAndPop([], parsersNS, node, objectStack);
    }
  }
  if (features === null) {
    features = [];
  }
  return features;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {GVol.geom.Geometry|undefined} Geometry.
 */
GVol.format.GMLBase.prototype.readGeometryElement = function(node, objectStack) {
  var context = /** @type {Object} */ (objectStack[0]);
  context['srsName'] = node.firstElementChild.getAttribute('srsName');
  /** @type {GVol.geom.Geometry} */
  var geometry = GVol.xml.pushParseAndPop(null,
      this.GEOMETRY_PARSERS_, node, objectStack, this);
  if (geometry) {
    return /** @type {GVol.geom.Geometry} */ (
      GVol.format.Feature.transformWithOptions(geometry, false, context));
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {GVol.Feature} Feature.
 */
GVol.format.GMLBase.prototype.readFeatureElement = function(node, objectStack) {
  var n;
  var fid = node.getAttribute('fid') ||
      GVol.xml.getAttributeNS(node, GVol.format.GMLBase.GMLNS, 'id');
  var values = {}, geometryName;
  for (n = node.firstElementChild; n; n = n.nextElementSibling) {
    var localName = n.localName;
    // 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 = GVol.xml.getAllTextContent(n, false);
      if (GVol.format.GMLBase.ONLY_WHITESPACE_RE_.test(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 GVol.Feature(values);
  if (geometryName) {
    feature.setGeometryName(geometryName);
  }
  if (fid) {
    feature.setId(fid);
  }
  return feature;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {GVol.geom.Point|undefined} Point.
 */
GVol.format.GMLBase.prototype.readPoint = function(node, objectStack) {
  var flatCoordinates =
      this.readFlatCoordinatesFromNode_(node, objectStack);
  if (flatCoordinates) {
    var point = new GVol.geom.Point(null);
    point.setFlatCoordinates(GVol.geom.GeometryLayout.XYZ, flatCoordinates);
    return point;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {GVol.geom.MultiPoint|undefined} MultiPoint.
 */
GVol.format.GMLBase.prototype.readMultiPoint = function(node, objectStack) {
  /** @type {Array.<Array.<number>>} */
  var coordinates = GVol.xml.pushParseAndPop([],
      this.MULTIPOINT_PARSERS_, node, objectStack, this);
  if (coordinates) {
    return new GVol.geom.MultiPoint(coordinates);
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {GVol.geom.MultiLineString|undefined} MultiLineString.
 */
GVol.format.GMLBase.prototype.readMultiLineString = function(node, objectStack) {
  /** @type {Array.<GVol.geom.LineString>} */
  var lineStrings = GVol.xml.pushParseAndPop([],
      this.MULTILINESTRING_PARSERS_, node, objectStack, this);
  if (lineStrings) {
    var multiLineString = new GVol.geom.MultiLineString(null);
    multiLineString.setLineStrings(lineStrings);
    return multiLineString;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {GVol.geom.MultiPGVolygon|undefined} MultiPGVolygon.
 */
GVol.format.GMLBase.prototype.readMultiPGVolygon = function(node, objectStack) {
  /** @type {Array.<GVol.geom.PGVolygon>} */
  var pGVolygons = GVol.xml.pushParseAndPop([],
      this.MULTIPOLYGON_PARSERS_, node, objectStack, this);
  if (pGVolygons) {
    var multiPGVolygon = new GVol.geom.MultiPGVolygon(null);
    multiPGVolygon.setPGVolygons(pGVolygons);
    return multiPGVolygon;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.GMLBase.prototype.pointMemberParser_ = function(node, objectStack) {
  GVol.xml.parseNode(this.POINTMEMBER_PARSERS_,
      node, objectStack, this);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.GMLBase.prototype.lineStringMemberParser_ = function(node, objectStack) {
  GVol.xml.parseNode(this.LINESTRINGMEMBER_PARSERS_,
      node, objectStack, this);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.GMLBase.prototype.pGVolygonMemberParser_ = function(node, objectStack) {
  GVol.xml.parseNode(this.POLYGONMEMBER_PARSERS_, node,
      objectStack, this);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {GVol.geom.LineString|undefined} LineString.
 */
GVol.format.GMLBase.prototype.readLineString = function(node, objectStack) {
  var flatCoordinates =
      this.readFlatCoordinatesFromNode_(node, objectStack);
  if (flatCoordinates) {
    var lineString = new GVol.geom.LineString(null);
    lineString.setFlatCoordinates(GVol.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.
 */
GVol.format.GMLBase.prototype.readFlatLinearRing_ = function(node, objectStack) {
  var ring = GVol.xml.pushParseAndPop(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 {GVol.geom.LinearRing|undefined} LinearRing.
 */
GVol.format.GMLBase.prototype.readLinearRing = function(node, objectStack) {
  var flatCoordinates =
      this.readFlatCoordinatesFromNode_(node, objectStack);
  if (flatCoordinates) {
    var ring = new GVol.geom.LinearRing(null);
    ring.setFlatCoordinates(GVol.geom.GeometryLayout.XYZ, flatCoordinates);
    return ring;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {GVol.geom.PGVolygon|undefined} PGVolygon.
 */
GVol.format.GMLBase.prototype.readPGVolygon = function(node, objectStack) {
  /** @type {Array.<Array.<number>>} */
  var flatLinearRings = GVol.xml.pushParseAndPop([null],
      this.FLAT_LINEAR_RINGS_PARSERS_, node, objectStack, this);
  if (flatLinearRings && flatLinearRings[0]) {
    var pGVolygon = new GVol.geom.PGVolygon(null);
    var flatCoordinates = flatLinearRings[0];
    var ends = [flatCoordinates.length];
    var i, ii;
    for (i = 1, ii = flatLinearRings.length; i < ii; ++i) {
      GVol.array.extend(flatCoordinates, flatLinearRings[i]);
      ends.push(flatCoordinates.length);
    }
    pGVolygon.setFlatCoordinates(
        GVol.geom.GeometryLayout.XYZ, flatCoordinates, ends);
    return pGVolygon;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<number>} Flat coordinates.
 */
GVol.format.GMLBase.prototype.readFlatCoordinatesFromNode_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(null,
      this.GEOMETRY_FLAT_COORDINATES_PARSERS_, node,
      objectStack, this);
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GMLBase.prototype.MULTIPOINT_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'pointMember': GVol.xml.makeArrayPusher(
        GVol.format.GMLBase.prototype.pointMemberParser_),
    'pointMembers': GVol.xml.makeArrayPusher(
        GVol.format.GMLBase.prototype.pointMemberParser_)
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GMLBase.prototype.MULTILINESTRING_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'lineStringMember': GVol.xml.makeArrayPusher(
        GVol.format.GMLBase.prototype.lineStringMemberParser_),
    'lineStringMembers': GVol.xml.makeArrayPusher(
        GVol.format.GMLBase.prototype.lineStringMemberParser_)
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GMLBase.prototype.MULTIPOLYGON_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'pGVolygonMember': GVol.xml.makeArrayPusher(
        GVol.format.GMLBase.prototype.pGVolygonMemberParser_),
    'pGVolygonMembers': GVol.xml.makeArrayPusher(
        GVol.format.GMLBase.prototype.pGVolygonMemberParser_)
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GMLBase.prototype.POINTMEMBER_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'Point': GVol.xml.makeArrayPusher(
        GVol.format.GMLBase.prototype.readFlatCoordinatesFromNode_)
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GMLBase.prototype.LINESTRINGMEMBER_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'LineString': GVol.xml.makeArrayPusher(
        GVol.format.GMLBase.prototype.readLineString)
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GMLBase.prototype.POLYGONMEMBER_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'PGVolygon': GVol.xml.makeArrayPusher(
        GVol.format.GMLBase.prototype.readPGVolygon)
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @protected
 */
GVol.format.GMLBase.prototype.RING_PARSERS = {
  'http://www.opengis.net/gml': {
    'LinearRing': GVol.xml.makeReplacer(
        GVol.format.GMLBase.prototype.readFlatLinearRing_)
  }
};


/**
 * @inheritDoc
 */
GVol.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 FeatureCGVollection.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {GVolx.format.ReadOptions=} opt_options Options.
 * @return {Array.<GVol.Feature>} Features.
 * @api
 */
GVol.format.GMLBase.prototype.readFeatures;


/**
 * @inheritDoc
 */
GVol.format.GMLBase.prototype.readFeaturesFromNode = function(node, opt_options) {
  var options = {
    featureType: this.featureType,
    featureNS: this.featureNS
  };
  if (opt_options) {
    GVol.obj.assign(options, this.getReadOptions(node, opt_options));
  }
  var features = this.readFeaturesInternal(node, [options]);
  return features || [];
};


/**
 * @inheritDoc
 */
GVol.format.GMLBase.prototype.readProjectionFromNode = function(node) {
  return GVol.proj.get(this.srsName ? this.srsName :
    node.firstElementChild.getAttribute('srsName'));
};

goog.provide('GVol.format.XSD');

goog.require('GVol.xml');
goog.require('GVol.string');


/**
 * @const
 * @type {string}
 */
GVol.format.XSD.NAMESPACE_URI = 'http://www.w3.org/2001/XMLSchema';


/**
 * @param {Node} node Node.
 * @return {boGVolean|undefined} BoGVolean.
 */
GVol.format.XSD.readBoGVolean = function(node) {
  var s = GVol.xml.getAllTextContent(node, false);
  return GVol.format.XSD.readBoGVoleanString(s);
};


/**
 * @param {string} string String.
 * @return {boGVolean|undefined} BoGVolean.
 */
GVol.format.XSD.readBoGVoleanString = 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.
 */
GVol.format.XSD.readDateTime = function(node) {
  var s = GVol.xml.getAllTextContent(node, false);
  var dateTime = Date.parse(s);
  return isNaN(dateTime) ? undefined : dateTime / 1000;
};


/**
 * @param {Node} node Node.
 * @return {number|undefined} Decimal.
 */
GVol.format.XSD.readDecimal = function(node) {
  var s = GVol.xml.getAllTextContent(node, false);
  return GVol.format.XSD.readDecimalString(s);
};


/**
 * @param {string} string String.
 * @return {number|undefined} Decimal.
 */
GVol.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.
 */
GVol.format.XSD.readNonNegativeInteger = function(node) {
  var s = GVol.xml.getAllTextContent(node, false);
  return GVol.format.XSD.readNonNegativeIntegerString(s);
};


/**
 * @param {string} string String.
 * @return {number|undefined} Non negative integer.
 */
GVol.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.
 */
GVol.format.XSD.readString = function(node) {
  return GVol.xml.getAllTextContent(node, false).trim();
};


/**
 * @param {Node} node Node to append a TextNode with the boGVolean to.
 * @param {boGVolean} boGVol BoGVolean.
 */
GVol.format.XSD.writeBoGVoleanTextNode = function(node, boGVol) {
  GVol.format.XSD.writeStringTextNode(node, (boGVol) ? '1' : '0');
};


/**
 * @param {Node} node Node to append a CDATA Section with the string to.
 * @param {string} string String.
 */
GVol.format.XSD.writeCDATASection = function(node, string) {
  node.appendChild(GVol.xml.DOCUMENT.createCDATASection(string));
};


/**
 * @param {Node} node Node to append a TextNode with the dateTime to.
 * @param {number} dateTime DateTime in seconds.
 */
GVol.format.XSD.writeDateTimeTextNode = function(node, dateTime) {
  var date = new Date(dateTime * 1000);
  var string = date.getUTCFullYear() + '-' +
      GVol.string.padNumber(date.getUTCMonth() + 1, 2) + '-' +
      GVol.string.padNumber(date.getUTCDate(), 2) + 'T' +
      GVol.string.padNumber(date.getUTCHours(), 2) + ':' +
      GVol.string.padNumber(date.getUTCMinutes(), 2) + ':' +
      GVol.string.padNumber(date.getUTCSeconds(), 2) + 'Z';
  node.appendChild(GVol.xml.DOCUMENT.createTextNode(string));
};


/**
 * @param {Node} node Node to append a TextNode with the decimal to.
 * @param {number} decimal Decimal.
 */
GVol.format.XSD.writeDecimalTextNode = function(node, decimal) {
  var string = decimal.toPrecision();
  node.appendChild(GVol.xml.DOCUMENT.createTextNode(string));
};


/**
 * @param {Node} node Node to append a TextNode with the decimal to.
 * @param {number} nonNegativeInteger Non negative integer.
 */
GVol.format.XSD.writeNonNegativeIntegerTextNode = function(node, nonNegativeInteger) {
  var string = nonNegativeInteger.toString();
  node.appendChild(GVol.xml.DOCUMENT.createTextNode(string));
};


/**
 * @param {Node} node Node to append a TextNode with the string to.
 * @param {string} string String.
 */
GVol.format.XSD.writeStringTextNode = function(node, string) {
  node.appendChild(GVol.xml.DOCUMENT.createTextNode(string));
};

goog.provide('GVol.format.GML3');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.extent');
goog.require('GVol.format.Feature');
goog.require('GVol.format.GMLBase');
goog.require('GVol.format.XSD');
goog.require('GVol.geom.Geometry');
goog.require('GVol.geom.GeometryLayout');
goog.require('GVol.geom.LineString');
goog.require('GVol.geom.MultiLineString');
goog.require('GVol.geom.MultiPGVolygon');
goog.require('GVol.geom.PGVolygon');
goog.require('GVol.obj');
goog.require('GVol.proj');
goog.require('GVol.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 {GVolx.format.GMLOptions=} opt_options
 *     Optional configuration object.
 * @extends {GVol.format.GMLBase}
 * @api
 */
GVol.format.GML3 = function(opt_options) {
  var options = /** @type {GVolx.format.GMLOptions} */
      (opt_options ? opt_options : {});

  GVol.format.GMLBase.call(this, options);

  /**
   * @private
   * @type {boGVolean}
   */
  this.surface_ = options.surface !== undefined ? options.surface : false;

  /**
   * @private
   * @type {boGVolean}
   */
  this.curve_ = options.curve !== undefined ? options.curve : false;

  /**
   * @private
   * @type {boGVolean}
   */
  this.multiCurve_ = options.multiCurve !== undefined ?
    options.multiCurve : true;

  /**
   * @private
   * @type {boGVolean}
   */
  this.multiSurface_ = options.multiSurface !== undefined ?
    options.multiSurface : true;

  /**
   * @inheritDoc
   */
  this.schemaLocation = options.schemaLocation ?
    options.schemaLocation : GVol.format.GML3.schemaLocation_;

  /**
   * @private
   * @type {boGVolean}
   */
  this.hasZ = options.hasZ !== undefined ?
    options.hasZ : false;

};
GVol.inherits(GVol.format.GML3, GVol.format.GMLBase);


/**
 * @const
 * @type {string}
 * @private
 */
GVol.format.GML3.schemaLocation_ = GVol.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 {GVol.geom.MultiLineString|undefined} MultiLineString.
 */
GVol.format.GML3.prototype.readMultiCurve_ = function(node, objectStack) {
  /** @type {Array.<GVol.geom.LineString>} */
  var lineStrings = GVol.xml.pushParseAndPop([],
      this.MULTICURVE_PARSERS_, node, objectStack, this);
  if (lineStrings) {
    var multiLineString = new GVol.geom.MultiLineString(null);
    multiLineString.setLineStrings(lineStrings);
    return multiLineString;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {GVol.geom.MultiPGVolygon|undefined} MultiPGVolygon.
 */
GVol.format.GML3.prototype.readMultiSurface_ = function(node, objectStack) {
  /** @type {Array.<GVol.geom.PGVolygon>} */
  var pGVolygons = GVol.xml.pushParseAndPop([],
      this.MULTISURFACE_PARSERS_, node, objectStack, this);
  if (pGVolygons) {
    var multiPGVolygon = new GVol.geom.MultiPGVolygon(null);
    multiPGVolygon.setPGVolygons(pGVolygons);
    return multiPGVolygon;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.GML3.prototype.curveMemberParser_ = function(node, objectStack) {
  GVol.xml.parseNode(this.CURVEMEMBER_PARSERS_, node, objectStack, this);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.GML3.prototype.surfaceMemberParser_ = function(node, objectStack) {
  GVol.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.
 */
GVol.format.GML3.prototype.readPatch_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop([null],
      this.PATCHES_PARSERS_, node, objectStack, this);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<number>|undefined} flat coordinates.
 */
GVol.format.GML3.prototype.readSegment_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop([null],
      this.SEGMENTS_PARSERS_, node, objectStack, this);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<(Array.<number>)>|undefined} flat coordinates.
 */
GVol.format.GML3.prototype.readPGVolygonPatch_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop([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.
 */
GVol.format.GML3.prototype.readLineStringSegment_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop([null],
      this.GEOMETRY_FLAT_COORDINATES_PARSERS_,
      node, objectStack, this);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.GML3.prototype.interiorParser_ = function(node, objectStack) {
  /** @type {Array.<number>|undefined} */
  var flatLinearRing = GVol.xml.pushParseAndPop(undefined,
      this.RING_PARSERS, node, objectStack, this);
  if (flatLinearRing) {
    var flatLinearRings = /** @type {Array.<Array.<number>>} */
        (objectStack[objectStack.length - 1]);
    flatLinearRings.push(flatLinearRing);
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.GML3.prototype.exteriorParser_ = function(node, objectStack) {
  /** @type {Array.<number>|undefined} */
  var flatLinearRing = GVol.xml.pushParseAndPop(undefined,
      this.RING_PARSERS, node, objectStack, this);
  if (flatLinearRing) {
    var flatLinearRings = /** @type {Array.<Array.<number>>} */
        (objectStack[objectStack.length - 1]);
    flatLinearRings[0] = flatLinearRing;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {GVol.geom.PGVolygon|undefined} PGVolygon.
 */
GVol.format.GML3.prototype.readSurface_ = function(node, objectStack) {
  /** @type {Array.<Array.<number>>} */
  var flatLinearRings = GVol.xml.pushParseAndPop([null],
      this.SURFACE_PARSERS_, node, objectStack, this);
  if (flatLinearRings && flatLinearRings[0]) {
    var pGVolygon = new GVol.geom.PGVolygon(null);
    var flatCoordinates = flatLinearRings[0];
    var ends = [flatCoordinates.length];
    var i, ii;
    for (i = 1, ii = flatLinearRings.length; i < ii; ++i) {
      GVol.array.extend(flatCoordinates, flatLinearRings[i]);
      ends.push(flatCoordinates.length);
    }
    pGVolygon.setFlatCoordinates(
        GVol.geom.GeometryLayout.XYZ, flatCoordinates, ends);
    return pGVolygon;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {GVol.geom.LineString|undefined} LineString.
 */
GVol.format.GML3.prototype.readCurve_ = function(node, objectStack) {
  /** @type {Array.<number>} */
  var flatCoordinates = GVol.xml.pushParseAndPop([null],
      this.CURVE_PARSERS_, node, objectStack, this);
  if (flatCoordinates) {
    var lineString = new GVol.geom.LineString(null);
    lineString.setFlatCoordinates(GVol.geom.GeometryLayout.XYZ, flatCoordinates);
    return lineString;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {GVol.Extent|undefined} Envelope.
 */
GVol.format.GML3.prototype.readEnvelope_ = function(node, objectStack) {
  /** @type {Array.<number>} */
  var flatCoordinates = GVol.xml.pushParseAndPop([null],
      this.ENVELOPE_PARSERS_, node, objectStack, this);
  return GVol.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.
 */
GVol.format.GML3.prototype.readFlatPos_ = function(node, objectStack) {
  var s = GVol.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];
  var containerSrs = context['srsName'];
  var axisOrientation = 'enu';
  if (containerSrs) {
    var proj = GVol.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.
 */
GVol.format.GML3.prototype.readFlatPosList_ = function(node, objectStack) {
  var s = GVol.xml.getAllTextContent(node, false).replace(/^\s*|\s*$/g, '');
  var context = objectStack[0];
  var containerSrs = context['srsName'];
  var containerDimension = node.parentNode.getAttribute('srsDimension');
  var axisOrientation = 'enu';
  if (containerSrs) {
    var proj = GVol.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 = GVol.format.XSD.readNonNegativeIntegerString(
        node.getAttribute('srsDimension'));
  } else if (node.getAttribute('dimension')) {
    dim = GVol.format.XSD.readNonNegativeIntegerString(
        node.getAttribute('dimension'));
  } else if (containerDimension) {
    dim = GVol.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, GVol.XmlParser>>}
 * @private
 */
GVol.format.GML3.prototype.GEOMETRY_FLAT_COORDINATES_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'pos': GVol.xml.makeReplacer(GVol.format.GML3.prototype.readFlatPos_),
    'posList': GVol.xml.makeReplacer(GVol.format.GML3.prototype.readFlatPosList_)
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GML3.prototype.FLAT_LINEAR_RINGS_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'interior': GVol.format.GML3.prototype.interiorParser_,
    'exterior': GVol.format.GML3.prototype.exteriorParser_
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GML3.prototype.GEOMETRY_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'Point': GVol.xml.makeReplacer(GVol.format.GMLBase.prototype.readPoint),
    'MultiPoint': GVol.xml.makeReplacer(
        GVol.format.GMLBase.prototype.readMultiPoint),
    'LineString': GVol.xml.makeReplacer(
        GVol.format.GMLBase.prototype.readLineString),
    'MultiLineString': GVol.xml.makeReplacer(
        GVol.format.GMLBase.prototype.readMultiLineString),
    'LinearRing': GVol.xml.makeReplacer(
        GVol.format.GMLBase.prototype.readLinearRing),
    'PGVolygon': GVol.xml.makeReplacer(GVol.format.GMLBase.prototype.readPGVolygon),
    'MultiPGVolygon': GVol.xml.makeReplacer(
        GVol.format.GMLBase.prototype.readMultiPGVolygon),
    'Surface': GVol.xml.makeReplacer(GVol.format.GML3.prototype.readSurface_),
    'MultiSurface': GVol.xml.makeReplacer(
        GVol.format.GML3.prototype.readMultiSurface_),
    'Curve': GVol.xml.makeReplacer(GVol.format.GML3.prototype.readCurve_),
    'MultiCurve': GVol.xml.makeReplacer(
        GVol.format.GML3.prototype.readMultiCurve_),
    'Envelope': GVol.xml.makeReplacer(GVol.format.GML3.prototype.readEnvelope_)
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GML3.prototype.MULTICURVE_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'curveMember': GVol.xml.makeArrayPusher(
        GVol.format.GML3.prototype.curveMemberParser_),
    'curveMembers': GVol.xml.makeArrayPusher(
        GVol.format.GML3.prototype.curveMemberParser_)
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GML3.prototype.MULTISURFACE_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'surfaceMember': GVol.xml.makeArrayPusher(
        GVol.format.GML3.prototype.surfaceMemberParser_),
    'surfaceMembers': GVol.xml.makeArrayPusher(
        GVol.format.GML3.prototype.surfaceMemberParser_)
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GML3.prototype.CURVEMEMBER_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'LineString': GVol.xml.makeArrayPusher(
        GVol.format.GMLBase.prototype.readLineString),
    'Curve': GVol.xml.makeArrayPusher(GVol.format.GML3.prototype.readCurve_)
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GML3.prototype.SURFACEMEMBER_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'PGVolygon': GVol.xml.makeArrayPusher(GVol.format.GMLBase.prototype.readPGVolygon),
    'Surface': GVol.xml.makeArrayPusher(GVol.format.GML3.prototype.readSurface_)
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GML3.prototype.SURFACE_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'patches': GVol.xml.makeReplacer(GVol.format.GML3.prototype.readPatch_)
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GML3.prototype.CURVE_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'segments': GVol.xml.makeReplacer(GVol.format.GML3.prototype.readSegment_)
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GML3.prototype.ENVELOPE_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'lowerCorner': GVol.xml.makeArrayPusher(
        GVol.format.GML3.prototype.readFlatPosList_),
    'upperCorner': GVol.xml.makeArrayPusher(
        GVol.format.GML3.prototype.readFlatPosList_)
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GML3.prototype.PATCHES_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'PGVolygonPatch': GVol.xml.makeReplacer(
        GVol.format.GML3.prototype.readPGVolygonPatch_)
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GML3.prototype.SEGMENTS_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'LineStringSegment': GVol.xml.makeReplacer(
        GVol.format.GML3.prototype.readLineStringSegment_)
  }
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.Point} value Point geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML3.prototype.writePos_ = function(node, value, objectStack) {
  var context = objectStack[objectStack.length - 1];
  var hasZ = context['hasZ'];
  var srsName = context['srsName'];
  var axisOrientation = 'enu';
  if (srsName) {
    axisOrientation = GVol.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]);
  }
  if (hasZ) {
    // For newly created points, Z can be undefined.
    var z = point[2] || 0;
    coords += ' ' + z;
  }
  GVol.format.XSD.writeStringTextNode(node, coords);
};


/**
 * @param {Array.<number>} point Point geometry.
 * @param {string=} opt_srsName Optional srsName
 * @param {boGVolean=} opt_hasZ whether the geometry has a Z coordinate (is 3D) or not.
 * @return {string} The coords string.
 * @private
 */
GVol.format.GML3.prototype.getCoords_ = function(point, opt_srsName, opt_hasZ) {
  var axisOrientation = 'enu';
  if (opt_srsName) {
    axisOrientation = GVol.proj.get(opt_srsName).getAxisOrientation();
  }
  var coords = ((axisOrientation.substr(0, 2) === 'en') ?
    point[0] + ' ' + point[1] :
    point[1] + ' ' + point[0]);
  if (opt_hasZ) {
    // For newly created points, Z can be undefined.
    var z = point[2] || 0;
    coords += ' ' + z;
  }

  return coords;
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.LineString|GVol.geom.LinearRing} value Geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML3.prototype.writePosList_ = function(node, value, objectStack) {
  var context = objectStack[objectStack.length - 1];
  var hasZ = context['hasZ'];
  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, hasZ);
  }
  GVol.format.XSD.writeStringTextNode(node, parts.join(' '));
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.Point} geometry Point geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML3.prototype.writePoint_ = function(node, geometry, objectStack) {
  var context = objectStack[objectStack.length - 1];
  var srsName = context['srsName'];
  if (srsName) {
    node.setAttribute('srsName', srsName);
  }
  var pos = GVol.xml.createElementNS(node.namespaceURI, 'pos');
  node.appendChild(pos);
  this.writePos_(pos, geometry, objectStack);
};


/**
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.GML3.ENVELOPE_SERIALIZERS_ = {
  'http://www.opengis.net/gml': {
    'lowerCorner': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
    'upperCorner': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode)
  }
};


/**
 * @param {Node} node Node.
 * @param {GVol.Extent} extent Extent.
 * @param {Array.<*>} objectStack Node stack.
 */
GVol.format.GML3.prototype.writeEnvelope = function(node, extent, objectStack) {
  var context = objectStack[objectStack.length - 1];
  var srsName = context['srsName'];
  if (srsName) {
    node.setAttribute('srsName', srsName);
  }
  var keys = ['lowerCorner', 'upperCorner'];
  var values = [extent[0] + ' ' + extent[1], extent[2] + ' ' + extent[3]];
  GVol.xml.pushSerializeAndPop(/** @type {GVol.XmlNodeStackItem} */
      ({node: node}), GVol.format.GML3.ENVELOPE_SERIALIZERS_,
      GVol.xml.OBJECT_PROPERTY_NODE_FACTORY,
      values,
      objectStack, keys, this);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.LinearRing} geometry LinearRing geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML3.prototype.writeLinearRing_ = function(node, geometry, objectStack) {
  var context = objectStack[objectStack.length - 1];
  var srsName = context['srsName'];
  if (srsName) {
    node.setAttribute('srsName', srsName);
  }
  var posList = GVol.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
 */
GVol.format.GML3.prototype.RING_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) {
  var context = objectStack[objectStack.length - 1];
  var parentNode = context.node;
  var exteriorWritten = context['exteriorWritten'];
  if (exteriorWritten === undefined) {
    context['exteriorWritten'] = true;
  }
  return GVol.xml.createElementNS(parentNode.namespaceURI,
      exteriorWritten !== undefined ? 'interior' : 'exterior');
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.PGVolygon} geometry PGVolygon geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML3.prototype.writeSurfaceOrPGVolygon_ = function(node, geometry, objectStack) {
  var context = objectStack[objectStack.length - 1];
  var hasZ = context['hasZ'];
  var srsName = context['srsName'];
  if (node.nodeName !== 'PGVolygonPatch' && srsName) {
    node.setAttribute('srsName', srsName);
  }
  if (node.nodeName === 'PGVolygon' || node.nodeName === 'PGVolygonPatch') {
    var rings = geometry.getLinearRings();
    GVol.xml.pushSerializeAndPop(
        {node: node, hasZ: hasZ, srsName: srsName},
        GVol.format.GML3.RING_SERIALIZERS_,
        this.RING_NODE_FACTORY_,
        rings, objectStack, undefined, this);
  } else if (node.nodeName === 'Surface') {
    var patches = GVol.xml.createElementNS(node.namespaceURI, 'patches');
    node.appendChild(patches);
    this.writeSurfacePatches_(
        patches, geometry, objectStack);
  }
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.LineString} geometry LineString geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML3.prototype.writeCurveOrLineString_ = function(node, geometry, objectStack) {
  var context = objectStack[objectStack.length - 1];
  var srsName = context['srsName'];
  if (node.nodeName !== 'LineStringSegment' && srsName) {
    node.setAttribute('srsName', srsName);
  }
  if (node.nodeName === 'LineString' ||
      node.nodeName === 'LineStringSegment') {
    var posList = GVol.xml.createElementNS(node.namespaceURI, 'posList');
    node.appendChild(posList);
    this.writePosList_(posList, geometry, objectStack);
  } else if (node.nodeName === 'Curve') {
    var segments = GVol.xml.createElementNS(node.namespaceURI, 'segments');
    node.appendChild(segments);
    this.writeCurveSegments_(segments,
        geometry, objectStack);
  }
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.MultiPGVolygon} geometry MultiPGVolygon geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML3.prototype.writeMultiSurfaceOrPGVolygon_ = function(node, geometry, objectStack) {
  var context = objectStack[objectStack.length - 1];
  var hasZ = context['hasZ'];
  var srsName = context['srsName'];
  var surface = context['surface'];
  if (srsName) {
    node.setAttribute('srsName', srsName);
  }
  var pGVolygons = geometry.getPGVolygons();
  GVol.xml.pushSerializeAndPop({node: node, hasZ: hasZ, srsName: srsName, surface: surface},
      GVol.format.GML3.SURFACEORPOLYGONMEMBER_SERIALIZERS_,
      this.MULTIGEOMETRY_MEMBER_NODE_FACTORY_, pGVolygons,
      objectStack, undefined, this);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.MultiPoint} geometry MultiPoint geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML3.prototype.writeMultiPoint_ = function(node, geometry,
    objectStack) {
  var context = objectStack[objectStack.length - 1];
  var srsName = context['srsName'];
  var hasZ = context['hasZ'];
  if (srsName) {
    node.setAttribute('srsName', srsName);
  }
  var points = geometry.getPoints();
  GVol.xml.pushSerializeAndPop({node: node, hasZ: hasZ, srsName: srsName},
      GVol.format.GML3.POINTMEMBER_SERIALIZERS_,
      GVol.xml.makeSimpleNodeFactory('pointMember'), points,
      objectStack, undefined, this);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.MultiLineString} geometry MultiLineString geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML3.prototype.writeMultiCurveOrLineString_ = function(node, geometry, objectStack) {
  var context = objectStack[objectStack.length - 1];
  var hasZ = context['hasZ'];
  var srsName = context['srsName'];
  var curve = context['curve'];
  if (srsName) {
    node.setAttribute('srsName', srsName);
  }
  var lines = geometry.getLineStrings();
  GVol.xml.pushSerializeAndPop({node: node, hasZ: hasZ, srsName: srsName, curve: curve},
      GVol.format.GML3.LINESTRINGORCURVEMEMBER_SERIALIZERS_,
      this.MULTIGEOMETRY_MEMBER_NODE_FACTORY_, lines,
      objectStack, undefined, this);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.LinearRing} ring LinearRing geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML3.prototype.writeRing_ = function(node, ring, objectStack) {
  var linearRing = GVol.xml.createElementNS(node.namespaceURI, 'LinearRing');
  node.appendChild(linearRing);
  this.writeLinearRing_(linearRing, ring, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.PGVolygon} pGVolygon PGVolygon geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML3.prototype.writeSurfaceOrPGVolygonMember_ = function(node, pGVolygon, objectStack) {
  var child = this.GEOMETRY_NODE_FACTORY_(
      pGVolygon, objectStack);
  if (child) {
    node.appendChild(child);
    this.writeSurfaceOrPGVolygon_(child, pGVolygon, objectStack);
  }
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.Point} point Point geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML3.prototype.writePointMember_ = function(node, point, objectStack) {
  var child = GVol.xml.createElementNS(node.namespaceURI, 'Point');
  node.appendChild(child);
  this.writePoint_(child, point, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.LineString} line LineString geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML3.prototype.writeLineStringOrCurveMember_ = function(node, line, objectStack) {
  var child = this.GEOMETRY_NODE_FACTORY_(line, objectStack);
  if (child) {
    node.appendChild(child);
    this.writeCurveOrLineString_(child, line, objectStack);
  }
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.PGVolygon} pGVolygon PGVolygon geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML3.prototype.writeSurfacePatches_ = function(node, pGVolygon, objectStack) {
  var child = GVol.xml.createElementNS(node.namespaceURI, 'PGVolygonPatch');
  node.appendChild(child);
  this.writeSurfaceOrPGVolygon_(child, pGVolygon, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.LineString} line LineString geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML3.prototype.writeCurveSegments_ = function(node, line, objectStack) {
  var child = GVol.xml.createElementNS(node.namespaceURI,
      'LineStringSegment');
  node.appendChild(child);
  this.writeCurveOrLineString_(child, line, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.Geometry|GVol.Extent} geometry Geometry.
 * @param {Array.<*>} objectStack Node stack.
 */
GVol.format.GML3.prototype.writeGeometryElement = function(node, geometry, objectStack) {
  var context = /** @type {GVolx.format.WriteOptions} */ (objectStack[objectStack.length - 1]);
  var item = GVol.obj.assign({}, context);
  item.node = node;
  var value;
  if (Array.isArray(geometry)) {
    if (context.dataProjection) {
      value = GVol.proj.transformExtent(
          geometry, context.featureProjection, context.dataProjection);
    } else {
      value = geometry;
    }
  } else {
    value =
        GVol.format.Feature.transformWithOptions(/** @type {GVol.geom.Geometry} */ (geometry), true, context);
  }
  GVol.xml.pushSerializeAndPop(/** @type {GVol.XmlNodeStackItem} */
      (item), GVol.format.GML3.GEOMETRY_SERIALIZERS_,
      this.GEOMETRY_NODE_FACTORY_, [value],
      objectStack, undefined, this);
};


/**
 * @param {Node} node Node.
 * @param {GVol.Feature} feature Feature.
 * @param {Array.<*>} objectStack Node stack.
 */
GVol.format.GML3.prototype.writeFeatureElement = function(node, feature, objectStack) {
  var fid = feature.getId();
  if (fid) {
    node.setAttribute('fid', fid);
  }
  var context = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  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 GVol.geom.Geometry) {
        if (!(key in context.serializers[featureNS])) {
          context.serializers[featureNS][key] = GVol.xml.makeChildAppender(
              this.writeGeometryElement, this);
        }
      } else {
        if (!(key in context.serializers[featureNS])) {
          context.serializers[featureNS][key] = GVol.xml.makeChildAppender(
              GVol.format.XSD.writeStringTextNode);
        }
      }
    }
  }
  var item = GVol.obj.assign({}, context);
  item.node = node;
  GVol.xml.pushSerializeAndPop(/** @type {GVol.XmlNodeStackItem} */
      (item), context.serializers,
      GVol.xml.makeSimpleNodeFactory(undefined, featureNS),
      values,
      objectStack, keys);
};


/**
 * @param {Node} node Node.
 * @param {Array.<GVol.Feature>} features Features.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML3.prototype.writeFeatureMembers_ = function(node, features, objectStack) {
  var context = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  var featureType = context['featureType'];
  var featureNS = context['featureNS'];
  var serializers = {};
  serializers[featureNS] = {};
  serializers[featureNS][featureType] = GVol.xml.makeChildAppender(
      this.writeFeatureElement, this);
  var item = GVol.obj.assign({}, context);
  item.node = node;
  GVol.xml.pushSerializeAndPop(/** @type {GVol.XmlNodeStackItem} */
      (item),
      serializers,
      GVol.xml.makeSimpleNodeFactory(featureType, featureNS), features,
      objectStack);
};


/**
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.GML3.SURFACEORPOLYGONMEMBER_SERIALIZERS_ = {
  'http://www.opengis.net/gml': {
    'surfaceMember': GVol.xml.makeChildAppender(
        GVol.format.GML3.prototype.writeSurfaceOrPGVolygonMember_),
    'pGVolygonMember': GVol.xml.makeChildAppender(
        GVol.format.GML3.prototype.writeSurfaceOrPGVolygonMember_)
  }
};


/**
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.GML3.POINTMEMBER_SERIALIZERS_ = {
  'http://www.opengis.net/gml': {
    'pointMember': GVol.xml.makeChildAppender(
        GVol.format.GML3.prototype.writePointMember_)
  }
};


/**
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.GML3.LINESTRINGORCURVEMEMBER_SERIALIZERS_ = {
  'http://www.opengis.net/gml': {
    'lineStringMember': GVol.xml.makeChildAppender(
        GVol.format.GML3.prototype.writeLineStringOrCurveMember_),
    'curveMember': GVol.xml.makeChildAppender(
        GVol.format.GML3.prototype.writeLineStringOrCurveMember_)
  }
};


/**
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.GML3.RING_SERIALIZERS_ = {
  'http://www.opengis.net/gml': {
    'exterior': GVol.xml.makeChildAppender(GVol.format.GML3.prototype.writeRing_),
    'interior': GVol.xml.makeChildAppender(GVol.format.GML3.prototype.writeRing_)
  }
};


/**
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.GML3.GEOMETRY_SERIALIZERS_ = {
  'http://www.opengis.net/gml': {
    'Curve': GVol.xml.makeChildAppender(
        GVol.format.GML3.prototype.writeCurveOrLineString_),
    'MultiCurve': GVol.xml.makeChildAppender(
        GVol.format.GML3.prototype.writeMultiCurveOrLineString_),
    'Point': GVol.xml.makeChildAppender(GVol.format.GML3.prototype.writePoint_),
    'MultiPoint': GVol.xml.makeChildAppender(
        GVol.format.GML3.prototype.writeMultiPoint_),
    'LineString': GVol.xml.makeChildAppender(
        GVol.format.GML3.prototype.writeCurveOrLineString_),
    'MultiLineString': GVol.xml.makeChildAppender(
        GVol.format.GML3.prototype.writeMultiCurveOrLineString_),
    'LinearRing': GVol.xml.makeChildAppender(
        GVol.format.GML3.prototype.writeLinearRing_),
    'PGVolygon': GVol.xml.makeChildAppender(
        GVol.format.GML3.prototype.writeSurfaceOrPGVolygon_),
    'MultiPGVolygon': GVol.xml.makeChildAppender(
        GVol.format.GML3.prototype.writeMultiSurfaceOrPGVolygon_),
    'Surface': GVol.xml.makeChildAppender(
        GVol.format.GML3.prototype.writeSurfaceOrPGVolygon_),
    'MultiSurface': GVol.xml.makeChildAppender(
        GVol.format.GML3.prototype.writeMultiSurfaceOrPGVolygon_),
    'Envelope': GVol.xml.makeChildAppender(
        GVol.format.GML3.prototype.writeEnvelope)
  }
};


/**
 * @const
 * @type {Object.<string, string>}
 * @private
 */
GVol.format.GML3.MULTIGEOMETRY_TO_MEMBER_NODENAME_ = {
  'MultiLineString': 'lineStringMember',
  'MultiCurve': 'curveMember',
  'MultiPGVolygon': 'pGVolygonMember',
  'MultiSurface': 'surfaceMember'
};


/**
 * @const
 * @param {*} value Value.
 * @param {Array.<*>} objectStack Object stack.
 * @param {string=} opt_nodeName Node name.
 * @return {Node|undefined} Node.
 * @private
 */
GVol.format.GML3.prototype.MULTIGEOMETRY_MEMBER_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) {
  var parentNode = objectStack[objectStack.length - 1].node;
  return GVol.xml.createElementNS('http://www.opengis.net/gml',
      GVol.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
 */
GVol.format.GML3.prototype.GEOMETRY_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) {
  var context = objectStack[objectStack.length - 1];
  var multiSurface = context['multiSurface'];
  var surface = context['surface'];
  var curve = context['curve'];
  var multiCurve = context['multiCurve'];
  var nodeName;
  if (!Array.isArray(value)) {
    nodeName = /** @type {GVol.geom.Geometry} */ (value).getType();
    if (nodeName === 'MultiPGVolygon' && multiSurface === true) {
      nodeName = 'MultiSurface';
    } else if (nodeName === 'PGVolygon' && surface === true) {
      nodeName = 'Surface';
    } else if (nodeName === 'LineString' && curve === true) {
      nodeName = 'Curve';
    } else if (nodeName === 'MultiLineString' && multiCurve === true) {
      nodeName = 'MultiCurve';
    }
  } else {
    nodeName = 'Envelope';
  }
  return GVol.xml.createElementNS('http://www.opengis.net/gml',
      nodeName);
};


/**
 * Encode a geometry in GML 3.1.1 Simple Features.
 *
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Options.
 * @return {Node} Node.
 * @override
 * @api
 */
GVol.format.GML3.prototype.writeGeometryNode = function(geometry, opt_options) {
  opt_options = this.adaptOptions(opt_options);
  var geom = GVol.xml.createElementNS('http://www.opengis.net/gml', 'geom');
  var context = {node: geom, hasZ: this.hasZ, srsName: this.srsName,
    curve: this.curve_, surface: this.surface_,
    multiSurface: this.multiSurface_, multiCurve: this.multiCurve_};
  if (opt_options) {
    GVol.obj.assign(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.<GVol.Feature>} features Features.
 * @param {GVolx.format.WriteOptions=} opt_options Options.
 * @return {string} Result.
 * @api
 */
GVol.format.GML3.prototype.writeFeatures;


/**
 * Encode an array of features in the GML 3.1.1 format as an XML node.
 *
 * @param {Array.<GVol.Feature>} features Features.
 * @param {GVolx.format.WriteOptions=} opt_options Options.
 * @return {Node} Node.
 * @override
 * @api
 */
GVol.format.GML3.prototype.writeFeaturesNode = function(features, opt_options) {
  opt_options = this.adaptOptions(opt_options);
  var node = GVol.xml.createElementNS('http://www.opengis.net/gml',
      'featureMembers');
  GVol.xml.setAttributeNS(node, 'http://www.w3.org/2001/XMLSchema-instance',
      'xsi:schemaLocation', this.schemaLocation);
  var context = {
    srsName: this.srsName,
    hasZ: this.hasZ,
    curve: this.curve_,
    surface: this.surface_,
    multiSurface: this.multiSurface_,
    multiCurve: this.multiCurve_,
    featureNS: this.featureNS,
    featureType: this.featureType
  };
  if (opt_options) {
    GVol.obj.assign(context, opt_options);
  }
  this.writeFeatureMembers_(node, features, [context]);
  return node;
};

goog.provide('GVol.format.GML');

goog.require('GVol.format.GML3');


/**
 * @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 {GVolx.format.GMLOptions=} opt_options
 *     Optional configuration object.
 * @extends {GVol.format.GMLBase}
 * @api
 */
GVol.format.GML = GVol.format.GML3;


/**
 * Encode an array of features in GML 3.1.1 Simple Features.
 *
 * @function
 * @param {Array.<GVol.Feature>} features Features.
 * @param {GVolx.format.WriteOptions=} opt_options Options.
 * @return {string} Result.
 * @api
 */
GVol.format.GML.prototype.writeFeatures;


/**
 * Encode an array of features in the GML 3.1.1 format as an XML node.
 *
 * @function
 * @param {Array.<GVol.Feature>} features Features.
 * @param {GVolx.format.WriteOptions=} opt_options Options.
 * @return {Node} Node.
 * @api
 */
GVol.format.GML.prototype.writeFeaturesNode;

goog.provide('GVol.format.GML2');

goog.require('GVol');
goog.require('GVol.extent');
goog.require('GVol.format.Feature');
goog.require('GVol.format.GMLBase');
goog.require('GVol.format.XSD');
goog.require('GVol.geom.Geometry');
goog.require('GVol.obj');
goog.require('GVol.proj');
goog.require('GVol.xml');


/**
 * @classdesc
 * Feature format for reading and writing data in the GML format,
 * version 2.1.2.
 *
 * @constructor
 * @param {GVolx.format.GMLOptions=} opt_options Optional configuration object.
 * @extends {GVol.format.GMLBase}
 * @api
 */
GVol.format.GML2 = function(opt_options) {
  var options = /** @type {GVolx.format.GMLOptions} */
      (opt_options ? opt_options : {});

  GVol.format.GMLBase.call(this, options);

  this.FEATURE_COLLECTION_PARSERS[GVol.format.GMLBase.GMLNS][
      'featureMember'] =
      GVol.xml.makeArrayPusher(GVol.format.GMLBase.prototype.readFeaturesInternal);

  /**
   * @inheritDoc
   */
  this.schemaLocation = options.schemaLocation ?
    options.schemaLocation : GVol.format.GML2.schemaLocation_;

};
GVol.inherits(GVol.format.GML2, GVol.format.GMLBase);


/**
 * @const
 * @type {string}
 * @private
 */
GVol.format.GML2.schemaLocation_ = GVol.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.
 */
GVol.format.GML2.prototype.readFlatCoordinates_ = function(node, objectStack) {
  var s = GVol.xml.getAllTextContent(node, false).replace(/^\s*|\s*$/g, '');
  var context = /** @type {GVol.XmlNodeStackItem} */ (objectStack[0]);
  var containerSrs = context['srsName'];
  var axisOrientation = 'enu';
  if (containerSrs) {
    var proj = GVol.proj.get(containerSrs);
    if (proj) {
      axisOrientation = proj.getAxisOrientation();
    }
  }
  var coordsGroups = s.trim().split(/\s+/);
  var x, y, z;
  var flatCoordinates = [];
  for (var i = 0, ii = coordsGroups.length; i < ii; i++) {
    var coords = coordsGroups[i].split(/,+/);
    x = parseFloat(coords[0]);
    y = parseFloat(coords[1]);
    z = (coords.length === 3) ? parseFloat(coords[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 {GVol.Extent|undefined} Envelope.
 */
GVol.format.GML2.prototype.readBox_ = function(node, objectStack) {
  /** @type {Array.<number>} */
  var flatCoordinates = GVol.xml.pushParseAndPop([null],
      this.BOX_PARSERS_, node, objectStack, this);
  return GVol.extent.createOrUpdate(flatCoordinates[1][0],
      flatCoordinates[1][1], flatCoordinates[1][3],
      flatCoordinates[1][4]);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.GML2.prototype.innerBoundaryIsParser_ = function(node, objectStack) {
  /** @type {Array.<number>|undefined} */
  var flatLinearRing = GVol.xml.pushParseAndPop(undefined,
      this.RING_PARSERS, node, objectStack, this);
  if (flatLinearRing) {
    var flatLinearRings = /** @type {Array.<Array.<number>>} */
        (objectStack[objectStack.length - 1]);
    flatLinearRings.push(flatLinearRing);
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.GML2.prototype.outerBoundaryIsParser_ = function(node, objectStack) {
  /** @type {Array.<number>|undefined} */
  var flatLinearRing = GVol.xml.pushParseAndPop(undefined,
      this.RING_PARSERS, node, objectStack, this);
  if (flatLinearRing) {
    var flatLinearRings = /** @type {Array.<Array.<number>>} */
        (objectStack[objectStack.length - 1]);
    flatLinearRings[0] = flatLinearRing;
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GML2.prototype.GEOMETRY_FLAT_COORDINATES_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'coordinates': GVol.xml.makeReplacer(
        GVol.format.GML2.prototype.readFlatCoordinates_)
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GML2.prototype.FLAT_LINEAR_RINGS_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'innerBoundaryIs': GVol.format.GML2.prototype.innerBoundaryIsParser_,
    'outerBoundaryIs': GVol.format.GML2.prototype.outerBoundaryIsParser_
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GML2.prototype.BOX_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'coordinates': GVol.xml.makeArrayPusher(
        GVol.format.GML2.prototype.readFlatCoordinates_)
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GML2.prototype.GEOMETRY_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'Point': GVol.xml.makeReplacer(GVol.format.GMLBase.prototype.readPoint),
    'MultiPoint': GVol.xml.makeReplacer(
        GVol.format.GMLBase.prototype.readMultiPoint),
    'LineString': GVol.xml.makeReplacer(
        GVol.format.GMLBase.prototype.readLineString),
    'MultiLineString': GVol.xml.makeReplacer(
        GVol.format.GMLBase.prototype.readMultiLineString),
    'LinearRing': GVol.xml.makeReplacer(
        GVol.format.GMLBase.prototype.readLinearRing),
    'PGVolygon': GVol.xml.makeReplacer(GVol.format.GMLBase.prototype.readPGVolygon),
    'MultiPGVolygon': GVol.xml.makeReplacer(
        GVol.format.GMLBase.prototype.readMultiPGVolygon),
    'Box': GVol.xml.makeReplacer(GVol.format.GML2.prototype.readBox_)
  }
};


/**
 * @const
 * @param {*} value Value.
 * @param {Array.<*>} objectStack Object stack.
 * @param {string=} opt_nodeName Node name.
 * @return {Node|undefined} Node.
 * @private
 */
GVol.format.GML2.prototype.GEOMETRY_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) {
  var context = objectStack[objectStack.length - 1];
  var multiSurface = context['multiSurface'];
  var surface = context['surface'];
  var multiCurve = context['multiCurve'];
  var nodeName;
  if (!Array.isArray(value)) {
    nodeName = /** @type {GVol.geom.Geometry} */ (value).getType();
    if (nodeName === 'MultiPGVolygon' && multiSurface === true) {
      nodeName = 'MultiSurface';
    } else if (nodeName === 'PGVolygon' && surface === true) {
      nodeName = 'Surface';
    } else if (nodeName === 'MultiLineString' && multiCurve === true) {
      nodeName = 'MultiCurve';
    }
  } else {
    nodeName = 'Envelope';
  }
  return GVol.xml.createElementNS('http://www.opengis.net/gml',
      nodeName);
};


/**
 * @param {Node} node Node.
 * @param {GVol.Feature} feature Feature.
 * @param {Array.<*>} objectStack Node stack.
 */
GVol.format.GML2.prototype.writeFeatureElement = function(node, feature, objectStack) {
  var fid = feature.getId();
  if (fid) {
    node.setAttribute('fid', fid);
  }
  var context = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  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 GVol.geom.Geometry) {
        if (!(key in context.serializers[featureNS])) {
          context.serializers[featureNS][key] = GVol.xml.makeChildAppender(
              this.writeGeometryElement, this);
        }
      } else {
        if (!(key in context.serializers[featureNS])) {
          context.serializers[featureNS][key] = GVol.xml.makeChildAppender(
              GVol.format.XSD.writeStringTextNode);
        }
      }
    }
  }
  var item = GVol.obj.assign({}, context);
  item.node = node;
  GVol.xml.pushSerializeAndPop(/** @type {GVol.XmlNodeStackItem} */
      (item), context.serializers,
      GVol.xml.makeSimpleNodeFactory(undefined, featureNS),
      values,
      objectStack, keys);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.Geometry|GVol.Extent} geometry Geometry.
 * @param {Array.<*>} objectStack Node stack.
 */
GVol.format.GML2.prototype.writeGeometryElement = function(node, geometry, objectStack) {
  var context = /** @type {GVolx.format.WriteOptions} */ (objectStack[objectStack.length - 1]);
  var item = GVol.obj.assign({}, context);
  item.node = node;
  var value;
  if (Array.isArray(geometry)) {
    if (context.dataProjection) {
      value = GVol.proj.transformExtent(
          geometry, context.featureProjection, context.dataProjection);
    } else {
      value = geometry;
    }
  } else {
    value =
        GVol.format.Feature.transformWithOptions(/** @type {GVol.geom.Geometry} */ (geometry), true, context);
  }
  GVol.xml.pushSerializeAndPop(/** @type {GVol.XmlNodeStackItem} */
      (item), GVol.format.GML2.GEOMETRY_SERIALIZERS_,
      this.GEOMETRY_NODE_FACTORY_, [value],
      objectStack, undefined, this);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.LineString} geometry LineString geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML2.prototype.writeCurveOrLineString_ = function(node, geometry, objectStack) {
  var context = objectStack[objectStack.length - 1];
  var srsName = context['srsName'];
  if (node.nodeName !== 'LineStringSegment' && srsName) {
    node.setAttribute('srsName', srsName);
  }
  if (node.nodeName === 'LineString' ||
      node.nodeName === 'LineStringSegment') {
    var coordinates = this.createCoordinatesNode_(node.namespaceURI);
    node.appendChild(coordinates);
    this.writeCoordinates_(coordinates, geometry, objectStack);
  } else if (node.nodeName === 'Curve') {
    var segments = GVol.xml.createElementNS(node.namespaceURI, 'segments');
    node.appendChild(segments);
    this.writeCurveSegments_(segments,
        geometry, objectStack);
  }
};


/**
 * @param {string} namespaceURI XML namespace.
 * @returns {Node} coordinates node.
 * @private
 */
GVol.format.GML2.prototype.createCoordinatesNode_ = function(namespaceURI) {
  var coordinates = GVol.xml.createElementNS(namespaceURI, 'coordinates');
  coordinates.setAttribute('decimal', '.');
  coordinates.setAttribute('cs', ',');
  coordinates.setAttribute('ts', ' ');

  return coordinates;
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.LineString|GVol.geom.LinearRing} value Geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML2.prototype.writeCoordinates_ = function(node, value, objectStack) {
  var context = objectStack[objectStack.length - 1];
  var hasZ = context['hasZ'];
  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, hasZ);
  }
  GVol.format.XSD.writeStringTextNode(node, parts.join(' '));
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.LineString} line LineString geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML2.prototype.writeCurveSegments_ = function(node, line, objectStack) {
  var child = GVol.xml.createElementNS(node.namespaceURI,
      'LineStringSegment');
  node.appendChild(child);
  this.writeCurveOrLineString_(child, line, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.PGVolygon} geometry PGVolygon geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML2.prototype.writeSurfaceOrPGVolygon_ = function(node, geometry, objectStack) {
  var context = objectStack[objectStack.length - 1];
  var hasZ = context['hasZ'];
  var srsName = context['srsName'];
  if (node.nodeName !== 'PGVolygonPatch' && srsName) {
    node.setAttribute('srsName', srsName);
  }
  if (node.nodeName === 'PGVolygon' || node.nodeName === 'PGVolygonPatch') {
    var rings = geometry.getLinearRings();
    GVol.xml.pushSerializeAndPop(
        {node: node, hasZ: hasZ, srsName: srsName},
        GVol.format.GML2.RING_SERIALIZERS_,
        this.RING_NODE_FACTORY_,
        rings, objectStack, undefined, this);
  } else if (node.nodeName === 'Surface') {
    var patches = GVol.xml.createElementNS(node.namespaceURI, 'patches');
    node.appendChild(patches);
    this.writeSurfacePatches_(
        patches, geometry, objectStack);
  }
};


/**
 * @param {*} value Value.
 * @param {Array.<*>} objectStack Object stack.
 * @param {string=} opt_nodeName Node name.
 * @return {Node} Node.
 * @private
 */
GVol.format.GML2.prototype.RING_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) {
  var context = objectStack[objectStack.length - 1];
  var parentNode = context.node;
  var exteriorWritten = context['exteriorWritten'];
  if (exteriorWritten === undefined) {
    context['exteriorWritten'] = true;
  }
  return GVol.xml.createElementNS(parentNode.namespaceURI,
      exteriorWritten !== undefined ? 'innerBoundaryIs' : 'outerBoundaryIs');
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.PGVolygon} pGVolygon PGVolygon geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML2.prototype.writeSurfacePatches_ = function(node, pGVolygon, objectStack) {
  var child = GVol.xml.createElementNS(node.namespaceURI, 'PGVolygonPatch');
  node.appendChild(child);
  this.writeSurfaceOrPGVolygon_(child, pGVolygon, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.LinearRing} ring LinearRing geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML2.prototype.writeRing_ = function(node, ring, objectStack) {
  var linearRing = GVol.xml.createElementNS(node.namespaceURI, 'LinearRing');
  node.appendChild(linearRing);
  this.writeLinearRing_(linearRing, ring, objectStack);
};


/**
 * @param {Array.<number>} point Point geometry.
 * @param {string=} opt_srsName Optional srsName
 * @param {boGVolean=} opt_hasZ whether the geometry has a Z coordinate (is 3D) or not.
 * @return {string} The coords string.
 * @private
 */
GVol.format.GML2.prototype.getCoords_ = function(point, opt_srsName, opt_hasZ) {
  var axisOrientation = 'enu';
  if (opt_srsName) {
    axisOrientation = GVol.proj.get(opt_srsName).getAxisOrientation();
  }
  var coords = ((axisOrientation.substr(0, 2) === 'en') ?
    point[0] + ',' + point[1] :
    point[1] + ',' + point[0]);
  if (opt_hasZ) {
    // For newly created points, Z can be undefined.
    var z = point[2] || 0;
    coords += ',' + z;
  }

  return coords;
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.MultiLineString} geometry MultiLineString geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML2.prototype.writeMultiCurveOrLineString_ = function(node, geometry, objectStack) {
  var context = objectStack[objectStack.length - 1];
  var hasZ = context['hasZ'];
  var srsName = context['srsName'];
  var curve = context['curve'];
  if (srsName) {
    node.setAttribute('srsName', srsName);
  }
  var lines = geometry.getLineStrings();
  GVol.xml.pushSerializeAndPop({node: node, hasZ: hasZ, srsName: srsName, curve: curve},
      GVol.format.GML2.LINESTRINGORCURVEMEMBER_SERIALIZERS_,
      this.MULTIGEOMETRY_MEMBER_NODE_FACTORY_, lines,
      objectStack, undefined, this);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.Point} geometry Point geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML2.prototype.writePoint_ = function(node, geometry, objectStack) {
  var context = objectStack[objectStack.length - 1];
  var hasZ = context['hasZ'];
  var srsName = context['srsName'];
  if (srsName) {
    node.setAttribute('srsName', srsName);
  }
  var coordinates = this.createCoordinatesNode_(node.namespaceURI);
  node.appendChild(coordinates);
  var point = geometry.getCoordinates();
  var coord = this.getCoords_(point, srsName, hasZ);
  GVol.format.XSD.writeStringTextNode(coordinates, coord);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.MultiPoint} geometry MultiPoint geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML2.prototype.writeMultiPoint_ = function(node, geometry,
    objectStack) {
  var context = objectStack[objectStack.length - 1];
  var hasZ = context['hasZ'];
  var srsName = context['srsName'];
  if (srsName) {
    node.setAttribute('srsName', srsName);
  }
  var points = geometry.getPoints();
  GVol.xml.pushSerializeAndPop({node: node, hasZ: hasZ, srsName: srsName},
      GVol.format.GML2.POINTMEMBER_SERIALIZERS_,
      GVol.xml.makeSimpleNodeFactory('pointMember'), points,
      objectStack, undefined, this);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.Point} point Point geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML2.prototype.writePointMember_ = function(node, point, objectStack) {
  var child = GVol.xml.createElementNS(node.namespaceURI, 'Point');
  node.appendChild(child);
  this.writePoint_(child, point, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.LineString} line LineString geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML2.prototype.writeLineStringOrCurveMember_ = function(node, line, objectStack) {
  var child = this.GEOMETRY_NODE_FACTORY_(line, objectStack);
  if (child) {
    node.appendChild(child);
    this.writeCurveOrLineString_(child, line, objectStack);
  }
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.LinearRing} geometry LinearRing geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML2.prototype.writeLinearRing_ = function(node, geometry, objectStack) {
  var context = objectStack[objectStack.length - 1];
  var srsName = context['srsName'];
  if (srsName) {
    node.setAttribute('srsName', srsName);
  }
  var coordinates = this.createCoordinatesNode_(node.namespaceURI);
  node.appendChild(coordinates);
  this.writeCoordinates_(coordinates, geometry, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.MultiPGVolygon} geometry MultiPGVolygon geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML2.prototype.writeMultiSurfaceOrPGVolygon_ = function(node, geometry, objectStack) {
  var context = objectStack[objectStack.length - 1];
  var hasZ = context['hasZ'];
  var srsName = context['srsName'];
  var surface = context['surface'];
  if (srsName) {
    node.setAttribute('srsName', srsName);
  }
  var pGVolygons = geometry.getPGVolygons();
  GVol.xml.pushSerializeAndPop({node: node, hasZ: hasZ, srsName: srsName, surface: surface},
      GVol.format.GML2.SURFACEORPOLYGONMEMBER_SERIALIZERS_,
      this.MULTIGEOMETRY_MEMBER_NODE_FACTORY_, pGVolygons,
      objectStack, undefined, this);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.PGVolygon} pGVolygon PGVolygon geometry.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML2.prototype.writeSurfaceOrPGVolygonMember_ = function(node, pGVolygon, objectStack) {
  var child = this.GEOMETRY_NODE_FACTORY_(
      pGVolygon, objectStack);
  if (child) {
    node.appendChild(child);
    this.writeSurfaceOrPGVolygon_(child, pGVolygon, objectStack);
  }
};


/**
 * @param {Node} node Node.
 * @param {GVol.Extent} extent Extent.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GML2.prototype.writeEnvelope = function(node, extent, objectStack) {
  var context = objectStack[objectStack.length - 1];
  var srsName = context['srsName'];
  if (srsName) {
    node.setAttribute('srsName', srsName);
  }
  var keys = ['lowerCorner', 'upperCorner'];
  var values = [extent[0] + ' ' + extent[1], extent[2] + ' ' + extent[3]];
  GVol.xml.pushSerializeAndPop(/** @type {GVol.XmlNodeStackItem} */
      ({node: node}), GVol.format.GML2.ENVELOPE_SERIALIZERS_,
      GVol.xml.OBJECT_PROPERTY_NODE_FACTORY,
      values,
      objectStack, keys, this);
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.GML2.GEOMETRY_SERIALIZERS_ = {
  'http://www.opengis.net/gml': {
    'Curve': GVol.xml.makeChildAppender(
        GVol.format.GML2.prototype.writeCurveOrLineString_),
    'MultiCurve': GVol.xml.makeChildAppender(
        GVol.format.GML2.prototype.writeMultiCurveOrLineString_),
    'Point': GVol.xml.makeChildAppender(GVol.format.GML2.prototype.writePoint_),
    'MultiPoint': GVol.xml.makeChildAppender(
        GVol.format.GML2.prototype.writeMultiPoint_),
    'LineString': GVol.xml.makeChildAppender(
        GVol.format.GML2.prototype.writeCurveOrLineString_),
    'MultiLineString': GVol.xml.makeChildAppender(
        GVol.format.GML2.prototype.writeMultiCurveOrLineString_),
    'LinearRing': GVol.xml.makeChildAppender(
        GVol.format.GML2.prototype.writeLinearRing_),
    'PGVolygon': GVol.xml.makeChildAppender(
        GVol.format.GML2.prototype.writeSurfaceOrPGVolygon_),
    'MultiPGVolygon': GVol.xml.makeChildAppender(
        GVol.format.GML2.prototype.writeMultiSurfaceOrPGVolygon_),
    'Surface': GVol.xml.makeChildAppender(
        GVol.format.GML2.prototype.writeSurfaceOrPGVolygon_),
    'MultiSurface': GVol.xml.makeChildAppender(
        GVol.format.GML2.prototype.writeMultiSurfaceOrPGVolygon_),
    'Envelope': GVol.xml.makeChildAppender(
        GVol.format.GML2.prototype.writeEnvelope)
  }
};


/**
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.GML2.RING_SERIALIZERS_ = {
  'http://www.opengis.net/gml': {
    'outerBoundaryIs': GVol.xml.makeChildAppender(GVol.format.GML2.prototype.writeRing_),
    'innerBoundaryIs': GVol.xml.makeChildAppender(GVol.format.GML2.prototype.writeRing_)
  }
};


/**
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.GML2.POINTMEMBER_SERIALIZERS_ = {
  'http://www.opengis.net/gml': {
    'pointMember': GVol.xml.makeChildAppender(
        GVol.format.GML2.prototype.writePointMember_)
  }
};


/**
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.GML2.LINESTRINGORCURVEMEMBER_SERIALIZERS_ = {
  'http://www.opengis.net/gml': {
    'lineStringMember': GVol.xml.makeChildAppender(
        GVol.format.GML2.prototype.writeLineStringOrCurveMember_),
    'curveMember': GVol.xml.makeChildAppender(
        GVol.format.GML2.prototype.writeLineStringOrCurveMember_)
  }
};


/**
 * @const
 * @param {*} value Value.
 * @param {Array.<*>} objectStack Object stack.
 * @param {string=} opt_nodeName Node name.
 * @return {Node|undefined} Node.
 * @private
 */
GVol.format.GML2.prototype.MULTIGEOMETRY_MEMBER_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) {
  var parentNode = objectStack[objectStack.length - 1].node;
  return GVol.xml.createElementNS('http://www.opengis.net/gml',
      GVol.format.GML2.MULTIGEOMETRY_TO_MEMBER_NODENAME_[parentNode.nodeName]);
};

/**
 * @const
 * @type {Object.<string, string>}
 * @private
 */
GVol.format.GML2.MULTIGEOMETRY_TO_MEMBER_NODENAME_ = {
  'MultiLineString': 'lineStringMember',
  'MultiCurve': 'curveMember',
  'MultiPGVolygon': 'pGVolygonMember',
  'MultiSurface': 'surfaceMember'
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.GML2.SURFACEORPOLYGONMEMBER_SERIALIZERS_ = {
  'http://www.opengis.net/gml': {
    'surfaceMember': GVol.xml.makeChildAppender(
        GVol.format.GML2.prototype.writeSurfaceOrPGVolygonMember_),
    'pGVolygonMember': GVol.xml.makeChildAppender(
        GVol.format.GML2.prototype.writeSurfaceOrPGVolygonMember_)
  }
};


/**
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.GML2.ENVELOPE_SERIALIZERS_ = {
  'http://www.opengis.net/gml': {
    'lowerCorner': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
    'upperCorner': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode)
  }
};

goog.provide('GVol.format.GPX');

goog.require('GVol');
goog.require('GVol.Feature');
goog.require('GVol.array');
goog.require('GVol.format.Feature');
goog.require('GVol.format.XMLFeature');
goog.require('GVol.format.XSD');
goog.require('GVol.geom.GeometryLayout');
goog.require('GVol.geom.LineString');
goog.require('GVol.geom.MultiLineString');
goog.require('GVol.geom.Point');
goog.require('GVol.proj');
goog.require('GVol.xml');


/**
 * @classdesc
 * Feature format for reading and writing data in the GPX format.
 *
 * @constructor
 * @extends {GVol.format.XMLFeature}
 * @param {GVolx.format.GPXOptions=} opt_options Options.
 * @api
 */
GVol.format.GPX = function(opt_options) {

  var options = opt_options ? opt_options : {};

  GVol.format.XMLFeature.call(this);

  /**
   * @inheritDoc
   */
  this.defaultDataProjection = GVol.proj.get('EPSG:4326');

  /**
   * @type {function(GVol.Feature, Node)|undefined}
   * @private
   */
  this.readExtensions_ = options.readExtensions;
};
GVol.inherits(GVol.format.GPX, GVol.format.XMLFeature);


/**
 * @const
 * @private
 * @type {Array.<string>}
 */
GVol.format.GPX.NAMESPACE_URIS_ = [
  null,
  'http://www.topografix.com/GPX/1/0',
  'http://www.topografix.com/GPX/1/1'
];


/**
 * @const
 * @type {string}
 * @private
 */
GVol.format.GPX.SCHEMA_LOCATION_ = 'http://www.topografix.com/GPX/1/1 ' +
    'http://www.topografix.com/GPX/1/1/gpx.xsd';


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {GVol.LayoutOptions} layoutOptions Layout options.
 * @param {Node} node Node.
 * @param {Object} values Values.
 * @private
 * @return {Array.<number>} Flat coordinates.
 */
GVol.format.GPX.appendCoordinate_ = function(flatCoordinates, layoutOptions, node, values) {
  flatCoordinates.push(
      parseFloat(node.getAttribute('lon')),
      parseFloat(node.getAttribute('lat')));
  if ('ele' in values) {
    flatCoordinates.push(/** @type {number} */ (values['ele']));
    delete values['ele'];
    layoutOptions.hasZ = true;
  } else {
    flatCoordinates.push(0);
  }
  if ('time' in values) {
    flatCoordinates.push(/** @type {number} */ (values['time']));
    delete values['time'];
    layoutOptions.hasM = true;
  } else {
    flatCoordinates.push(0);
  }
  return flatCoordinates;
};


/**
 * Choose GeometryLayout based on flags in layoutOptions and adjust flatCoordinates
 * and ends arrays by shrinking them accordingly (removing unused zero entries).
 *
 * @param {GVol.LayoutOptions} layoutOptions Layout options.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {Array.<number>=} ends Ends.
 * @return {GVol.geom.GeometryLayout} Layout.
 */
GVol.format.GPX.applyLayoutOptions_ = function(layoutOptions, flatCoordinates, ends) {
  var layout = GVol.geom.GeometryLayout.XY;
  var stride = 2;
  if (layoutOptions.hasZ && layoutOptions.hasM) {
    layout = GVol.geom.GeometryLayout.XYZM;
    stride = 4;
  } else if (layoutOptions.hasZ) {
    layout = GVol.geom.GeometryLayout.XYZ;
    stride = 3;
  } else if (layoutOptions.hasM) {
    layout = GVol.geom.GeometryLayout.XYM;
    stride = 3;
  }
  if (stride !== 4) {
    var i, ii;
    for (i = 0, ii = flatCoordinates.length / 4; i < ii; i++) {
      flatCoordinates[i * stride] = flatCoordinates[i * 4];
      flatCoordinates[i * stride + 1] = flatCoordinates[i * 4 + 1];
      if (layoutOptions.hasZ) {
        flatCoordinates[i * stride + 2] = flatCoordinates[i * 4 + 2];
      }
      if (layoutOptions.hasM) {
        flatCoordinates[i * stride + 2] = flatCoordinates[i * 4 + 3];
      }
    }
    flatCoordinates.length = flatCoordinates.length / 4 * stride;
    if (ends) {
      for (i = 0, ii = ends.length; i < ii; i++) {
        ends[i] = ends[i] / 4 * stride;
      }
    }
  }
  return layout;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.GPX.parseLink_ = function(node, objectStack) {
  var values = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  var href = node.getAttribute('href');
  if (href !== null) {
    values['link'] = href;
  }
  GVol.xml.parseNode(GVol.format.GPX.LINK_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.GPX.parseExtensions_ = function(node, objectStack) {
  var values = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  values['extensionsNode_'] = node;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.GPX.parseRtePt_ = function(node, objectStack) {
  var values = GVol.xml.pushParseAndPop(
      {}, GVol.format.GPX.RTEPT_PARSERS_, node, objectStack);
  if (values) {
    var rteValues = /** @type {Object} */ (objectStack[objectStack.length - 1]);
    var flatCoordinates = /** @type {Array.<number>} */
        (rteValues['flatCoordinates']);
    var layoutOptions = /** @type {GVol.LayoutOptions} */
        (rteValues['layoutOptions']);
    GVol.format.GPX.appendCoordinate_(flatCoordinates, layoutOptions, node, values);
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.GPX.parseTrkPt_ = function(node, objectStack) {
  var values = GVol.xml.pushParseAndPop(
      {}, GVol.format.GPX.TRKPT_PARSERS_, node, objectStack);
  if (values) {
    var trkValues = /** @type {Object} */ (objectStack[objectStack.length - 1]);
    var flatCoordinates = /** @type {Array.<number>} */
        (trkValues['flatCoordinates']);
    var layoutOptions = /** @type {GVol.LayoutOptions} */
        (trkValues['layoutOptions']);
    GVol.format.GPX.appendCoordinate_(flatCoordinates, layoutOptions, node, values);
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.GPX.parseTrkSeg_ = function(node, objectStack) {
  var values = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  GVol.xml.parseNode(GVol.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 {GVol.Feature|undefined} Track.
 */
GVol.format.GPX.readRte_ = function(node, objectStack) {
  var options = /** @type {GVolx.format.ReadOptions} */ (objectStack[0]);
  var values = GVol.xml.pushParseAndPop({
    'flatCoordinates': [],
    'layoutOptions': {}
  }, GVol.format.GPX.RTE_PARSERS_, node, objectStack);
  if (!values) {
    return undefined;
  }
  var flatCoordinates = /** @type {Array.<number>} */
      (values['flatCoordinates']);
  delete values['flatCoordinates'];
  var layoutOptions = /** @type {GVol.LayoutOptions} */ (values['layoutOptions']);
  delete values['layoutOptions'];
  var layout = GVol.format.GPX.applyLayoutOptions_(layoutOptions, flatCoordinates);
  var geometry = new GVol.geom.LineString(null);
  geometry.setFlatCoordinates(layout, flatCoordinates);
  GVol.format.Feature.transformWithOptions(geometry, false, options);
  var feature = new GVol.Feature(geometry);
  feature.setProperties(values);
  return feature;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {GVol.Feature|undefined} Track.
 */
GVol.format.GPX.readTrk_ = function(node, objectStack) {
  var options = /** @type {GVolx.format.ReadOptions} */ (objectStack[0]);
  var values = GVol.xml.pushParseAndPop({
    'flatCoordinates': [],
    'ends': [],
    'layoutOptions': {}
  }, GVol.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 layoutOptions = /** @type {GVol.LayoutOptions} */ (values['layoutOptions']);
  delete values['layoutOptions'];
  var layout = GVol.format.GPX.applyLayoutOptions_(layoutOptions, flatCoordinates, ends);
  var geometry = new GVol.geom.MultiLineString(null);
  geometry.setFlatCoordinates(layout, flatCoordinates, ends);
  GVol.format.Feature.transformWithOptions(geometry, false, options);
  var feature = new GVol.Feature(geometry);
  feature.setProperties(values);
  return feature;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {GVol.Feature|undefined} Waypoint.
 */
GVol.format.GPX.readWpt_ = function(node, objectStack) {
  var options = /** @type {GVolx.format.ReadOptions} */ (objectStack[0]);
  var values = GVol.xml.pushParseAndPop(
      {}, GVol.format.GPX.WPT_PARSERS_, node, objectStack);
  if (!values) {
    return undefined;
  }
  var layoutOptions = /** @type {GVol.LayoutOptions} */ ({});
  var coordinates = GVol.format.GPX.appendCoordinate_([], layoutOptions, node, values);
  var layout = GVol.format.GPX.applyLayoutOptions_(layoutOptions, coordinates);
  var geometry = new GVol.geom.Point(coordinates, layout);
  GVol.format.Feature.transformWithOptions(geometry, false, options);
  var feature = new GVol.Feature(geometry);
  feature.setProperties(values);
  return feature;
};


/**
 * @const
 * @type {Object.<string, function(Node, Array.<*>): (GVol.Feature|undefined)>}
 * @private
 */
GVol.format.GPX.FEATURE_READER_ = {
  'rte': GVol.format.GPX.readRte_,
  'trk': GVol.format.GPX.readTrk_,
  'wpt': GVol.format.GPX.readWpt_
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GPX.GPX_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.GPX.NAMESPACE_URIS_, {
      'rte': GVol.xml.makeArrayPusher(GVol.format.GPX.readRte_),
      'trk': GVol.xml.makeArrayPusher(GVol.format.GPX.readTrk_),
      'wpt': GVol.xml.makeArrayPusher(GVol.format.GPX.readWpt_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GPX.LINK_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.GPX.NAMESPACE_URIS_, {
      'text':
          GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString, 'linkText'),
      'type':
          GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString, 'linkType')
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GPX.RTE_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.GPX.NAMESPACE_URIS_, {
      'name': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'cmt': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'desc': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'src': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'link': GVol.format.GPX.parseLink_,
      'number':
          GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readNonNegativeInteger),
      'extensions': GVol.format.GPX.parseExtensions_,
      'type': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'rtept': GVol.format.GPX.parseRtePt_
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GPX.RTEPT_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.GPX.NAMESPACE_URIS_, {
      'ele': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal),
      'time': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDateTime)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GPX.TRK_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.GPX.NAMESPACE_URIS_, {
      'name': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'cmt': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'desc': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'src': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'link': GVol.format.GPX.parseLink_,
      'number':
          GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readNonNegativeInteger),
      'type': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'extensions': GVol.format.GPX.parseExtensions_,
      'trkseg': GVol.format.GPX.parseTrkSeg_
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GPX.TRKSEG_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.GPX.NAMESPACE_URIS_, {
      'trkpt': GVol.format.GPX.parseTrkPt_
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GPX.TRKPT_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.GPX.NAMESPACE_URIS_, {
      'ele': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal),
      'time': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDateTime)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.GPX.WPT_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.GPX.NAMESPACE_URIS_, {
      'ele': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal),
      'time': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDateTime),
      'magvar': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal),
      'geoidheight': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal),
      'name': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'cmt': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'desc': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'src': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'link': GVol.format.GPX.parseLink_,
      'sym': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'type': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'fix': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'sat': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readNonNegativeInteger),
      'hdop': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal),
      'vdop': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal),
      'pdop': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal),
      'ageofdgpsdata':
          GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal),
      'dgpsid':
          GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readNonNegativeInteger),
      'extensions': GVol.format.GPX.parseExtensions_
    });


/**
 * @param {Array.<GVol.Feature>} features List of features.
 * @private
 */
GVol.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.
 * Routes (`<rte>`) are converted into LineString geometries, and tracks (`<trk>`)
 * into MultiLineString. Any properties on route and track waypoints are ignored.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {GVol.Feature} Feature.
 * @api
 */
GVol.format.GPX.prototype.readFeature;


/**
 * @inheritDoc
 */
GVol.format.GPX.prototype.readFeatureFromNode = function(node, opt_options) {
  if (!GVol.array.includes(GVol.format.GPX.NAMESPACE_URIS_, node.namespaceURI)) {
    return null;
  }
  var featureReader = GVol.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.
 * Routes (`<rte>`) are converted into LineString geometries, and tracks (`<trk>`)
 * into MultiLineString. Any properties on route and track waypoints are ignored.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<GVol.Feature>} Features.
 * @api
 */
GVol.format.GPX.prototype.readFeatures;


/**
 * @inheritDoc
 */
GVol.format.GPX.prototype.readFeaturesFromNode = function(node, opt_options) {
  if (!GVol.array.includes(GVol.format.GPX.NAMESPACE_URIS_, node.namespaceURI)) {
    return [];
  }
  if (node.localName == 'gpx') {
    /** @type {Array.<GVol.Feature>} */
    var features = GVol.xml.pushParseAndPop([], GVol.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 {GVol.proj.Projection} Projection.
 * @api
 */
GVol.format.GPX.prototype.readProjection;


/**
 * @param {Node} node Node.
 * @param {string} value Value for the link's `href` attribute.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.GPX.writeLink_ = function(node, value, objectStack) {
  node.setAttribute('href', value);
  var context = objectStack[objectStack.length - 1];
  var properties = context['properties'];
  var link = [
    properties['linkText'],
    properties['linkType']
  ];
  GVol.xml.pushSerializeAndPop(/** @type {GVol.XmlNodeStackItem} */ ({node: node}),
      GVol.format.GPX.LINK_SERIALIZERS_, GVol.xml.OBJECT_PROPERTY_NODE_FACTORY,
      link, objectStack, GVol.format.GPX.LINK_SEQUENCE_);
};


/**
 * @param {Node} node Node.
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.GPX.writeWptType_ = function(node, coordinate, objectStack) {
  var context = objectStack[objectStack.length - 1];
  var parentNode = context.node;
  var namespaceURI = parentNode.namespaceURI;
  var properties = context['properties'];
  //FIXME Projection handling
  GVol.xml.setAttributeNS(node, null, 'lat', coordinate[1]);
  GVol.xml.setAttributeNS(node, null, 'lon', coordinate[0]);
  var geometryLayout = context['geometryLayout'];
  switch (geometryLayout) {
    case GVol.geom.GeometryLayout.XYZM:
      if (coordinate[3] !== 0) {
        properties['time'] = coordinate[3];
      }
      // fall through
    case GVol.geom.GeometryLayout.XYZ:
      if (coordinate[2] !== 0) {
        properties['ele'] = coordinate[2];
      }
      break;
    case GVol.geom.GeometryLayout.XYM:
      if (coordinate[2] !== 0) {
        properties['time'] = coordinate[2];
      }
      break;
    default:
      // pass
  }
  var orderedKeys = (node.nodeName == 'rtept') ?
    GVol.format.GPX.RTEPT_TYPE_SEQUENCE_[namespaceURI] :
    GVol.format.GPX.WPT_TYPE_SEQUENCE_[namespaceURI];
  var values = GVol.xml.makeSequence(properties, orderedKeys);
  GVol.xml.pushSerializeAndPop(/** @type {GVol.XmlNodeStackItem} */
      ({node: node, 'properties': properties}),
      GVol.format.GPX.WPT_TYPE_SERIALIZERS_, GVol.xml.OBJECT_PROPERTY_NODE_FACTORY,
      values, objectStack, orderedKeys);
};


/**
 * @param {Node} node Node.
 * @param {GVol.Feature} feature Feature.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.GPX.writeRte_ = function(node, feature, objectStack) {
  var options = /** @type {GVolx.format.WriteOptions} */ (objectStack[0]);
  var properties = feature.getProperties();
  var context = {node: node, 'properties': properties};
  var geometry = feature.getGeometry();
  if (geometry) {
    geometry = /** @type {GVol.geom.LineString} */
      (GVol.format.Feature.transformWithOptions(geometry, true, options));
    context['geometryLayout'] = geometry.getLayout();
    properties['rtept'] = geometry.getCoordinates();
  }
  var parentNode = objectStack[objectStack.length - 1].node;
  var orderedKeys = GVol.format.GPX.RTE_SEQUENCE_[parentNode.namespaceURI];
  var values = GVol.xml.makeSequence(properties, orderedKeys);
  GVol.xml.pushSerializeAndPop(context,
      GVol.format.GPX.RTE_SERIALIZERS_, GVol.xml.OBJECT_PROPERTY_NODE_FACTORY,
      values, objectStack, orderedKeys);
};


/**
 * @param {Node} node Node.
 * @param {GVol.Feature} feature Feature.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.GPX.writeTrk_ = function(node, feature, objectStack) {
  var options = /** @type {GVolx.format.WriteOptions} */ (objectStack[0]);
  var properties = feature.getProperties();
  /** @type {GVol.XmlNodeStackItem} */
  var context = {node: node, 'properties': properties};
  var geometry = feature.getGeometry();
  if (geometry) {
    geometry = /** @type {GVol.geom.MultiLineString} */
      (GVol.format.Feature.transformWithOptions(geometry, true, options));
    properties['trkseg'] = geometry.getLineStrings();
  }
  var parentNode = objectStack[objectStack.length - 1].node;
  var orderedKeys = GVol.format.GPX.TRK_SEQUENCE_[parentNode.namespaceURI];
  var values = GVol.xml.makeSequence(properties, orderedKeys);
  GVol.xml.pushSerializeAndPop(context,
      GVol.format.GPX.TRK_SERIALIZERS_, GVol.xml.OBJECT_PROPERTY_NODE_FACTORY,
      values, objectStack, orderedKeys);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.LineString} lineString LineString.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.GPX.writeTrkSeg_ = function(node, lineString, objectStack) {
  /** @type {GVol.XmlNodeStackItem} */
  var context = {node: node, 'geometryLayout': lineString.getLayout(),
    'properties': {}};
  GVol.xml.pushSerializeAndPop(context,
      GVol.format.GPX.TRKSEG_SERIALIZERS_, GVol.format.GPX.TRKSEG_NODE_FACTORY_,
      lineString.getCoordinates(), objectStack);
};


/**
 * @param {Node} node Node.
 * @param {GVol.Feature} feature Feature.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.GPX.writeWpt_ = function(node, feature, objectStack) {
  var options = /** @type {GVolx.format.WriteOptions} */ (objectStack[0]);
  var context = objectStack[objectStack.length - 1];
  context['properties'] = feature.getProperties();
  var geometry = feature.getGeometry();
  if (geometry) {
    geometry = /** @type {GVol.geom.Point} */
      (GVol.format.Feature.transformWithOptions(geometry, true, options));
    context['geometryLayout'] = geometry.getLayout();
    GVol.format.GPX.writeWptType_(node, geometry.getCoordinates(), objectStack);
  }
};


/**
 * @const
 * @type {Array.<string>}
 * @private
 */
GVol.format.GPX.LINK_SEQUENCE_ = ['text', 'type'];


/**
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.GPX.LINK_SERIALIZERS_ = GVol.xml.makeStructureNS(
    GVol.format.GPX.NAMESPACE_URIS_, {
      'text': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'type': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode)
    });


/**
 * @const
 * @type {Object.<string, Array.<string>>}
 * @private
 */
GVol.format.GPX.RTE_SEQUENCE_ = GVol.xml.makeStructureNS(
    GVol.format.GPX.NAMESPACE_URIS_, [
      'name', 'cmt', 'desc', 'src', 'link', 'number', 'type', 'rtept'
    ]);


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.GPX.RTE_SERIALIZERS_ = GVol.xml.makeStructureNS(
    GVol.format.GPX.NAMESPACE_URIS_, {
      'name': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'cmt': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'desc': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'src': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'link': GVol.xml.makeChildAppender(GVol.format.GPX.writeLink_),
      'number': GVol.xml.makeChildAppender(
          GVol.format.XSD.writeNonNegativeIntegerTextNode),
      'type': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'rtept': GVol.xml.makeArraySerializer(GVol.xml.makeChildAppender(
          GVol.format.GPX.writeWptType_))
    });


/**
 * @const
 * @type {Object.<string, Array.<string>>}
 * @private
 */
GVol.format.GPX.RTEPT_TYPE_SEQUENCE_ = GVol.xml.makeStructureNS(
    GVol.format.GPX.NAMESPACE_URIS_, [
      'ele', 'time'
    ]);


/**
 * @const
 * @type {Object.<string, Array.<string>>}
 * @private
 */
GVol.format.GPX.TRK_SEQUENCE_ = GVol.xml.makeStructureNS(
    GVol.format.GPX.NAMESPACE_URIS_, [
      'name', 'cmt', 'desc', 'src', 'link', 'number', 'type', 'trkseg'
    ]);


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.GPX.TRK_SERIALIZERS_ = GVol.xml.makeStructureNS(
    GVol.format.GPX.NAMESPACE_URIS_, {
      'name': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'cmt': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'desc': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'src': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'link': GVol.xml.makeChildAppender(GVol.format.GPX.writeLink_),
      'number': GVol.xml.makeChildAppender(
          GVol.format.XSD.writeNonNegativeIntegerTextNode),
      'type': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'trkseg': GVol.xml.makeArraySerializer(GVol.xml.makeChildAppender(
          GVol.format.GPX.writeTrkSeg_))
    });


/**
 * @const
 * @type {function(*, Array.<*>, string=): (Node|undefined)}
 * @private
 */
GVol.format.GPX.TRKSEG_NODE_FACTORY_ = GVol.xml.makeSimpleNodeFactory('trkpt');


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.GPX.TRKSEG_SERIALIZERS_ = GVol.xml.makeStructureNS(
    GVol.format.GPX.NAMESPACE_URIS_, {
      'trkpt': GVol.xml.makeChildAppender(GVol.format.GPX.writeWptType_)
    });


/**
 * @const
 * @type {Object.<string, Array.<string>>}
 * @private
 */
GVol.format.GPX.WPT_TYPE_SEQUENCE_ = GVol.xml.makeStructureNS(
    GVol.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, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.GPX.WPT_TYPE_SERIALIZERS_ = GVol.xml.makeStructureNS(
    GVol.format.GPX.NAMESPACE_URIS_, {
      'ele': GVol.xml.makeChildAppender(GVol.format.XSD.writeDecimalTextNode),
      'time': GVol.xml.makeChildAppender(GVol.format.XSD.writeDateTimeTextNode),
      'magvar': GVol.xml.makeChildAppender(GVol.format.XSD.writeDecimalTextNode),
      'geoidheight': GVol.xml.makeChildAppender(
          GVol.format.XSD.writeDecimalTextNode),
      'name': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'cmt': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'desc': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'src': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'link': GVol.xml.makeChildAppender(GVol.format.GPX.writeLink_),
      'sym': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'type': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'fix': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'sat': GVol.xml.makeChildAppender(
          GVol.format.XSD.writeNonNegativeIntegerTextNode),
      'hdop': GVol.xml.makeChildAppender(GVol.format.XSD.writeDecimalTextNode),
      'vdop': GVol.xml.makeChildAppender(GVol.format.XSD.writeDecimalTextNode),
      'pdop': GVol.xml.makeChildAppender(GVol.format.XSD.writeDecimalTextNode),
      'ageofdgpsdata': GVol.xml.makeChildAppender(
          GVol.format.XSD.writeDecimalTextNode),
      'dgpsid': GVol.xml.makeChildAppender(
          GVol.format.XSD.writeNonNegativeIntegerTextNode)
    });


/**
 * @const
 * @type {Object.<string, string>}
 * @private
 */
GVol.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
 */
GVol.format.GPX.GPX_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) {
  var geometry = /** @type {GVol.Feature} */ (value).getGeometry();
  if (geometry) {
    var nodeName = GVol.format.GPX.GEOMETRY_TYPE_TO_NODENAME_[geometry.getType()];
    if (nodeName) {
      var parentNode = objectStack[objectStack.length - 1].node;
      return GVol.xml.createElementNS(parentNode.namespaceURI, nodeName);
    }
  }
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.GPX.GPX_SERIALIZERS_ = GVol.xml.makeStructureNS(
    GVol.format.GPX.NAMESPACE_URIS_, {
      'rte': GVol.xml.makeChildAppender(GVol.format.GPX.writeRte_),
      'trk': GVol.xml.makeChildAppender(GVol.format.GPX.writeTrk_),
      'wpt': GVol.xml.makeChildAppender(GVol.format.GPX.writeWpt_)
    });


/**
 * Encode an array of features in the GPX format.
 * LineString geometries are output as routes (`<rte>`), and MultiLineString
 * as tracks (`<trk>`).
 *
 * @function
 * @param {Array.<GVol.Feature>} features Features.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {string} Result.
 * @api
 */
GVol.format.GPX.prototype.writeFeatures;


/**
 * Encode an array of features in the GPX format as an XML node.
 * LineString geometries are output as routes (`<rte>`), and MultiLineString
 * as tracks (`<trk>`).
 *
 * @param {Array.<GVol.Feature>} features Features.
 * @param {GVolx.format.WriteOptions=} opt_options Options.
 * @return {Node} Node.
 * @override
 * @api
 */
GVol.format.GPX.prototype.writeFeaturesNode = function(features, opt_options) {
  opt_options = this.adaptOptions(opt_options);
  //FIXME Serialize metadata
  var gpx = GVol.xml.createElementNS('http://www.topografix.com/GPX/1/1', 'gpx');
  var xmlnsUri = 'http://www.w3.org/2000/xmlns/';
  var xmlSchemaInstanceUri = 'http://www.w3.org/2001/XMLSchema-instance';
  GVol.xml.setAttributeNS(gpx, xmlnsUri, 'xmlns:xsi', xmlSchemaInstanceUri);
  GVol.xml.setAttributeNS(gpx, xmlSchemaInstanceUri, 'xsi:schemaLocation',
      GVol.format.GPX.SCHEMA_LOCATION_);
  gpx.setAttribute('version', '1.1');
  gpx.setAttribute('creator', 'OpenLayers');

  GVol.xml.pushSerializeAndPop(/** @type {GVol.XmlNodeStackItem} */
      ({node: gpx}), GVol.format.GPX.GPX_SERIALIZERS_,
      GVol.format.GPX.GPX_NODE_FACTORY_, features, [opt_options]);
  return gpx;
};

goog.provide('GVol.format.IGCZ');

/**
 * IGC altitude/z. One of 'barometric', 'gps', 'none'.
 * @enum {string}
 */
GVol.format.IGCZ = {
  BAROMETRIC: 'barometric',
  GPS: 'gps',
  NONE: 'none'
};

goog.provide('GVol.format.TextFeature');

goog.require('GVol');
goog.require('GVol.format.Feature');
goog.require('GVol.format.FormatType');


/**
 * @classdesc
 * Abstract base class; normally only used for creating subclasses and not
 * instantiated in apps.
 * Base class for text feature formats.
 *
 * @constructor
 * @abstract
 * @extends {GVol.format.Feature}
 */
GVol.format.TextFeature = function() {
  GVol.format.Feature.call(this);
};
GVol.inherits(GVol.format.TextFeature, GVol.format.Feature);


/**
 * @param {Document|Node|Object|string} source Source.
 * @private
 * @return {string} Text.
 */
GVol.format.TextFeature.prototype.getText_ = function(source) {
  if (typeof source === 'string') {
    return source;
  } else {
    return '';
  }
};


/**
 * @inheritDoc
 */
GVol.format.TextFeature.prototype.getType = function() {
  return GVol.format.FormatType.TEXT;
};


/**
 * @inheritDoc
 */
GVol.format.TextFeature.prototype.readFeature = function(source, opt_options) {
  return this.readFeatureFromText(
      this.getText_(source), this.adaptOptions(opt_options));
};


/**
 * @abstract
 * @param {string} text Text.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @protected
 * @return {GVol.Feature} Feature.
 */
GVol.format.TextFeature.prototype.readFeatureFromText = function(text, opt_options) {};


/**
 * @inheritDoc
 */
GVol.format.TextFeature.prototype.readFeatures = function(source, opt_options) {
  return this.readFeaturesFromText(
      this.getText_(source), this.adaptOptions(opt_options));
};


/**
 * @abstract
 * @param {string} text Text.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @protected
 * @return {Array.<GVol.Feature>} Features.
 */
GVol.format.TextFeature.prototype.readFeaturesFromText = function(text, opt_options) {};


/**
 * @inheritDoc
 */
GVol.format.TextFeature.prototype.readGeometry = function(source, opt_options) {
  return this.readGeometryFromText(
      this.getText_(source), this.adaptOptions(opt_options));
};


/**
 * @abstract
 * @param {string} text Text.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @protected
 * @return {GVol.geom.Geometry} Geometry.
 */
GVol.format.TextFeature.prototype.readGeometryFromText = function(text, opt_options) {};


/**
 * @inheritDoc
 */
GVol.format.TextFeature.prototype.readProjection = function(source) {
  return this.readProjectionFromText(this.getText_(source));
};


/**
 * @param {string} text Text.
 * @protected
 * @return {GVol.proj.Projection} Projection.
 */
GVol.format.TextFeature.prototype.readProjectionFromText = function(text) {
  return this.defaultDataProjection;
};


/**
 * @inheritDoc
 */
GVol.format.TextFeature.prototype.writeFeature = function(feature, opt_options) {
  return this.writeFeatureText(feature, this.adaptOptions(opt_options));
};


/**
 * @abstract
 * @param {GVol.Feature} feature Features.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @protected
 * @return {string} Text.
 */
GVol.format.TextFeature.prototype.writeFeatureText = function(feature, opt_options) {};


/**
 * @inheritDoc
 */
GVol.format.TextFeature.prototype.writeFeatures = function(
    features, opt_options) {
  return this.writeFeaturesText(features, this.adaptOptions(opt_options));
};


/**
 * @abstract
 * @param {Array.<GVol.Feature>} features Features.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @protected
 * @return {string} Text.
 */
GVol.format.TextFeature.prototype.writeFeaturesText = function(features, opt_options) {};


/**
 * @inheritDoc
 */
GVol.format.TextFeature.prototype.writeGeometry = function(
    geometry, opt_options) {
  return this.writeGeometryText(geometry, this.adaptOptions(opt_options));
};


/**
 * @abstract
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @protected
 * @return {string} Text.
 */
GVol.format.TextFeature.prototype.writeGeometryText = function(geometry, opt_options) {};

goog.provide('GVol.format.IGC');

goog.require('GVol');
goog.require('GVol.Feature');
goog.require('GVol.format.Feature');
goog.require('GVol.format.IGCZ');
goog.require('GVol.format.TextFeature');
goog.require('GVol.geom.GeometryLayout');
goog.require('GVol.geom.LineString');
goog.require('GVol.proj');


/**
 * @classdesc
 * Feature format for `*.igc` flight recording files.
 *
 * @constructor
 * @extends {GVol.format.TextFeature}
 * @param {GVolx.format.IGCOptions=} opt_options Options.
 * @api
 */
GVol.format.IGC = function(opt_options) {

  var options = opt_options ? opt_options : {};

  GVol.format.TextFeature.call(this);

  /**
   * @inheritDoc
   */
  this.defaultDataProjection = GVol.proj.get('EPSG:4326');

  /**
   * @private
   * @type {GVol.format.IGCZ}
   */
  this.altitudeMode_ = options.altitudeMode ?
    options.altitudeMode : GVol.format.IGCZ.NONE;

};
GVol.inherits(GVol.format.IGC, GVol.format.TextFeature);


/**
 * @const
 * @type {RegExp}
 * @private
 */
GVol.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
 */
GVol.format.IGC.H_RECORD_RE_ = /^H.([A-Z]{3}).*?:(.*)/;


/**
 * @const
 * @type {RegExp}
 * @private
 */
GVol.format.IGC.HFDTE_RECORD_RE_ = /^HFDTE(\d{2})(\d{2})(\d{2})/;


/**
 * A regular expression matching the newline characters `\r\n`, `\r` and `\n`.
 *
 * @const
 * @type {RegExp}
 * @private
 */
GVol.format.IGC.NEWLINE_RE_ = /\r\n|\r|\n/;


/**
 * Read the feature from the IGC source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {GVol.Feature} Feature.
 * @api
 */
GVol.format.IGC.prototype.readFeature;


/**
 * @inheritDoc
 */
GVol.format.IGC.prototype.readFeatureFromText = function(text, opt_options) {
  var altitudeMode = this.altitudeMode_;
  var lines = text.split(GVol.format.IGC.NEWLINE_RE_);
  /** @type {Object.<string, string>} */
  var properties = {};
  var flatCoordinates = [];
  var year = 2000;
  var month = 0;
  var day = 1;
  var lastDateTime = -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 = GVol.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 != GVol.format.IGCZ.NONE) {
          var z;
          if (altitudeMode == GVol.format.IGCZ.GPS) {
            z = parseInt(m[11], 10);
          } else if (altitudeMode == GVol.format.IGCZ.BAROMETRIC) {
            z = parseInt(m[12], 10);
          } else {
            z = 0;
          }
          flatCoordinates.push(z);
        }
        var dateTime = Date.UTC(year, month, day, hour, minute, second);
        // Detect UTC midnight wrap around.
        if (dateTime < lastDateTime) {
          dateTime = Date.UTC(year, month, day + 1, hour, minute, second);
        }
        flatCoordinates.push(dateTime / 1000);
        lastDateTime = dateTime;
      }
    } else if (line.charAt(0) == 'H') {
      m = GVol.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 = GVol.format.IGC.H_RECORD_RE_.exec(line);
        if (m) {
          properties[m[1]] = m[2].trim();
        }
      }
    }
  }
  if (flatCoordinates.length === 0) {
    return null;
  }
  var lineString = new GVol.geom.LineString(null);
  var layout = altitudeMode == GVol.format.IGCZ.NONE ?
    GVol.geom.GeometryLayout.XYM : GVol.geom.GeometryLayout.XYZM;
  lineString.setFlatCoordinates(layout, flatCoordinates);
  var feature = new GVol.Feature(GVol.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 {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<GVol.Feature>} Features.
 * @api
 */
GVol.format.IGC.prototype.readFeatures;


/**
 * @inheritDoc
 */
GVol.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 {GVol.proj.Projection} Projection.
 * @api
 */
GVol.format.IGC.prototype.readProjection;


/**
 * Not implemented.
 * @inheritDoc
 */
GVol.format.IGC.prototype.writeFeatureText = function(feature, opt_options) {};


/**
 * Not implemented.
 * @inheritDoc
 */
GVol.format.IGC.prototype.writeFeaturesText = function(features, opt_options) {};


/**
 * Not implemented.
 * @inheritDoc
 */
GVol.format.IGC.prototype.writeGeometryText = function(geometry, opt_options) {};


/**
 * Not implemented.
 * @inheritDoc
 */
GVol.format.IGC.prototype.readGeometryFromText = function(text, opt_options) {};

goog.provide('GVol.style.IconAnchorUnits');

/**
 * Icon anchor units. One of 'fraction', 'pixels'.
 * @enum {string}
 */
GVol.style.IconAnchorUnits = {
  FRACTION: 'fraction',
  PIXELS: 'pixels'
};

goog.provide('GVol.style.IconImage');

goog.require('GVol');
goog.require('GVol.dom');
goog.require('GVol.events');
goog.require('GVol.events.EventTarget');
goog.require('GVol.events.EventType');
goog.require('GVol.ImageState');
goog.require('GVol.style');


/**
 * @constructor
 * @param {Image|HTMLCanvasElement} image Image.
 * @param {string|undefined} src Src.
 * @param {GVol.Size} size Size.
 * @param {?string} crossOrigin Cross origin.
 * @param {GVol.ImageState} imageState Image state.
 * @param {GVol.CGVolor} cGVolor CGVolor.
 * @extends {GVol.events.EventTarget}
 */
GVol.style.IconImage = function(image, src, size, crossOrigin, imageState,
    cGVolor) {

  GVol.events.EventTarget.call(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_ = cGVolor ?
    /** @type {HTMLCanvasElement} */ (document.createElement('CANVAS')) :
    null;

  /**
   * @private
   * @type {GVol.CGVolor}
   */
  this.cGVolor_ = cGVolor;

  /**
   * @private
   * @type {Array.<GVol.EventsKey>}
   */
  this.imageListenerKeys_ = null;

  /**
   * @private
   * @type {GVol.ImageState}
   */
  this.imageState_ = imageState;

  /**
   * @private
   * @type {GVol.Size}
   */
  this.size_ = size;

  /**
   * @private
   * @type {string|undefined}
   */
  this.src_ = src;

  /**
   * @private
   * @type {boGVolean}
   */
  this.tainting_ = false;
  if (this.imageState_ == GVol.ImageState.LOADED) {
    this.determineTainting_();
  }

};
GVol.inherits(GVol.style.IconImage, GVol.events.EventTarget);


/**
 * @param {Image|HTMLCanvasElement} image Image.
 * @param {string} src Src.
 * @param {GVol.Size} size Size.
 * @param {?string} crossOrigin Cross origin.
 * @param {GVol.ImageState} imageState Image state.
 * @param {GVol.CGVolor} cGVolor CGVolor.
 * @return {GVol.style.IconImage} Icon image.
 */
GVol.style.IconImage.get = function(image, src, size, crossOrigin, imageState,
    cGVolor) {
  var iconImageCache = GVol.style.iconImageCache;
  var iconImage = iconImageCache.get(src, crossOrigin, cGVolor);
  if (!iconImage) {
    iconImage = new GVol.style.IconImage(
        image, src, size, crossOrigin, imageState, cGVolor);
    iconImageCache.set(src, crossOrigin, cGVolor, iconImage);
  }
  return iconImage;
};


/**
 * @private
 */
GVol.style.IconImage.prototype.determineTainting_ = function() {
  var context = GVol.dom.createCanvasContext2D(1, 1);
  try {
    context.drawImage(this.image_, 0, 0);
    context.getImageData(0, 0, 1, 1);
  } catch (e) {
    this.tainting_ = true;
  }
};


/**
 * @private
 */
GVol.style.IconImage.prototype.dispatchChangeEvent_ = function() {
  this.dispatchEvent(GVol.events.EventType.CHANGE);
};


/**
 * @private
 */
GVol.style.IconImage.prototype.handleImageError_ = function() {
  this.imageState_ = GVol.ImageState.ERROR;
  this.unlistenImage_();
  this.dispatchChangeEvent_();
};


/**
 * @private
 */
GVol.style.IconImage.prototype.handleImageLoad_ = function() {
  this.imageState_ = GVol.ImageState.LOADED;
  if (this.size_) {
    this.image_.width = this.size_[0];
    this.image_.height = this.size_[1];
  }
  this.size_ = [this.image_.width, this.image_.height];
  this.unlistenImage_();
  this.determineTainting_();
  this.replaceCGVolor_();
  this.dispatchChangeEvent_();
};


/**
 * @param {number} pixelRatio Pixel ratio.
 * @return {Image|HTMLCanvasElement} Image or Canvas element.
 */
GVol.style.IconImage.prototype.getImage = function(pixelRatio) {
  return this.canvas_ ? this.canvas_ : this.image_;
};


/**
 * @return {GVol.ImageState} Image state.
 */
GVol.style.IconImage.prototype.getImageState = function() {
  return this.imageState_;
};


/**
 * @param {number} pixelRatio Pixel ratio.
 * @return {Image|HTMLCanvasElement} Image element.
 */
GVol.style.IconImage.prototype.getHitDetectionImage = function(pixelRatio) {
  if (!this.hitDetectionImage_) {
    if (this.tainting_) {
      var width = this.size_[0];
      var height = this.size_[1];
      var context = GVol.dom.createCanvasContext2D(width, height);
      context.fillRect(0, 0, width, height);
      this.hitDetectionImage_ = context.canvas;
    } else {
      this.hitDetectionImage_ = this.image_;
    }
  }
  return this.hitDetectionImage_;
};


/**
 * @return {GVol.Size} Image size.
 */
GVol.style.IconImage.prototype.getSize = function() {
  return this.size_;
};


/**
 * @return {string|undefined} Image src.
 */
GVol.style.IconImage.prototype.getSrc = function() {
  return this.src_;
};


/**
 * Load not yet loaded URI.
 */
GVol.style.IconImage.prototype.load = function() {
  if (this.imageState_ == GVol.ImageState.IDLE) {
    this.imageState_ = GVol.ImageState.LOADING;
    this.imageListenerKeys_ = [
      GVol.events.listenOnce(this.image_, GVol.events.EventType.ERROR,
          this.handleImageError_, this),
      GVol.events.listenOnce(this.image_, GVol.events.EventType.LOAD,
          this.handleImageLoad_, this)
    ];
    try {
      this.image_.src = this.src_;
    } catch (e) {
      this.handleImageError_();
    }
  }
};


/**
 * @private
 */
GVol.style.IconImage.prototype.replaceCGVolor_ = function() {
  if (this.tainting_ || this.cGVolor_ === null) {
    return;
  }

  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.cGVolor_[0] / 255.0;
  var g = this.cGVolor_[1] / 255.0;
  var b = this.cGVolor_[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
 */
GVol.style.IconImage.prototype.unlistenImage_ = function() {
  this.imageListenerKeys_.forEach(GVol.events.unlistenByKey);
  this.imageListenerKeys_ = null;
};

goog.provide('GVol.style.IconOrigin');

/**
 * Icon origin. One of 'bottom-left', 'bottom-right', 'top-left', 'top-right'.
 * @enum {string}
 */
GVol.style.IconOrigin = {
  BOTTOM_LEFT: 'bottom-left',
  BOTTOM_RIGHT: 'bottom-right',
  TOP_LEFT: 'top-left',
  TOP_RIGHT: 'top-right'
};

goog.provide('GVol.style.Icon');

goog.require('GVol');
goog.require('GVol.ImageState');
goog.require('GVol.asserts');
goog.require('GVol.cGVolor');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.style.IconAnchorUnits');
goog.require('GVol.style.IconImage');
goog.require('GVol.style.IconOrigin');
goog.require('GVol.style.Image');


/**
 * @classdesc
 * Set icon style for vector features.
 *
 * @constructor
 * @param {GVolx.style.IconOptions=} opt_options Options.
 * @extends {GVol.style.Image}
 * @api
 */
GVol.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 {GVol.style.IconOrigin}
   */
  this.anchorOrigin_ = options.anchorOrigin !== undefined ?
    options.anchorOrigin : GVol.style.IconOrigin.TOP_LEFT;

  /**
   * @private
   * @type {GVol.style.IconAnchorUnits}
   */
  this.anchorXUnits_ = options.anchorXUnits !== undefined ?
    options.anchorXUnits : GVol.style.IconAnchorUnits.FRACTION;

  /**
   * @private
   * @type {GVol.style.IconAnchorUnits}
   */
  this.anchorYUnits_ = options.anchorYUnits !== undefined ?
    options.anchorYUnits : GVol.style.IconAnchorUnits.FRACTION;

  /**
   * @private
   * @type {?string}
   */
  this.crossOrigin_ =
      options.crossOrigin !== undefined ? options.crossOrigin : null;

  /**
   * @type {Image|HTMLCanvasElement}
   */
  var image = options.img !== undefined ? options.img : null;

  /**
   * @type {GVol.Size}
   */
  var imgSize = options.imgSize !== undefined ? options.imgSize : null;

  /**
   * @type {string|undefined}
   */
  var src = options.src;

  GVol.asserts.assert(!(src !== undefined && image),
      4); // `image` and `src` cannot be provided at the same time
  GVol.asserts.assert(!image || (image && imgSize),
      5); // `imgSize` must be set when `image` is provided

  if ((src === undefined || src.length === 0) && image) {
    src = image.src || GVol.getUid(image).toString();
  }
  GVol.asserts.assert(src !== undefined && src.length > 0,
      6); // A defined and non-empty `src` or `image` must be provided

  /**
   * @type {GVol.ImageState}
   */
  var imageState = options.src !== undefined ?
    GVol.ImageState.IDLE : GVol.ImageState.LOADED;

  /**
   * @private
   * @type {GVol.CGVolor}
   */
  this.cGVolor_ = options.cGVolor !== undefined ? GVol.cGVolor.asArray(options.cGVolor) :
    null;

  /**
   * @private
   * @type {GVol.style.IconImage}
   */
  this.iconImage_ = GVol.style.IconImage.get(
      image, /** @type {string} */ (src), imgSize, this.crossOrigin_, imageState, this.cGVolor_);

  /**
   * @private
   * @type {Array.<number>}
   */
  this.offset_ = options.offset !== undefined ? options.offset : [0, 0];

  /**
   * @private
   * @type {GVol.style.IconOrigin}
   */
  this.offsetOrigin_ = options.offsetOrigin !== undefined ?
    options.offsetOrigin : GVol.style.IconOrigin.TOP_LEFT;

  /**
   * @private
   * @type {Array.<number>}
   */
  this.origin_ = null;

  /**
   * @private
   * @type {GVol.Size}
   */
  this.size_ = options.size !== undefined ? options.size : null;

  /**
   * @type {number}
   */
  var opacity = options.opacity !== undefined ? options.opacity : 1;

  /**
   * @type {boGVolean}
   */
  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 {boGVolean}
   */
  var snapToPixel = options.snapToPixel !== undefined ?
    options.snapToPixel : true;

  GVol.style.Image.call(this, {
    opacity: opacity,
    rotation: rotation,
    scale: scale,
    snapToPixel: snapToPixel,
    rotateWithView: rotateWithView
  });

};
GVol.inherits(GVol.style.Icon, GVol.style.Image);


/**
 * Clones the style.
 * @return {GVol.style.Icon} The cloned style.
 * @api
 */
GVol.style.Icon.prototype.clone = function() {
  var GVoldImage = this.getImage(1);
  var newImage;
  if (this.iconImage_.getImageState() === GVol.ImageState.LOADED) {
    if (GVoldImage.tagName.toUpperCase() === 'IMG') {
      newImage = /** @type {Image} */ (GVoldImage.cloneNode(true));
    } else {
      newImage = /** @type {HTMLCanvasElement} */ (document.createElement('canvas'));
      var context = newImage.getContext('2d');
      newImage.width = GVoldImage.width;
      newImage.height = GVoldImage.height;
      context.drawImage(GVoldImage, 0, 0);
    }
  }
  return new GVol.style.Icon({
    anchor: this.anchor_.slice(),
    anchorOrigin: this.anchorOrigin_,
    anchorXUnits: this.anchorXUnits_,
    anchorYUnits: this.anchorYUnits_,
    crossOrigin: this.crossOrigin_,
    cGVolor: (this.cGVolor_ && this.cGVolor_.slice) ? this.cGVolor_.slice() : this.cGVolor_ || undefined,
    img: newImage ? newImage : undefined,
    imgSize: newImage ? this.iconImage_.getSize().slice() : undefined,
    src: newImage ? undefined : this.getSrc(),
    offset: this.offset_.slice(),
    offsetOrigin: this.offsetOrigin_,
    size: this.size_ !== null ? this.size_.slice() : undefined,
    opacity: this.getOpacity(),
    scale: this.getScale(),
    snapToPixel: this.getSnapToPixel(),
    rotation: this.getRotation(),
    rotateWithView: this.getRotateWithView()
  });
};


/**
 * @inheritDoc
 * @api
 */
GVol.style.Icon.prototype.getAnchor = function() {
  if (this.normalizedAnchor_) {
    return this.normalizedAnchor_;
  }
  var anchor = this.anchor_;
  var size = this.getSize();
  if (this.anchorXUnits_ == GVol.style.IconAnchorUnits.FRACTION ||
      this.anchorYUnits_ == GVol.style.IconAnchorUnits.FRACTION) {
    if (!size) {
      return null;
    }
    anchor = this.anchor_.slice();
    if (this.anchorXUnits_ == GVol.style.IconAnchorUnits.FRACTION) {
      anchor[0] *= size[0];
    }
    if (this.anchorYUnits_ == GVol.style.IconAnchorUnits.FRACTION) {
      anchor[1] *= size[1];
    }
  }

  if (this.anchorOrigin_ != GVol.style.IconOrigin.TOP_LEFT) {
    if (!size) {
      return null;
    }
    if (anchor === this.anchor_) {
      anchor = this.anchor_.slice();
    }
    if (this.anchorOrigin_ == GVol.style.IconOrigin.TOP_RIGHT ||
        this.anchorOrigin_ == GVol.style.IconOrigin.BOTTOM_RIGHT) {
      anchor[0] = -anchor[0] + size[0];
    }
    if (this.anchorOrigin_ == GVol.style.IconOrigin.BOTTOM_LEFT ||
        this.anchorOrigin_ == GVol.style.IconOrigin.BOTTOM_RIGHT) {
      anchor[1] = -anchor[1] + size[1];
    }
  }
  this.normalizedAnchor_ = anchor;
  return this.normalizedAnchor_;
};


/**
 * Get the icon cGVolor.
 * @return {GVol.CGVolor} CGVolor.
 * @api
 */
GVol.style.Icon.prototype.getCGVolor = function() {
  return this.cGVolor_;
};


/**
 * Get the image icon.
 * @param {number} pixelRatio Pixel ratio.
 * @return {Image|HTMLCanvasElement} Image or Canvas element.
 * @override
 * @api
 */
GVol.style.Icon.prototype.getImage = function(pixelRatio) {
  return this.iconImage_.getImage(pixelRatio);
};


/**
 * @override
 */
GVol.style.Icon.prototype.getImageSize = function() {
  return this.iconImage_.getSize();
};


/**
 * @override
 */
GVol.style.Icon.prototype.getHitDetectionImageSize = function() {
  return this.getImageSize();
};


/**
 * @override
 */
GVol.style.Icon.prototype.getImageState = function() {
  return this.iconImage_.getImageState();
};


/**
 * @override
 */
GVol.style.Icon.prototype.getHitDetectionImage = function(pixelRatio) {
  return this.iconImage_.getHitDetectionImage(pixelRatio);
};


/**
 * @inheritDoc
 * @api
 */
GVol.style.Icon.prototype.getOrigin = function() {
  if (this.origin_) {
    return this.origin_;
  }
  var offset = this.offset_;

  if (this.offsetOrigin_ != GVol.style.IconOrigin.TOP_LEFT) {
    var size = this.getSize();
    var iconImageSize = this.iconImage_.getSize();
    if (!size || !iconImageSize) {
      return null;
    }
    offset = offset.slice();
    if (this.offsetOrigin_ == GVol.style.IconOrigin.TOP_RIGHT ||
        this.offsetOrigin_ == GVol.style.IconOrigin.BOTTOM_RIGHT) {
      offset[0] = iconImageSize[0] - size[0] - offset[0];
    }
    if (this.offsetOrigin_ == GVol.style.IconOrigin.BOTTOM_LEFT ||
        this.offsetOrigin_ == GVol.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
 */
GVol.style.Icon.prototype.getSrc = function() {
  return this.iconImage_.getSrc();
};


/**
 * @inheritDoc
 * @api
 */
GVol.style.Icon.prototype.getSize = function() {
  return !this.size_ ? this.iconImage_.getSize() : this.size_;
};


/**
 * @override
 */
GVol.style.Icon.prototype.listenImageChange = function(listener, thisArg) {
  return GVol.events.listen(this.iconImage_, GVol.events.EventType.CHANGE,
      listener, 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.
 * @override
 * @api
 */
GVol.style.Icon.prototype.load = function() {
  this.iconImage_.load();
};


/**
 * @override
 */
GVol.style.Icon.prototype.unlistenImageChange = function(listener, thisArg) {
  GVol.events.unlisten(this.iconImage_, GVol.events.EventType.CHANGE,
      listener, thisArg);
};

goog.provide('GVol.style.Text');


goog.require('GVol.style.Fill');


/**
 * @classdesc
 * Set text style for vector features.
 *
 * @constructor
 * @param {GVolx.style.TextOptions=} opt_options Options.
 * @api
 */
GVol.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 {boGVolean|undefined}
   */
  this.rotateWithView_ = options.rotateWithView;

  /**
   * @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 {GVol.style.Fill}
   */
  this.fill_ = options.fill !== undefined ? options.fill :
    new GVol.style.Fill({cGVolor: GVol.style.Text.DEFAULT_FILL_COLOR_});

  /**
   * @private
   * @type {GVol.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 cGVolor to use if no fill was set at construction time; a
 * blackish `#333`.
 *
 * @const {string}
 * @private
 */
GVol.style.Text.DEFAULT_FILL_COLOR_ = '#333';


/**
 * Clones the style.
 * @return {GVol.style.Text} The cloned style.
 * @api
 */
GVol.style.Text.prototype.clone = function() {
  return new GVol.style.Text({
    font: this.getFont(),
    rotation: this.getRotation(),
    rotateWithView: this.getRotateWithView(),
    scale: this.getScale(),
    text: this.getText(),
    textAlign: this.getTextAlign(),
    textBaseline: this.getTextBaseline(),
    fill: this.getFill() ? this.getFill().clone() : undefined,
    stroke: this.getStroke() ? this.getStroke().clone() : undefined,
    offsetX: this.getOffsetX(),
    offsetY: this.getOffsetY()
  });
};


/**
 * Get the font name.
 * @return {string|undefined} Font.
 * @api
 */
GVol.style.Text.prototype.getFont = function() {
  return this.font_;
};


/**
 * Get the x-offset for the text.
 * @return {number} Horizontal text offset.
 * @api
 */
GVol.style.Text.prototype.getOffsetX = function() {
  return this.offsetX_;
};


/**
 * Get the y-offset for the text.
 * @return {number} Vertical text offset.
 * @api
 */
GVol.style.Text.prototype.getOffsetY = function() {
  return this.offsetY_;
};


/**
 * Get the fill style for the text.
 * @return {GVol.style.Fill} Fill style.
 * @api
 */
GVol.style.Text.prototype.getFill = function() {
  return this.fill_;
};


/**
 * Determine whether the text rotates with the map.
 * @return {boGVolean|undefined} Rotate with map.
 * @api
 */
GVol.style.Text.prototype.getRotateWithView = function() {
  return this.rotateWithView_;
};


/**
 * Get the text rotation.
 * @return {number|undefined} Rotation.
 * @api
 */
GVol.style.Text.prototype.getRotation = function() {
  return this.rotation_;
};


/**
 * Get the text scale.
 * @return {number|undefined} Scale.
 * @api
 */
GVol.style.Text.prototype.getScale = function() {
  return this.scale_;
};


/**
 * Get the stroke style for the text.
 * @return {GVol.style.Stroke} Stroke style.
 * @api
 */
GVol.style.Text.prototype.getStroke = function() {
  return this.stroke_;
};


/**
 * Get the text to be rendered.
 * @return {string|undefined} Text.
 * @api
 */
GVol.style.Text.prototype.getText = function() {
  return this.text_;
};


/**
 * Get the text alignment.
 * @return {string|undefined} Text align.
 * @api
 */
GVol.style.Text.prototype.getTextAlign = function() {
  return this.textAlign_;
};


/**
 * Get the text baseline.
 * @return {string|undefined} Text baseline.
 * @api
 */
GVol.style.Text.prototype.getTextBaseline = function() {
  return this.textBaseline_;
};


/**
 * Set the font.
 *
 * @param {string|undefined} font Font.
 * @api
 */
GVol.style.Text.prototype.setFont = function(font) {
  this.font_ = font;
};


/**
 * Set the x offset.
 *
 * @param {number} offsetX Horizontal text offset.
 * @api
 */
GVol.style.Text.prototype.setOffsetX = function(offsetX) {
  this.offsetX_ = offsetX;
};


/**
 * Set the y offset.
 *
 * @param {number} offsetY Vertical text offset.
 * @api
 */
GVol.style.Text.prototype.setOffsetY = function(offsetY) {
  this.offsetY_ = offsetY;
};


/**
 * Set the fill.
 *
 * @param {GVol.style.Fill} fill Fill style.
 * @api
 */
GVol.style.Text.prototype.setFill = function(fill) {
  this.fill_ = fill;
};


/**
 * Set the rotation.
 *
 * @param {number|undefined} rotation Rotation.
 * @api
 */
GVol.style.Text.prototype.setRotation = function(rotation) {
  this.rotation_ = rotation;
};


/**
 * Set the scale.
 *
 * @param {number|undefined} scale Scale.
 * @api
 */
GVol.style.Text.prototype.setScale = function(scale) {
  this.scale_ = scale;
};


/**
 * Set the stroke.
 *
 * @param {GVol.style.Stroke} stroke Stroke style.
 * @api
 */
GVol.style.Text.prototype.setStroke = function(stroke) {
  this.stroke_ = stroke;
};


/**
 * Set the text.
 *
 * @param {string|undefined} text Text.
 * @api
 */
GVol.style.Text.prototype.setText = function(text) {
  this.text_ = text;
};


/**
 * Set the text alignment.
 *
 * @param {string|undefined} textAlign Text align.
 * @api
 */
GVol.style.Text.prototype.setTextAlign = function(textAlign) {
  this.textAlign_ = textAlign;
};


/**
 * Set the text baseline.
 *
 * @param {string|undefined} textBaseline Text baseline.
 * @api
 */
GVol.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 serialize arbitrary feature properties
// FIXME don't parse style if extractStyles is false

goog.provide('GVol.format.KML');

goog.require('GVol');
goog.require('GVol.Feature');
goog.require('GVol.array');
goog.require('GVol.asserts');
goog.require('GVol.cGVolor');
goog.require('GVol.format.Feature');
goog.require('GVol.format.XMLFeature');
goog.require('GVol.format.XSD');
goog.require('GVol.geom.GeometryCGVollection');
goog.require('GVol.geom.GeometryLayout');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.geom.LineString');
goog.require('GVol.geom.MultiLineString');
goog.require('GVol.geom.MultiPoint');
goog.require('GVol.geom.MultiPGVolygon');
goog.require('GVol.geom.Point');
goog.require('GVol.geom.PGVolygon');
goog.require('GVol.math');
goog.require('GVol.proj');
goog.require('GVol.style.Fill');
goog.require('GVol.style.Icon');
goog.require('GVol.style.IconAnchorUnits');
goog.require('GVol.style.IconOrigin');
goog.require('GVol.style.Stroke');
goog.require('GVol.style.Style');
goog.require('GVol.style.Text');
goog.require('GVol.xml');


/**
 * @classdesc
 * Feature format for reading and writing data in the KML format.
 *
 * Note that the KML format uses the URL() constructor. Older browsers such as IE
 * which do not support this will need a URL pGVolyfill to be loaded before use.
 *
 * @constructor
 * @extends {GVol.format.XMLFeature}
 * @param {GVolx.format.KMLOptions=} opt_options Options.
 * @api
 */
GVol.format.KML = function(opt_options) {

  var options = opt_options ? opt_options : {};

  GVol.format.XMLFeature.call(this);

  if (!GVol.format.KML.DEFAULT_STYLE_ARRAY_) {
    GVol.format.KML.createStyleDefaults_();
  }

  /**
   * @inheritDoc
   */
  this.defaultDataProjection = GVol.proj.get('EPSG:4326');

  /**
   * @private
   * @type {Array.<GVol.style.Style>}
   */
  this.defaultStyle_ = options.defaultStyle ?
    options.defaultStyle : GVol.format.KML.DEFAULT_STYLE_ARRAY_;

  /**
   * @private
   * @type {boGVolean}
   */
  this.extractStyles_ = options.extractStyles !== undefined ?
    options.extractStyles : true;

  /**
   * @private
   * @type {boGVolean}
   */
  this.writeStyles_ = options.writeStyles !== undefined ?
    options.writeStyles : true;

  /**
   * @private
   * @type {Object.<string, (Array.<GVol.style.Style>|string)>}
   */
  this.sharedStyles_ = {};

  /**
   * @private
   * @type {boGVolean}
   */
  this.showPointNames_ = options.showPointNames !== undefined ?
    options.showPointNames : true;

};
GVol.inherits(GVol.format.KML, GVol.format.XMLFeature);


/**
 * @const
 * @type {Array.<string>}
 * @private
 */
GVol.format.KML.GX_NAMESPACE_URIS_ = [
  'http://www.google.com/kml/ext/2.2'
];


/**
 * @const
 * @type {Array.<string>}
 * @private
 */
GVol.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
 */
GVol.format.KML.SCHEMA_LOCATION_ = 'http://www.opengis.net/kml/2.2 ' +
    'https://developers.google.com/kml/schema/kml22gx.xsd';


/**
 * @return {Array.<GVol.style.Style>} Default style.
 * @private
 */
GVol.format.KML.createStyleDefaults_ = function() {
  /**
   * @const
   * @type {GVol.CGVolor}
   * @private
   */
  GVol.format.KML.DEFAULT_COLOR_ = [255, 255, 255, 1];

  /**
   * @const
   * @type {GVol.style.Fill}
   * @private
   */
  GVol.format.KML.DEFAULT_FILL_STYLE_ = new GVol.style.Fill({
    cGVolor: GVol.format.KML.DEFAULT_COLOR_
  });

  /**
   * @const
   * @type {GVol.Size}
   * @private
   */
  GVol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_ = [20, 2]; // FIXME maybe [8, 32] ?

  /**
   * @const
   * @type {GVol.style.IconAnchorUnits}
   * @private
   */
  GVol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_X_UNITS_ =
      GVol.style.IconAnchorUnits.PIXELS;

  /**
   * @const
   * @type {GVol.style.IconAnchorUnits}
   * @private
   */
  GVol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_Y_UNITS_ =
      GVol.style.IconAnchorUnits.PIXELS;

  /**
   * @const
   * @type {GVol.Size}
   * @private
   */
  GVol.format.KML.DEFAULT_IMAGE_STYLE_SIZE_ = [64, 64];

  /**
   * @const
   * @type {string}
   * @private
   */
  GVol.format.KML.DEFAULT_IMAGE_STYLE_SRC_ =
      'https://maps.google.com/mapfiles/kml/pushpin/ylw-pushpin.png';

  /**
   * @const
   * @type {number}
   * @private
   */
  GVol.format.KML.DEFAULT_IMAGE_SCALE_MULTIPLIER_ = 0.5;

  /**
   * @const
   * @type {GVol.style.Image}
   * @private
   */
  GVol.format.KML.DEFAULT_IMAGE_STYLE_ = new GVol.style.Icon({
    anchor: GVol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_,
    anchorOrigin: GVol.style.IconOrigin.BOTTOM_LEFT,
    anchorXUnits: GVol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_X_UNITS_,
    anchorYUnits: GVol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_Y_UNITS_,
    crossOrigin: 'anonymous',
    rotation: 0,
    scale: GVol.format.KML.DEFAULT_IMAGE_SCALE_MULTIPLIER_,
    size: GVol.format.KML.DEFAULT_IMAGE_STYLE_SIZE_,
    src: GVol.format.KML.DEFAULT_IMAGE_STYLE_SRC_
  });

  /**
   * @const
   * @type {string}
   * @private
   */
  GVol.format.KML.DEFAULT_NO_IMAGE_STYLE_ = 'NO_IMAGE';

  /**
   * @const
   * @type {GVol.style.Stroke}
   * @private
   */
  GVol.format.KML.DEFAULT_STROKE_STYLE_ = new GVol.style.Stroke({
    cGVolor: GVol.format.KML.DEFAULT_COLOR_,
    width: 1
  });

  /**
   * @const
   * @type {GVol.style.Stroke}
   * @private
   */
  GVol.format.KML.DEFAULT_TEXT_STROKE_STYLE_ = new GVol.style.Stroke({
    cGVolor: [51, 51, 51, 1],
    width: 2
  });

  /**
   * @const
   * @type {GVol.style.Text}
   * @private
   */
  GVol.format.KML.DEFAULT_TEXT_STYLE_ = new GVol.style.Text({
    font: 'bGVold 16px Helvetica',
    fill: GVol.format.KML.DEFAULT_FILL_STYLE_,
    stroke: GVol.format.KML.DEFAULT_TEXT_STROKE_STYLE_,
    scale: 0.8
  });

  /**
   * @const
   * @type {GVol.style.Style}
   * @private
   */
  GVol.format.KML.DEFAULT_STYLE_ = new GVol.style.Style({
    fill: GVol.format.KML.DEFAULT_FILL_STYLE_,
    image: GVol.format.KML.DEFAULT_IMAGE_STYLE_,
    text: GVol.format.KML.DEFAULT_TEXT_STYLE_,
    stroke: GVol.format.KML.DEFAULT_STROKE_STYLE_,
    zIndex: 0
  });

  /**
   * @const
   * @type {Array.<GVol.style.Style>}
   * @private
   */
  GVol.format.KML.DEFAULT_STYLE_ARRAY_ = [GVol.format.KML.DEFAULT_STYLE_];

  return GVol.format.KML.DEFAULT_STYLE_ARRAY_;
};


/**
 * @const
 * @type {Object.<string, GVol.style.IconAnchorUnits>}
 * @private
 */
GVol.format.KML.ICON_ANCHOR_UNITS_MAP_ = {
  'fraction': GVol.style.IconAnchorUnits.FRACTION,
  'pixels': GVol.style.IconAnchorUnits.PIXELS,
  'insetPixels': GVol.style.IconAnchorUnits.PIXELS
};


/**
 * @param {GVol.style.Style|undefined} foundStyle Style.
 * @param {string} name Name.
 * @return {GVol.style.Style} style Style.
 * @private
 */
GVol.format.KML.createNameStyleFunction_ = function(foundStyle, name) {
  var textStyle = null;
  var textOffset = [0, 0];
  var textAlign = 'start';
  if (foundStyle.getImage()) {
    var imageSize = foundStyle.getImage().getImageSize();
    if (imageSize === null) {
      imageSize = GVol.format.KML.DEFAULT_IMAGE_STYLE_SIZE_;
    }
    if (imageSize.length == 2) {
      var imageScale = foundStyle.getImage().getScale();
      // Offset the label to be centered to the right of the icon, if there is
      // one.
      textOffset[0] = imageScale * imageSize[0] / 2;
      textOffset[1] = -imageScale * imageSize[1] / 2;
      textAlign = 'left';
    }
  }
  if (foundStyle.getText() !== null) {
    // clone the text style, customizing it with name, alignments and offset.
    // Note that kml does not support many text options that OpenLayers does (rotation, textBaseline).
    var foundText = foundStyle.getText();
    textStyle = foundText.clone();
    textStyle.setFont(foundText.getFont() || GVol.format.KML.DEFAULT_TEXT_STYLE_.getFont());
    textStyle.setScale(foundText.getScale() || GVol.format.KML.DEFAULT_TEXT_STYLE_.getScale());
    textStyle.setFill(foundText.getFill() || GVol.format.KML.DEFAULT_TEXT_STYLE_.getFill());
    textStyle.setStroke(foundText.getStroke() || GVol.format.KML.DEFAULT_TEXT_STROKE_STYLE_);
  } else {
    textStyle = GVol.format.KML.DEFAULT_TEXT_STYLE_.clone();
  }
  textStyle.setText(name);
  textStyle.setOffsetX(textOffset[0]);
  textStyle.setOffsetY(textOffset[1]);
  textStyle.setTextAlign(textAlign);

  var nameStyle = new GVol.style.Style({
    text: textStyle
  });
  return nameStyle;
};


/**
 * @param {Array.<GVol.style.Style>|undefined} style Style.
 * @param {string} styleUrl Style URL.
 * @param {Array.<GVol.style.Style>} defaultStyle Default style.
 * @param {Object.<string, (Array.<GVol.style.Style>|string)>} sharedStyles Shared
 *          styles.
 * @param {boGVolean|undefined} showPointNames true to show names for point
 *          placemarks.
 * @return {GVol.FeatureStyleFunction} Feature style function.
 * @private
 */
GVol.format.KML.createFeatureStyleFunction_ = function(style, styleUrl,
    defaultStyle, sharedStyles, showPointNames) {

  return (
  /**
       * @param {number} resGVolution ResGVolution.
       * @return {Array.<GVol.style.Style>} Style.
       * @this {GVol.Feature}
       */
    function(resGVolution) {
      var drawName = showPointNames;
      /** @type {GVol.style.Style|undefined} */
      var nameStyle;
      var name = '';
      if (drawName) {
        if (this.getGeometry()) {
          drawName = (this.getGeometry().getType() ===
                        GVol.geom.GeometryType.POINT);
        }
      }

      if (drawName) {
        name = /** @type {string} */ (this.get('name'));
        drawName = drawName && name;
      }

      if (style) {
        if (drawName) {
          nameStyle = GVol.format.KML.createNameStyleFunction_(style[0],
              name);
          return style.concat(nameStyle);
        }
        return style;
      }
      if (styleUrl) {
        var foundStyle = GVol.format.KML.findStyle_(styleUrl, defaultStyle,
            sharedStyles);
        if (drawName) {
          nameStyle = GVol.format.KML.createNameStyleFunction_(foundStyle[0],
              name);
          return foundStyle.concat(nameStyle);
        }
        return foundStyle;
      }
      if (drawName) {
        nameStyle = GVol.format.KML.createNameStyleFunction_(defaultStyle[0],
            name);
        return defaultStyle.concat(nameStyle);
      }
      return defaultStyle;
    });
};


/**
 * @param {Array.<GVol.style.Style>|string|undefined} styleValue Style value.
 * @param {Array.<GVol.style.Style>} defaultStyle Default style.
 * @param {Object.<string, (Array.<GVol.style.Style>|string)>} sharedStyles
 * Shared styles.
 * @return {Array.<GVol.style.Style>} Style.
 * @private
 */
GVol.format.KML.findStyle_ = function(styleValue, defaultStyle, sharedStyles) {
  if (Array.isArray(styleValue)) {
    return styleValue;
  } else if (typeof styleValue === 'string') {
    // 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 GVol.format.KML.findStyle_(
        sharedStyles[styleValue], defaultStyle, sharedStyles);
  } else {
    return defaultStyle;
  }
};


/**
 * @param {Node} node Node.
 * @private
 * @return {GVol.CGVolor|undefined} CGVolor.
 */
GVol.format.KML.readCGVolor_ = function(node) {
  var s = GVol.xml.getAllTextContent(node, false);
  // The KML specification states that cGVolors should not include a leading `#`
  // but we tGVolerate them.
  var m = /^\s*#?\s*([0-9A-Fa-f]{8})\s*$/.exec(s);
  if (m) {
    var hexCGVolor = m[1];
    return [
      parseInt(hexCGVolor.substr(6, 2), 16),
      parseInt(hexCGVolor.substr(4, 2), 16),
      parseInt(hexCGVolor.substr(2, 2), 16),
      parseInt(hexCGVolor.substr(0, 2), 16) / 255
    ];

  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @private
 * @return {Array.<number>|undefined} Flat coordinates.
 */
GVol.format.KML.readFlatCoordinates_ = function(node) {
  var s = GVol.xml.getAllTextContent(node, false);
  var flatCoordinates = [];
  // The KML specification states that coordinate tuples should not include
  // spaces, but we tGVolerate 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} URI.
 */
GVol.format.KML.readURI_ = function(node) {
  var s = GVol.xml.getAllTextContent(node, false).trim();
  if (node.baseURI && node.baseURI !== 'about:blank') {
    var url = new URL(s, node.baseURI);
    return url.href;
  } else {
    return s;
  }
};


/**
 * @param {Node} node Node.
 * @private
 * @return {GVol.KMLVec2_} Vec2.
 */
GVol.format.KML.readVec2_ = function(node) {
  var xunits = node.getAttribute('xunits');
  var yunits = node.getAttribute('yunits');
  var origin;
  if (xunits !== 'insetPixels') {
    if (yunits !== 'insetPixels') {
      origin = GVol.style.IconOrigin.BOTTOM_LEFT;
    } else {
      origin = GVol.style.IconOrigin.TOP_LEFT;
    }
  } else {
    if (yunits !== 'insetPixels') {
      origin = GVol.style.IconOrigin.BOTTOM_RIGHT;
    } else {
      origin = GVol.style.IconOrigin.TOP_RIGHT;
    }
  }
  return {
    x: parseFloat(node.getAttribute('x')),
    xunits: GVol.format.KML.ICON_ANCHOR_UNITS_MAP_[xunits],
    y: parseFloat(node.getAttribute('y')),
    yunits: GVol.format.KML.ICON_ANCHOR_UNITS_MAP_[yunits],
    origin: origin
  };
};


/**
 * @param {Node} node Node.
 * @private
 * @return {number|undefined} Scale.
 */
GVol.format.KML.readScale_ = function(node) {
  return GVol.format.XSD.readDecimal(node);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<GVol.style.Style>|string|undefined} StyleMap.
 */
GVol.format.KML.readStyleMapValue_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(undefined,
      GVol.format.KML.STYLE_MAP_PARSERS_, node, objectStack);
};
/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.IconStyleParser_ = function(node, objectStack) {
  // FIXME refreshMode
  // FIXME refreshInterval
  // FIXME viewRefreshTime
  // FIXME viewBoundScale
  // FIXME viewFormat
  // FIXME httpQuery
  var object = GVol.xml.pushParseAndPop(
      {}, GVol.format.KML.ICON_STYLE_PARSERS_, node, objectStack);
  if (!object) {
    return;
  }
  var styleObject = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  var IconObject = 'Icon' in object ? object['Icon'] : {};
  var drawIcon = (!('Icon' in object) || Object.keys(IconObject).length > 0);
  var src;
  var href = /** @type {string|undefined} */
      (IconObject['href']);
  if (href) {
    src = href;
  } else if (drawIcon) {
    src = GVol.format.KML.DEFAULT_IMAGE_STYLE_SRC_;
  }
  var anchor, anchorXUnits, anchorYUnits;
  var anchorOrigin = GVol.style.IconOrigin.BOTTOM_LEFT;
  var hotSpot = /** @type {GVol.KMLVec2_|undefined} */
      (object['hotSpot']);
  if (hotSpot) {
    anchor = [hotSpot.x, hotSpot.y];
    anchorXUnits = hotSpot.xunits;
    anchorYUnits = hotSpot.yunits;
    anchorOrigin = hotSpot.origin;
  } else if (src === GVol.format.KML.DEFAULT_IMAGE_STYLE_SRC_) {
    anchor = GVol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_;
    anchorXUnits = GVol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_X_UNITS_;
    anchorYUnits = GVol.format.KML.DEFAULT_IMAGE_STYLE_ANCHOR_Y_UNITS_;
  } else if (/^http:\/\/maps\.(?:google|gstatic)\.com\//.test(src)) {
    anchor = [0.5, 0];
    anchorXUnits = GVol.style.IconAnchorUnits.FRACTION;
    anchorYUnits = GVol.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 = GVol.math.toRadians(heading);
  }

  var scale = /** @type {number|undefined} */
      (object['scale']);

  if (drawIcon) {
    if (src == GVol.format.KML.DEFAULT_IMAGE_STYLE_SRC_) {
      size = GVol.format.KML.DEFAULT_IMAGE_STYLE_SIZE_;
      if (scale === undefined) {
        scale = GVol.format.KML.DEFAULT_IMAGE_SCALE_MULTIPLIER_;
      }
    }

    var imageStyle = new GVol.style.Icon({
      anchor: anchor,
      anchorOrigin: anchorOrigin,
      anchorXUnits: anchorXUnits,
      anchorYUnits: anchorYUnits,
      crossOrigin: 'anonymous', // FIXME should this be configurable?
      offset: offset,
      offsetOrigin: GVol.style.IconOrigin.BOTTOM_LEFT,
      rotation: rotation,
      scale: scale,
      size: size,
      src: src
    });
    styleObject['imageStyle'] = imageStyle;
  } else {
    // handle the case when we explicitly want to draw no icon.
    styleObject['imageStyle'] = GVol.format.KML.DEFAULT_NO_IMAGE_STYLE_;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.LabelStyleParser_ = function(node, objectStack) {
  // FIXME cGVolorMode
  var object = GVol.xml.pushParseAndPop(
      {}, GVol.format.KML.LABEL_STYLE_PARSERS_, node, objectStack);
  if (!object) {
    return;
  }
  var styleObject = objectStack[objectStack.length - 1];
  var textStyle = new GVol.style.Text({
    fill: new GVol.style.Fill({
      cGVolor: /** @type {GVol.CGVolor} */
          ('cGVolor' in object ? object['cGVolor'] : GVol.format.KML.DEFAULT_COLOR_)
    }),
    scale: /** @type {number|undefined} */
        (object['scale'])
  });
  styleObject['textStyle'] = textStyle;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.LineStyleParser_ = function(node, objectStack) {
  // FIXME cGVolorMode
  // FIXME gx:outerCGVolor
  // FIXME gx:outerWidth
  // FIXME gx:physicalWidth
  // FIXME gx:labelVisibility
  var object = GVol.xml.pushParseAndPop(
      {}, GVol.format.KML.LINE_STYLE_PARSERS_, node, objectStack);
  if (!object) {
    return;
  }
  var styleObject = objectStack[objectStack.length - 1];
  var strokeStyle = new GVol.style.Stroke({
    cGVolor: /** @type {GVol.CGVolor} */
        ('cGVolor' in object ? object['cGVolor'] : GVol.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
 */
GVol.format.KML.PGVolyStyleParser_ = function(node, objectStack) {
  // FIXME cGVolorMode
  var object = GVol.xml.pushParseAndPop(
      {}, GVol.format.KML.POLY_STYLE_PARSERS_, node, objectStack);
  if (!object) {
    return;
  }
  var styleObject = objectStack[objectStack.length - 1];
  var fillStyle = new GVol.style.Fill({
    cGVolor: /** @type {GVol.CGVolor} */
        ('cGVolor' in object ? object['cGVolor'] : GVol.format.KML.DEFAULT_COLOR_)
  });
  styleObject['fillStyle'] = fillStyle;
  var fill = /** @type {boGVolean|undefined} */ (object['fill']);
  if (fill !== undefined) {
    styleObject['fill'] = fill;
  }
  var outline =
      /** @type {boGVolean|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.
 */
GVol.format.KML.readFlatLinearRing_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(null,
      GVol.format.KML.FLAT_LINEAR_RING_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.gxCoordParser_ = function(node, objectStack) {
  var gxTrackObject = /** @type {GVol.KMLGxTrackObject_} */
      (objectStack[objectStack.length - 1]);
  var flatCoordinates = gxTrackObject.flatCoordinates;
  var s = GVol.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 {GVol.geom.MultiLineString|undefined} MultiLineString.
 */
GVol.format.KML.readGxMultiTrack_ = function(node, objectStack) {
  var lineStrings = GVol.xml.pushParseAndPop([],
      GVol.format.KML.GX_MULTITRACK_GEOMETRY_PARSERS_, node, objectStack);
  if (!lineStrings) {
    return undefined;
  }
  var multiLineString = new GVol.geom.MultiLineString(null);
  multiLineString.setLineStrings(lineStrings);
  return multiLineString;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {GVol.geom.LineString|undefined} LineString.
 */
GVol.format.KML.readGxTrack_ = function(node, objectStack) {
  var gxTrackObject = GVol.xml.pushParseAndPop(
      /** @type {GVol.KMLGxTrackObject_} */ ({
        flatCoordinates: [],
        whens: []
      }), GVol.format.KML.GX_TRACK_PARSERS_, node, objectStack);
  if (!gxTrackObject) {
    return undefined;
  }
  var flatCoordinates = gxTrackObject.flatCoordinates;
  var whens = gxTrackObject.whens;
  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 GVol.geom.LineString(null);
  lineString.setFlatCoordinates(GVol.geom.GeometryLayout.XYZM, flatCoordinates);
  return lineString;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object} Icon object.
 */
GVol.format.KML.readIcon_ = function(node, objectStack) {
  var iconObject = GVol.xml.pushParseAndPop(
      {}, GVol.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.
 */
GVol.format.KML.readFlatCoordinatesFromNode_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(null,
      GVol.format.KML.GEOMETRY_FLAT_COORDINATES_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {GVol.geom.LineString|undefined} LineString.
 */
GVol.format.KML.readLineString_ = function(node, objectStack) {
  var properties = GVol.xml.pushParseAndPop({},
      GVol.format.KML.EXTRUDE_AND_ALTITUDE_MODE_PARSERS_, node,
      objectStack);
  var flatCoordinates =
      GVol.format.KML.readFlatCoordinatesFromNode_(node, objectStack);
  if (flatCoordinates) {
    var lineString = new GVol.geom.LineString(null);
    lineString.setFlatCoordinates(GVol.geom.GeometryLayout.XYZ, flatCoordinates);
    lineString.setProperties(properties);
    return lineString;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {GVol.geom.PGVolygon|undefined} PGVolygon.
 */
GVol.format.KML.readLinearRing_ = function(node, objectStack) {
  var properties = GVol.xml.pushParseAndPop({},
      GVol.format.KML.EXTRUDE_AND_ALTITUDE_MODE_PARSERS_, node,
      objectStack);
  var flatCoordinates =
      GVol.format.KML.readFlatCoordinatesFromNode_(node, objectStack);
  if (flatCoordinates) {
    var pGVolygon = new GVol.geom.PGVolygon(null);
    pGVolygon.setFlatCoordinates(GVol.geom.GeometryLayout.XYZ, flatCoordinates,
        [flatCoordinates.length]);
    pGVolygon.setProperties(properties);
    return pGVolygon;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {GVol.geom.Geometry} Geometry.
 */
GVol.format.KML.readMultiGeometry_ = function(node, objectStack) {
  var geometries = GVol.xml.pushParseAndPop([],
      GVol.format.KML.MULTI_GEOMETRY_PARSERS_, node, objectStack);
  if (!geometries) {
    return null;
  }
  if (geometries.length === 0) {
    return new GVol.geom.GeometryCGVollection(geometries);
  }
  /** @type {GVol.geom.Geometry} */
  var multiGeometry;
  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) {
    var layout;
    var flatCoordinates;
    if (type == GVol.geom.GeometryType.POINT) {
      var point = geometries[0];
      layout = point.getLayout();
      flatCoordinates = point.getFlatCoordinates();
      for (i = 1, ii = geometries.length; i < ii; ++i) {
        geometry = geometries[i];
        GVol.array.extend(flatCoordinates, geometry.getFlatCoordinates());
      }
      multiGeometry = new GVol.geom.MultiPoint(null);
      multiGeometry.setFlatCoordinates(layout, flatCoordinates);
      GVol.format.KML.setCommonGeometryProperties_(multiGeometry, geometries);
    } else if (type == GVol.geom.GeometryType.LINE_STRING) {
      multiGeometry = new GVol.geom.MultiLineString(null);
      multiGeometry.setLineStrings(geometries);
      GVol.format.KML.setCommonGeometryProperties_(multiGeometry, geometries);
    } else if (type == GVol.geom.GeometryType.POLYGON) {
      multiGeometry = new GVol.geom.MultiPGVolygon(null);
      multiGeometry.setPGVolygons(geometries);
      GVol.format.KML.setCommonGeometryProperties_(multiGeometry, geometries);
    } else if (type == GVol.geom.GeometryType.GEOMETRY_COLLECTION) {
      multiGeometry = new GVol.geom.GeometryCGVollection(geometries);
    } else {
      GVol.asserts.assert(false, 37); // Unknown geometry type found
    }
  } else {
    multiGeometry = new GVol.geom.GeometryCGVollection(geometries);
  }
  return /** @type {GVol.geom.Geometry} */ (multiGeometry);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {GVol.geom.Point|undefined} Point.
 */
GVol.format.KML.readPoint_ = function(node, objectStack) {
  var properties = GVol.xml.pushParseAndPop({},
      GVol.format.KML.EXTRUDE_AND_ALTITUDE_MODE_PARSERS_, node,
      objectStack);
  var flatCoordinates =
      GVol.format.KML.readFlatCoordinatesFromNode_(node, objectStack);
  if (flatCoordinates) {
    var point = new GVol.geom.Point(null);
    point.setFlatCoordinates(GVol.geom.GeometryLayout.XYZ, flatCoordinates);
    point.setProperties(properties);
    return point;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {GVol.geom.PGVolygon|undefined} PGVolygon.
 */
GVol.format.KML.readPGVolygon_ = function(node, objectStack) {
  var properties = GVol.xml.pushParseAndPop(/** @type {Object<string,*>} */ ({}),
      GVol.format.KML.EXTRUDE_AND_ALTITUDE_MODE_PARSERS_, node,
      objectStack);
  var flatLinearRings = GVol.xml.pushParseAndPop([null],
      GVol.format.KML.FLAT_LINEAR_RINGS_PARSERS_, node, objectStack);
  if (flatLinearRings && flatLinearRings[0]) {
    var pGVolygon = new GVol.geom.PGVolygon(null);
    var flatCoordinates = flatLinearRings[0];
    var ends = [flatCoordinates.length];
    var i, ii;
    for (i = 1, ii = flatLinearRings.length; i < ii; ++i) {
      GVol.array.extend(flatCoordinates, flatLinearRings[i]);
      ends.push(flatCoordinates.length);
    }
    pGVolygon.setFlatCoordinates(
        GVol.geom.GeometryLayout.XYZ, flatCoordinates, ends);
    pGVolygon.setProperties(properties);
    return pGVolygon;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<GVol.style.Style>} Style.
 */
GVol.format.KML.readStyle_ = function(node, objectStack) {
  var styleObject = GVol.xml.pushParseAndPop(
      {}, GVol.format.KML.STYLE_PARSERS_, node, objectStack);
  if (!styleObject) {
    return null;
  }
  var fillStyle = /** @type {GVol.style.Fill} */
      ('fillStyle' in styleObject ?
        styleObject['fillStyle'] : GVol.format.KML.DEFAULT_FILL_STYLE_);
  var fill = /** @type {boGVolean|undefined} */ (styleObject['fill']);
  if (fill !== undefined && !fill) {
    fillStyle = null;
  }
  var imageStyle = /** @type {GVol.style.Image} */
      ('imageStyle' in styleObject ?
        styleObject['imageStyle'] : GVol.format.KML.DEFAULT_IMAGE_STYLE_);
  if (imageStyle == GVol.format.KML.DEFAULT_NO_IMAGE_STYLE_) {
    imageStyle = undefined;
  }
  var textStyle = /** @type {GVol.style.Text} */
      ('textStyle' in styleObject ?
        styleObject['textStyle'] : GVol.format.KML.DEFAULT_TEXT_STYLE_);
  var strokeStyle = /** @type {GVol.style.Stroke} */
      ('strokeStyle' in styleObject ?
        styleObject['strokeStyle'] : GVol.format.KML.DEFAULT_STROKE_STYLE_);
  var outline = /** @type {boGVolean|undefined} */
      (styleObject['outline']);
  if (outline !== undefined && !outline) {
    strokeStyle = null;
  }
  return [new GVol.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 {GVol.geom.MultiPoint|GVol.geom.MultiLineString|GVol.geom.MultiPGVolygon}
 *     multiGeometry A multi-geometry.
 * @param {Array.<GVol.geom.Geometry>} geometries List of geometries.
 * @private
 */
GVol.format.KML.setCommonGeometryProperties_ = function(multiGeometry,
    geometries) {
  var ii = geometries.length;
  var extrudes = new Array(geometries.length);
  var tessellates = new Array(geometries.length);
  var altitudeModes = new Array(geometries.length);
  var geometry, i, hasExtrude, hasTessellate, hasAltitudeMode;
  hasExtrude = hasTessellate = hasAltitudeMode = false;
  for (i = 0; i < ii; ++i) {
    geometry = geometries[i];
    extrudes[i] = geometry.get('extrude');
    tessellates[i] = geometry.get('tessellate');
    altitudeModes[i] = geometry.get('altitudeMode');
    hasExtrude = hasExtrude || extrudes[i] !== undefined;
    hasTessellate = hasTessellate || tessellates[i] !== undefined;
    hasAltitudeMode = hasAltitudeMode || altitudeModes[i];
  }
  if (hasExtrude) {
    multiGeometry.set('extrude', extrudes);
  }
  if (hasTessellate) {
    multiGeometry.set('tessellate', tessellates);
  }
  if (hasAltitudeMode) {
    multiGeometry.set('altitudeMode', altitudeModes);
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.DataParser_ = function(node, objectStack) {
  var name = node.getAttribute('name');
  GVol.xml.parseNode(GVol.format.KML.DATA_PARSERS_, node, objectStack);
  var featureObject = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  if (name !== null) {
    featureObject[name] = featureObject.value;
  } else if (featureObject.displayName !== null) {
    featureObject[featureObject.displayName] = featureObject.value;
  }
  delete featureObject['value'];
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.ExtendedDataParser_ = function(node, objectStack) {
  GVol.xml.parseNode(GVol.format.KML.EXTENDED_DATA_PARSERS_, node, objectStack);
};

/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.RegionParser_ = function(node, objectStack) {
  GVol.xml.parseNode(GVol.format.KML.REGION_PARSERS_, node, objectStack);
};

/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.PairDataParser_ = function(node, objectStack) {
  var pairObject = GVol.xml.pushParseAndPop(
      {}, GVol.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 {GVol.style.Style} */
        (pairObject['Style']);
    if (Style) {
      objectStack[objectStack.length - 1] = Style;
    }
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.PlacemarkStyleMapParser_ = function(node, objectStack) {
  var styleMapValue = GVol.format.KML.readStyleMapValue_(node, objectStack);
  if (!styleMapValue) {
    return;
  }
  var placemarkObject = objectStack[objectStack.length - 1];
  if (Array.isArray(styleMapValue)) {
    placemarkObject['Style'] = styleMapValue;
  } else if (typeof styleMapValue === 'string') {
    placemarkObject['styleUrl'] = styleMapValue;
  } else {
    GVol.asserts.assert(false, 38); // `styleMapValue` has an unknown type
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.SchemaDataParser_ = function(node, objectStack) {
  GVol.xml.parseNode(GVol.format.KML.SCHEMA_DATA_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.SimpleDataParser_ = function(node, objectStack) {
  var name = node.getAttribute('name');
  if (name !== null) {
    var data = GVol.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
 */
GVol.format.KML.LatLonAltBoxParser_ = function(node, objectStack) {
  var object = GVol.xml.pushParseAndPop({}, GVol.format.KML.LAT_LON_ALT_BOX_PARSERS_, node, objectStack);
  if (!object) {
    return;
  }
  var regionObject = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  var extent = [
    parseFloat(object['west']),
    parseFloat(object['south']),
    parseFloat(object['east']),
    parseFloat(object['north'])
  ];
  regionObject['extent'] = extent;
  regionObject['altitudeMode'] = object['altitudeMode'];
  regionObject['minAltitude'] = parseFloat(object['minAltitude']);
  regionObject['maxAltitude'] = parseFloat(object['maxAltitude']);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.LodParser_ = function(node, objectStack) {
  var object = GVol.xml.pushParseAndPop({}, GVol.format.KML.LOD_PARSERS_, node, objectStack);
  if (!object) {
    return;
  }
  var lodObject = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  lodObject['minLodPixels'] = parseFloat(object['minLodPixels']);
  lodObject['maxLodPixels'] = parseFloat(object['maxLodPixels']);
  lodObject['minFadeExtent'] = parseFloat(object['minFadeExtent']);
  lodObject['maxFadeExtent'] = parseFloat(object['maxFadeExtent']);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.innerBoundaryIsParser_ = function(node, objectStack) {
  /** @type {Array.<number>|undefined} */
  var flatLinearRing = GVol.xml.pushParseAndPop(undefined,
      GVol.format.KML.INNER_BOUNDARY_IS_PARSERS_, node, objectStack);
  if (flatLinearRing) {
    var flatLinearRings = /** @type {Array.<Array.<number>>} */
        (objectStack[objectStack.length - 1]);
    flatLinearRings.push(flatLinearRing);
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.outerBoundaryIsParser_ = function(node, objectStack) {
  /** @type {Array.<number>|undefined} */
  var flatLinearRing = GVol.xml.pushParseAndPop(undefined,
      GVol.format.KML.OUTER_BOUNDARY_IS_PARSERS_, node, objectStack);
  if (flatLinearRing) {
    var flatLinearRings = /** @type {Array.<Array.<number>>} */
        (objectStack[objectStack.length - 1]);
    flatLinearRings[0] = flatLinearRing;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.LinkParser_ = function(node, objectStack) {
  GVol.xml.parseNode(GVol.format.KML.LINK_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.whenParser_ = function(node, objectStack) {
  var gxTrackObject = /** @type {GVol.KMLGxTrackObject_} */
      (objectStack[objectStack.length - 1]);
  var whens = gxTrackObject.whens;
  var s = GVol.xml.getAllTextContent(node, false);
  var when = Date.parse(s);
  whens.push(isNaN(when) ? 0 : when);
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.DATA_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'displayName': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'value': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.EXTENDED_DATA_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'Data': GVol.format.KML.DataParser_,
      'SchemaData': GVol.format.KML.SchemaDataParser_
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.REGION_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'LatLonAltBox': GVol.format.KML.LatLonAltBoxParser_,
      'Lod': GVol.format.KML.LodParser_
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.LAT_LON_ALT_BOX_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'altitudeMode': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'minAltitude': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal),
      'maxAltitude': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal),
      'north': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal),
      'south': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal),
      'east': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal),
      'west': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.LOD_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'minLodPixels': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal),
      'maxLodPixels': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal),
      'minFadeExtent': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal),
      'maxFadeExtent': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.EXTRUDE_AND_ALTITUDE_MODE_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'extrude': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readBoGVolean),
      'tessellate': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readBoGVolean),
      'altitudeMode': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.FLAT_LINEAR_RING_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'coordinates': GVol.xml.makeReplacer(GVol.format.KML.readFlatCoordinates_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.FLAT_LINEAR_RINGS_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'innerBoundaryIs': GVol.format.KML.innerBoundaryIsParser_,
      'outerBoundaryIs': GVol.format.KML.outerBoundaryIsParser_
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.GX_TRACK_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'when': GVol.format.KML.whenParser_
    }, GVol.xml.makeStructureNS(
        GVol.format.KML.GX_NAMESPACE_URIS_, {
          'coord': GVol.format.KML.gxCoordParser_
        }));


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.GEOMETRY_FLAT_COORDINATES_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'coordinates': GVol.xml.makeReplacer(GVol.format.KML.readFlatCoordinates_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.ICON_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'href': GVol.xml.makeObjectPropertySetter(GVol.format.KML.readURI_)
    }, GVol.xml.makeStructureNS(
        GVol.format.KML.GX_NAMESPACE_URIS_, {
          'x': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal),
          'y': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal),
          'w': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal),
          'h': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal)
        }));


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.ICON_STYLE_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'Icon': GVol.xml.makeObjectPropertySetter(GVol.format.KML.readIcon_),
      'heading': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal),
      'hotSpot': GVol.xml.makeObjectPropertySetter(GVol.format.KML.readVec2_),
      'scale': GVol.xml.makeObjectPropertySetter(GVol.format.KML.readScale_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.INNER_BOUNDARY_IS_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'LinearRing': GVol.xml.makeReplacer(GVol.format.KML.readFlatLinearRing_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.LABEL_STYLE_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'cGVolor': GVol.xml.makeObjectPropertySetter(GVol.format.KML.readCGVolor_),
      'scale': GVol.xml.makeObjectPropertySetter(GVol.format.KML.readScale_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.LINE_STYLE_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'cGVolor': GVol.xml.makeObjectPropertySetter(GVol.format.KML.readCGVolor_),
      'width': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readDecimal)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.MULTI_GEOMETRY_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'LineString': GVol.xml.makeArrayPusher(GVol.format.KML.readLineString_),
      'LinearRing': GVol.xml.makeArrayPusher(GVol.format.KML.readLinearRing_),
      'MultiGeometry': GVol.xml.makeArrayPusher(GVol.format.KML.readMultiGeometry_),
      'Point': GVol.xml.makeArrayPusher(GVol.format.KML.readPoint_),
      'PGVolygon': GVol.xml.makeArrayPusher(GVol.format.KML.readPGVolygon_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.GX_MULTITRACK_GEOMETRY_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.GX_NAMESPACE_URIS_, {
      'Track': GVol.xml.makeArrayPusher(GVol.format.KML.readGxTrack_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.NETWORK_LINK_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'ExtendedData': GVol.format.KML.ExtendedDataParser_,
      'Region': GVol.format.KML.RegionParser_,
      'Link': GVol.format.KML.LinkParser_,
      'address': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'description': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'name': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'open': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readBoGVolean),
      'phoneNumber': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'visibility': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readBoGVolean)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.LINK_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'href': GVol.xml.makeObjectPropertySetter(GVol.format.KML.readURI_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.OUTER_BOUNDARY_IS_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'LinearRing': GVol.xml.makeReplacer(GVol.format.KML.readFlatLinearRing_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.PAIR_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'Style': GVol.xml.makeObjectPropertySetter(GVol.format.KML.readStyle_),
      'key': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'styleUrl': GVol.xml.makeObjectPropertySetter(GVol.format.KML.readURI_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.PLACEMARK_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'ExtendedData': GVol.format.KML.ExtendedDataParser_,
      'Region': GVol.format.KML.RegionParser_,
      'MultiGeometry': GVol.xml.makeObjectPropertySetter(
          GVol.format.KML.readMultiGeometry_, 'geometry'),
      'LineString': GVol.xml.makeObjectPropertySetter(
          GVol.format.KML.readLineString_, 'geometry'),
      'LinearRing': GVol.xml.makeObjectPropertySetter(
          GVol.format.KML.readLinearRing_, 'geometry'),
      'Point': GVol.xml.makeObjectPropertySetter(
          GVol.format.KML.readPoint_, 'geometry'),
      'PGVolygon': GVol.xml.makeObjectPropertySetter(
          GVol.format.KML.readPGVolygon_, 'geometry'),
      'Style': GVol.xml.makeObjectPropertySetter(GVol.format.KML.readStyle_),
      'StyleMap': GVol.format.KML.PlacemarkStyleMapParser_,
      'address': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'description': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'name': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'open': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readBoGVolean),
      'phoneNumber': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'styleUrl': GVol.xml.makeObjectPropertySetter(GVol.format.KML.readURI_),
      'visibility': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readBoGVolean)
    }, GVol.xml.makeStructureNS(
        GVol.format.KML.GX_NAMESPACE_URIS_, {
          'MultiTrack': GVol.xml.makeObjectPropertySetter(
              GVol.format.KML.readGxMultiTrack_, 'geometry'),
          'Track': GVol.xml.makeObjectPropertySetter(
              GVol.format.KML.readGxTrack_, 'geometry')
        }
    ));


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.POLY_STYLE_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'cGVolor': GVol.xml.makeObjectPropertySetter(GVol.format.KML.readCGVolor_),
      'fill': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readBoGVolean),
      'outline': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readBoGVolean)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.SCHEMA_DATA_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'SimpleData': GVol.format.KML.SimpleDataParser_
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.STYLE_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'IconStyle': GVol.format.KML.IconStyleParser_,
      'LabelStyle': GVol.format.KML.LabelStyleParser_,
      'LineStyle': GVol.format.KML.LineStyleParser_,
      'PGVolyStyle': GVol.format.KML.PGVolyStyleParser_
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.KML.STYLE_MAP_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'Pair': GVol.format.KML.PairDataParser_
    });


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<GVol.Feature>|undefined} Features.
 */
GVol.format.KML.prototype.readDocumentOrFGVolder_ = function(node, objectStack) {
  // FIXME use scope somehow
  var parsersNS = GVol.xml.makeStructureNS(
      GVol.format.KML.NAMESPACE_URIS_, {
        'Document': GVol.xml.makeArrayExtender(this.readDocumentOrFGVolder_, this),
        'FGVolder': GVol.xml.makeArrayExtender(this.readDocumentOrFGVolder_, this),
        'Placemark': GVol.xml.makeArrayPusher(this.readPlacemark_, this),
        'Style': this.readSharedStyle_.bind(this),
        'StyleMap': this.readSharedStyleMap_.bind(this)
      });
  /** @type {Array.<GVol.Feature>} */
  var features = GVol.xml.pushParseAndPop([], parsersNS, node, objectStack, this);
  if (features) {
    return features;
  } else {
    return undefined;
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {GVol.Feature|undefined} Feature.
 */
GVol.format.KML.prototype.readPlacemark_ = function(node, objectStack) {
  var object = GVol.xml.pushParseAndPop({'geometry': null},
      GVol.format.KML.PLACEMARK_PARSERS_, node, objectStack);
  if (!object) {
    return undefined;
  }
  var feature = new GVol.Feature();
  var id = node.getAttribute('id');
  if (id !== null) {
    feature.setId(id);
  }
  var options = /** @type {GVolx.format.ReadOptions} */ (objectStack[0]);

  var geometry = object['geometry'];
  if (geometry) {
    GVol.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 = GVol.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
 */
GVol.format.KML.prototype.readSharedStyle_ = function(node, objectStack) {
  var id = node.getAttribute('id');
  if (id !== null) {
    var style = GVol.format.KML.readStyle_(node, objectStack);
    if (style) {
      var styleUri;
      if (node.baseURI && node.baseURI !== 'about:blank') {
        var url = new URL('#' + id, node.baseURI);
        styleUri = url.href;
      } else {
        styleUri = '#' + id;
      }
      this.sharedStyles_[styleUri] = style;
    }
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.prototype.readSharedStyleMap_ = function(node, objectStack) {
  var id = node.getAttribute('id');
  if (id === null) {
    return;
  }
  var styleMapValue = GVol.format.KML.readStyleMapValue_(node, objectStack);
  if (!styleMapValue) {
    return;
  }
  var styleUri;
  if (node.baseURI && node.baseURI !== 'about:blank') {
    var url = new URL('#' + id, node.baseURI);
    styleUri = url.href;
  } else {
    styleUri = '#' + id;
  }
  this.sharedStyles_[styleUri] = styleMapValue;
};


/**
 * Read the first feature from a KML source. MultiGeometries are converted into
 * GeometryCGVollections if they are a mix of geometry types, and into MultiPoint/
 * MultiLineString/MultiPGVolygon if they are all of the same type.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {GVol.Feature} Feature.
 * @api
 */
GVol.format.KML.prototype.readFeature;


/**
 * @inheritDoc
 */
GVol.format.KML.prototype.readFeatureFromNode = function(node, opt_options) {
  if (!GVol.array.includes(GVol.format.KML.NAMESPACE_URIS_, node.namespaceURI)) {
    return null;
  }
  var feature = this.readPlacemark_(
      node, [this.getReadOptions(node, opt_options)]);
  if (feature) {
    return feature;
  } else {
    return null;
  }
};


/**
 * Read all features from a KML source. MultiGeometries are converted into
 * GeometryCGVollections if they are a mix of geometry types, and into MultiPoint/
 * MultiLineString/MultiPGVolygon if they are all of the same type.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<GVol.Feature>} Features.
 * @api
 */
GVol.format.KML.prototype.readFeatures;


/**
 * @inheritDoc
 */
GVol.format.KML.prototype.readFeaturesFromNode = function(node, opt_options) {
  if (!GVol.array.includes(GVol.format.KML.NAMESPACE_URIS_, node.namespaceURI)) {
    return [];
  }
  var features;
  var localName = node.localName;
  if (localName == 'Document' || localName == 'FGVolder') {
    features = this.readDocumentOrFGVolder_(
        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) {
        GVol.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
 */
GVol.format.KML.prototype.readName = function(source) {
  if (GVol.xml.isDocument(source)) {
    return this.readNameFromDocument(/** @type {Document} */ (source));
  } else if (GVol.xml.isNode(source)) {
    return this.readNameFromNode(/** @type {Node} */ (source));
  } else if (typeof source === 'string') {
    var doc = GVol.xml.parse(source);
    return this.readNameFromDocument(doc);
  } else {
    return undefined;
  }
};


/**
 * @param {Document} doc Document.
 * @return {string|undefined} Name.
 */
GVol.format.KML.prototype.readNameFromDocument = function(doc) {
  var n;
  for (n = doc.firstChild; n; n = n.nextSibling) {
    if (n.nodeType == Node.ELEMENT_NODE) {
      var name = this.readNameFromNode(n);
      if (name) {
        return name;
      }
    }
  }
  return undefined;
};


/**
 * @param {Node} node Node.
 * @return {string|undefined} Name.
 */
GVol.format.KML.prototype.readNameFromNode = function(node) {
  var n;
  for (n = node.firstElementChild; n; n = n.nextElementSibling) {
    if (GVol.array.includes(GVol.format.KML.NAMESPACE_URIS_, n.namespaceURI) &&
        n.localName == 'name') {
      return GVol.format.XSD.readString(n);
    }
  }
  for (n = node.firstElementChild; n; n = n.nextElementSibling) {
    var localName = n.localName;
    if (GVol.array.includes(GVol.format.KML.NAMESPACE_URIS_, n.namespaceURI) &&
        (localName == 'Document' ||
         localName == 'FGVolder' ||
         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
 */
GVol.format.KML.prototype.readNetworkLinks = function(source) {
  var networkLinks = [];
  if (GVol.xml.isDocument(source)) {
    GVol.array.extend(networkLinks, this.readNetworkLinksFromDocument(
        /** @type {Document} */ (source)));
  } else if (GVol.xml.isNode(source)) {
    GVol.array.extend(networkLinks, this.readNetworkLinksFromNode(
        /** @type {Node} */ (source)));
  } else if (typeof source === 'string') {
    var doc = GVol.xml.parse(source);
    GVol.array.extend(networkLinks, this.readNetworkLinksFromDocument(doc));
  }
  return networkLinks;
};


/**
 * @param {Document} doc Document.
 * @return {Array.<Object>} Network links.
 */
GVol.format.KML.prototype.readNetworkLinksFromDocument = function(doc) {
  var n, networkLinks = [];
  for (n = doc.firstChild; n; n = n.nextSibling) {
    if (n.nodeType == Node.ELEMENT_NODE) {
      GVol.array.extend(networkLinks, this.readNetworkLinksFromNode(n));
    }
  }
  return networkLinks;
};


/**
 * @param {Node} node Node.
 * @return {Array.<Object>} Network links.
 */
GVol.format.KML.prototype.readNetworkLinksFromNode = function(node) {
  var n, networkLinks = [];
  for (n = node.firstElementChild; n; n = n.nextElementSibling) {
    if (GVol.array.includes(GVol.format.KML.NAMESPACE_URIS_, n.namespaceURI) &&
        n.localName == 'NetworkLink') {
      var obj = GVol.xml.pushParseAndPop({}, GVol.format.KML.NETWORK_LINK_PARSERS_,
          n, []);
      networkLinks.push(obj);
    }
  }
  for (n = node.firstElementChild; n; n = n.nextElementSibling) {
    var localName = n.localName;
    if (GVol.array.includes(GVol.format.KML.NAMESPACE_URIS_, n.namespaceURI) &&
        (localName == 'Document' ||
         localName == 'FGVolder' ||
         localName == 'kml')) {
      GVol.array.extend(networkLinks, this.readNetworkLinksFromNode(n));
    }
  }
  return networkLinks;
};


/**
 * Read the regions of the KML.
 *
 * @param {Document|Node|string} source Source.
 * @return {Array.<Object>} Regions.
 * @api
 */
GVol.format.KML.prototype.readRegion = function(source) {
  var regions = [];
  if (GVol.xml.isDocument(source)) {
    GVol.array.extend(regions, this.readRegionFromDocument(
        /** @type {Document} */ (source)));
  } else if (GVol.xml.isNode(source)) {
    GVol.array.extend(regions, this.readRegionFromNode(
        /** @type {Node} */ (source)));
  } else if (typeof source === 'string') {
    var doc = GVol.xml.parse(source);
    GVol.array.extend(regions, this.readRegionFromDocument(doc));
  }
  return regions;
};


/**
 * @param {Document} doc Document.
 * @return {Array.<Object>} Region.
 */
GVol.format.KML.prototype.readRegionFromDocument = function(doc) {
  var n, regions = [];
  for (n = doc.firstChild; n; n = n.nextSibling) {
    if (n.nodeType == Node.ELEMENT_NODE) {
      GVol.array.extend(regions, this.readRegionFromNode(n));
    }
  }
  return regions;
};


/**
 * @param {Node} node Node.
 * @return {Array.<Object>} Region.
 * @api
 */
GVol.format.KML.prototype.readRegionFromNode = function(node) {
  var n, regions = [];
  for (n = node.firstElementChild; n; n = n.nextElementSibling) {
    if (GVol.array.includes(GVol.format.KML.NAMESPACE_URIS_, n.namespaceURI) &&
        n.localName == 'Region') {
      var obj = GVol.xml.pushParseAndPop({}, GVol.format.KML.REGION_PARSERS_,
          n, []);
      regions.push(obj);
    }
  }
  for (n = node.firstElementChild; n; n = n.nextElementSibling) {
    var localName = n.localName;
    if (GVol.array.includes(GVol.format.KML.NAMESPACE_URIS_, n.namespaceURI) &&
        (localName == 'Document' ||
         localName == 'FGVolder' ||
         localName == 'kml')) {
      GVol.array.extend(regions, this.readRegionFromNode(n));
    }
  }
  return regions;
};


/**
 * Read the projection from a KML source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @return {GVol.proj.Projection} Projection.
 * @api
 */
GVol.format.KML.prototype.readProjection;


/**
 * @param {Node} node Node to append a TextNode with the cGVolor to.
 * @param {GVol.CGVolor|string} cGVolor CGVolor.
 * @private
 */
GVol.format.KML.writeCGVolorTextNode_ = function(node, cGVolor) {
  var rgba = GVol.cGVolor.asArray(cGVolor);
  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;
  }
  GVol.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
 */
GVol.format.KML.writeCoordinatesTextNode_ = function(node, coordinates, objectStack) {
  var context = objectStack[objectStack.length - 1];

  var layout = context['layout'];
  var stride = context['stride'];

  var dimension;
  if (layout == GVol.geom.GeometryLayout.XY ||
      layout == GVol.geom.GeometryLayout.XYM) {
    dimension = 2;
  } else if (layout == GVol.geom.GeometryLayout.XYZ ||
      layout == GVol.geom.GeometryLayout.XYZM) {
    dimension = 3;
  } else {
    GVol.asserts.assert(false, 34); // Invalid 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];
      }
    }
  }
  GVol.format.XSD.writeStringTextNode(node, text);
};


/**
 * @param {Node} node Node.
 * @param {{name: *, value: *}} pair Name value pair.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.writeDataNode_ = function(node, pair, objectStack) {
  node.setAttribute('name', pair.name);
  var /** @type {GVol.XmlNodeStackItem} */ context = {node: node};
  var value = pair.value;

  if (typeof value == 'object') {
    if (value !== null && value.displayName) {
      GVol.xml.pushSerializeAndPop(context, GVol.format.KML.EXTENDEDDATA_NODE_SERIALIZERS_,
          GVol.xml.OBJECT_PROPERTY_NODE_FACTORY, [value.displayName], objectStack, ['displayName']);
    }

    if (value !== null && value.value) {
      GVol.xml.pushSerializeAndPop(context, GVol.format.KML.EXTENDEDDATA_NODE_SERIALIZERS_,
          GVol.xml.OBJECT_PROPERTY_NODE_FACTORY, [value.value], objectStack, ['value']);
    }
  } else {
    GVol.xml.pushSerializeAndPop(context, GVol.format.KML.EXTENDEDDATA_NODE_SERIALIZERS_,
        GVol.xml.OBJECT_PROPERTY_NODE_FACTORY, [value], objectStack, ['value']);
  }
};


/**
 * @param {Node} node Node to append a TextNode with the name to.
 * @param {string} name DisplayName.
 * @private
 */
GVol.format.KML.writeDataNodeName_ = function(node, name) {
  GVol.format.XSD.writeCDATASection(node, name);
};


/**
 * @param {Node} node Node to append a CDATA Section with the value to.
 * @param {string} value Value.
 * @private
 */
GVol.format.KML.writeDataNodeValue_ = function(node, value) {
  GVol.format.XSD.writeStringTextNode(node, value);
};


/**
 * @param {Node} node Node.
 * @param {Array.<GVol.Feature>} features Features.
 * @param {Array.<*>} objectStack Object stack.
 * @this {GVol.format.KML}
 * @private
 */
GVol.format.KML.writeDocument_ = function(node, features, objectStack) {
  var /** @type {GVol.XmlNodeStackItem} */ context = {node: node};
  GVol.xml.pushSerializeAndPop(context, GVol.format.KML.DOCUMENT_SERIALIZERS_,
      GVol.format.KML.DOCUMENT_NODE_FACTORY_, features, objectStack, undefined,
      this);
};


/**
 * @param {Node} node Node.
 * @param {{names: Array<string>, values: (Array<*>)}} namesAndValues Names and values.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.writeExtendedData_ = function(node, namesAndValues, objectStack) {
  var /** @type {GVol.XmlNodeStackItem} */ context = {node: node};
  var names = namesAndValues.names, values = namesAndValues.values;
  var length = names.length;

  for (var i = 0; i < length; i++) {
    GVol.xml.pushSerializeAndPop(context, GVol.format.KML.EXTENDEDDATA_NODE_SERIALIZERS_,
        GVol.format.KML.DATA_NODE_FACTORY_, [{name: names[i], value: values[i]}], objectStack);
  }
};


/**
 * @param {Node} node Node.
 * @param {Object} icon Icon object.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.writeIcon_ = function(node, icon, objectStack) {
  var /** @type {GVol.XmlNodeStackItem} */ context = {node: node};
  var parentNode = objectStack[objectStack.length - 1].node;
  var orderedKeys = GVol.format.KML.ICON_SEQUENCE_[parentNode.namespaceURI];
  var values = GVol.xml.makeSequence(icon, orderedKeys);
  GVol.xml.pushSerializeAndPop(context,
      GVol.format.KML.ICON_SERIALIZERS_, GVol.xml.OBJECT_PROPERTY_NODE_FACTORY,
      values, objectStack, orderedKeys);
  orderedKeys =
      GVol.format.KML.ICON_SEQUENCE_[GVol.format.KML.GX_NAMESPACE_URIS_[0]];
  values = GVol.xml.makeSequence(icon, orderedKeys);
  GVol.xml.pushSerializeAndPop(context, GVol.format.KML.ICON_SERIALIZERS_,
      GVol.format.KML.GX_NODE_FACTORY_, values, objectStack, orderedKeys);
};


/**
 * @param {Node} node Node.
 * @param {GVol.style.Icon} style Icon style.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.writeIconStyle_ = function(node, style, objectStack) {
  var /** @type {GVol.XmlNodeStackItem} */ 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] !== size[0] / 2 || anchor[1] !== size[1] / 2)) {
      var /** @type {GVol.KMLVec2_} */ hotSpot = {
        x: anchor[0],
        xunits: GVol.style.IconAnchorUnits.PIXELS,
        y: size[1] - anchor[1],
        yunits: GVol.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 = GVol.format.KML.ICON_STYLE_SEQUENCE_[parentNode.namespaceURI];
  var values = GVol.xml.makeSequence(properties, orderedKeys);
  GVol.xml.pushSerializeAndPop(context, GVol.format.KML.ICON_STYLE_SERIALIZERS_,
      GVol.xml.OBJECT_PROPERTY_NODE_FACTORY, values, objectStack, orderedKeys);
};


/**
 * @param {Node} node Node.
 * @param {GVol.style.Text} style style.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.writeLabelStyle_ = function(node, style, objectStack) {
  var /** @type {GVol.XmlNodeStackItem} */ context = {node: node};
  var properties = {};
  var fill = style.getFill();
  if (fill) {
    properties['cGVolor'] = fill.getCGVolor();
  }
  var scale = style.getScale();
  if (scale && scale !== 1) {
    properties['scale'] = scale;
  }
  var parentNode = objectStack[objectStack.length - 1].node;
  var orderedKeys =
      GVol.format.KML.LABEL_STYLE_SEQUENCE_[parentNode.namespaceURI];
  var values = GVol.xml.makeSequence(properties, orderedKeys);
  GVol.xml.pushSerializeAndPop(context, GVol.format.KML.LABEL_STYLE_SERIALIZERS_,
      GVol.xml.OBJECT_PROPERTY_NODE_FACTORY, values, objectStack, orderedKeys);
};


/**
 * @param {Node} node Node.
 * @param {GVol.style.Stroke} style style.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.writeLineStyle_ = function(node, style, objectStack) {
  var /** @type {GVol.XmlNodeStackItem} */ context = {node: node};
  var properties = {
    'cGVolor': style.getCGVolor(),
    'width': style.getWidth()
  };
  var parentNode = objectStack[objectStack.length - 1].node;
  var orderedKeys = GVol.format.KML.LINE_STYLE_SEQUENCE_[parentNode.namespaceURI];
  var values = GVol.xml.makeSequence(properties, orderedKeys);
  GVol.xml.pushSerializeAndPop(context, GVol.format.KML.LINE_STYLE_SERIALIZERS_,
      GVol.xml.OBJECT_PROPERTY_NODE_FACTORY, values, objectStack, orderedKeys);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.writeMultiGeometry_ = function(node, geometry, objectStack) {
  /** @type {GVol.XmlNodeStackItem} */
  var context = {node: node};
  var type = geometry.getType();
  /** @type {Array.<GVol.geom.Geometry>} */
  var geometries;
  /** @type {function(*, Array.<*>, string=): (Node|undefined)} */
  var factory;
  if (type == GVol.geom.GeometryType.GEOMETRY_COLLECTION) {
    geometries = /** @type {GVol.geom.GeometryCGVollection} */ (geometry).getGeometries();
    factory = GVol.format.KML.GEOMETRY_NODE_FACTORY_;
  } else if (type == GVol.geom.GeometryType.MULTI_POINT) {
    geometries = /** @type {GVol.geom.MultiPoint} */ (geometry).getPoints();
    factory = GVol.format.KML.POINT_NODE_FACTORY_;
  } else if (type == GVol.geom.GeometryType.MULTI_LINE_STRING) {
    geometries =
        (/** @type {GVol.geom.MultiLineString} */ (geometry)).getLineStrings();
    factory = GVol.format.KML.LINE_STRING_NODE_FACTORY_;
  } else if (type == GVol.geom.GeometryType.MULTI_POLYGON) {
    geometries =
        (/** @type {GVol.geom.MultiPGVolygon} */ (geometry)).getPGVolygons();
    factory = GVol.format.KML.POLYGON_NODE_FACTORY_;
  } else {
    GVol.asserts.assert(false, 39); // Unknown geometry type
  }
  GVol.xml.pushSerializeAndPop(context,
      GVol.format.KML.MULTI_GEOMETRY_SERIALIZERS_, factory,
      geometries, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.LinearRing} linearRing Linear ring.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.writeBoundaryIs_ = function(node, linearRing, objectStack) {
  var /** @type {GVol.XmlNodeStackItem} */ context = {node: node};
  GVol.xml.pushSerializeAndPop(context,
      GVol.format.KML.BOUNDARY_IS_SERIALIZERS_,
      GVol.format.KML.LINEAR_RING_NODE_FACTORY_, [linearRing], objectStack);
};


/**
 * FIXME currently we do serialize arbitrary/custom feature properties
 * (ExtendedData).
 * @param {Node} node Node.
 * @param {GVol.Feature} feature Feature.
 * @param {Array.<*>} objectStack Object stack.
 * @this {GVol.format.KML}
 * @private
 */
GVol.format.KML.writePlacemark_ = function(node, feature, objectStack) {
  var /** @type {GVol.XmlNodeStackItem} */ 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();

  // don't export these to ExtendedData
  var filter = {'address': 1, 'description': 1, 'name': 1, 'open': 1,
    'phoneNumber': 1, 'styleUrl': 1, 'visibility': 1};
  filter[feature.getGeometryName()] = 1;
  var keys = Object.keys(properties || {}).sort().filter(function(v) {
    return !filter[v];
  });

  if (keys.length > 0) {
    var sequence = GVol.xml.makeSequence(properties, keys);
    var namesAndValues = {names: keys, values: sequence};
    GVol.xml.pushSerializeAndPop(context, GVol.format.KML.PLACEMARK_SERIALIZERS_,
        GVol.format.KML.EXTENDEDDATA_NODE_FACTORY_, [namesAndValues], objectStack);
  }

  var styleFunction = feature.getStyleFunction();
  if (styleFunction) {
    // FIXME the styles returned by the style function are supposed to be
    // resGVolution-independent here
    var styles = styleFunction.call(feature, 0);
    if (styles) {
      var style = Array.isArray(styles) ? styles[0] : styles;
      if (this.writeStyles_) {
        properties['Style'] = style;
      }
      var textStyle = style.getText();
      if (textStyle) {
        properties['name'] = textStyle.getText();
      }
    }
  }
  var parentNode = objectStack[objectStack.length - 1].node;
  var orderedKeys = GVol.format.KML.PLACEMARK_SEQUENCE_[parentNode.namespaceURI];
  var values = GVol.xml.makeSequence(properties, orderedKeys);
  GVol.xml.pushSerializeAndPop(context, GVol.format.KML.PLACEMARK_SERIALIZERS_,
      GVol.xml.OBJECT_PROPERTY_NODE_FACTORY, values, objectStack, orderedKeys);

  // serialize geometry
  var options = /** @type {GVolx.format.WriteOptions} */ (objectStack[0]);
  var geometry = feature.getGeometry();
  if (geometry) {
    geometry =
        GVol.format.Feature.transformWithOptions(geometry, true, options);
  }
  GVol.xml.pushSerializeAndPop(context, GVol.format.KML.PLACEMARK_SERIALIZERS_,
      GVol.format.KML.GEOMETRY_NODE_FACTORY_, [geometry], objectStack);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.SimpleGeometry} geometry Geometry.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.writePrimitiveGeometry_ = function(node, geometry, objectStack) {
  var flatCoordinates = geometry.getFlatCoordinates();
  var /** @type {GVol.XmlNodeStackItem} */ context = {node: node};
  context['layout'] = geometry.getLayout();
  context['stride'] = geometry.getStride();

  // serialize properties (properties unknown to KML are not serialized)
  var properties = geometry.getProperties();
  properties.coordinates = flatCoordinates;

  var parentNode = objectStack[objectStack.length - 1].node;
  var orderedKeys = GVol.format.KML.PRIMITIVE_GEOMETRY_SEQUENCE_[parentNode.namespaceURI];
  var values = GVol.xml.makeSequence(properties, orderedKeys);
  GVol.xml.pushSerializeAndPop(context, GVol.format.KML.PRIMITIVE_GEOMETRY_SERIALIZERS_,
      GVol.xml.OBJECT_PROPERTY_NODE_FACTORY, values, objectStack, orderedKeys);
};


/**
 * @param {Node} node Node.
 * @param {GVol.geom.PGVolygon} pGVolygon PGVolygon.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.writePGVolygon_ = function(node, pGVolygon, objectStack) {
  var linearRings = pGVolygon.getLinearRings();
  var outerRing = linearRings.shift();
  var /** @type {GVol.XmlNodeStackItem} */ context = {node: node};
  // inner rings
  GVol.xml.pushSerializeAndPop(context,
      GVol.format.KML.POLYGON_SERIALIZERS_,
      GVol.format.KML.INNER_BOUNDARY_NODE_FACTORY_,
      linearRings, objectStack);
  // outer ring
  GVol.xml.pushSerializeAndPop(context,
      GVol.format.KML.POLYGON_SERIALIZERS_,
      GVol.format.KML.OUTER_BOUNDARY_NODE_FACTORY_,
      [outerRing], objectStack);
};


/**
 * @param {Node} node Node.
 * @param {GVol.style.Fill} style Style.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.writePGVolyStyle_ = function(node, style, objectStack) {
  var /** @type {GVol.XmlNodeStackItem} */ context = {node: node};
  GVol.xml.pushSerializeAndPop(context, GVol.format.KML.POLY_STYLE_SERIALIZERS_,
      GVol.format.KML.COLOR_NODE_FACTORY_, [style.getCGVolor()], objectStack);
};


/**
 * @param {Node} node Node to append a TextNode with the scale to.
 * @param {number|undefined} scale Scale.
 * @private
 */
GVol.format.KML.writeScaleTextNode_ = function(node, scale) {
  // the Math is to remove any excess decimals created by float arithmetic
  GVol.format.XSD.writeDecimalTextNode(node,
      Math.round(scale * 1e6) / 1e6);
};


/**
 * @param {Node} node Node.
 * @param {GVol.style.Style} style Style.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.KML.writeStyle_ = function(node, style, objectStack) {
  var /** @type {GVol.XmlNodeStackItem} */ context = {node: node};
  var properties = {};
  var fillStyle = style.getFill();
  var strokeStyle = style.getStroke();
  var imageStyle = style.getImage();
  var textStyle = style.getText();
  if (imageStyle instanceof GVol.style.Icon) {
    properties['IconStyle'] = imageStyle;
  }
  if (textStyle) {
    properties['LabelStyle'] = textStyle;
  }
  if (strokeStyle) {
    properties['LineStyle'] = strokeStyle;
  }
  if (fillStyle) {
    properties['PGVolyStyle'] = fillStyle;
  }
  var parentNode = objectStack[objectStack.length - 1].node;
  var orderedKeys = GVol.format.KML.STYLE_SEQUENCE_[parentNode.namespaceURI];
  var values = GVol.xml.makeSequence(properties, orderedKeys);
  GVol.xml.pushSerializeAndPop(context, GVol.format.KML.STYLE_SERIALIZERS_,
      GVol.xml.OBJECT_PROPERTY_NODE_FACTORY, values, objectStack, orderedKeys);
};


/**
 * @param {Node} node Node to append a TextNode with the Vec2 to.
 * @param {GVol.KMLVec2_} vec2 Vec2.
 * @private
 */
GVol.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
 */
GVol.format.KML.KML_SEQUENCE_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, [
      'Document', 'Placemark'
    ]);


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.KML.KML_SERIALIZERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'Document': GVol.xml.makeChildAppender(GVol.format.KML.writeDocument_),
      'Placemark': GVol.xml.makeChildAppender(GVol.format.KML.writePlacemark_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.KML.DOCUMENT_SERIALIZERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'Placemark': GVol.xml.makeChildAppender(GVol.format.KML.writePlacemark_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.KML.EXTENDEDDATA_NODE_SERIALIZERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'Data': GVol.xml.makeChildAppender(GVol.format.KML.writeDataNode_),
      'value': GVol.xml.makeChildAppender(GVol.format.KML.writeDataNodeValue_),
      'displayName': GVol.xml.makeChildAppender(GVol.format.KML.writeDataNodeName_)
    });


/**
 * @const
 * @type {Object.<string, string>}
 * @private
 */
GVol.format.KML.GEOMETRY_TYPE_TO_NODENAME_ = {
  'Point': 'Point',
  'LineString': 'LineString',
  'LinearRing': 'LinearRing',
  'PGVolygon': 'PGVolygon',
  'MultiPoint': 'MultiGeometry',
  'MultiLineString': 'MultiGeometry',
  'MultiPGVolygon': 'MultiGeometry',
  'GeometryCGVollection': 'MultiGeometry'
};

/**
 * @const
 * @type {Object.<string, Array.<string>>}
 * @private
 */
GVol.format.KML.ICON_SEQUENCE_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, [
      'href'
    ],
    GVol.xml.makeStructureNS(GVol.format.KML.GX_NAMESPACE_URIS_, [
      'x', 'y', 'w', 'h'
    ]));


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.KML.ICON_SERIALIZERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'href': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode)
    }, GVol.xml.makeStructureNS(
        GVol.format.KML.GX_NAMESPACE_URIS_, {
          'x': GVol.xml.makeChildAppender(GVol.format.XSD.writeDecimalTextNode),
          'y': GVol.xml.makeChildAppender(GVol.format.XSD.writeDecimalTextNode),
          'w': GVol.xml.makeChildAppender(GVol.format.XSD.writeDecimalTextNode),
          'h': GVol.xml.makeChildAppender(GVol.format.XSD.writeDecimalTextNode)
        }));


/**
 * @const
 * @type {Object.<string, Array.<string>>}
 * @private
 */
GVol.format.KML.ICON_STYLE_SEQUENCE_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, [
      'scale', 'heading', 'Icon', 'hotSpot'
    ]);


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.KML.ICON_STYLE_SERIALIZERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'Icon': GVol.xml.makeChildAppender(GVol.format.KML.writeIcon_),
      'heading': GVol.xml.makeChildAppender(GVol.format.XSD.writeDecimalTextNode),
      'hotSpot': GVol.xml.makeChildAppender(GVol.format.KML.writeVec2_),
      'scale': GVol.xml.makeChildAppender(GVol.format.KML.writeScaleTextNode_)
    });


/**
 * @const
 * @type {Object.<string, Array.<string>>}
 * @private
 */
GVol.format.KML.LABEL_STYLE_SEQUENCE_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, [
      'cGVolor', 'scale'
    ]);


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.KML.LABEL_STYLE_SERIALIZERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'cGVolor': GVol.xml.makeChildAppender(GVol.format.KML.writeCGVolorTextNode_),
      'scale': GVol.xml.makeChildAppender(GVol.format.KML.writeScaleTextNode_)
    });


/**
 * @const
 * @type {Object.<string, Array.<string>>}
 * @private
 */
GVol.format.KML.LINE_STYLE_SEQUENCE_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, [
      'cGVolor', 'width'
    ]);


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.KML.LINE_STYLE_SERIALIZERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'cGVolor': GVol.xml.makeChildAppender(GVol.format.KML.writeCGVolorTextNode_),
      'width': GVol.xml.makeChildAppender(GVol.format.XSD.writeDecimalTextNode)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.KML.BOUNDARY_IS_SERIALIZERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'LinearRing': GVol.xml.makeChildAppender(
          GVol.format.KML.writePrimitiveGeometry_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.KML.MULTI_GEOMETRY_SERIALIZERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'LineString': GVol.xml.makeChildAppender(
          GVol.format.KML.writePrimitiveGeometry_),
      'Point': GVol.xml.makeChildAppender(
          GVol.format.KML.writePrimitiveGeometry_),
      'PGVolygon': GVol.xml.makeChildAppender(GVol.format.KML.writePGVolygon_),
      'GeometryCGVollection': GVol.xml.makeChildAppender(
          GVol.format.KML.writeMultiGeometry_)
    });


/**
 * @const
 * @type {Object.<string, Array.<string>>}
 * @private
 */
GVol.format.KML.PLACEMARK_SEQUENCE_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, [
      'name', 'open', 'visibility', 'address', 'phoneNumber', 'description',
      'styleUrl', 'Style'
    ]);


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.KML.PLACEMARK_SERIALIZERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'ExtendedData': GVol.xml.makeChildAppender(
          GVol.format.KML.writeExtendedData_),
      'MultiGeometry': GVol.xml.makeChildAppender(
          GVol.format.KML.writeMultiGeometry_),
      'LineString': GVol.xml.makeChildAppender(
          GVol.format.KML.writePrimitiveGeometry_),
      'LinearRing': GVol.xml.makeChildAppender(
          GVol.format.KML.writePrimitiveGeometry_),
      'Point': GVol.xml.makeChildAppender(
          GVol.format.KML.writePrimitiveGeometry_),
      'PGVolygon': GVol.xml.makeChildAppender(GVol.format.KML.writePGVolygon_),
      'Style': GVol.xml.makeChildAppender(GVol.format.KML.writeStyle_),
      'address': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'description': GVol.xml.makeChildAppender(
          GVol.format.XSD.writeStringTextNode),
      'name': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'open': GVol.xml.makeChildAppender(GVol.format.XSD.writeBoGVoleanTextNode),
      'phoneNumber': GVol.xml.makeChildAppender(
          GVol.format.XSD.writeStringTextNode),
      'styleUrl': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'visibility': GVol.xml.makeChildAppender(
          GVol.format.XSD.writeBoGVoleanTextNode)
    });


/**
 * @const
 * @type {Object.<string, Array.<string>>}
 * @private
 */
GVol.format.KML.PRIMITIVE_GEOMETRY_SEQUENCE_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, [
      'extrude', 'tessellate', 'altitudeMode', 'coordinates'
    ]);


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.KML.PRIMITIVE_GEOMETRY_SERIALIZERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'extrude': GVol.xml.makeChildAppender(GVol.format.XSD.writeBoGVoleanTextNode),
      'tessellate': GVol.xml.makeChildAppender(GVol.format.XSD.writeBoGVoleanTextNode),
      'altitudeMode': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode),
      'coordinates': GVol.xml.makeChildAppender(
          GVol.format.KML.writeCoordinatesTextNode_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.KML.POLYGON_SERIALIZERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'outerBoundaryIs': GVol.xml.makeChildAppender(
          GVol.format.KML.writeBoundaryIs_),
      'innerBoundaryIs': GVol.xml.makeChildAppender(
          GVol.format.KML.writeBoundaryIs_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.KML.POLY_STYLE_SERIALIZERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'cGVolor': GVol.xml.makeChildAppender(GVol.format.KML.writeCGVolorTextNode_)
    });


/**
 * @const
 * @type {Object.<string, Array.<string>>}
 * @private
 */
GVol.format.KML.STYLE_SEQUENCE_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, [
      'IconStyle', 'LabelStyle', 'LineStyle', 'PGVolyStyle'
    ]);


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.KML.STYLE_SERIALIZERS_ = GVol.xml.makeStructureNS(
    GVol.format.KML.NAMESPACE_URIS_, {
      'IconStyle': GVol.xml.makeChildAppender(GVol.format.KML.writeIconStyle_),
      'LabelStyle': GVol.xml.makeChildAppender(GVol.format.KML.writeLabelStyle_),
      'LineStyle': GVol.xml.makeChildAppender(GVol.format.KML.writeLineStyle_),
      'PGVolyStyle': GVol.xml.makeChildAppender(GVol.format.KML.writePGVolyStyle_)
    });


/**
 * @const
 * @param {*} value Value.
 * @param {Array.<*>} objectStack Object stack.
 * @param {string=} opt_nodeName Node name.
 * @return {Node|undefined} Node.
 * @private
 */
GVol.format.KML.GX_NODE_FACTORY_ = function(value, objectStack, opt_nodeName) {
  return GVol.xml.createElementNS(GVol.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
 */
GVol.format.KML.DOCUMENT_NODE_FACTORY_ = function(value, objectStack,
    opt_nodeName) {
  var parentNode = objectStack[objectStack.length - 1].node;
  return GVol.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
 */
GVol.format.KML.GEOMETRY_NODE_FACTORY_ = function(value, objectStack,
    opt_nodeName) {
  if (value) {
    var parentNode = objectStack[objectStack.length - 1].node;
    return GVol.xml.createElementNS(parentNode.namespaceURI,
        GVol.format.KML.GEOMETRY_TYPE_TO_NODENAME_[/** @type {GVol.geom.Geometry} */ (value).getType()]);
  }
};


/**
 * A factory for creating coordinates nodes.
 * @const
 * @type {function(*, Array.<*>, string=): (Node|undefined)}
 * @private
 */
GVol.format.KML.COLOR_NODE_FACTORY_ = GVol.xml.makeSimpleNodeFactory('cGVolor');


/**
 * A factory for creating Data nodes.
 * @const
 * @type {function(*, Array.<*>): (Node|undefined)}
 * @private
 */
GVol.format.KML.DATA_NODE_FACTORY_ =
    GVol.xml.makeSimpleNodeFactory('Data');


/**
 * A factory for creating ExtendedData nodes.
 * @const
 * @type {function(*, Array.<*>): (Node|undefined)}
 * @private
 */
GVol.format.KML.EXTENDEDDATA_NODE_FACTORY_ =
    GVol.xml.makeSimpleNodeFactory('ExtendedData');


/**
 * A factory for creating innerBoundaryIs nodes.
 * @const
 * @type {function(*, Array.<*>, string=): (Node|undefined)}
 * @private
 */
GVol.format.KML.INNER_BOUNDARY_NODE_FACTORY_ =
    GVol.xml.makeSimpleNodeFactory('innerBoundaryIs');


/**
 * A factory for creating Point nodes.
 * @const
 * @type {function(*, Array.<*>, string=): (Node|undefined)}
 * @private
 */
GVol.format.KML.POINT_NODE_FACTORY_ =
    GVol.xml.makeSimpleNodeFactory('Point');


/**
 * A factory for creating LineString nodes.
 * @const
 * @type {function(*, Array.<*>, string=): (Node|undefined)}
 * @private
 */
GVol.format.KML.LINE_STRING_NODE_FACTORY_ =
    GVol.xml.makeSimpleNodeFactory('LineString');


/**
 * A factory for creating LinearRing nodes.
 * @const
 * @type {function(*, Array.<*>, string=): (Node|undefined)}
 * @private
 */
GVol.format.KML.LINEAR_RING_NODE_FACTORY_ =
    GVol.xml.makeSimpleNodeFactory('LinearRing');


/**
 * A factory for creating PGVolygon nodes.
 * @const
 * @type {function(*, Array.<*>, string=): (Node|undefined)}
 * @private
 */
GVol.format.KML.POLYGON_NODE_FACTORY_ =
    GVol.xml.makeSimpleNodeFactory('PGVolygon');


/**
 * A factory for creating outerBoundaryIs nodes.
 * @const
 * @type {function(*, Array.<*>, string=): (Node|undefined)}
 * @private
 */
GVol.format.KML.OUTER_BOUNDARY_NODE_FACTORY_ =
    GVol.xml.makeSimpleNodeFactory('outerBoundaryIs');


/**
 * Encode an array of features in the KML format. GeometryCGVollections, MultiPoints,
 * MultiLineStrings, and MultiPGVolygons are output as MultiGeometries.
 *
 * @function
 * @param {Array.<GVol.Feature>} features Features.
 * @param {GVolx.format.WriteOptions=} opt_options Options.
 * @return {string} Result.
 * @api
 */
GVol.format.KML.prototype.writeFeatures;


/**
 * Encode an array of features in the KML format as an XML node. GeometryCGVollections,
 * MultiPoints, MultiLineStrings, and MultiPGVolygons are output as MultiGeometries.
 *
 * @param {Array.<GVol.Feature>} features Features.
 * @param {GVolx.format.WriteOptions=} opt_options Options.
 * @return {Node} Node.
 * @override
 * @api
 */
GVol.format.KML.prototype.writeFeaturesNode = function(features, opt_options) {
  opt_options = this.adaptOptions(opt_options);
  var kml = GVol.xml.createElementNS(GVol.format.KML.NAMESPACE_URIS_[4], 'kml');
  var xmlnsUri = 'http://www.w3.org/2000/xmlns/';
  var xmlSchemaInstanceUri = 'http://www.w3.org/2001/XMLSchema-instance';
  GVol.xml.setAttributeNS(kml, xmlnsUri, 'xmlns:gx',
      GVol.format.KML.GX_NAMESPACE_URIS_[0]);
  GVol.xml.setAttributeNS(kml, xmlnsUri, 'xmlns:xsi', xmlSchemaInstanceUri);
  GVol.xml.setAttributeNS(kml, xmlSchemaInstanceUri, 'xsi:schemaLocation',
      GVol.format.KML.SCHEMA_LOCATION_);

  var /** @type {GVol.XmlNodeStackItem} */ context = {node: kml};
  var properties = {};
  if (features.length > 1) {
    properties['Document'] = features;
  } else if (features.length == 1) {
    properties['Placemark'] = features[0];
  }
  var orderedKeys = GVol.format.KML.KML_SEQUENCE_[kml.namespaceURI];
  var values = GVol.xml.makeSequence(properties, orderedKeys);
  GVol.xml.pushSerializeAndPop(context, GVol.format.KML.KML_SERIALIZERS_,
      GVol.xml.OBJECT_PROPERTY_NODE_FACTORY, values, [opt_options], orderedKeys,
      this);
  return kml;
};


/**
 * @fileoverview
 * @suppress {accessContrGVols, ambiguousFunctionDecl, checkDebuggerStatement, checkRegExp, checkTypes, checkVars, const, constantProperty, deprecated, duplicate, es5Strict, fileoverviewTags, missingProperties, nonStandardJsDocs, strictModuleDepCheck, suspiciousCode, undefinedNames, undefinedVars, unknownDefines, unusedLocalVariables, uselessCode, visibility}
 */
goog.provide('GVol.ext.PBF');

/** @typedef {function(*)} */
GVol.ext.PBF = function() {};

(function() {(function (exports) {
'use strict';

var 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)
};
var 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;
};
var ieee754 = {
	read: read,
	write: write
};

var pbf = Pbf;
function Pbf(buf) {
    this.buf = ArrayBuffer.isView && ArrayBuffer.isView(buf) ? buf : new Uint8Array(buf || 0);
    this.pos = 0;
    this.type = 0;
    this.length = this.buf.length;
}
Pbf.Varint  = 0;
Pbf.Fixed64 = 1;
Pbf.Bytes   = 2;
Pbf.Fixed32 = 5;
var SHIFT_LEFT_32 = (1 << 16) * (1 << 16);
var SHIFT_RIGHT_32 = 1 / SHIFT_LEFT_32;
Pbf.prototype = {
    destroy: function() {
        this.buf = null;
    },
    readFields: function(readField, result, end) {
        end = end || this.length;
        while (this.pos < end) {
            var val = this.readVarint(),
                tag = val >> 3,
                startPos = this.pos;
            this.type = val & 0x7;
            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 = readUInt32(this.buf, this.pos);
        this.pos += 4;
        return val;
    },
    readSFixed32: function() {
        var val = readInt32(this.buf, this.pos);
        this.pos += 4;
        return val;
    },
    readFixed64: function() {
        var val = readUInt32(this.buf, this.pos) + readUInt32(this.buf, this.pos + 4) * SHIFT_LEFT_32;
        this.pos += 8;
        return val;
    },
    readSFixed64: function() {
        var val = readUInt32(this.buf, this.pos) + readInt32(this.buf, this.pos + 4) * SHIFT_LEFT_32;
        this.pos += 8;
        return val;
    },
    readFloat: function() {
        var val = ieee754.read(this.buf, this.pos, true, 23, 4);
        this.pos += 4;
        return val;
    },
    readDouble: function() {
        var val = ieee754.read(this.buf, this.pos, true, 52, 8);
        this.pos += 8;
        return val;
    },
    readVarint: function(isSigned) {
        var buf = this.buf,
            val, b;
        b = buf[this.pos++]; val  =  b & 0x7f;        if (b < 0x80) return val;
        b = buf[this.pos++]; val |= (b & 0x7f) << 7;  if (b < 0x80) return val;
        b = buf[this.pos++]; val |= (b & 0x7f) << 14; if (b < 0x80) return val;
        b = buf[this.pos++]; val |= (b & 0x7f) << 21; if (b < 0x80) return val;
        b = buf[this.pos];   val |= (b & 0x0f) << 28;
        return readVarintRemainder(val, isSigned, this);
    },
    readVarint64: function() {
        return this.readVarint(true);
    },
    readSVarint: function() {
        var num = this.readVarint();
        return num % 2 === 1 ? (num + 1) / -2 : num / 2;
    },
    readBoGVolean: function() {
        return BoGVolean(this.readVarint());
    },
    readString: function() {
        var end = this.readVarint() + this.pos,
            str = readUtf8(this.buf, this.pos, end);
        this.pos = end;
        return str;
    },
    readBytes: function() {
        var end = this.readVarint() + this.pos,
            buffer = this.buf.subarray(this.pos, end);
        this.pos = end;
        return buffer;
    },
    readPackedVarint: function(arr, isSigned) {
        var end = readPackedEnd(this);
        arr = arr || [];
        while (this.pos < end) arr.push(this.readVarint(isSigned));
        return arr;
    },
    readPackedSVarint: function(arr) {
        var end = readPackedEnd(this);
        arr = arr || [];
        while (this.pos < end) arr.push(this.readSVarint());
        return arr;
    },
    readPackedBoGVolean: function(arr) {
        var end = readPackedEnd(this);
        arr = arr || [];
        while (this.pos < end) arr.push(this.readBoGVolean());
        return arr;
    },
    readPackedFloat: function(arr) {
        var end = readPackedEnd(this);
        arr = arr || [];
        while (this.pos < end) arr.push(this.readFloat());
        return arr;
    },
    readPackedDouble: function(arr) {
        var end = readPackedEnd(this);
        arr = arr || [];
        while (this.pos < end) arr.push(this.readDouble());
        return arr;
    },
    readPackedFixed32: function(arr) {
        var end = readPackedEnd(this);
        arr = arr || [];
        while (this.pos < end) arr.push(this.readFixed32());
        return arr;
    },
    readPackedSFixed32: function(arr) {
        var end = readPackedEnd(this);
        arr = arr || [];
        while (this.pos < end) arr.push(this.readSFixed32());
        return arr;
    },
    readPackedFixed64: function(arr) {
        var end = readPackedEnd(this);
        arr = arr || [];
        while (this.pos < end) arr.push(this.readFixed64());
        return arr;
    },
    readPackedSFixed64: function(arr) {
        var end = readPackedEnd(this);
        arr = 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);
    },
    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 Uint8Array(length);
            buf.set(this.buf);
            this.buf = buf;
            this.length = length;
        }
    },
    finish: function() {
        this.length = this.pos;
        this.pos = 0;
        return this.buf.subarray(0, this.length);
    },
    writeFixed32: function(val) {
        this.realloc(4);
        writeInt32(this.buf, val, this.pos);
        this.pos += 4;
    },
    writeSFixed32: function(val) {
        this.realloc(4);
        writeInt32(this.buf, val, this.pos);
        this.pos += 4;
    },
    writeFixed64: function(val) {
        this.realloc(8);
        writeInt32(this.buf, val & -1, this.pos);
        writeInt32(this.buf, Math.floor(val * SHIFT_RIGHT_32), this.pos + 4);
        this.pos += 8;
    },
    writeSFixed64: function(val) {
        this.realloc(8);
        writeInt32(this.buf, val & -1, this.pos);
        writeInt32(this.buf, Math.floor(val * SHIFT_RIGHT_32), this.pos + 4);
        this.pos += 8;
    },
    writeVarint: function(val) {
        val = +val || 0;
        if (val > 0xfffffff || val < 0) {
            writeBigVarint(val, this);
            return;
        }
        this.realloc(4);
        this.buf[this.pos++] =           val & 0x7f  | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;
        this.buf[this.pos++] = ((val >>>= 7) & 0x7f) | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;
        this.buf[this.pos++] = ((val >>>= 7) & 0x7f) | (val > 0x7f ? 0x80 : 0); if (val <= 0x7f) return;
        this.buf[this.pos++] =   (val >>> 7) & 0x7f;
    },
    writeSVarint: function(val) {
        this.writeVarint(val < 0 ? -val * 2 - 1 : val * 2);
    },
    writeBoGVolean: function(val) {
        this.writeVarint(BoGVolean(val));
    },
    writeString: function(str) {
        str = String(str);
        this.realloc(str.length * 4);
        this.pos++;
        var startPos = this.pos;
        this.pos = writeUtf8(this.buf, str, this.pos);
        var len = this.pos - startPos;
        if (len >= 0x80) makeRoomForExtraLength(startPos, len, this);
        this.pos = startPos - 1;
        this.writeVarint(len);
        this.pos += len;
    },
    writeFloat: function(val) {
        this.realloc(4);
        ieee754.write(this.buf, val, this.pos, true, 23, 4);
        this.pos += 4;
    },
    writeDouble: function(val) {
        this.realloc(8);
        ieee754.write(this.buf, val, this.pos, true, 52, 8);
        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++;
        var startPos = this.pos;
        fn(obj, this);
        var len = this.pos - startPos;
        if (len >= 0x80) makeRoomForExtraLength(startPos, len, this);
        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);  },
    writePackedBoGVolean:  function(tag, arr) { this.writeMessage(tag, writePackedBoGVolean, 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);
    },
    writeBoGVoleanField: function(tag, val) {
        this.writeVarintField(tag, BoGVolean(val));
    }
};
function readVarintRemainder(l, s, p) {
    var buf = p.buf,
        h, b;
    b = buf[p.pos++]; h  = (b & 0x70) >> 4;  if (b < 0x80) return toNum(l, h, s);
    b = buf[p.pos++]; h |= (b & 0x7f) << 3;  if (b < 0x80) return toNum(l, h, s);
    b = buf[p.pos++]; h |= (b & 0x7f) << 10; if (b < 0x80) return toNum(l, h, s);
    b = buf[p.pos++]; h |= (b & 0x7f) << 17; if (b < 0x80) return toNum(l, h, s);
    b = buf[p.pos++]; h |= (b & 0x7f) << 24; if (b < 0x80) return toNum(l, h, s);
    b = buf[p.pos++]; h |= (b & 0x01) << 31; if (b < 0x80) return toNum(l, h, s);
    throw new Error('Expected varint not more than 10 bytes');
}
function readPackedEnd(pbf) {
    return pbf.type === Pbf.Bytes ?
        pbf.readVarint() + pbf.pos : pbf.pos + 1;
}
function toNum(low, high, isSigned) {
    if (isSigned) {
        return high * 0x100000000 + (low >>> 0);
    }
    return ((high >>> 0) * 0x100000000) + (low >>> 0);
}
function writeBigVarint(val, pbf) {
    var low, high;
    if (val >= 0) {
        low  = (val % 0x100000000) | 0;
        high = (val / 0x100000000) | 0;
    } else {
        low  = ~(-val % 0x100000000);
        high = ~(-val / 0x100000000);
        if (low ^ 0xffffffff) {
            low = (low + 1) | 0;
        } else {
            low = 0;
            high = (high + 1) | 0;
        }
    }
    if (val >= 0x10000000000000000 || val < -0x10000000000000000) {
        throw new Error('Given varint doesn\'t fit into 10 bytes');
    }
    pbf.realloc(10);
    writeBigVarintLow(low, high, pbf);
    writeBigVarintHigh(high, pbf);
}
function writeBigVarintLow(low, high, pbf) {
    pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;
    pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;
    pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;
    pbf.buf[pbf.pos++] = low & 0x7f | 0x80; low >>>= 7;
    pbf.buf[pbf.pos]   = low & 0x7f;
}
function writeBigVarintHigh(high, pbf) {
    var lsb = (high & 0x07) << 4;
    pbf.buf[pbf.pos++] |= lsb         | ((high >>>= 3) ? 0x80 : 0); if (!high) return;
    pbf.buf[pbf.pos++]  = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;
    pbf.buf[pbf.pos++]  = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;
    pbf.buf[pbf.pos++]  = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;
    pbf.buf[pbf.pos++]  = high & 0x7f | ((high >>>= 7) ? 0x80 : 0); if (!high) return;
    pbf.buf[pbf.pos++]  = high & 0x7f;
}
function makeRoomForExtraLength(startPos, len, pbf) {
    var extraLen =
        len <= 0x3fff ? 1 :
        len <= 0x1fffff ? 2 :
        len <= 0xfffffff ? 3 : Math.ceil(Math.log(len) / (Math.LN2 * 7));
    pbf.realloc(extraLen);
    for (var i = pbf.pos - 1; i >= startPos; i--) pbf.buf[i + extraLen] = pbf.buf[i];
}
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 writePackedBoGVolean(arr, pbf)  { for (var i = 0; i < arr.length; i++) pbf.writeBoGVolean(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]); }
function readUInt32(buf, pos) {
    return ((buf[pos]) |
        (buf[pos + 1] << 8) |
        (buf[pos + 2] << 16)) +
        (buf[pos + 3] * 0x1000000);
}
function writeInt32(buf, val, pos) {
    buf[pos] = val;
    buf[pos + 1] = (val >>> 8);
    buf[pos + 2] = (val >>> 16);
    buf[pos + 3] = (val >>> 24);
}
function readInt32(buf, pos) {
    return ((buf[pos]) |
        (buf[pos + 1] << 8) |
        (buf[pos + 2] << 16)) +
        (buf[pos + 3] << 24);
}
function readUtf8(buf, pos, end) {
    var str = '';
    var i = pos;
    while (i < end) {
        var b0 = buf[i];
        var c = null;
        var bytesPerSequence =
            b0 > 0xEF ? 4 :
            b0 > 0xDF ? 3 :
            b0 > 0xBF ? 2 : 1;
        if (i + bytesPerSequence > end) break;
        var b1, b2, b3;
        if (bytesPerSequence === 1) {
            if (b0 < 0x80) {
                c = b0;
            }
        } else if (bytesPerSequence === 2) {
            b1 = buf[i + 1];
            if ((b1 & 0xC0) === 0x80) {
                c = (b0 & 0x1F) << 0x6 | (b1 & 0x3F);
                if (c <= 0x7F) {
                    c = null;
                }
            }
        } else if (bytesPerSequence === 3) {
            b1 = buf[i + 1];
            b2 = buf[i + 2];
            if ((b1 & 0xC0) === 0x80 && (b2 & 0xC0) === 0x80) {
                c = (b0 & 0xF) << 0xC | (b1 & 0x3F) << 0x6 | (b2 & 0x3F);
                if (c <= 0x7FF || (c >= 0xD800 && c <= 0xDFFF)) {
                    c = null;
                }
            }
        } else if (bytesPerSequence === 4) {
            b1 = buf[i + 1];
            b2 = buf[i + 2];
            b3 = buf[i + 3];
            if ((b1 & 0xC0) === 0x80 && (b2 & 0xC0) === 0x80 && (b3 & 0xC0) === 0x80) {
                c = (b0 & 0xF) << 0x12 | (b1 & 0x3F) << 0xC | (b2 & 0x3F) << 0x6 | (b3 & 0x3F);
                if (c <= 0xFFFF || c >= 0x110000) {
                    c = null;
                }
            }
        }
        if (c === null) {
            c = 0xFFFD;
            bytesPerSequence = 1;
        } else if (c > 0xFFFF) {
            c -= 0x10000;
            str += String.fromCharCode(c >>> 10 & 0x3FF | 0xD800);
            c = 0xDC00 | c & 0x3FF;
        }
        str += String.fromCharCode(c);
        i += bytesPerSequence;
    }
    return str;
}
function writeUtf8(buf, str, pos) {
    for (var i = 0, c, lead; i < str.length; i++) {
        c = str.charCodeAt(i);
        if (c > 0xD7FF && c < 0xE000) {
            if (lead) {
                if (c < 0xDC00) {
                    buf[pos++] = 0xEF;
                    buf[pos++] = 0xBF;
                    buf[pos++] = 0xBD;
                    lead = c;
                    continue;
                } else {
                    c = lead - 0xD800 << 10 | c - 0xDC00 | 0x10000;
                    lead = null;
                }
            } else {
                if (c > 0xDBFF || (i + 1 === str.length)) {
                    buf[pos++] = 0xEF;
                    buf[pos++] = 0xBF;
                    buf[pos++] = 0xBD;
                } else {
                    lead = c;
                }
                continue;
            }
        } else if (lead) {
            buf[pos++] = 0xEF;
            buf[pos++] = 0xBF;
            buf[pos++] = 0xBD;
            lead = null;
        }
        if (c < 0x80) {
            buf[pos++] = c;
        } else {
            if (c < 0x800) {
                buf[pos++] = c >> 0x6 | 0xC0;
            } else {
                if (c < 0x10000) {
                    buf[pos++] = c >> 0xC | 0xE0;
                } else {
                    buf[pos++] = c >> 0x12 | 0xF0;
                    buf[pos++] = c >> 0xC & 0x3F | 0x80;
                }
                buf[pos++] = c >> 0x6 & 0x3F | 0x80;
            }
            buf[pos++] = c & 0x3F | 0x80;
        }
    }
    return pos;
}

exports['default'] = pbf;

}((this.PBF = this.PBF || {})));}).call(GVol.ext);
GVol.ext.PBF = GVol.ext.PBF.default;


/**
 * @fileoverview
 * @suppress {accessContrGVols, ambiguousFunctionDecl, checkDebuggerStatement, checkRegExp, checkTypes, checkVars, const, constantProperty, deprecated, duplicate, es5Strict, fileoverviewTags, missingProperties, nonStandardJsDocs, strictModuleDepCheck, suspiciousCode, undefinedNames, undefinedVars, unknownDefines, unusedLocalVariables, uselessCode, visibility}
 */
goog.provide('GVol.ext.vectortile.VectorTile');

/** @typedef {function(*)} */
GVol.ext.vectortile.VectorTile = function() {};

(function() {(function (exports) {
'use strict';

var pointGeometry = 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); },
    multByPoint:    function(p) { return this.clone()._multByPoint(p); },
    divByPoint:     function(p) { return this.clone()._divByPoint(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); },
    rotateAround:  function(a,p) { return this.clone()._rotateAround(a,p); },
    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(other) {
        return this.x === other.x &&
               this.y === other.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);
    },
    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;
    },
    _multByPoint: function(p) {
        this.x *= p.x;
        this.y *= p.y;
        return this;
    },
    _divByPoint: function(p) {
        this.x /= p.x;
        this.y /= p.y;
        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;
    },
    _rotateAround: function(angle, p) {
        var cos = Math.cos(angle),
            sin = Math.sin(angle),
            x = p.x + cos * (this.x - p.x) - sin * (this.y - p.y),
            y = p.y + sin * (this.x - p.x) + cos * (this.y - p.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;
    }
};
Point.convert = function (a) {
    if (a instanceof Point) {
        return a;
    }
    if (Array.isArray(a)) {
        return new Point(a[0], a[1]);
    }
    return a;
};

var vectortilefeature = VectorTileFeature$1;
function VectorTileFeature$1(pbf, end, extent, keys, values) {
    this.properties = {};
    this.extent = extent;
    this.type = 0;
    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$1.types = ['Unknown', 'Point', 'LineString', 'PGVolygon'];
VectorTileFeature$1.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) {
                if (line) lines.push(line);
                line = [];
            }
            line.push(new pointGeometry(x, y));
        } else if (cmd === 7) {
            if (line) {
                line.push(line[0].clone());
            }
        } else {
            throw new Error('unknown command ' + cmd);
        }
    }
    if (line) lines.push(line);
    return lines;
};
VectorTileFeature$1.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$1.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$1.types[this.type],
        i, j;
    function project(line) {
        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
            ];
        }
    }
    switch (this.type) {
    case 1:
        var points = [];
        for (i = 0; i < coords.length; i++) {
            points[i] = coords[i][0];
        }
        coords = points;
        project(coords);
        break;
    case 2:
        for (i = 0; i < coords.length; i++) {
            project(coords[i]);
        }
        break;
    case 3:
        coords = classifyRings(coords);
        for (i = 0; i < coords.length; i++) {
            for (j = 0; j < coords[i].length; j++) {
                project(coords[i][j]);
            }
        }
        break;
    }
    if (coords.length === 1) {
        coords = coords[0];
    } else {
        type = 'Multi' + type;
    }
    var result = {
        type: "Feature",
        geometry: {
            type: type,
            coordinates: coords
        },
        properties: this.properties
    };
    if ('id' in this) {
        result.id = this.id;
    }
    return result;
};
function classifyRings(rings) {
    var len = rings.length;
    if (len <= 1) return [rings];
    var pGVolygons = [],
        pGVolygon,
        ccw;
    for (var i = 0; i < len; i++) {
        var area = signedArea(rings[i]);
        if (area === 0) continue;
        if (ccw === undefined) ccw = area < 0;
        if (ccw === area < 0) {
            if (pGVolygon) pGVolygons.push(pGVolygon);
            pGVolygon = [rings[i]];
        } else {
            pGVolygon.push(rings[i]);
        }
    }
    if (pGVolygon) pGVolygons.push(pGVolygon);
    return pGVolygons;
}
function signedArea(ring) {
    var sum = 0;
    for (var i = 0, len = ring.length, j = len - 1, p1, p2; i < len; j = i++) {
        p1 = ring[i];
        p2 = ring[j];
        sum += (p2.x - p1.x) * (p1.y + p2.y);
    }
    return sum;
}

var vectortilelayer = VectorTileLayer$1;
function VectorTileLayer$1(pbf, end) {
    this.version = 1;
    this.name = null;
    this.extent = 4096;
    this.length = 0;
    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.readBoGVolean() : null;
    }
    return value;
}
VectorTileLayer$1.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);
};

var vectortile = VectorTile$1;
function VectorTile$1(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;
    }
}

var VectorTile = vectortile;
var VectorTileFeature = vectortilefeature;
var VectorTileLayer = vectortilelayer;
var vectorTile = {
	VectorTile: VectorTile,
	VectorTileFeature: VectorTileFeature,
	VectorTileLayer: VectorTileLayer
};

exports['default'] = vectorTile;
exports.VectorTile = VectorTile;
exports.VectorTileFeature = VectorTileFeature;
exports.VectorTileLayer = VectorTileLayer;

}((this.vectortile = this.vectortile || {})));}).call(GVol.ext);

goog.provide('GVol.render.Feature');

goog.require('GVol');
goog.require('GVol.extent');
goog.require('GVol.geom.GeometryType');


/**
 * Lightweight, read-only, {@link GVol.Feature} and {@link GVol.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 {GVol.geom.GeometryType} type Geometry type.
 * @param {Array.<number>} flatCoordinates Flat coordinates. These always need
 *     to be right-handed for pGVolygons.
 * @param {Array.<number>|Array.<Array.<number>>} ends Ends or Endss.
 * @param {Object.<string, *>} properties Properties.
 * @param {number|string|undefined} id Feature id.
 */
GVol.render.Feature = function(type, flatCoordinates, ends, properties, id) {
  /**
   * @private
   * @type {GVol.Extent|undefined}
   */
  this.extent_;

  /**
   * @private
   * @type {number|string|undefined}
   */
  this.id_ = id;

  /**
   * @private
   * @type {GVol.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
 */
GVol.render.Feature.prototype.get = function(key) {
  return this.properties_[key];
};


/**
 * @return {Array.<number>|Array.<Array.<number>>} Ends or endss.
 */
GVol.render.Feature.prototype.getEnds = function() {
  return this.ends_;
};


/**
 * Get the extent of this feature's geometry.
 * @return {GVol.Extent} Extent.
 * @api
 */
GVol.render.Feature.prototype.getExtent = function() {
  if (!this.extent_) {
    this.extent_ = this.type_ === GVol.geom.GeometryType.POINT ?
      GVol.extent.createOrUpdateFromCoordinate(this.flatCoordinates_) :
      GVol.extent.createOrUpdateFromFlatCoordinates(
          this.flatCoordinates_, 0, this.flatCoordinates_.length, 2);

  }
  return this.extent_;
};

/**
 * Get the feature identifier.  This is a stable identifier for the feature and
 * is set when reading data from a remote source.
 * @return {number|string|undefined} Id.
 * @api
 */
GVol.render.Feature.prototype.getId = function() {
  return this.id_;
};


/**
 * @return {Array.<number>} Flat coordinates.
 */
GVol.render.Feature.prototype.getOrientedFlatCoordinates = function() {
  return this.flatCoordinates_;
};


/**
 * @return {Array.<number>} Flat coordinates.
 */
GVol.render.Feature.prototype.getFlatCoordinates =
    GVol.render.Feature.prototype.getOrientedFlatCoordinates;


/**
 * For API compatibility with {@link GVol.Feature}, this method is useful when
 * determining the geometry type in style function (see {@link #getType}).
 * @return {GVol.render.Feature} Feature.
 * @api
 */
GVol.render.Feature.prototype.getGeometry = function() {
  return this;
};


/**
 * Get the feature properties.
 * @return {Object.<string, *>} Feature properties.
 * @api
 */
GVol.render.Feature.prototype.getProperties = function() {
  return this.properties_;
};


/**
 * Get the feature for working with its geometry.
 * @return {GVol.render.Feature} Feature.
 */
GVol.render.Feature.prototype.getSimplifiedGeometry =
    GVol.render.Feature.prototype.getGeometry;


/**
 * @return {number} Stride.
 */
GVol.render.Feature.prototype.getStride = function() {
  return 2;
};


/**
 * @return {undefined}
 */
GVol.render.Feature.prototype.getStyleFunction = GVol.nullFunction;


/**
 * Get the type of this feature's geometry.
 * @return {GVol.geom.GeometryType} Geometry type.
 * @api
 */
GVol.render.Feature.prototype.getType = function() {
  return this.type_;
};

//FIXME Implement projection handling

goog.provide('GVol.format.MVT');

goog.require('GVol');
goog.require('GVol.ext.PBF');
goog.require('GVol.ext.vectortile.VectorTile');
goog.require('GVol.format.Feature');
goog.require('GVol.format.FormatType');
goog.require('GVol.geom.GeometryLayout');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.geom.LineString');
goog.require('GVol.geom.MultiLineString');
goog.require('GVol.geom.MultiPoint');
goog.require('GVol.geom.Point');
goog.require('GVol.geom.PGVolygon');
goog.require('GVol.proj.Projection');
goog.require('GVol.proj.Units');
goog.require('GVol.render.Feature');


/**
 * @classdesc
 * Feature format for reading data in the Mapbox MVT format.
 *
 * @constructor
 * @extends {GVol.format.Feature}
 * @param {GVolx.format.MVTOptions=} opt_options Options.
 * @api
 */
GVol.format.MVT = function(opt_options) {

  GVol.format.Feature.call(this);

  var options = opt_options ? opt_options : {};

  /**
   * @type {GVol.proj.Projection}
   */
  this.defaultDataProjection = new GVol.proj.Projection({
    code: '',
    units: GVol.proj.Units.TILE_PIXELS
  });

  /**
   * @private
   * @type {function((GVol.geom.Geometry|Object.<string,*>)=)|
   *     function(GVol.geom.GeometryType,Array.<number>,
   *         (Array.<number>|Array.<Array.<number>>),Object.<string,*>,number)}
   */
  this.featureClass_ = options.featureClass ?
    options.featureClass : GVol.render.Feature;

  /**
   * @private
   * @type {string|undefined}
   */
  this.geometryName_ = options.geometryName;

  /**
   * @private
   * @type {string}
   */
  this.layerName_ = options.layerName ? options.layerName : 'layer';

  /**
   * @private
   * @type {Array.<string>}
   */
  this.layers_ = options.layers ? options.layers : null;

  /**
   * @private
   * @type {GVol.Extent}
   */
  this.extent_ = null;

};
GVol.inherits(GVol.format.MVT, GVol.format.Feature);


/**
 * @inheritDoc
 * @api
 */
GVol.format.MVT.prototype.getLastExtent = function() {
  return this.extent_;
};


/**
 * @inheritDoc
 */
GVol.format.MVT.prototype.getType = function() {
  return GVol.format.FormatType.ARRAY_BUFFER;
};


/**
 * @private
 * @param {Object} rawFeature Raw Mapbox feature.
 * @param {string} layer Layer.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {GVol.Feature} Feature.
 */
GVol.format.MVT.prototype.readFeature_ = function(
    rawFeature, layer, opt_options) {
  var feature = new this.featureClass_();
  var id = rawFeature.id;
  var values = rawFeature.properties;
  values[this.layerName_] = layer;
  if (this.geometryName_) {
    feature.setGeometryName(this.geometryName_);
  }
  var geometry = GVol.format.Feature.transformWithOptions(
      GVol.format.MVT.readGeometry_(rawFeature), false,
      this.adaptOptions(opt_options));
  feature.setGeometry(geometry);
  feature.setId(id);
  feature.setProperties(values);
  return feature;
};


/**
 * @private
 * @param {Object} rawFeature Raw Mapbox feature.
 * @param {string} layer Layer.
 * @return {GVol.render.Feature} Feature.
 */
GVol.format.MVT.prototype.readRenderFeature_ = function(rawFeature, layer) {
  var coords = rawFeature.loadGeometry();
  var ends = [];
  var flatCoordinates = [];
  GVol.format.MVT.calculateFlatCoordinates_(coords, flatCoordinates, ends);

  var type = rawFeature.type;
  /** @type {GVol.geom.GeometryType} */
  var geometryType;
  if (type === 1) {
    geometryType = coords.length === 1 ?
      GVol.geom.GeometryType.POINT : GVol.geom.GeometryType.MULTI_POINT;
  } else if (type === 2) {
    if (coords.length === 1) {
      geometryType = GVol.geom.GeometryType.LINE_STRING;
    } else {
      geometryType = GVol.geom.GeometryType.MULTI_LINE_STRING;
    }
  } else if (type === 3) {
    geometryType = GVol.geom.GeometryType.POLYGON;
  }

  var values = rawFeature.properties;
  values[this.layerName_] = layer;
  var id = rawFeature.id;

  return new this.featureClass_(geometryType, flatCoordinates, ends, values, id);
};


/**
 * @inheritDoc
 * @api
 */
GVol.format.MVT.prototype.readFeatures = function(source, opt_options) {
  var layers = this.layers_;

  var pbf = new GVol.ext.PBF(/** @type {ArrayBuffer} */ (source));
  var tile = new GVol.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];

    var rawFeature;
    for (var i = 0, ii = layer.length; i < ii; ++i) {
      rawFeature = layer.feature(i);
      if (featureClass === GVol.render.Feature) {
        feature = this.readRenderFeature_(rawFeature, name);
      } else {
        feature = this.readFeature_(rawFeature, name, opt_options);
      }
      features.push(feature);
    }
    this.extent_ = layer ? [0, 0, layer.extent, layer.extent] : null;
  }

  return features;
};


/**
 * @inheritDoc
 * @api
 */
GVol.format.MVT.prototype.readProjection = function(source) {
  return this.defaultDataProjection;
};


/**
 * Sets the layers that features will be read from.
 * @param {Array.<string>} layers Layers.
 * @api
 */
GVol.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.
 */
GVol.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 {GVol.geom.Geometry} Geometry.
 */
GVol.format.MVT.readGeometry_ = function(rawFeature) {
  var type = rawFeature.type;
  if (type === 0) {
    return null;
  }

  var coords = rawFeature.loadGeometry();
  var ends = [];
  var flatCoordinates = [];
  GVol.format.MVT.calculateFlatCoordinates_(coords, flatCoordinates, ends);

  var geom;
  if (type === 1) {
    geom = coords.length === 1 ?
      new GVol.geom.Point(null) : new GVol.geom.MultiPoint(null);
  } else if (type === 2) {
    if (coords.length === 1) {
      geom = new GVol.geom.LineString(null);
    } else {
      geom = new GVol.geom.MultiLineString(null);
    }
  } else if (type === 3) {
    geom = new GVol.geom.PGVolygon(null);
  }

  geom.setFlatCoordinates(GVol.geom.GeometryLayout.XY, flatCoordinates,
      ends);

  return geom;
};


/**
 * Not implemented.
 * @override
 */
GVol.format.MVT.prototype.readFeature = function() {};


/**
 * Not implemented.
 * @override
 */
GVol.format.MVT.prototype.readGeometry = function() {};


/**
 * Not implemented.
 * @override
 */
GVol.format.MVT.prototype.writeFeature = function() {};


/**
 * Not implemented.
 * @override
 */
GVol.format.MVT.prototype.writeGeometry = function() {};


/**
 * Not implemented.
 * @override
 */
GVol.format.MVT.prototype.writeFeatures = function() {};

// FIXME add typedef for stack state objects
goog.provide('GVol.format.OSMXML');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.Feature');
goog.require('GVol.format.Feature');
goog.require('GVol.format.XMLFeature');
goog.require('GVol.geom.GeometryLayout');
goog.require('GVol.geom.LineString');
goog.require('GVol.geom.Point');
goog.require('GVol.geom.PGVolygon');
goog.require('GVol.obj');
goog.require('GVol.proj');
goog.require('GVol.xml');


/**
 * @classdesc
 * Feature format for reading data in the
 * [OSMXML format](http://wiki.openstreetmap.org/wiki/OSM_XML).
 *
 * @constructor
 * @extends {GVol.format.XMLFeature}
 * @api
 */
GVol.format.OSMXML = function() {
  GVol.format.XMLFeature.call(this);

  /**
   * @inheritDoc
   */
  this.defaultDataProjection = GVol.proj.get('EPSG:4326');
};
GVol.inherits(GVol.format.OSMXML, GVol.format.XMLFeature);


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.OSMXML.readNode_ = function(node, objectStack) {
  var options = /** @type {GVolx.format.ReadOptions} */ (objectStack[0]);
  var state = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  var id = node.getAttribute('id');
  /** @type {GVol.Coordinate} */
  var coordinates = [
    parseFloat(node.getAttribute('lon')),
    parseFloat(node.getAttribute('lat'))
  ];
  state.nodes[id] = coordinates;

  var values = GVol.xml.pushParseAndPop({
    tags: {}
  }, GVol.format.OSMXML.NODE_PARSERS_, node, objectStack);
  if (!GVol.obj.isEmpty(values.tags)) {
    var geometry = new GVol.geom.Point(coordinates);
    GVol.format.Feature.transformWithOptions(geometry, false, options);
    var feature = new GVol.Feature(geometry);
    feature.setId(id);
    feature.setProperties(values.tags);
    state.features.push(feature);
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.OSMXML.readWay_ = function(node, objectStack) {
  var options = /** @type {GVolx.format.ReadOptions} */ (objectStack[0]);
  var id = node.getAttribute('id');
  var values = GVol.xml.pushParseAndPop({
    ndrefs: [],
    tags: {}
  }, GVol.format.OSMXML.WAY_PARSERS_, node, objectStack);
  var state = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  /** @type {Array.<number>} */
  var flatCoordinates = [];
  for (var i = 0, ii = values.ndrefs.length; i < ii; i++) {
    var point = state.nodes[values.ndrefs[i]];
    GVol.array.extend(flatCoordinates, point);
  }
  var geometry;
  if (values.ndrefs[0] == values.ndrefs[values.ndrefs.length - 1]) {
    // closed way
    geometry = new GVol.geom.PGVolygon(null);
    geometry.setFlatCoordinates(GVol.geom.GeometryLayout.XY, flatCoordinates,
        [flatCoordinates.length]);
  } else {
    geometry = new GVol.geom.LineString(null);
    geometry.setFlatCoordinates(GVol.geom.GeometryLayout.XY, flatCoordinates);
  }
  GVol.format.Feature.transformWithOptions(geometry, false, options);
  var feature = new GVol.Feature(geometry);
  feature.setId(id);
  feature.setProperties(values.tags);
  state.features.push(feature);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.OSMXML.readNd_ = function(node, objectStack) {
  var values = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  values.ndrefs.push(node.getAttribute('ref'));
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.OSMXML.readTag_ = function(node, objectStack) {
  var values = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  values.tags[node.getAttribute('k')] = node.getAttribute('v');
};


/**
 * @const
 * @private
 * @type {Array.<string>}
 */
GVol.format.OSMXML.NAMESPACE_URIS_ = [
  null
];


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.OSMXML.WAY_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.OSMXML.NAMESPACE_URIS_, {
      'nd': GVol.format.OSMXML.readNd_,
      'tag': GVol.format.OSMXML.readTag_
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.OSMXML.PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.OSMXML.NAMESPACE_URIS_, {
      'node': GVol.format.OSMXML.readNode_,
      'way': GVol.format.OSMXML.readWay_
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.OSMXML.NODE_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.OSMXML.NAMESPACE_URIS_, {
      'tag': GVol.format.OSMXML.readTag_
    });


/**
 * Read all features from an OSM source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<GVol.Feature>} Features.
 * @api
 */
GVol.format.OSMXML.prototype.readFeatures;


/**
 * @inheritDoc
 */
GVol.format.OSMXML.prototype.readFeaturesFromNode = function(node, opt_options) {
  var options = this.getReadOptions(node, opt_options);
  if (node.localName == 'osm') {
    var state = GVol.xml.pushParseAndPop({
      nodes: {},
      features: []
    }, GVol.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 {GVol.proj.Projection} Projection.
 * @api
 */
GVol.format.OSMXML.prototype.readProjection;


/**
 * Not implemented.
 * @inheritDoc
 */
GVol.format.OSMXML.prototype.writeFeatureNode = function(feature, opt_options) {};


/**
 * Not implemented.
 * @inheritDoc
 */
GVol.format.OSMXML.prototype.writeFeaturesNode = function(features, opt_options) {};


/**
 * Not implemented.
 * @inheritDoc
 */
GVol.format.OSMXML.prototype.writeGeometryNode = function(geometry, opt_options) {};

goog.provide('GVol.format.XLink');


/**
 * @const
 * @type {string}
 */
GVol.format.XLink.NAMESPACE_URI = 'http://www.w3.org/1999/xlink';


/**
 * @param {Node} node Node.
 * @return {boGVolean|undefined} BoGVolean.
 */
GVol.format.XLink.readHref = function(node) {
  return node.getAttributeNS(GVol.format.XLink.NAMESPACE_URI, 'href');
};

goog.provide('GVol.format.XML');

goog.require('GVol.xml');


/**
 * @classdesc
 * Generic format for reading non-feature XML data
 *
 * @constructor
 * @abstract
 * @struct
 */
GVol.format.XML = function() {
};


/**
 * @param {Document|Node|string} source Source.
 * @return {Object} The parsed result.
 */
GVol.format.XML.prototype.read = function(source) {
  if (GVol.xml.isDocument(source)) {
    return this.readFromDocument(/** @type {Document} */ (source));
  } else if (GVol.xml.isNode(source)) {
    return this.readFromNode(/** @type {Node} */ (source));
  } else if (typeof source === 'string') {
    var doc = GVol.xml.parse(source);
    return this.readFromDocument(doc);
  } else {
    return null;
  }
};


/**
 * @abstract
 * @param {Document} doc Document.
 * @return {Object} Object
 */
GVol.format.XML.prototype.readFromDocument = function(doc) {};


/**
 * @abstract
 * @param {Node} node Node.
 * @return {Object} Object
 */
GVol.format.XML.prototype.readFromNode = function(node) {};

goog.provide('GVol.format.OWS');

goog.require('GVol');
goog.require('GVol.format.XLink');
goog.require('GVol.format.XML');
goog.require('GVol.format.XSD');
goog.require('GVol.xml');


/**
 * @constructor
 * @extends {GVol.format.XML}
 */
GVol.format.OWS = function() {
  GVol.format.XML.call(this);
};
GVol.inherits(GVol.format.OWS, GVol.format.XML);


/**
 * @inheritDoc
 */
GVol.format.OWS.prototype.readFromDocument = function(doc) {
  for (var n = doc.firstChild; n; n = n.nextSibling) {
    if (n.nodeType == Node.ELEMENT_NODE) {
      return this.readFromNode(n);
    }
  }
  return null;
};


/**
 * @inheritDoc
 */
GVol.format.OWS.prototype.readFromNode = function(node) {
  var owsObject = GVol.xml.pushParseAndPop({},
      GVol.format.OWS.PARSERS_, node, []);
  return owsObject ? owsObject : null;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The address.
 */
GVol.format.OWS.readAddress_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop({},
      GVol.format.OWS.ADDRESS_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The values.
 */
GVol.format.OWS.readAllowedValues_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop({},
      GVol.format.OWS.ALLOWED_VALUES_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The constraint.
 */
GVol.format.OWS.readConstraint_ = function(node, objectStack) {
  var name = node.getAttribute('name');
  if (!name) {
    return undefined;
  }
  return GVol.xml.pushParseAndPop({'name': name},
      GVol.format.OWS.CONSTRAINT_PARSERS_, node,
      objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The contact info.
 */
GVol.format.OWS.readContactInfo_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop({},
      GVol.format.OWS.CONTACT_INFO_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The DCP.
 */
GVol.format.OWS.readDcp_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop({},
      GVol.format.OWS.DCP_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The GET object.
 */
GVol.format.OWS.readGet_ = function(node, objectStack) {
  var href = GVol.format.XLink.readHref(node);
  if (!href) {
    return undefined;
  }
  return GVol.xml.pushParseAndPop({'href': href},
      GVol.format.OWS.REQUEST_METHOD_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The HTTP object.
 */
GVol.format.OWS.readHttp_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop({}, GVol.format.OWS.HTTP_PARSERS_,
      node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The operation.
 */
GVol.format.OWS.readOperation_ = function(node, objectStack) {
  var name = node.getAttribute('name');
  var value = GVol.xml.pushParseAndPop({},
      GVol.format.OWS.OPERATION_PARSERS_, node, objectStack);
  if (!value) {
    return undefined;
  }
  var object = /** @type {Object} */
      (objectStack[objectStack.length - 1]);
  object[name] = value;
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The operations metadata.
 */
GVol.format.OWS.readOperationsMetadata_ = function(node,
    objectStack) {
  return GVol.xml.pushParseAndPop({},
      GVol.format.OWS.OPERATIONS_METADATA_PARSERS_, node,
      objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The phone.
 */
GVol.format.OWS.readPhone_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop({},
      GVol.format.OWS.PHONE_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The service identification.
 */
GVol.format.OWS.readServiceIdentification_ = function(node,
    objectStack) {
  return GVol.xml.pushParseAndPop(
      {}, GVol.format.OWS.SERVICE_IDENTIFICATION_PARSERS_, node,
      objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The service contact.
 */
GVol.format.OWS.readServiceContact_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(
      {}, GVol.format.OWS.SERVICE_CONTACT_PARSERS_, node,
      objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} The service provider.
 */
GVol.format.OWS.readServiceProvider_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(
      {}, GVol.format.OWS.SERVICE_PROVIDER_PARSERS_, node,
      objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {string|undefined} The value.
 */
GVol.format.OWS.readValue_ = function(node, objectStack) {
  return GVol.format.XSD.readString(node);
};


/**
 * @const
 * @type {Array.<string>}
 * @private
 */
GVol.format.OWS.NAMESPACE_URIS_ = [
  null,
  'http://www.opengis.net/ows/1.1'
];


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.OWS.PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.OWS.NAMESPACE_URIS_, {
      'ServiceIdentification': GVol.xml.makeObjectPropertySetter(
          GVol.format.OWS.readServiceIdentification_),
      'ServiceProvider': GVol.xml.makeObjectPropertySetter(
          GVol.format.OWS.readServiceProvider_),
      'OperationsMetadata': GVol.xml.makeObjectPropertySetter(
          GVol.format.OWS.readOperationsMetadata_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.OWS.ADDRESS_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.OWS.NAMESPACE_URIS_, {
      'DeliveryPoint': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString),
      'City': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'AdministrativeArea': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString),
      'PostalCode': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'Country': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'ElectronicMailAddress': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.OWS.ALLOWED_VALUES_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.OWS.NAMESPACE_URIS_, {
      'Value': GVol.xml.makeObjectPropertyPusher(GVol.format.OWS.readValue_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.OWS.CONSTRAINT_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.OWS.NAMESPACE_URIS_, {
      'AllowedValues': GVol.xml.makeObjectPropertySetter(
          GVol.format.OWS.readAllowedValues_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.OWS.CONTACT_INFO_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.OWS.NAMESPACE_URIS_, {
      'Phone': GVol.xml.makeObjectPropertySetter(GVol.format.OWS.readPhone_),
      'Address': GVol.xml.makeObjectPropertySetter(GVol.format.OWS.readAddress_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.OWS.DCP_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.OWS.NAMESPACE_URIS_, {
      'HTTP': GVol.xml.makeObjectPropertySetter(GVol.format.OWS.readHttp_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.OWS.HTTP_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.OWS.NAMESPACE_URIS_, {
      'Get': GVol.xml.makeObjectPropertyPusher(GVol.format.OWS.readGet_),
      'Post': undefined // TODO
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.OWS.OPERATION_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.OWS.NAMESPACE_URIS_, {
      'DCP': GVol.xml.makeObjectPropertySetter(GVol.format.OWS.readDcp_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.OWS.OPERATIONS_METADATA_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.OWS.NAMESPACE_URIS_, {
      'Operation': GVol.format.OWS.readOperation_
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.OWS.PHONE_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.OWS.NAMESPACE_URIS_, {
      'Voice': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'Facsimile': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.OWS.REQUEST_METHOD_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.OWS.NAMESPACE_URIS_, {
      'Constraint': GVol.xml.makeObjectPropertyPusher(
          GVol.format.OWS.readConstraint_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.OWS.SERVICE_CONTACT_PARSERS_ =
    GVol.xml.makeStructureNS(
        GVol.format.OWS.NAMESPACE_URIS_, {
          'IndividualName': GVol.xml.makeObjectPropertySetter(
              GVol.format.XSD.readString),
          'PositionName': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
          'ContactInfo': GVol.xml.makeObjectPropertySetter(
              GVol.format.OWS.readContactInfo_)
        });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.OWS.SERVICE_IDENTIFICATION_PARSERS_ =
    GVol.xml.makeStructureNS(
        GVol.format.OWS.NAMESPACE_URIS_, {
          'Title': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
          'ServiceTypeVersion': GVol.xml.makeObjectPropertySetter(
              GVol.format.XSD.readString),
          'ServiceType': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString)
        });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.OWS.SERVICE_PROVIDER_PARSERS_ =
    GVol.xml.makeStructureNS(
        GVol.format.OWS.NAMESPACE_URIS_, {
          'ProviderName': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
          'ProviderSite': GVol.xml.makeObjectPropertySetter(GVol.format.XLink.readHref),
          'ServiceContact': GVol.xml.makeObjectPropertySetter(
              GVol.format.OWS.readServiceContact_)
        });

goog.provide('GVol.geom.flat.flip');


/**
 * @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.
 */
GVol.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 {
    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('GVol.format.PGVolyline');

goog.require('GVol');
goog.require('GVol.asserts');
goog.require('GVol.Feature');
goog.require('GVol.format.Feature');
goog.require('GVol.format.TextFeature');
goog.require('GVol.geom.GeometryLayout');
goog.require('GVol.geom.LineString');
goog.require('GVol.geom.SimpleGeometry');
goog.require('GVol.geom.flat.flip');
goog.require('GVol.geom.flat.inflate');
goog.require('GVol.proj');


/**
 * @classdesc
 * Feature format for reading and writing data in the Encoded
 * PGVolyline Algorithm Format.
 *
 * @constructor
 * @extends {GVol.format.TextFeature}
 * @param {GVolx.format.PGVolylineOptions=} opt_options
 *     Optional configuration object.
 * @api
 */
GVol.format.PGVolyline = function(opt_options) {

  var options = opt_options ? opt_options : {};

  GVol.format.TextFeature.call(this);

  /**
   * @inheritDoc
   */
  this.defaultDataProjection = GVol.proj.get('EPSG:4326');

  /**
   * @private
   * @type {number}
   */
  this.factor_ = options.factor ? options.factor : 1e5;

  /**
   * @private
   * @type {GVol.geom.GeometryLayout}
   */
  this.geometryLayout_ = options.geometryLayout ?
    options.geometryLayout : GVol.geom.GeometryLayout.XY;
};
GVol.inherits(GVol.format.PGVolyline, GVol.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
 */
GVol.format.PGVolyline.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 GVol.format.PGVolyline.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
 */
GVol.format.PGVolyline.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 = GVol.format.PGVolyline.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
 */
GVol.format.PGVolyline.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 GVol.format.PGVolyline.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
 */
GVol.format.PGVolyline.decodeFloats = function(encoded, opt_factor) {
  var factor = opt_factor ? opt_factor : 1e5;
  var numbers = GVol.format.PGVolyline.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.
 */
GVol.format.PGVolyline.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 GVol.format.PGVolyline.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.
 */
GVol.format.PGVolyline.decodeSignedIntegers = function(encoded) {
  var numbers = GVol.format.PGVolyline.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.
 */
GVol.format.PGVolyline.encodeUnsignedIntegers = function(numbers) {
  var encoded = '';
  var i, ii;
  for (i = 0, ii = numbers.length; i < ii; ++i) {
    encoded += GVol.format.PGVolyline.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.
 */
GVol.format.PGVolyline.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.
 */
GVol.format.PGVolyline.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 PGVolyline source. The coordinates are assumed to be
 * in two dimensions and in latitude, longitude order.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {GVol.Feature} Feature.
 * @api
 */
GVol.format.PGVolyline.prototype.readFeature;


/**
 * @inheritDoc
 */
GVol.format.PGVolyline.prototype.readFeatureFromText = function(text, opt_options) {
  var geometry = this.readGeometryFromText(text, opt_options);
  return new GVol.Feature(geometry);
};


/**
 * Read the feature from the source. As PGVolyline sources contain a single
 * feature, this will return the feature in an array.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<GVol.Feature>} Features.
 * @api
 */
GVol.format.PGVolyline.prototype.readFeatures;


/**
 * @inheritDoc
 */
GVol.format.PGVolyline.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 {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {GVol.geom.Geometry} Geometry.
 * @api
 */
GVol.format.PGVolyline.prototype.readGeometry;


/**
 * @inheritDoc
 */
GVol.format.PGVolyline.prototype.readGeometryFromText = function(text, opt_options) {
  var stride = GVol.geom.SimpleGeometry.getStrideForLayout(this.geometryLayout_);
  var flatCoordinates = GVol.format.PGVolyline.decodeDeltas(
      text, stride, this.factor_);
  GVol.geom.flat.flip.flipXY(
      flatCoordinates, 0, flatCoordinates.length, stride, flatCoordinates);
  var coordinates = GVol.geom.flat.inflate.coordinates(
      flatCoordinates, 0, flatCoordinates.length, stride);

  return /** @type {GVol.geom.Geometry} */ (
    GVol.format.Feature.transformWithOptions(
        new GVol.geom.LineString(coordinates, this.geometryLayout_), false,
        this.adaptOptions(opt_options)));
};


/**
 * Read the projection from a PGVolyline source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @return {GVol.proj.Projection} Projection.
 * @api
 */
GVol.format.PGVolyline.prototype.readProjection;


/**
 * @inheritDoc
 */
GVol.format.PGVolyline.prototype.writeFeatureText = function(feature, opt_options) {
  var geometry = feature.getGeometry();
  if (geometry) {
    return this.writeGeometryText(geometry, opt_options);
  } else {
    GVol.asserts.assert(false, 40); // Expected `feature` to have a geometry
    return '';
  }
};


/**
 * @inheritDoc
 */
GVol.format.PGVolyline.prototype.writeFeaturesText = function(features, opt_options) {
  return this.writeFeatureText(features[0], opt_options);
};


/**
 * Write a single geometry in PGVolyline format.
 *
 * @function
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {string} Geometry.
 * @api
 */
GVol.format.PGVolyline.prototype.writeGeometry;


/**
 * @inheritDoc
 */
GVol.format.PGVolyline.prototype.writeGeometryText = function(geometry, opt_options) {
  geometry = /** @type {GVol.geom.LineString} */
    (GVol.format.Feature.transformWithOptions(
        geometry, true, this.adaptOptions(opt_options)));
  var flatCoordinates = geometry.getFlatCoordinates();
  var stride = geometry.getStride();
  GVol.geom.flat.flip.flipXY(
      flatCoordinates, 0, flatCoordinates.length, stride, flatCoordinates);
  return GVol.format.PGVolyline.encodeDeltas(flatCoordinates, stride, this.factor_);
};

goog.provide('GVol.format.TopoJSON');

goog.require('GVol');
goog.require('GVol.Feature');
goog.require('GVol.format.Feature');
goog.require('GVol.format.JSONFeature');
goog.require('GVol.geom.LineString');
goog.require('GVol.geom.MultiLineString');
goog.require('GVol.geom.MultiPoint');
goog.require('GVol.geom.MultiPGVolygon');
goog.require('GVol.geom.Point');
goog.require('GVol.geom.PGVolygon');
goog.require('GVol.proj');


/**
 * @classdesc
 * Feature format for reading data in the TopoJSON format.
 *
 * @constructor
 * @extends {GVol.format.JSONFeature}
 * @param {GVolx.format.TopoJSONOptions=} opt_options Options.
 * @api
 */
GVol.format.TopoJSON = function(opt_options) {

  var options = opt_options ? opt_options : {};

  GVol.format.JSONFeature.call(this);

  /**
   * @private
   * @type {string|undefined}
   */
  this.layerName_ = options.layerName;

  /**
   * @private
   * @type {Array.<string>}
   */
  this.layers_ = options.layers ? options.layers : null;

  /**
   * @inheritDoc
   */
  this.defaultDataProjection = GVol.proj.get(
      options.defaultDataProjection ?
        options.defaultDataProjection : 'EPSG:4326');

};
GVol.inherits(GVol.format.TopoJSON, GVol.format.JSONFeature);


/**
 * 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.<GVol.Coordinate>>} arcs Array of arcs (already
 *     transformed).
 * @return {Array.<GVol.Coordinate>} Coordinates array.
 * @private
 */
GVol.format.TopoJSON.concatenateArcs_ = function(indices, arcs) {
  /** @type {Array.<GVol.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 {GVol.geom.Point} Geometry.
 * @private
 */
GVol.format.TopoJSON.readPointGeometry_ = function(object, scale, translate) {
  var coordinates = object.coordinates;
  if (scale && translate) {
    GVol.format.TopoJSON.transformVertex_(coordinates, scale, translate);
  }
  return new GVol.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 {GVol.geom.MultiPoint} Geometry.
 * @private
 */
GVol.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) {
      GVol.format.TopoJSON.transformVertex_(coordinates[i], scale, translate);
    }
  }
  return new GVol.geom.MultiPoint(coordinates);
};


/**
 * Create a linestring from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<Array.<GVol.Coordinate>>} arcs Array of arcs.
 * @return {GVol.geom.LineString} Geometry.
 * @private
 */
GVol.format.TopoJSON.readLineStringGeometry_ = function(object, arcs) {
  var coordinates = GVol.format.TopoJSON.concatenateArcs_(object.arcs, arcs);
  return new GVol.geom.LineString(coordinates);
};


/**
 * Create a multi-linestring from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<Array.<GVol.Coordinate>>} arcs Array of arcs.
 * @return {GVol.geom.MultiLineString} Geometry.
 * @private
 */
GVol.format.TopoJSON.readMultiLineStringGeometry_ = function(object, arcs) {
  var coordinates = [];
  var i, ii;
  for (i = 0, ii = object.arcs.length; i < ii; ++i) {
    coordinates[i] = GVol.format.TopoJSON.concatenateArcs_(object.arcs[i], arcs);
  }
  return new GVol.geom.MultiLineString(coordinates);
};


/**
 * Create a pGVolygon from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<Array.<GVol.Coordinate>>} arcs Array of arcs.
 * @return {GVol.geom.PGVolygon} Geometry.
 * @private
 */
GVol.format.TopoJSON.readPGVolygonGeometry_ = function(object, arcs) {
  var coordinates = [];
  var i, ii;
  for (i = 0, ii = object.arcs.length; i < ii; ++i) {
    coordinates[i] = GVol.format.TopoJSON.concatenateArcs_(object.arcs[i], arcs);
  }
  return new GVol.geom.PGVolygon(coordinates);
};


/**
 * Create a multi-pGVolygon from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON object.
 * @param {Array.<Array.<GVol.Coordinate>>} arcs Array of arcs.
 * @return {GVol.geom.MultiPGVolygon} Geometry.
 * @private
 */
GVol.format.TopoJSON.readMultiPGVolygonGeometry_ = function(object, arcs) {
  var coordinates = [];
  var pGVolyArray, ringCoords, j, jj;
  var i, ii;
  for (i = 0, ii = object.arcs.length; i < ii; ++i) {
    // for each pGVolygon
    pGVolyArray = object.arcs[i];
    ringCoords = [];
    for (j = 0, jj = pGVolyArray.length; j < jj; ++j) {
      // for each ring
      ringCoords[j] = GVol.format.TopoJSON.concatenateArcs_(pGVolyArray[j], arcs);
    }
    coordinates[i] = ringCoords;
  }
  return new GVol.geom.MultiPGVolygon(coordinates);
};


/**
 * Create features from a TopoJSON GeometryCGVollection object.
 *
 * @param {TopoJSONGeometryCGVollection} cGVollection TopoJSON Geometry
 *     object.
 * @param {Array.<Array.<GVol.Coordinate>>} arcs Array of arcs.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @param {string|undefined} property Property to set the `GeometryCGVollection`'s parent
 *     object to.
 * @param {string} name Name of the `TopGVology`'s child object.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<GVol.Feature>} Array of features.
 * @private
 */
GVol.format.TopoJSON.readFeaturesFromGeometryCGVollection_ = function(
    cGVollection, arcs, scale, translate, property, name, opt_options) {
  var geometries = cGVollection.geometries;
  var features = [];
  var i, ii;
  for (i = 0, ii = geometries.length; i < ii; ++i) {
    features[i] = GVol.format.TopoJSON.readFeatureFromGeometry_(
        geometries[i], arcs, scale, translate, property, name, opt_options);
  }
  return features;
};


/**
 * Create a feature from a TopoJSON geometry object.
 *
 * @param {TopoJSONGeometry} object TopoJSON geometry object.
 * @param {Array.<Array.<GVol.Coordinate>>} arcs Array of arcs.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @param {string|undefined} property Property to set the `GeometryCGVollection`'s parent
 *     object to.
 * @param {string} name Name of the `TopGVology`'s child object.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {GVol.Feature} Feature.
 * @private
 */
GVol.format.TopoJSON.readFeatureFromGeometry_ = function(object, arcs,
    scale, translate, property, name, opt_options) {
  var geometry;
  var type = object.type;
  var geometryReader = GVol.format.TopoJSON.GEOMETRY_READERS_[type];
  if ((type === 'Point') || (type === 'MultiPoint')) {
    geometry = geometryReader(object, scale, translate);
  } else {
    geometry = geometryReader(object, arcs);
  }
  var feature = new GVol.Feature();
  feature.setGeometry(/** @type {GVol.geom.Geometry} */ (
    GVol.format.Feature.transformWithOptions(geometry, false, opt_options)));
  if (object.id !== undefined) {
    feature.setId(object.id);
  }
  var properties = object.properties;
  if (property) {
    if (!properties) {
      properties = {};
    }
    properties[property] = name;
  }
  if (properties) {
    feature.setProperties(properties);
  }
  return feature;
};


/**
 * Read all features from a TopoJSON source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @return {Array.<GVol.Feature>} Features.
 * @api
 */
GVol.format.TopoJSON.prototype.readFeatures;


/**
 * @inheritDoc
 */
GVol.format.TopoJSON.prototype.readFeaturesFromObject = function(
    object, opt_options) {
  if (object.type == 'TopGVology') {
    var topoJSONTopGVology = /** @type {TopoJSONTopGVology} */ (object);
    var transform, scale = null, translate = null;
    if (topoJSONTopGVology.transform) {
      transform = topoJSONTopGVology.transform;
      scale = transform.scale;
      translate = transform.translate;
    }
    var arcs = topoJSONTopGVology.arcs;
    if (transform) {
      GVol.format.TopoJSON.transformArcs_(arcs, scale, translate);
    }
    /** @type {Array.<GVol.Feature>} */
    var features = [];
    var topoJSONFeatures = topoJSONTopGVology.objects;
    var property = this.layerName_;
    var objectName, feature;
    for (objectName in topoJSONFeatures) {
      if (this.layers_ && this.layers_.indexOf(objectName) == -1) {
        continue;
      }
      if (topoJSONFeatures[objectName].type === 'GeometryCGVollection') {
        feature = /** @type {TopoJSONGeometryCGVollection} */
          (topoJSONFeatures[objectName]);
        features.push.apply(features,
            GVol.format.TopoJSON.readFeaturesFromGeometryCGVollection_(
                feature, arcs, scale, translate, property, objectName, opt_options));
      } else {
        feature = /** @type {TopoJSONGeometry} */
          (topoJSONFeatures[objectName]);
        features.push(GVol.format.TopoJSON.readFeatureFromGeometry_(
            feature, arcs, scale, translate, property, objectName, 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.<GVol.Coordinate>>} arcs Array of arcs.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @private
 */
GVol.format.TopoJSON.transformArcs_ = function(arcs, scale, translate) {
  var i, ii;
  for (i = 0, ii = arcs.length; i < ii; ++i) {
    GVol.format.TopoJSON.transformArc_(arcs[i], scale, translate);
  }
};


/**
 * Apply a linear transform to an arc.  The provided arc is modified in place.
 *
 * @param {Array.<GVol.Coordinate>} arc Arc.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @private
 */
GVol.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;
    GVol.format.TopoJSON.transformVertex_(vertex, scale, translate);
  }
};


/**
 * Apply a linear transform to a vertex.  The provided vertex is modified in
 * place.
 *
 * @param {GVol.Coordinate} vertex Vertex.
 * @param {Array.<number>} scale Scale for each dimension.
 * @param {Array.<number>} translate Translation for each dimension.
 * @private
 */
GVol.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.
 *
 * @param {Document|Node|Object|string} object Source.
 * @return {GVol.proj.Projection} Projection.
 * @override
 * @api
 */
GVol.format.TopoJSON.prototype.readProjection;


/**
 * @inheritDoc
 */
GVol.format.TopoJSON.prototype.readProjectionFromObject = function(object) {
  return this.defaultDataProjection;
};


/**
 * @const
 * @private
 * @type {Object.<string, function(TopoJSONGeometry, Array, ...Array): GVol.geom.Geometry>}
 */
GVol.format.TopoJSON.GEOMETRY_READERS_ = {
  'Point': GVol.format.TopoJSON.readPointGeometry_,
  'LineString': GVol.format.TopoJSON.readLineStringGeometry_,
  'PGVolygon': GVol.format.TopoJSON.readPGVolygonGeometry_,
  'MultiPoint': GVol.format.TopoJSON.readMultiPointGeometry_,
  'MultiLineString': GVol.format.TopoJSON.readMultiLineStringGeometry_,
  'MultiPGVolygon': GVol.format.TopoJSON.readMultiPGVolygonGeometry_
};


/**
 * Not implemented.
 * @inheritDoc
 */
GVol.format.TopoJSON.prototype.writeFeatureObject = function(feature, opt_options) {};


/**
 * Not implemented.
 * @inheritDoc
 */
GVol.format.TopoJSON.prototype.writeFeaturesObject = function(features, opt_options) {};


/**
 * Not implemented.
 * @inheritDoc
 */
GVol.format.TopoJSON.prototype.writeGeometryObject = function(geometry, opt_options) {};


/**
 * Not implemented.
 * @override
 */
GVol.format.TopoJSON.prototype.readGeometryFromObject = function() {};


/**
 * Not implemented.
 * @override
 */
GVol.format.TopoJSON.prototype.readFeatureFromObject = function() {};

goog.provide('GVol.format.WFS');

goog.require('GVol');
goog.require('GVol.asserts');
goog.require('GVol.format.GML2');
goog.require('GVol.format.GML3');
goog.require('GVol.format.GMLBase');
goog.require('GVol.format.filter');
goog.require('GVol.format.XMLFeature');
goog.require('GVol.format.XSD');
goog.require('GVol.geom.Geometry');
goog.require('GVol.obj');
goog.require('GVol.proj');
goog.require('GVol.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 GVol.format.GMLBase} which is used by this format.
 *
 * @constructor
 * @param {GVolx.format.WFSOptions=} opt_options
 *     Optional configuration object.
 * @extends {GVol.format.XMLFeature}
 * @api
 */
GVol.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 {GVol.format.GMLBase}
   */
  this.gmlFormat_ = options.gmlFormat ?
    options.gmlFormat : new GVol.format.GML3();

  /**
   * @private
   * @type {string}
   */
  this.schemaLocation_ = options.schemaLocation ?
    options.schemaLocation :
    GVol.format.WFS.SCHEMA_LOCATIONS[GVol.format.WFS.DEFAULT_VERSION];

  GVol.format.XMLFeature.call(this);
};
GVol.inherits(GVol.format.WFS, GVol.format.XMLFeature);


/**
 * @const
 * @type {string}
 */
GVol.format.WFS.FEATURE_PREFIX = 'feature';


/**
 * @const
 * @type {string}
 */
GVol.format.WFS.XMLNS = 'http://www.w3.org/2000/xmlns/';


/**
 * @const
 * @type {string}
 */
GVol.format.WFS.OGCNS = 'http://www.opengis.net/ogc';


/**
 * @const
 * @type {string}
 */
GVol.format.WFS.WFSNS = 'http://www.opengis.net/wfs';


/**
 * @const
 * @type {string}
 */
GVol.format.WFS.FESNS = 'http://www.opengis.net/fes';


/**
 * @const
 * @type {Object.<string, string>}
 */
GVol.format.WFS.SCHEMA_LOCATIONS = {
  '1.1.0': 'http://www.opengis.net/wfs ' +
      'http://schemas.opengis.net/wfs/1.1.0/wfs.xsd',
  '1.0.0': 'http://www.opengis.net/wfs ' +
      'http://schemas.opengis.net/wfs/1.0.0/wfs.xsd'
};


/**
 * @const
 * @type {string}
 */
GVol.format.WFS.DEFAULT_VERSION = '1.1.0';


/**
 * Read all features from a WFS FeatureCGVollection.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<GVol.Feature>} Features.
 * @api
 */
GVol.format.WFS.prototype.readFeatures;


/**
 * @inheritDoc
 */
GVol.format.WFS.prototype.readFeaturesFromNode = function(node, opt_options) {
  var context = /** @type {GVol.XmlNodeStackItem} */ ({
    'featureType': this.featureType_,
    'featureNS': this.featureNS_
  });
  GVol.obj.assign(context, this.getReadOptions(node,
      opt_options ? opt_options : {}));
  var objectStack = [context];
  this.gmlFormat_.FEATURE_COLLECTION_PARSERS[GVol.format.GMLBase.GMLNS][
      'featureMember'] =
      GVol.xml.makeArrayPusher(GVol.format.GMLBase.prototype.readFeaturesInternal);
  var features = GVol.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 {GVol.WFSTransactionResponse|undefined} Transaction response.
 * @api
 */
GVol.format.WFS.prototype.readTransactionResponse = function(source) {
  if (GVol.xml.isDocument(source)) {
    return this.readTransactionResponseFromDocument(
        /** @type {Document} */ (source));
  } else if (GVol.xml.isNode(source)) {
    return this.readTransactionResponseFromNode(/** @type {Node} */ (source));
  } else if (typeof source === 'string') {
    var doc = GVol.xml.parse(source);
    return this.readTransactionResponseFromDocument(doc);
  } else {
    return undefined;
  }
};


/**
 * Read feature cGVollection metadata of the source.
 *
 * @param {Document|Node|Object|string} source Source.
 * @return {GVol.WFSFeatureCGVollectionMetadata|undefined}
 *     FeatureCGVollection metadata.
 * @api
 */
GVol.format.WFS.prototype.readFeatureCGVollectionMetadata = function(source) {
  if (GVol.xml.isDocument(source)) {
    return this.readFeatureCGVollectionMetadataFromDocument(
        /** @type {Document} */ (source));
  } else if (GVol.xml.isNode(source)) {
    return this.readFeatureCGVollectionMetadataFromNode(
        /** @type {Node} */ (source));
  } else if (typeof source === 'string') {
    var doc = GVol.xml.parse(source);
    return this.readFeatureCGVollectionMetadataFromDocument(doc);
  } else {
    return undefined;
  }
};


/**
 * @param {Document} doc Document.
 * @return {GVol.WFSFeatureCGVollectionMetadata|undefined}
 *     FeatureCGVollection metadata.
 */
GVol.format.WFS.prototype.readFeatureCGVollectionMetadataFromDocument = function(doc) {
  for (var n = doc.firstChild; n; n = n.nextSibling) {
    if (n.nodeType == Node.ELEMENT_NODE) {
      return this.readFeatureCGVollectionMetadataFromNode(n);
    }
  }
  return undefined;
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WFS.FEATURE_COLLECTION_PARSERS_ = {
  'http://www.opengis.net/gml': {
    'boundedBy': GVol.xml.makeObjectPropertySetter(
        GVol.format.GMLBase.prototype.readGeometryElement, 'bounds')
  }
};


/**
 * @param {Node} node Node.
 * @return {GVol.WFSFeatureCGVollectionMetadata|undefined}
 *     FeatureCGVollection metadata.
 */
GVol.format.WFS.prototype.readFeatureCGVollectionMetadataFromNode = function(node) {
  var result = {};
  var value = GVol.format.XSD.readNonNegativeIntegerString(
      node.getAttribute('numberOfFeatures'));
  result['numberOfFeatures'] = value;
  return GVol.xml.pushParseAndPop(
      /** @type {GVol.WFSFeatureCGVollectionMetadata} */ (result),
      GVol.format.WFS.FEATURE_COLLECTION_PARSERS_, node, [], this.gmlFormat_);
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WFS.TRANSACTION_SUMMARY_PARSERS_ = {
  'http://www.opengis.net/wfs': {
    'totalInserted': GVol.xml.makeObjectPropertySetter(
        GVol.format.XSD.readNonNegativeInteger),
    'totalUpdated': GVol.xml.makeObjectPropertySetter(
        GVol.format.XSD.readNonNegativeInteger),
    'totalDeleted': GVol.xml.makeObjectPropertySetter(
        GVol.format.XSD.readNonNegativeInteger)
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Transaction Summary.
 * @private
 */
GVol.format.WFS.readTransactionSummary_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(
      {}, GVol.format.WFS.TRANSACTION_SUMMARY_PARSERS_, node, objectStack);
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WFS.OGC_FID_PARSERS_ = {
  'http://www.opengis.net/ogc': {
    'FeatureId': GVol.xml.makeArrayPusher(function(node, objectStack) {
      return node.getAttribute('fid');
    })
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 */
GVol.format.WFS.fidParser_ = function(node, objectStack) {
  GVol.xml.parseNode(GVol.format.WFS.OGC_FID_PARSERS_, node, objectStack);
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WFS.INSERT_RESULTS_PARSERS_ = {
  'http://www.opengis.net/wfs': {
    'Feature': GVol.format.WFS.fidParser_
  }
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Array.<string>|undefined} Insert results.
 * @private
 */
GVol.format.WFS.readInsertResults_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(
      [], GVol.format.WFS.INSERT_RESULTS_PARSERS_, node, objectStack);
};


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WFS.TRANSACTION_RESPONSE_PARSERS_ = {
  'http://www.opengis.net/wfs': {
    'TransactionSummary': GVol.xml.makeObjectPropertySetter(
        GVol.format.WFS.readTransactionSummary_, 'transactionSummary'),
    'InsertResults': GVol.xml.makeObjectPropertySetter(
        GVol.format.WFS.readInsertResults_, 'insertIds')
  }
};


/**
 * @param {Document} doc Document.
 * @return {GVol.WFSTransactionResponse|undefined} Transaction response.
 */
GVol.format.WFS.prototype.readTransactionResponseFromDocument = function(doc) {
  for (var n = doc.firstChild; n; n = n.nextSibling) {
    if (n.nodeType == Node.ELEMENT_NODE) {
      return this.readTransactionResponseFromNode(n);
    }
  }
  return undefined;
};


/**
 * @param {Node} node Node.
 * @return {GVol.WFSTransactionResponse|undefined} Transaction response.
 */
GVol.format.WFS.prototype.readTransactionResponseFromNode = function(node) {
  return GVol.xml.pushParseAndPop(
      /** @type {GVol.WFSTransactionResponse} */({}),
      GVol.format.WFS.TRANSACTION_RESPONSE_PARSERS_, node, []);
};


/**
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.WFS.QUERY_SERIALIZERS_ = {
  'http://www.opengis.net/wfs': {
    'PropertyName': GVol.xml.makeChildAppender(GVol.format.XSD.writeStringTextNode)
  }
};


/**
 * @param {Node} node Node.
 * @param {GVol.Feature} feature Feature.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.WFS.writeFeature_ = function(node, feature, objectStack) {
  var context = objectStack[objectStack.length - 1];
  var featureType = context['featureType'];
  var featureNS = context['featureNS'];
  var gmlVersion = context['gmlVersion'];
  var child = GVol.xml.createElementNS(featureNS, featureType);
  node.appendChild(child);
  if (gmlVersion === 2) {
    GVol.format.GML2.prototype.writeFeatureElement(child, feature, objectStack);
  } else {
    GVol.format.GML3.prototype.writeFeatureElement(child, feature, objectStack);
  }
};


/**
 * @param {Node} node Node.
 * @param {number|string} fid Feature identifier.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.WFS.writeOgcFidFilter_ = function(node, fid, objectStack) {
  var filter = GVol.xml.createElementNS(GVol.format.WFS.OGCNS, 'Filter');
  var child = GVol.xml.createElementNS(GVol.format.WFS.OGCNS, 'FeatureId');
  filter.appendChild(child);
  child.setAttribute('fid', fid);
  node.appendChild(filter);
};


/**
 * @param {string|undefined} featurePrefix The prefix of the feature.
 * @param {string} featureType The type of the feature.
 * @returns {string} The value of the typeName property.
 * @private
 */
GVol.format.WFS.getTypeName_ = function(featurePrefix, featureType) {
  featurePrefix = featurePrefix ? featurePrefix :
    GVol.format.WFS.FEATURE_PREFIX;
  var prefix = featurePrefix + ':';
  // The featureType already contains the prefix.
  if (featureType.indexOf(prefix) === 0) {
    return featureType;
  } else {
    return prefix + featureType;
  }
};


/**
 * @param {Node} node Node.
 * @param {GVol.Feature} feature Feature.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.WFS.writeDelete_ = function(node, feature, objectStack) {
  var context = objectStack[objectStack.length - 1];
  GVol.asserts.assert(feature.getId() !== undefined, 26); // Features must have an id set
  var featureType = context['featureType'];
  var featurePrefix = context['featurePrefix'];
  var featureNS = context['featureNS'];
  var typeName = GVol.format.WFS.getTypeName_(featurePrefix, featureType);
  node.setAttribute('typeName', typeName);
  GVol.xml.setAttributeNS(node, GVol.format.WFS.XMLNS, 'xmlns:' + featurePrefix,
      featureNS);
  var fid = feature.getId();
  if (fid !== undefined) {
    GVol.format.WFS.writeOgcFidFilter_(node, fid, objectStack);
  }
};


/**
 * @param {Node} node Node.
 * @param {GVol.Feature} feature Feature.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.WFS.writeUpdate_ = function(node, feature, objectStack) {
  var context = objectStack[objectStack.length - 1];
  GVol.asserts.assert(feature.getId() !== undefined, 27); // Features must have an id set
  var featureType = context['featureType'];
  var featurePrefix = context['featurePrefix'];
  var featureNS = context['featureNS'];
  var typeName = GVol.format.WFS.getTypeName_(featurePrefix, featureType);
  node.setAttribute('typeName', typeName);
  GVol.xml.setAttributeNS(node, GVol.format.WFS.XMLNS, 'xmlns:' + featurePrefix,
      featureNS);
  var fid = feature.getId();
  if (fid !== undefined) {
    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});
      }
    }
    GVol.xml.pushSerializeAndPop(/** @type {GVol.XmlNodeStackItem} */ (
      {'gmlVersion': context['gmlVersion'], node: node,
        'hasZ': context['hasZ'], 'srsName': context['srsName']}),
    GVol.format.WFS.TRANSACTION_SERIALIZERS_,
    GVol.xml.makeSimpleNodeFactory('Property'), values,
    objectStack);
    GVol.format.WFS.writeOgcFidFilter_(node, fid, objectStack);
  }
};


/**
 * @param {Node} node Node.
 * @param {Object} pair Property name and value.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.WFS.writeProperty_ = function(node, pair, objectStack) {
  var name = GVol.xml.createElementNS(GVol.format.WFS.WFSNS, 'Name');
  var context = objectStack[objectStack.length - 1];
  var gmlVersion = context['gmlVersion'];
  node.appendChild(name);
  GVol.format.XSD.writeStringTextNode(name, pair.name);
  if (pair.value !== undefined && pair.value !== null) {
    var value = GVol.xml.createElementNS(GVol.format.WFS.WFSNS, 'Value');
    node.appendChild(value);
    if (pair.value instanceof GVol.geom.Geometry) {
      if (gmlVersion === 2) {
        GVol.format.GML2.prototype.writeGeometryElement(value,
            pair.value, objectStack);
      } else {
        GVol.format.GML3.prototype.writeGeometryElement(value,
            pair.value, objectStack);
      }
    } else {
      GVol.format.XSD.writeStringTextNode(value, pair.value);
    }
  }
};


/**
 * @param {Node} node Node.
 * @param {{vendorId: string, safeToIgnore: boGVolean, value: string}}
 *     nativeElement The native element.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.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) {
    GVol.format.XSD.writeStringTextNode(node, nativeElement.value);
  }
};


/**
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.WFS.TRANSACTION_SERIALIZERS_ = {
  'http://www.opengis.net/wfs': {
    'Insert': GVol.xml.makeChildAppender(GVol.format.WFS.writeFeature_),
    'Update': GVol.xml.makeChildAppender(GVol.format.WFS.writeUpdate_),
    'Delete': GVol.xml.makeChildAppender(GVol.format.WFS.writeDelete_),
    'Property': GVol.xml.makeChildAppender(GVol.format.WFS.writeProperty_),
    'Native': GVol.xml.makeChildAppender(GVol.format.WFS.writeNative_)
  }
};


/**
 * @param {Node} node Node.
 * @param {string} featureType Feature type.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.WFS.writeQuery_ = function(node, featureType, objectStack) {
  var context = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  var featurePrefix = context['featurePrefix'];
  var featureNS = context['featureNS'];
  var propertyNames = context['propertyNames'];
  var srsName = context['srsName'];
  var typeName;
  // If feature prefix is not defined, we must not use the default prefix.
  if (featurePrefix) {
    typeName = GVol.format.WFS.getTypeName_(featurePrefix, featureType);
  } else {
    typeName = featureType;
  }
  node.setAttribute('typeName', typeName);
  if (srsName) {
    node.setAttribute('srsName', srsName);
  }
  if (featureNS) {
    GVol.xml.setAttributeNS(node, GVol.format.WFS.XMLNS, 'xmlns:' + featurePrefix,
        featureNS);
  }
  var item = /** @type {GVol.XmlNodeStackItem} */ (GVol.obj.assign({}, context));
  item.node = node;
  GVol.xml.pushSerializeAndPop(item,
      GVol.format.WFS.QUERY_SERIALIZERS_,
      GVol.xml.makeSimpleNodeFactory('PropertyName'), propertyNames,
      objectStack);
  var filter = context['filter'];
  if (filter) {
    var child = GVol.xml.createElementNS(GVol.format.WFS.OGCNS, 'Filter');
    node.appendChild(child);
    GVol.format.WFS.writeFilterCondition_(child, filter, objectStack);
  }
};


/**
 * @param {Node} node Node.
 * @param {GVol.format.filter.Filter} filter Filter.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.WFS.writeFilterCondition_ = function(node, filter, objectStack) {
  /** @type {GVol.XmlNodeStackItem} */
  var item = {node: node};
  GVol.xml.pushSerializeAndPop(item,
      GVol.format.WFS.GETFEATURE_SERIALIZERS_,
      GVol.xml.makeSimpleNodeFactory(filter.getTagName()),
      [filter], objectStack);
};


/**
 * @param {Node} node Node.
 * @param {GVol.format.filter.Bbox} filter Filter.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.WFS.writeBboxFilter_ = function(node, filter, objectStack) {
  var context = objectStack[objectStack.length - 1];
  context['srsName'] = filter.srsName;

  GVol.format.WFS.writeOgcPropertyName_(node, filter.geometryName);
  GVol.format.GML3.prototype.writeGeometryElement(node, filter.extent, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {GVol.format.filter.Intersects} filter Filter.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.WFS.writeIntersectsFilter_ = function(node, filter, objectStack) {
  var context = objectStack[objectStack.length - 1];
  context['srsName'] = filter.srsName;

  GVol.format.WFS.writeOgcPropertyName_(node, filter.geometryName);
  GVol.format.GML3.prototype.writeGeometryElement(node, filter.geometry, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {GVol.format.filter.Within} filter Filter.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.WFS.writeWithinFilter_ = function(node, filter, objectStack) {
  var context = objectStack[objectStack.length - 1];
  context['srsName'] = filter.srsName;

  GVol.format.WFS.writeOgcPropertyName_(node, filter.geometryName);
  GVol.format.GML3.prototype.writeGeometryElement(node, filter.geometry, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {GVol.format.filter.During} filter Filter.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.WFS.writeDuringFilter_ = function(node, filter, objectStack) {

  var valueReference = GVol.xml.createElementNS(GVol.format.WFS.FESNS, 'ValueReference');
  GVol.format.XSD.writeStringTextNode(valueReference, filter.propertyName);
  node.appendChild(valueReference);

  var timePeriod = GVol.xml.createElementNS(GVol.format.GMLBase.GMLNS, 'TimePeriod');

  node.appendChild(timePeriod);

  var begin = GVol.xml.createElementNS(GVol.format.GMLBase.GMLNS, 'begin');
  timePeriod.appendChild(begin);
  GVol.format.WFS.writeTimeInstant_(begin, filter.begin);

  var end = GVol.xml.createElementNS(GVol.format.GMLBase.GMLNS, 'end');
  timePeriod.appendChild(end);
  GVol.format.WFS.writeTimeInstant_(end, filter.end);
};


/**
 * @param {Node} node Node.
 * @param {GVol.format.filter.LogicalNary} filter Filter.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.WFS.writeLogicalFilter_ = function(node, filter, objectStack) {
  /** @type {GVol.XmlNodeStackItem} */
  var item = {node: node};
  var conditions = filter.conditions;
  for (var i = 0, ii = conditions.length; i < ii; ++i) {
    var condition = conditions[i];
    GVol.xml.pushSerializeAndPop(item,
        GVol.format.WFS.GETFEATURE_SERIALIZERS_,
        GVol.xml.makeSimpleNodeFactory(condition.getTagName()),
        [condition], objectStack);
  }
};


/**
 * @param {Node} node Node.
 * @param {GVol.format.filter.Not} filter Filter.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.WFS.writeNotFilter_ = function(node, filter, objectStack) {
  /** @type {GVol.XmlNodeStackItem} */
  var item = {node: node};
  var condition = filter.condition;
  GVol.xml.pushSerializeAndPop(item,
      GVol.format.WFS.GETFEATURE_SERIALIZERS_,
      GVol.xml.makeSimpleNodeFactory(condition.getTagName()),
      [condition], objectStack);
};


/**
 * @param {Node} node Node.
 * @param {GVol.format.filter.ComparisonBinary} filter Filter.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.WFS.writeComparisonFilter_ = function(node, filter, objectStack) {
  if (filter.matchCase !== undefined) {
    node.setAttribute('matchCase', filter.matchCase.toString());
  }
  GVol.format.WFS.writeOgcPropertyName_(node, filter.propertyName);
  GVol.format.WFS.writeOgcLiteral_(node, '' + filter.expression);
};


/**
 * @param {Node} node Node.
 * @param {GVol.format.filter.IsNull} filter Filter.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.WFS.writeIsNullFilter_ = function(node, filter, objectStack) {
  GVol.format.WFS.writeOgcPropertyName_(node, filter.propertyName);
};


/**
 * @param {Node} node Node.
 * @param {GVol.format.filter.IsBetween} filter Filter.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.WFS.writeIsBetweenFilter_ = function(node, filter, objectStack) {
  GVol.format.WFS.writeOgcPropertyName_(node, filter.propertyName);

  var lowerBoundary = GVol.xml.createElementNS(GVol.format.WFS.OGCNS, 'LowerBoundary');
  node.appendChild(lowerBoundary);
  GVol.format.WFS.writeOgcLiteral_(lowerBoundary, '' + filter.lowerBoundary);

  var upperBoundary = GVol.xml.createElementNS(GVol.format.WFS.OGCNS, 'UpperBoundary');
  node.appendChild(upperBoundary);
  GVol.format.WFS.writeOgcLiteral_(upperBoundary, '' + filter.upperBoundary);
};


/**
 * @param {Node} node Node.
 * @param {GVol.format.filter.IsLike} filter Filter.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.WFS.writeIsLikeFilter_ = function(node, filter, objectStack) {
  node.setAttribute('wildCard', filter.wildCard);
  node.setAttribute('singleChar', filter.singleChar);
  node.setAttribute('escapeChar', filter.escapeChar);
  if (filter.matchCase !== undefined) {
    node.setAttribute('matchCase', filter.matchCase.toString());
  }
  GVol.format.WFS.writeOgcPropertyName_(node, filter.propertyName);
  GVol.format.WFS.writeOgcLiteral_(node, '' + filter.pattern);
};


/**
 * @param {string} tagName Tag name.
 * @param {Node} node Node.
 * @param {string} value Value.
 * @private
 */
GVol.format.WFS.writeOgcExpression_ = function(tagName, node, value) {
  var property = GVol.xml.createElementNS(GVol.format.WFS.OGCNS, tagName);
  GVol.format.XSD.writeStringTextNode(property, value);
  node.appendChild(property);
};


/**
 * @param {Node} node Node.
 * @param {string} value PropertyName value.
 * @private
 */
GVol.format.WFS.writeOgcPropertyName_ = function(node, value) {
  GVol.format.WFS.writeOgcExpression_('PropertyName', node, value);
};


/**
 * @param {Node} node Node.
 * @param {string} value PropertyName value.
 * @private
 */
GVol.format.WFS.writeOgcLiteral_ = function(node, value) {
  GVol.format.WFS.writeOgcExpression_('Literal', node, value);
};


/**
 * @param {Node} node Node.
 * @param {string} time PropertyName value.
 * @private
 */
GVol.format.WFS.writeTimeInstant_ = function(node, time) {
  var timeInstant = GVol.xml.createElementNS(GVol.format.GMLBase.GMLNS, 'TimeInstant');
  node.appendChild(timeInstant);

  var timePosition = GVol.xml.createElementNS(GVol.format.GMLBase.GMLNS, 'timePosition');
  timeInstant.appendChild(timePosition);
  GVol.format.XSD.writeStringTextNode(timePosition, time);
};


/**
 * @type {Object.<string, Object.<string, GVol.XmlSerializer>>}
 * @private
 */
GVol.format.WFS.GETFEATURE_SERIALIZERS_ = {
  'http://www.opengis.net/wfs': {
    'Query': GVol.xml.makeChildAppender(GVol.format.WFS.writeQuery_)
  },
  'http://www.opengis.net/ogc': {
    'During': GVol.xml.makeChildAppender(GVol.format.WFS.writeDuringFilter_),
    'And': GVol.xml.makeChildAppender(GVol.format.WFS.writeLogicalFilter_),
    'Or': GVol.xml.makeChildAppender(GVol.format.WFS.writeLogicalFilter_),
    'Not': GVol.xml.makeChildAppender(GVol.format.WFS.writeNotFilter_),
    'BBOX': GVol.xml.makeChildAppender(GVol.format.WFS.writeBboxFilter_),
    'Intersects': GVol.xml.makeChildAppender(GVol.format.WFS.writeIntersectsFilter_),
    'Within': GVol.xml.makeChildAppender(GVol.format.WFS.writeWithinFilter_),
    'PropertyIsEqualTo': GVol.xml.makeChildAppender(GVol.format.WFS.writeComparisonFilter_),
    'PropertyIsNotEqualTo': GVol.xml.makeChildAppender(GVol.format.WFS.writeComparisonFilter_),
    'PropertyIsLessThan': GVol.xml.makeChildAppender(GVol.format.WFS.writeComparisonFilter_),
    'PropertyIsLessThanOrEqualTo': GVol.xml.makeChildAppender(GVol.format.WFS.writeComparisonFilter_),
    'PropertyIsGreaterThan': GVol.xml.makeChildAppender(GVol.format.WFS.writeComparisonFilter_),
    'PropertyIsGreaterThanOrEqualTo': GVol.xml.makeChildAppender(GVol.format.WFS.writeComparisonFilter_),
    'PropertyIsNull': GVol.xml.makeChildAppender(GVol.format.WFS.writeIsNullFilter_),
    'PropertyIsBetween': GVol.xml.makeChildAppender(GVol.format.WFS.writeIsBetweenFilter_),
    'PropertyIsLike': GVol.xml.makeChildAppender(GVol.format.WFS.writeIsLikeFilter_)
  }
};


/**
 * Encode filter as WFS `Filter` and return the Node.
 *
 * @param {GVol.format.filter.Filter} filter Filter.
 * @return {Node} Result.
 * @api
 */
GVol.format.WFS.writeFilter = function(filter) {
  var child = GVol.xml.createElementNS(GVol.format.WFS.OGCNS, 'Filter');
  GVol.format.WFS.writeFilterCondition_(child, filter, []);
  return child;
};


/**
 * @param {Node} node Node.
 * @param {Array.<string>} featureTypes Feature types.
 * @param {Array.<*>} objectStack Node stack.
 * @private
 */
GVol.format.WFS.writeGetFeature_ = function(node, featureTypes, objectStack) {
  var context = /** @type {Object} */ (objectStack[objectStack.length - 1]);
  var item = /** @type {GVol.XmlNodeStackItem} */ (GVol.obj.assign({}, context));
  item.node = node;
  GVol.xml.pushSerializeAndPop(item,
      GVol.format.WFS.GETFEATURE_SERIALIZERS_,
      GVol.xml.makeSimpleNodeFactory('Query'), featureTypes,
      objectStack);
};


/**
 * Encode format as WFS `GetFeature` and return the Node.
 *
 * @param {GVolx.format.WFSWriteGetFeatureOptions} options Options.
 * @return {Node} Result.
 * @api
 */
GVol.format.WFS.prototype.writeGetFeature = function(options) {
  var node = GVol.xml.createElementNS(GVol.format.WFS.WFSNS, 'GetFeature');
  node.setAttribute('service', 'WFS');
  node.setAttribute('version', '1.1.0');
  var filter;
  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);
    }
    filter = options.filter;
    if (options.bbox) {
      GVol.asserts.assert(options.geometryName,
          12); // `options.geometryName` must also be provided when `options.bbox` is set
      var bbox = GVol.format.filter.bbox(
          /** @type {string} */ (options.geometryName), options.bbox, options.srsName);
      if (filter) {
        // if bbox and filter are both set, combine the two into a single filter
        filter = GVol.format.filter.and(filter, bbox);
      } else {
        filter = bbox;
      }
    }
  }
  GVol.xml.setAttributeNS(node, 'http://www.w3.org/2001/XMLSchema-instance',
      'xsi:schemaLocation', this.schemaLocation_);
  /** @type {GVol.XmlNodeStackItem} */
  var context = {
    node: node,
    'srsName': options.srsName,
    'featureNS': options.featureNS ? options.featureNS : this.featureNS_,
    'featurePrefix': options.featurePrefix,
    'geometryName': options.geometryName,
    'filter': filter,
    'propertyNames': options.propertyNames ? options.propertyNames : []
  };
  GVol.asserts.assert(Array.isArray(options.featureTypes),
      11); // `options.featureTypes` should be an Array
  GVol.format.WFS.writeGetFeature_(node, /** @type {!Array.<string>} */ (options.featureTypes), [context]);
  return node;
};


/**
 * Encode format as WFS `Transaction` and return the Node.
 *
 * @param {Array.<GVol.Feature>} inserts The features to insert.
 * @param {Array.<GVol.Feature>} updates The features to update.
 * @param {Array.<GVol.Feature>} deletes The features to delete.
 * @param {GVolx.format.WFSWriteTransactionOptions} options Write options.
 * @return {Node} Result.
 * @api
 */
GVol.format.WFS.prototype.writeTransaction = function(inserts, updates, deletes,
    options) {
  var objectStack = [];
  var node = GVol.xml.createElementNS(GVol.format.WFS.WFSNS, 'Transaction');
  var version = options.version ?
    options.version : GVol.format.WFS.DEFAULT_VERSION;
  var gmlVersion = version === '1.0.0' ? 2 : 3;
  node.setAttribute('service', 'WFS');
  node.setAttribute('version', version);
  var baseObj;
  /** @type {GVol.XmlNodeStackItem} */
  var obj;
  if (options) {
    baseObj = options.gmlOptions ? options.gmlOptions : {};
    if (options.handle) {
      node.setAttribute('handle', options.handle);
    }
  }
  var schemaLocation = GVol.format.WFS.SCHEMA_LOCATIONS[version];
  GVol.xml.setAttributeNS(node, 'http://www.w3.org/2001/XMLSchema-instance',
      'xsi:schemaLocation', schemaLocation);
  var featurePrefix = options.featurePrefix ? options.featurePrefix : GVol.format.WFS.FEATURE_PREFIX;
  if (inserts) {
    obj = {node: node, 'featureNS': options.featureNS,
      'featureType': options.featureType, 'featurePrefix': featurePrefix,
      'gmlVersion': gmlVersion, 'hasZ': options.hasZ, 'srsName': options.srsName};
    GVol.obj.assign(obj, baseObj);
    GVol.xml.pushSerializeAndPop(obj,
        GVol.format.WFS.TRANSACTION_SERIALIZERS_,
        GVol.xml.makeSimpleNodeFactory('Insert'), inserts,
        objectStack);
  }
  if (updates) {
    obj = {node: node, 'featureNS': options.featureNS,
      'featureType': options.featureType, 'featurePrefix': featurePrefix,
      'gmlVersion': gmlVersion, 'hasZ': options.hasZ, 'srsName': options.srsName};
    GVol.obj.assign(obj, baseObj);
    GVol.xml.pushSerializeAndPop(obj,
        GVol.format.WFS.TRANSACTION_SERIALIZERS_,
        GVol.xml.makeSimpleNodeFactory('Update'), updates,
        objectStack);
  }
  if (deletes) {
    GVol.xml.pushSerializeAndPop({node: node, 'featureNS': options.featureNS,
      'featureType': options.featureType, 'featurePrefix': featurePrefix,
      'gmlVersion': gmlVersion, 'srsName': options.srsName},
    GVol.format.WFS.TRANSACTION_SERIALIZERS_,
    GVol.xml.makeSimpleNodeFactory('Delete'), deletes,
    objectStack);
  }
  if (options.nativeElements) {
    GVol.xml.pushSerializeAndPop({node: node, 'featureNS': options.featureNS,
      'featureType': options.featureType, 'featurePrefix': featurePrefix,
      'gmlVersion': gmlVersion, 'srsName': options.srsName},
    GVol.format.WFS.TRANSACTION_SERIALIZERS_,
    GVol.xml.makeSimpleNodeFactory('Native'), options.nativeElements,
    objectStack);
  }
  return node;
};


/**
 * Read the projection from a WFS source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @return {?GVol.proj.Projection} Projection.
 * @api
 */
GVol.format.WFS.prototype.readProjection;


/**
 * @inheritDoc
 */
GVol.format.WFS.prototype.readProjectionFromDocument = function(doc) {
  for (var n = doc.firstChild; n; n = n.nextSibling) {
    if (n.nodeType == Node.ELEMENT_NODE) {
      return this.readProjectionFromNode(n);
    }
  }
  return null;
};


/**
 * @inheritDoc
 */
GVol.format.WFS.prototype.readProjectionFromNode = function(node) {
  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 GVol.proj.get(objectStack.pop().srsName);
      }
    }
  }

  return null;
};

goog.provide('GVol.format.WKT');

goog.require('GVol');
goog.require('GVol.Feature');
goog.require('GVol.format.Feature');
goog.require('GVol.format.TextFeature');
goog.require('GVol.geom.GeometryCGVollection');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.geom.GeometryLayout');
goog.require('GVol.geom.LineString');
goog.require('GVol.geom.MultiLineString');
goog.require('GVol.geom.MultiPoint');
goog.require('GVol.geom.MultiPGVolygon');
goog.require('GVol.geom.Point');
goog.require('GVol.geom.PGVolygon');
goog.require('GVol.geom.SimpleGeometry');


/**
 * @classdesc
 * Geometry format for reading and writing data in the `WellKnownText` (WKT)
 * format.
 *
 * @constructor
 * @extends {GVol.format.TextFeature}
 * @param {GVolx.format.WKTOptions=} opt_options Options.
 * @api
 */
GVol.format.WKT = function(opt_options) {

  var options = opt_options ? opt_options : {};

  GVol.format.TextFeature.call(this);

  /**
   * Split GeometryCGVollection into multiple features.
   * @type {boGVolean}
   * @private
   */
  this.splitCGVollection_ = options.splitCGVollection !== undefined ?
    options.splitCGVollection : false;

};
GVol.inherits(GVol.format.WKT, GVol.format.TextFeature);


/**
 * @const
 * @type {string}
 */
GVol.format.WKT.EMPTY = 'EMPTY';


/**
 * @const
 * @type {string}
 */
GVol.format.WKT.Z = 'Z';


/**
 * @const
 * @type {string}
 */
GVol.format.WKT.M = 'M';


/**
 * @const
 * @type {string}
 */
GVol.format.WKT.ZM = 'ZM';


/**
 * @param {GVol.geom.Point} geom Point geometry.
 * @return {string} Coordinates part of Point as WKT.
 * @private
 */
GVol.format.WKT.encodePointGeometry_ = function(geom) {
  var coordinates = geom.getCoordinates();
  if (coordinates.length === 0) {
    return '';
  }
  return coordinates.join(' ');
};


/**
 * @param {GVol.geom.MultiPoint} geom MultiPoint geometry.
 * @return {string} Coordinates part of MultiPoint as WKT.
 * @private
 */
GVol.format.WKT.encodeMultiPointGeometry_ = function(geom) {
  var array = [];
  var components = geom.getPoints();
  for (var i = 0, ii = components.length; i < ii; ++i) {
    array.push('(' + GVol.format.WKT.encodePointGeometry_(components[i]) + ')');
  }
  return array.join(',');
};


/**
 * @param {GVol.geom.GeometryCGVollection} geom GeometryCGVollection geometry.
 * @return {string} Coordinates part of GeometryCGVollection as WKT.
 * @private
 */
GVol.format.WKT.encodeGeometryCGVollectionGeometry_ = function(geom) {
  var array = [];
  var geoms = geom.getGeometries();
  for (var i = 0, ii = geoms.length; i < ii; ++i) {
    array.push(GVol.format.WKT.encode_(geoms[i]));
  }
  return array.join(',');
};


/**
 * @param {GVol.geom.LineString|GVol.geom.LinearRing} geom LineString geometry.
 * @return {string} Coordinates part of LineString as WKT.
 * @private
 */
GVol.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].join(' '));
  }
  return array.join(',');
};


/**
 * @param {GVol.geom.MultiLineString} geom MultiLineString geometry.
 * @return {string} Coordinates part of MultiLineString as WKT.
 * @private
 */
GVol.format.WKT.encodeMultiLineStringGeometry_ = function(geom) {
  var array = [];
  var components = geom.getLineStrings();
  for (var i = 0, ii = components.length; i < ii; ++i) {
    array.push('(' + GVol.format.WKT.encodeLineStringGeometry_(
        components[i]) + ')');
  }
  return array.join(',');
};


/**
 * @param {GVol.geom.PGVolygon} geom PGVolygon geometry.
 * @return {string} Coordinates part of PGVolygon as WKT.
 * @private
 */
GVol.format.WKT.encodePGVolygonGeometry_ = function(geom) {
  var array = [];
  var rings = geom.getLinearRings();
  for (var i = 0, ii = rings.length; i < ii; ++i) {
    array.push('(' + GVol.format.WKT.encodeLineStringGeometry_(
        rings[i]) + ')');
  }
  return array.join(',');
};


/**
 * @param {GVol.geom.MultiPGVolygon} geom MultiPGVolygon geometry.
 * @return {string} Coordinates part of MultiPGVolygon as WKT.
 * @private
 */
GVol.format.WKT.encodeMultiPGVolygonGeometry_ = function(geom) {
  var array = [];
  var components = geom.getPGVolygons();
  for (var i = 0, ii = components.length; i < ii; ++i) {
    array.push('(' + GVol.format.WKT.encodePGVolygonGeometry_(
        components[i]) + ')');
  }
  return array.join(',');
};

/**
 * @param {GVol.geom.SimpleGeometry} geom SimpleGeometry geometry.
 * @return {string} Potential dimensional information for WKT type.
 * @private
 */
GVol.format.WKT.encodeGeometryLayout_ = function(geom) {
  var layout = geom.getLayout();
  var dimInfo = '';
  if (layout === GVol.geom.GeometryLayout.XYZ || layout === GVol.geom.GeometryLayout.XYZM) {
    dimInfo += GVol.format.WKT.Z;
  }
  if (layout === GVol.geom.GeometryLayout.XYM || layout === GVol.geom.GeometryLayout.XYZM) {
    dimInfo += GVol.format.WKT.M;
  }
  return dimInfo;
};


/**
 * Encode a geometry as WKT.
 * @param {GVol.geom.Geometry} geom The geometry to encode.
 * @return {string} WKT string for the geometry.
 * @private
 */
GVol.format.WKT.encode_ = function(geom) {
  var type = geom.getType();
  var geometryEncoder = GVol.format.WKT.GeometryEncoder_[type];
  var enc = geometryEncoder(geom);
  type = type.toUpperCase();
  if (geom instanceof GVol.geom.SimpleGeometry) {
    var dimInfo = GVol.format.WKT.encodeGeometryLayout_(geom);
    if (dimInfo.length > 0) {
      type += ' ' + dimInfo;
    }
  }
  if (enc.length === 0) {
    return type + ' ' + GVol.format.WKT.EMPTY;
  }
  return type + '(' + enc + ')';
};


/**
 * @const
 * @type {Object.<string, function(GVol.geom.Geometry): string>}
 * @private
 */
GVol.format.WKT.GeometryEncoder_ = {
  'Point': GVol.format.WKT.encodePointGeometry_,
  'LineString': GVol.format.WKT.encodeLineStringGeometry_,
  'PGVolygon': GVol.format.WKT.encodePGVolygonGeometry_,
  'MultiPoint': GVol.format.WKT.encodeMultiPointGeometry_,
  'MultiLineString': GVol.format.WKT.encodeMultiLineStringGeometry_,
  'MultiPGVolygon': GVol.format.WKT.encodeMultiPGVolygonGeometry_,
  'GeometryCGVollection': GVol.format.WKT.encodeGeometryCGVollectionGeometry_
};


/**
 * Parse a WKT string.
 * @param {string} wkt WKT string.
 * @return {GVol.geom.Geometry|undefined}
 *     The geometry created.
 * @private
 */
GVol.format.WKT.prototype.parse_ = function(wkt) {
  var lexer = new GVol.format.WKT.Lexer(wkt);
  var parser = new GVol.format.WKT.Parser(lexer);
  return parser.parse();
};


/**
 * Read a feature from a WKT source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {GVol.Feature} Feature.
 * @api
 */
GVol.format.WKT.prototype.readFeature;


/**
 * @inheritDoc
 */
GVol.format.WKT.prototype.readFeatureFromText = function(text, opt_options) {
  var geom = this.readGeometryFromText(text, opt_options);
  if (geom) {
    var feature = new GVol.Feature();
    feature.setGeometry(geom);
    return feature;
  }
  return null;
};


/**
 * Read all features from a WKT source.
 *
 * @function
 * @param {Document|Node|Object|string} source Source.
 * @param {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {Array.<GVol.Feature>} Features.
 * @api
 */
GVol.format.WKT.prototype.readFeatures;


/**
 * @inheritDoc
 */
GVol.format.WKT.prototype.readFeaturesFromText = function(text, opt_options) {
  var geometries = [];
  var geometry = this.readGeometryFromText(text, opt_options);
  if (this.splitCGVollection_ &&
      geometry.getType() == GVol.geom.GeometryType.GEOMETRY_COLLECTION) {
    geometries = (/** @type {GVol.geom.GeometryCGVollection} */ (geometry))
        .getGeometriesArray();
  } else {
    geometries = [geometry];
  }
  var feature, features = [];
  for (var i = 0, ii = geometries.length; i < ii; ++i) {
    feature = new GVol.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 {GVolx.format.ReadOptions=} opt_options Read options.
 * @return {GVol.geom.Geometry} Geometry.
 * @api
 */
GVol.format.WKT.prototype.readGeometry;


/**
 * @inheritDoc
 */
GVol.format.WKT.prototype.readGeometryFromText = function(text, opt_options) {
  var geometry = this.parse_(text);
  if (geometry) {
    return /** @type {GVol.geom.Geometry} */ (
      GVol.format.Feature.transformWithOptions(geometry, false, opt_options));
  } else {
    return null;
  }
};


/**
 * Encode a feature as a WKT string.
 *
 * @function
 * @param {GVol.Feature} feature Feature.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {string} WKT string.
 * @api
 */
GVol.format.WKT.prototype.writeFeature;


/**
 * @inheritDoc
 */
GVol.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.<GVol.Feature>} features Features.
 * @param {GVolx.format.WriteOptions=} opt_options Write options.
 * @return {string} WKT string.
 * @api
 */
GVol.format.WKT.prototype.writeFeatures;


/**
 * @inheritDoc
 */
GVol.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 cGVollection = new GVol.geom.GeometryCGVollection(geometries);
  return this.writeGeometryText(cGVollection, opt_options);
};


/**
 * Write a single geometry as a WKT string.
 *
 * @function
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @return {string} WKT string.
 * @api
 */
GVol.format.WKT.prototype.writeGeometry;


/**
 * @inheritDoc
 */
GVol.format.WKT.prototype.writeGeometryText = function(geometry, opt_options) {
  return GVol.format.WKT.encode_(/** @type {GVol.geom.Geometry} */ (
    GVol.format.Feature.transformWithOptions(geometry, true, opt_options)));
};


/**
 * @const
 * @enum {number}
 * @private
 */
GVol.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
 */
GVol.format.WKT.Lexer = function(wkt) {

  /**
   * @type {string}
   */
  this.wkt = wkt;

  /**
   * @type {number}
   * @private
   */
  this.index_ = -1;
};


/**
 * @param {string} c Character.
 * @return {boGVolean} Whether the character is alphabetic.
 * @private
 */
GVol.format.WKT.Lexer.prototype.isAlpha_ = function(c) {
  return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z';
};


/**
 * @param {string} c Character.
 * @param {boGVolean=} opt_decimal Whether the string number
 *     contains a dot, i.e. is a decimal number.
 * @return {boGVolean} Whether the character is numeric.
 * @private
 */
GVol.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 {boGVolean} Whether the character is whitespace.
 * @private
 */
GVol.format.WKT.Lexer.prototype.isWhiteSpace_ = function(c) {
  return c == ' ' || c == '\t' || c == '\r' || c == '\n';
};


/**
 * @return {string} Next string character.
 * @private
 */
GVol.format.WKT.Lexer.prototype.nextChar_ = function() {
  return this.wkt.charAt(++this.index_);
};


/**
 * Fetch and return the next token.
 * @return {!GVol.WKTToken} Next string token.
 */
GVol.format.WKT.Lexer.prototype.nextToken = function() {
  var c = this.nextChar_();
  var token = {position: this.index_, value: c};

  if (c == '(') {
    token.type = GVol.format.WKT.TokenType_.LEFT_PAREN;
  } else if (c == ',') {
    token.type = GVol.format.WKT.TokenType_.COMMA;
  } else if (c == ')') {
    token.type = GVol.format.WKT.TokenType_.RIGHT_PAREN;
  } else if (this.isNumeric_(c) || c == '-') {
    token.type = GVol.format.WKT.TokenType_.NUMBER;
    token.value = this.readNumber_();
  } else if (this.isAlpha_(c)) {
    token.type = GVol.format.WKT.TokenType_.TEXT;
    token.value = this.readText_();
  } else if (this.isWhiteSpace_(c)) {
    return this.nextToken();
  } else if (c === '') {
    token.type = GVol.format.WKT.TokenType_.EOF;
  } else {
    throw new Error('Unexpected character: ' + c);
  }

  return token;
};


/**
 * @return {number} Numeric token value.
 * @private
 */
GVol.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
 */
GVol.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 {GVol.format.WKT.Lexer} lexer The lexer.
 * @constructor
 * @protected
 */
GVol.format.WKT.Parser = function(lexer) {

  /**
   * @type {GVol.format.WKT.Lexer}
   * @private
   */
  this.lexer_ = lexer;

  /**
   * @type {GVol.WKTToken}
   * @private
   */
  this.token_;

  /**
   * @type {GVol.geom.GeometryLayout}
   * @private
   */
  this.layout_ = GVol.geom.GeometryLayout.XY;
};


/**
 * Fetch the next token form the lexer and replace the active token.
 * @private
 */
GVol.format.WKT.Parser.prototype.consume_ = function() {
  this.token_ = this.lexer_.nextToken();
};

/**
 * Tests if the given type matches the type of the current token.
 * @param {GVol.format.WKT.TokenType_} type Token type.
 * @return {boGVolean} Whether the token matches the given type.
 */
GVol.format.WKT.Parser.prototype.isTokenType = function(type) {
  var isMatch = this.token_.type == type;
  return isMatch;
};


/**
 * If the given type matches the current token, consume it.
 * @param {GVol.format.WKT.TokenType_} type Token type.
 * @return {boGVolean} Whether the token matches the given type.
 */
GVol.format.WKT.Parser.prototype.match = function(type) {
  var isMatch = this.isTokenType(type);
  if (isMatch) {
    this.consume_();
  }
  return isMatch;
};


/**
 * Try to parse the tokens provided by the lexer.
 * @return {GVol.geom.Geometry} The geometry.
 */
GVol.format.WKT.Parser.prototype.parse = function() {
  this.consume_();
  var geometry = this.parseGeometry_();
  return geometry;
};


/**
 * Try to parse the dimensional info.
 * @return {GVol.geom.GeometryLayout} The layout.
 * @private
 */
GVol.format.WKT.Parser.prototype.parseGeometryLayout_ = function() {
  var layout = GVol.geom.GeometryLayout.XY;
  var dimToken = this.token_;
  if (this.isTokenType(GVol.format.WKT.TokenType_.TEXT)) {
    var dimInfo = dimToken.value;
    if (dimInfo === GVol.format.WKT.Z) {
      layout = GVol.geom.GeometryLayout.XYZ;
    } else if (dimInfo === GVol.format.WKT.M) {
      layout = GVol.geom.GeometryLayout.XYM;
    } else if (dimInfo === GVol.format.WKT.ZM) {
      layout = GVol.geom.GeometryLayout.XYZM;
    }
    if (layout !== GVol.geom.GeometryLayout.XY) {
      this.consume_();
    }
  }
  return layout;
};


/**
 * @return {!GVol.geom.Geometry} The geometry.
 * @private
 */
GVol.format.WKT.Parser.prototype.parseGeometry_ = function() {
  var token = this.token_;
  if (this.match(GVol.format.WKT.TokenType_.TEXT)) {
    var geomType = token.value;
    this.layout_ = this.parseGeometryLayout_();
    if (geomType == GVol.geom.GeometryType.GEOMETRY_COLLECTION.toUpperCase()) {
      var geometries = this.parseGeometryCGVollectionText_();
      return new GVol.geom.GeometryCGVollection(geometries);
    } else {
      var parser = GVol.format.WKT.Parser.GeometryParser_[geomType];
      var ctor = GVol.format.WKT.Parser.GeometryConstructor_[geomType];
      if (!parser || !ctor) {
        throw new Error('Invalid geometry type: ' + geomType);
      }
      var coordinates = parser.call(this);
      return new ctor(coordinates, this.layout_);
    }
  }
  throw new Error(this.formatErrorMessage_());
};


/**
 * @return {!Array.<GVol.geom.Geometry>} A cGVollection of geometries.
 * @private
 */
GVol.format.WKT.Parser.prototype.parseGeometryCGVollectionText_ = function() {
  if (this.match(GVol.format.WKT.TokenType_.LEFT_PAREN)) {
    var geometries = [];
    do {
      geometries.push(this.parseGeometry_());
    } while (this.match(GVol.format.WKT.TokenType_.COMMA));
    if (this.match(GVol.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
 */
GVol.format.WKT.Parser.prototype.parsePointText_ = function() {
  if (this.match(GVol.format.WKT.TokenType_.LEFT_PAREN)) {
    var coordinates = this.parsePoint_();
    if (this.match(GVol.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
 */
GVol.format.WKT.Parser.prototype.parseLineStringText_ = function() {
  if (this.match(GVol.format.WKT.TokenType_.LEFT_PAREN)) {
    var coordinates = this.parsePointList_();
    if (this.match(GVol.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 pGVolygon.
 * @private
 */
GVol.format.WKT.Parser.prototype.parsePGVolygonText_ = function() {
  if (this.match(GVol.format.WKT.TokenType_.LEFT_PAREN)) {
    var coordinates = this.parseLineStringTextList_();
    if (this.match(GVol.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
 */
GVol.format.WKT.Parser.prototype.parseMultiPointText_ = function() {
  if (this.match(GVol.format.WKT.TokenType_.LEFT_PAREN)) {
    var coordinates;
    if (this.token_.type == GVol.format.WKT.TokenType_.LEFT_PAREN) {
      coordinates = this.parsePointTextList_();
    } else {
      coordinates = this.parsePointList_();
    }
    if (this.match(GVol.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
 */
GVol.format.WKT.Parser.prototype.parseMultiLineStringText_ = function() {
  if (this.match(GVol.format.WKT.TokenType_.LEFT_PAREN)) {
    var coordinates = this.parseLineStringTextList_();
    if (this.match(GVol.format.WKT.TokenType_.RIGHT_PAREN)) {
      return coordinates;
    }
  } else if (this.isEmptyGeometry_()) {
    return [];
  }
  throw new Error(this.formatErrorMessage_());
};


/**
 * @return {!Array.<!Array.<number>>} All pGVolygon points in a multipGVolygon.
 * @private
 */
GVol.format.WKT.Parser.prototype.parseMultiPGVolygonText_ = function() {
  if (this.match(GVol.format.WKT.TokenType_.LEFT_PAREN)) {
    var coordinates = this.parsePGVolygonTextList_();
    if (this.match(GVol.format.WKT.TokenType_.RIGHT_PAREN)) {
      return coordinates;
    }
  } else if (this.isEmptyGeometry_()) {
    return [];
  }
  throw new Error(this.formatErrorMessage_());
};


/**
 * @return {!Array.<number>} A point.
 * @private
 */
GVol.format.WKT.Parser.prototype.parsePoint_ = function() {
  var coordinates = [];
  var dimensions = this.layout_.length;
  for (var i = 0; i < dimensions; ++i) {
    var token = this.token_;
    if (this.match(GVol.format.WKT.TokenType_.NUMBER)) {
      coordinates.push(token.value);
    } else {
      break;
    }
  }
  if (coordinates.length == dimensions) {
    return coordinates;
  }
  throw new Error(this.formatErrorMessage_());
};


/**
 * @return {!Array.<!Array.<number>>} An array of points.
 * @private
 */
GVol.format.WKT.Parser.prototype.parsePointList_ = function() {
  var coordinates = [this.parsePoint_()];
  while (this.match(GVol.format.WKT.TokenType_.COMMA)) {
    coordinates.push(this.parsePoint_());
  }
  return coordinates;
};


/**
 * @return {!Array.<!Array.<number>>} An array of points.
 * @private
 */
GVol.format.WKT.Parser.prototype.parsePointTextList_ = function() {
  var coordinates = [this.parsePointText_()];
  while (this.match(GVol.format.WKT.TokenType_.COMMA)) {
    coordinates.push(this.parsePointText_());
  }
  return coordinates;
};


/**
 * @return {!Array.<!Array.<number>>} An array of points.
 * @private
 */
GVol.format.WKT.Parser.prototype.parseLineStringTextList_ = function() {
  var coordinates = [this.parseLineStringText_()];
  while (this.match(GVol.format.WKT.TokenType_.COMMA)) {
    coordinates.push(this.parseLineStringText_());
  }
  return coordinates;
};


/**
 * @return {!Array.<!Array.<number>>} An array of points.
 * @private
 */
GVol.format.WKT.Parser.prototype.parsePGVolygonTextList_ = function() {
  var coordinates = [this.parsePGVolygonText_()];
  while (this.match(GVol.format.WKT.TokenType_.COMMA)) {
    coordinates.push(this.parsePGVolygonText_());
  }
  return coordinates;
};


/**
 * @return {boGVolean} Whether the token implies an empty geometry.
 * @private
 */
GVol.format.WKT.Parser.prototype.isEmptyGeometry_ = function() {
  var isEmpty = this.isTokenType(GVol.format.WKT.TokenType_.TEXT) &&
      this.token_.value == GVol.format.WKT.EMPTY;
  if (isEmpty) {
    this.consume_();
  }
  return isEmpty;
};


/**
 * Create an error message for an unexpected token error.
 * @return {string} Error message.
 * @private
 */
GVol.format.WKT.Parser.prototype.formatErrorMessage_ = function() {
  return 'Unexpected `' + this.token_.value + '` at position ' +
      this.token_.position + ' in `' + this.lexer_.wkt + '`';
};


/**
 * @enum {function (new:GVol.geom.Geometry, Array, GVol.geom.GeometryLayout)}
 * @private
 */
GVol.format.WKT.Parser.GeometryConstructor_ = {
  'POINT': GVol.geom.Point,
  'LINESTRING': GVol.geom.LineString,
  'POLYGON': GVol.geom.PGVolygon,
  'MULTIPOINT': GVol.geom.MultiPoint,
  'MULTILINESTRING': GVol.geom.MultiLineString,
  'MULTIPOLYGON': GVol.geom.MultiPGVolygon
};


/**
 * @enum {(function(): Array)}
 * @private
 */
GVol.format.WKT.Parser.GeometryParser_ = {
  'POINT': GVol.format.WKT.Parser.prototype.parsePointText_,
  'LINESTRING': GVol.format.WKT.Parser.prototype.parseLineStringText_,
  'POLYGON': GVol.format.WKT.Parser.prototype.parsePGVolygonText_,
  'MULTIPOINT': GVol.format.WKT.Parser.prototype.parseMultiPointText_,
  'MULTILINESTRING': GVol.format.WKT.Parser.prototype.parseMultiLineStringText_,
  'MULTIPOLYGON': GVol.format.WKT.Parser.prototype.parseMultiPGVolygonText_
};

goog.provide('GVol.format.WMSCapabilities');

goog.require('GVol');
goog.require('GVol.format.XLink');
goog.require('GVol.format.XML');
goog.require('GVol.format.XSD');
goog.require('GVol.xml');


/**
 * @classdesc
 * Format for reading WMS capabilities data
 *
 * @constructor
 * @extends {GVol.format.XML}
 * @api
 */
GVol.format.WMSCapabilities = function() {

  GVol.format.XML.call(this);

  /**
   * @type {string|undefined}
   */
  this.version = undefined;
};
GVol.inherits(GVol.format.WMSCapabilities, GVol.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
 */
GVol.format.WMSCapabilities.prototype.read;


/**
 * @inheritDoc
 */
GVol.format.WMSCapabilities.prototype.readFromDocument = function(doc) {
  for (var n = doc.firstChild; n; n = n.nextSibling) {
    if (n.nodeType == Node.ELEMENT_NODE) {
      return this.readFromNode(n);
    }
  }
  return null;
};


/**
 * @inheritDoc
 */
GVol.format.WMSCapabilities.prototype.readFromNode = function(node) {
  this.version = node.getAttribute('version').trim();
  var wmsCapabilityObject = GVol.xml.pushParseAndPop({
    'version': this.version
  }, GVol.format.WMSCapabilities.PARSERS_, node, []);
  return wmsCapabilityObject ? wmsCapabilityObject : null;
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Attribution object.
 */
GVol.format.WMSCapabilities.readAttribution_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(
      {}, GVol.format.WMSCapabilities.ATTRIBUTION_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object} Bounding box object.
 */
GVol.format.WMSCapabilities.readBoundingBox_ = function(node, objectStack) {
  var extent = [
    GVol.format.XSD.readDecimalString(node.getAttribute('minx')),
    GVol.format.XSD.readDecimalString(node.getAttribute('miny')),
    GVol.format.XSD.readDecimalString(node.getAttribute('maxx')),
    GVol.format.XSD.readDecimalString(node.getAttribute('maxy'))
  ];

  var resGVolutions = [
    GVol.format.XSD.readDecimalString(node.getAttribute('resx')),
    GVol.format.XSD.readDecimalString(node.getAttribute('resy'))
  ];

  return {
    'crs': node.getAttribute('CRS'),
    'extent': extent,
    'res': resGVolutions
  };
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {GVol.Extent|undefined} Bounding box object.
 */
GVol.format.WMSCapabilities.readEXGeographicBoundingBox_ = function(node, objectStack) {
  var geographicBoundingBox = GVol.xml.pushParseAndPop(
      {},
      GVol.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 [
    westBoundLongitude, southBoundLatitude,
    eastBoundLongitude, northBoundLatitude
  ];
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} Capability object.
 */
GVol.format.WMSCapabilities.readCapability_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(
      {}, GVol.format.WMSCapabilities.CAPABILITY_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} Service object.
 */
GVol.format.WMSCapabilities.readService_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(
      {}, GVol.format.WMSCapabilities.SERVICE_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} Contact information object.
 */
GVol.format.WMSCapabilities.readContactInformation_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(
      {}, GVol.format.WMSCapabilities.CONTACT_INFORMATION_PARSERS_,
      node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} Contact person object.
 */
GVol.format.WMSCapabilities.readContactPersonPrimary_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(
      {}, GVol.format.WMSCapabilities.CONTACT_PERSON_PARSERS_,
      node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} Contact address object.
 */
GVol.format.WMSCapabilities.readContactAddress_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(
      {}, GVol.format.WMSCapabilities.CONTACT_ADDRESS_PARSERS_,
      node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Array.<string>|undefined} Format array.
 */
GVol.format.WMSCapabilities.readException_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(
      [], GVol.format.WMSCapabilities.EXCEPTION_PARSERS_, node, objectStack);
};


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @private
 * @return {Object|undefined} Layer object.
 */
GVol.format.WMSCapabilities.readCapabilityLayer_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(
      {}, GVol.format.WMSCapabilities.LAYER_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Layer object.
 */
GVol.format.WMSCapabilities.readLayer_ = function(node, objectStack) {
  var parentLayerObject = /**  @type {Object.<string,*>} */
        (objectStack[objectStack.length - 1]);

  var layerObject = GVol.xml.pushParseAndPop(
      {}, GVol.format.WMSCapabilities.LAYER_PARSERS_, node, objectStack);

  if (!layerObject) {
    return undefined;
  }
  var queryable =
        GVol.format.XSD.readBoGVoleanString(node.getAttribute('queryable'));
  if (queryable === undefined) {
    queryable = parentLayerObject['queryable'];
  }
  layerObject['queryable'] = queryable !== undefined ? queryable : false;

  var cascaded = GVol.format.XSD.readNonNegativeIntegerString(
      node.getAttribute('cascaded'));
  if (cascaded === undefined) {
    cascaded = parentLayerObject['cascaded'];
  }
  layerObject['cascaded'] = cascaded;

  var opaque = GVol.format.XSD.readBoGVoleanString(node.getAttribute('opaque'));
  if (opaque === undefined) {
    opaque = parentLayerObject['opaque'];
  }
  layerObject['opaque'] = opaque !== undefined ? opaque : false;

  var noSubsets =
        GVol.format.XSD.readBoGVoleanString(node.getAttribute('noSubsets'));
  if (noSubsets === undefined) {
    noSubsets = parentLayerObject['noSubsets'];
  }
  layerObject['noSubsets'] = noSubsets !== undefined ? noSubsets : false;

  var fixedWidth =
        GVol.format.XSD.readDecimalString(node.getAttribute('fixedWidth'));
  if (!fixedWidth) {
    fixedWidth = parentLayerObject['fixedWidth'];
  }
  layerObject['fixedWidth'] = fixedWidth;

  var fixedHeight =
        GVol.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 = layerObject[key] || [];
      layerObject[key] = childValue.concat(parentLayerObject[key]);
    }
  });

  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.
 */
GVol.format.WMSCapabilities.readDimension_ = function(node, objectStack) {
  var dimensionObject = {
    'name': node.getAttribute('name'),
    'units': node.getAttribute('units'),
    'unitSymbGVol': node.getAttribute('unitSymbGVol'),
    'default': node.getAttribute('default'),
    'multipleValues': GVol.format.XSD.readBoGVoleanString(
        node.getAttribute('multipleValues')),
    'nearestValue': GVol.format.XSD.readBoGVoleanString(
        node.getAttribute('nearestValue')),
    'current': GVol.format.XSD.readBoGVoleanString(node.getAttribute('current')),
    'values': GVol.format.XSD.readString(node)
  };
  return dimensionObject;
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Online resource object.
 */
GVol.format.WMSCapabilities.readFormatOnlineresource_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(
      {}, GVol.format.WMSCapabilities.FORMAT_ONLINERESOURCE_PARSERS_,
      node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Request object.
 */
GVol.format.WMSCapabilities.readRequest_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(
      {}, GVol.format.WMSCapabilities.REQUEST_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} DCP type object.
 */
GVol.format.WMSCapabilities.readDCPType_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(
      {}, GVol.format.WMSCapabilities.DCPTYPE_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} HTTP object.
 */
GVol.format.WMSCapabilities.readHTTP_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(
      {}, GVol.format.WMSCapabilities.HTTP_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Operation type object.
 */
GVol.format.WMSCapabilities.readOperationType_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(
      {}, GVol.format.WMSCapabilities.OPERATIONTYPE_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Online resource object.
 */
GVol.format.WMSCapabilities.readSizedFormatOnlineresource_ = function(node, objectStack) {
  var formatOnlineresource =
        GVol.format.WMSCapabilities.readFormatOnlineresource_(node, objectStack);
  if (formatOnlineresource) {
    var size = [
      GVol.format.XSD.readNonNegativeIntegerString(node.getAttribute('width')),
      GVol.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.
 */
GVol.format.WMSCapabilities.readAuthorityURL_ = function(node, objectStack) {
  var authorityObject =
        GVol.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.
 */
GVol.format.WMSCapabilities.readMetadataURL_ = function(node, objectStack) {
  var metadataObject =
        GVol.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.
 */
GVol.format.WMSCapabilities.readStyle_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(
      {}, GVol.format.WMSCapabilities.STYLE_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Array.<string>|undefined} Keyword list.
 */
GVol.format.WMSCapabilities.readKeywordList_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop(
      [], GVol.format.WMSCapabilities.KEYWORDLIST_PARSERS_, node, objectStack);
};


/**
 * @const
 * @private
 * @type {Array.<string>}
 */
GVol.format.WMSCapabilities.NAMESPACE_URIS_ = [
  null,
  'http://www.opengis.net/wms'
];


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMSCapabilities.PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Service': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMSCapabilities.readService_),
      'Capability': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMSCapabilities.readCapability_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMSCapabilities.CAPABILITY_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Request': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMSCapabilities.readRequest_),
      'Exception': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMSCapabilities.readException_),
      'Layer': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMSCapabilities.readCapabilityLayer_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMSCapabilities.SERVICE_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Name': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'Title': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'Abstract': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'KeywordList': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMSCapabilities.readKeywordList_),
      'OnlineResource': GVol.xml.makeObjectPropertySetter(
          GVol.format.XLink.readHref),
      'ContactInformation': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMSCapabilities.readContactInformation_),
      'Fees': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'AccessConstraints': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString),
      'LayerLimit': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readNonNegativeInteger),
      'MaxWidth': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readNonNegativeInteger),
      'MaxHeight': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readNonNegativeInteger)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMSCapabilities.CONTACT_INFORMATION_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'ContactPersonPrimary': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMSCapabilities.readContactPersonPrimary_),
      'ContactPosition': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString),
      'ContactAddress': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMSCapabilities.readContactAddress_),
      'ContactVoiceTelephone': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString),
      'ContactFacsimileTelephone': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString),
      'ContactElectronicMailAddress': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMSCapabilities.CONTACT_PERSON_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'ContactPerson': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString),
      'ContactOrganization': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMSCapabilities.CONTACT_ADDRESS_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'AddressType': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'Address': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'City': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'StateOrProvince': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString),
      'PostCode': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'Country': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMSCapabilities.EXCEPTION_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Format': GVol.xml.makeArrayPusher(GVol.format.XSD.readString)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMSCapabilities.LAYER_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Name': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'Title': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'Abstract': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'KeywordList': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMSCapabilities.readKeywordList_),
      'CRS': GVol.xml.makeObjectPropertyPusher(GVol.format.XSD.readString),
      'EX_GeographicBoundingBox': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMSCapabilities.readEXGeographicBoundingBox_),
      'BoundingBox': GVol.xml.makeObjectPropertyPusher(
          GVol.format.WMSCapabilities.readBoundingBox_),
      'Dimension': GVol.xml.makeObjectPropertyPusher(
          GVol.format.WMSCapabilities.readDimension_),
      'Attribution': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMSCapabilities.readAttribution_),
      'AuthorityURL': GVol.xml.makeObjectPropertyPusher(
          GVol.format.WMSCapabilities.readAuthorityURL_),
      'Identifier': GVol.xml.makeObjectPropertyPusher(GVol.format.XSD.readString),
      'MetadataURL': GVol.xml.makeObjectPropertyPusher(
          GVol.format.WMSCapabilities.readMetadataURL_),
      'DataURL': GVol.xml.makeObjectPropertyPusher(
          GVol.format.WMSCapabilities.readFormatOnlineresource_),
      'FeatureListURL': GVol.xml.makeObjectPropertyPusher(
          GVol.format.WMSCapabilities.readFormatOnlineresource_),
      'Style': GVol.xml.makeObjectPropertyPusher(
          GVol.format.WMSCapabilities.readStyle_),
      'MinScaleDenominator': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readDecimal),
      'MaxScaleDenominator': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readDecimal),
      'Layer': GVol.xml.makeObjectPropertyPusher(
          GVol.format.WMSCapabilities.readLayer_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMSCapabilities.ATTRIBUTION_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Title': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'OnlineResource': GVol.xml.makeObjectPropertySetter(
          GVol.format.XLink.readHref),
      'LogoURL': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMSCapabilities.readSizedFormatOnlineresource_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMSCapabilities.EX_GEOGRAPHIC_BOUNDING_BOX_PARSERS_ =
    GVol.xml.makeStructureNS(GVol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'westBoundLongitude': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readDecimal),
      'eastBoundLongitude': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readDecimal),
      'southBoundLatitude': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readDecimal),
      'northBoundLatitude': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readDecimal)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMSCapabilities.REQUEST_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'GetCapabilities': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMSCapabilities.readOperationType_),
      'GetMap': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMSCapabilities.readOperationType_),
      'GetFeatureInfo': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMSCapabilities.readOperationType_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMSCapabilities.OPERATIONTYPE_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Format': GVol.xml.makeObjectPropertyPusher(GVol.format.XSD.readString),
      'DCPType': GVol.xml.makeObjectPropertyPusher(
          GVol.format.WMSCapabilities.readDCPType_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMSCapabilities.DCPTYPE_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'HTTP': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMSCapabilities.readHTTP_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMSCapabilities.HTTP_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Get': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMSCapabilities.readFormatOnlineresource_),
      'Post': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMSCapabilities.readFormatOnlineresource_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMSCapabilities.STYLE_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Name': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'Title': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'Abstract': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'LegendURL': GVol.xml.makeObjectPropertyPusher(
          GVol.format.WMSCapabilities.readSizedFormatOnlineresource_),
      'StyleSheetURL': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMSCapabilities.readFormatOnlineresource_),
      'StyleURL': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMSCapabilities.readFormatOnlineresource_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMSCapabilities.FORMAT_ONLINERESOURCE_PARSERS_ =
    GVol.xml.makeStructureNS(GVol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Format': GVol.xml.makeObjectPropertySetter(GVol.format.XSD.readString),
      'OnlineResource': GVol.xml.makeObjectPropertySetter(
          GVol.format.XLink.readHref)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMSCapabilities.KEYWORDLIST_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMSCapabilities.NAMESPACE_URIS_, {
      'Keyword': GVol.xml.makeArrayPusher(GVol.format.XSD.readString)
    });

goog.provide('GVol.format.WMSGetFeatureInfo');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.format.GML2');
goog.require('GVol.format.XMLFeature');
goog.require('GVol.obj');
goog.require('GVol.xml');


/**
 * @classdesc
 * Format for reading WMSGetFeatureInfo format. It uses
 * {@link GVol.format.GML2} to read features.
 *
 * @constructor
 * @extends {GVol.format.XMLFeature}
 * @param {GVolx.format.WMSGetFeatureInfoOptions=} opt_options Options.
 * @api
 */
GVol.format.WMSGetFeatureInfo = function(opt_options) {

  var options = opt_options ? opt_options : {};

  /**
   * @private
   * @type {string}
   */
  this.featureNS_ = 'http://mapserver.gis.umn.edu/mapserver';


  /**
   * @private
   * @type {GVol.format.GML2}
   */
  this.gmlFormat_ = new GVol.format.GML2();


  /**
   * @private
   * @type {Array.<string>}
   */
  this.layers_ = options.layers ? options.layers : null;

  GVol.format.XMLFeature.call(this);
};
GVol.inherits(GVol.format.WMSGetFeatureInfo, GVol.format.XMLFeature);


/**
 * @const
 * @type {string}
 * @private
 */
GVol.format.WMSGetFeatureInfo.featureIdentifier_ = '_feature';


/**
 * @const
 * @type {string}
 * @private
 */
GVol.format.WMSGetFeatureInfo.layerIdentifier_ = '_layer';


/**
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Array.<GVol.Feature>} Features.
 * @private
 */
GVol.format.WMSGetFeatureInfo.prototype.readFeatures_ = function(node, objectStack) {
  node.setAttribute('namespaceURI', this.featureNS_);
  var localName = node.localName;
  /** @type {Array.<GVol.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 !== Node.ELEMENT_NODE) {
        continue;
      }
      var context = objectStack[0];

      var toRemove = GVol.format.WMSGetFeatureInfo.layerIdentifier_;
      var layerName = layer.localName.replace(toRemove, '');

      if (this.layers_ && !GVol.array.includes(this.layers_, layerName)) {
        continue;
      }

      var featureType = layerName +
          GVol.format.WMSGetFeatureInfo.featureIdentifier_;

      context['featureType'] = featureType;
      context['featureNS'] = this.featureNS_;

      var parsers = {};
      parsers[featureType] = GVol.xml.makeArrayPusher(
          this.gmlFormat_.readFeatureElement, this.gmlFormat_);
      var parsersNS = GVol.xml.makeStructureNS(
          [context['featureNS'], null], parsers);
      layer.setAttribute('namespaceURI', this.featureNS_);
      var layerFeatures = GVol.xml.pushParseAndPop(
          [], parsersNS, layer, objectStack, this.gmlFormat_);
      if (layerFeatures) {
        GVol.array.extend(features, layerFeatures);
      }
    }
  }
  if (localName == 'FeatureCGVollection') {
    var gmlFeatures = GVol.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 {GVolx.format.ReadOptions=} opt_options Options.
 * @return {Array.<GVol.Feature>} Features.
 * @api
 */
GVol.format.WMSGetFeatureInfo.prototype.readFeatures;


/**
 * @inheritDoc
 */
GVol.format.WMSGetFeatureInfo.prototype.readFeaturesFromNode = function(node, opt_options) {
  var options = {};
  if (opt_options) {
    GVol.obj.assign(options, this.getReadOptions(node, opt_options));
  }
  return this.readFeatures_(node, [options]);
};


/**
 * Not implemented.
 * @inheritDoc
 */
GVol.format.WMSGetFeatureInfo.prototype.writeFeatureNode = function(feature, opt_options) {};


/**
 * Not implemented.
 * @inheritDoc
 */
GVol.format.WMSGetFeatureInfo.prototype.writeFeaturesNode = function(features, opt_options) {};


/**
 * Not implemented.
 * @inheritDoc
 */
GVol.format.WMSGetFeatureInfo.prototype.writeGeometryNode = function(geometry, opt_options) {};

goog.provide('GVol.format.WMTSCapabilities');

goog.require('GVol');
goog.require('GVol.extent');
goog.require('GVol.format.OWS');
goog.require('GVol.format.XLink');
goog.require('GVol.format.XML');
goog.require('GVol.format.XSD');
goog.require('GVol.xml');


/**
 * @classdesc
 * Format for reading WMTS capabilities data.
 *
 * @constructor
 * @extends {GVol.format.XML}
 * @api
 */
GVol.format.WMTSCapabilities = function() {
  GVol.format.XML.call(this);

  /**
   * @type {GVol.format.OWS}
   * @private
   */
  this.owsParser_ = new GVol.format.OWS();
};
GVol.inherits(GVol.format.WMTSCapabilities, GVol.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
 */
GVol.format.WMTSCapabilities.prototype.read;


/**
 * @inheritDoc
 */
GVol.format.WMTSCapabilities.prototype.readFromDocument = function(doc) {
  for (var n = doc.firstChild; n; n = n.nextSibling) {
    if (n.nodeType == Node.ELEMENT_NODE) {
      return this.readFromNode(n);
    }
  }
  return null;
};


/**
 * @inheritDoc
 */
GVol.format.WMTSCapabilities.prototype.readFromNode = function(node) {
  var version = node.getAttribute('version').trim();
  var WMTSCapabilityObject = this.owsParser_.readFromNode(node);
  if (!WMTSCapabilityObject) {
    return null;
  }
  WMTSCapabilityObject['version'] = version;
  WMTSCapabilityObject = GVol.xml.pushParseAndPop(WMTSCapabilityObject,
      GVol.format.WMTSCapabilities.PARSERS_, node, []);
  return WMTSCapabilityObject ? WMTSCapabilityObject : null;
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Attribution object.
 */
GVol.format.WMTSCapabilities.readContents_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop({},
      GVol.format.WMTSCapabilities.CONTENTS_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Layers object.
 */
GVol.format.WMTSCapabilities.readLayer_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop({},
      GVol.format.WMTSCapabilities.LAYER_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Tile Matrix Set object.
 */
GVol.format.WMTSCapabilities.readTileMatrixSet_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop({},
      GVol.format.WMTSCapabilities.TMS_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Style object.
 */
GVol.format.WMTSCapabilities.readStyle_ = function(node, objectStack) {
  var style = GVol.xml.pushParseAndPop({},
      GVol.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.
 */
GVol.format.WMTSCapabilities.readTileMatrixSetLink_ = function(node,
    objectStack) {
  return GVol.xml.pushParseAndPop({},
      GVol.format.WMTSCapabilities.TMS_LINKS_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Dimension object.
 */
GVol.format.WMTSCapabilities.readDimensions_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop({},
      GVol.format.WMTSCapabilities.DIMENSION_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Resource URL object.
 */
GVol.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.
 */
GVol.format.WMTSCapabilities.readWgs84BoundingBox_ = function(node, objectStack) {
  var coordinates = GVol.xml.pushParseAndPop([],
      GVol.format.WMTSCapabilities.WGS84_BBOX_READERS_, node, objectStack);
  if (coordinates.length != 2) {
    return undefined;
  }
  return GVol.extent.boundingExtent(coordinates);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Legend object.
 */
GVol.format.WMTSCapabilities.readLegendUrl_ = function(node, objectStack) {
  var legend = {};
  legend['format'] = node.getAttribute('format');
  legend['href'] = GVol.format.XLink.readHref(node);
  return legend;
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} Coordinates object.
 */
GVol.format.WMTSCapabilities.readCoordinates_ = function(node, objectStack) {
  var coordinates = GVol.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.
 */
GVol.format.WMTSCapabilities.readTileMatrix_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop({},
      GVol.format.WMTSCapabilities.TM_PARSERS_, node, objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} TileMatrixSetLimits Object.
 */
GVol.format.WMTSCapabilities.readTileMatrixLimitsList_ = function(node,
    objectStack) {
  return GVol.xml.pushParseAndPop([],
      GVol.format.WMTSCapabilities.TMS_LIMITS_LIST_PARSERS_, node,
      objectStack);
};


/**
 * @private
 * @param {Node} node Node.
 * @param {Array.<*>} objectStack Object stack.
 * @return {Object|undefined} TileMatrixLimits Array.
 */
GVol.format.WMTSCapabilities.readTileMatrixLimits_ = function(node, objectStack) {
  return GVol.xml.pushParseAndPop({},
      GVol.format.WMTSCapabilities.TMS_LIMITS_PARSERS_, node, objectStack);
};


/**
 * @const
 * @private
 * @type {Array.<string>}
 */
GVol.format.WMTSCapabilities.NAMESPACE_URIS_ = [
  null,
  'http://www.opengis.net/wmts/1.0'
];


/**
 * @const
 * @private
 * @type {Array.<string>}
 */
GVol.format.WMTSCapabilities.OWS_NAMESPACE_URIS_ = [
  null,
  'http://www.opengis.net/ows/1.1'
];


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMTSCapabilities.PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMTSCapabilities.NAMESPACE_URIS_, {
      'Contents': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMTSCapabilities.readContents_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMTSCapabilities.CONTENTS_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMTSCapabilities.NAMESPACE_URIS_, {
      'Layer': GVol.xml.makeObjectPropertyPusher(
          GVol.format.WMTSCapabilities.readLayer_),
      'TileMatrixSet': GVol.xml.makeObjectPropertyPusher(
          GVol.format.WMTSCapabilities.readTileMatrixSet_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMTSCapabilities.LAYER_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMTSCapabilities.NAMESPACE_URIS_, {
      'Style': GVol.xml.makeObjectPropertyPusher(
          GVol.format.WMTSCapabilities.readStyle_),
      'Format': GVol.xml.makeObjectPropertyPusher(
          GVol.format.XSD.readString),
      'TileMatrixSetLink': GVol.xml.makeObjectPropertyPusher(
          GVol.format.WMTSCapabilities.readTileMatrixSetLink_),
      'Dimension': GVol.xml.makeObjectPropertyPusher(
          GVol.format.WMTSCapabilities.readDimensions_),
      'ResourceURL': GVol.xml.makeObjectPropertyPusher(
          GVol.format.WMTSCapabilities.readResourceUrl_)
    }, GVol.xml.makeStructureNS(GVol.format.WMTSCapabilities.OWS_NAMESPACE_URIS_, {
      'Title': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString),
      'Abstract': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString),
      'WGS84BoundingBox': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMTSCapabilities.readWgs84BoundingBox_),
      'Identifier': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString)
    }));


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMTSCapabilities.STYLE_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMTSCapabilities.NAMESPACE_URIS_, {
      'LegendURL': GVol.xml.makeObjectPropertyPusher(
          GVol.format.WMTSCapabilities.readLegendUrl_)
    }, GVol.xml.makeStructureNS(GVol.format.WMTSCapabilities.OWS_NAMESPACE_URIS_, {
      'Title': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString),
      'Identifier': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString)
    }));


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMTSCapabilities.TMS_LINKS_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMTSCapabilities.NAMESPACE_URIS_, {
      'TileMatrixSet': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString),
      'TileMatrixSetLimits': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMTSCapabilities.readTileMatrixLimitsList_)
    });

/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMTSCapabilities.TMS_LIMITS_LIST_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMTSCapabilities.NAMESPACE_URIS_, {
      'TileMatrixLimits': GVol.xml.makeArrayPusher(
          GVol.format.WMTSCapabilities.readTileMatrixLimits_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMTSCapabilities.TMS_LIMITS_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMTSCapabilities.NAMESPACE_URIS_, {
      'TileMatrix': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString),
      'MinTileRow': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readNonNegativeInteger),
      'MaxTileRow': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readNonNegativeInteger),
      'MinTileCGVol': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readNonNegativeInteger),
      'MaxTileCGVol': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readNonNegativeInteger)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMTSCapabilities.DIMENSION_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMTSCapabilities.NAMESPACE_URIS_, {
      'Default': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString),
      'Value': GVol.xml.makeObjectPropertyPusher(
          GVol.format.XSD.readString)
    }, GVol.xml.makeStructureNS(GVol.format.WMTSCapabilities.OWS_NAMESPACE_URIS_, {
      'Identifier': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString)
    }));


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMTSCapabilities.WGS84_BBOX_READERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMTSCapabilities.OWS_NAMESPACE_URIS_, {
      'LowerCorner': GVol.xml.makeArrayPusher(
          GVol.format.WMTSCapabilities.readCoordinates_),
      'UpperCorner': GVol.xml.makeArrayPusher(
          GVol.format.WMTSCapabilities.readCoordinates_)
    });


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMTSCapabilities.TMS_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMTSCapabilities.NAMESPACE_URIS_, {
      'WellKnownScaleSet': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString),
      'TileMatrix': GVol.xml.makeObjectPropertyPusher(
          GVol.format.WMTSCapabilities.readTileMatrix_)
    }, GVol.xml.makeStructureNS(GVol.format.WMTSCapabilities.OWS_NAMESPACE_URIS_, {
      'SupportedCRS': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString),
      'Identifier': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString)
    }));


/**
 * @const
 * @type {Object.<string, Object.<string, GVol.XmlParser>>}
 * @private
 */
GVol.format.WMTSCapabilities.TM_PARSERS_ = GVol.xml.makeStructureNS(
    GVol.format.WMTSCapabilities.NAMESPACE_URIS_, {
      'TopLeftCorner': GVol.xml.makeObjectPropertySetter(
          GVol.format.WMTSCapabilities.readCoordinates_),
      'ScaleDenominator': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readDecimal),
      'TileWidth': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readNonNegativeInteger),
      'TileHeight': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readNonNegativeInteger),
      'MatrixWidth': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readNonNegativeInteger),
      'MatrixHeight': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readNonNegativeInteger)
    }, GVol.xml.makeStructureNS(GVol.format.WMTSCapabilities.OWS_NAMESPACE_URIS_, {
      'Identifier': GVol.xml.makeObjectPropertySetter(
          GVol.format.XSD.readString)
    }));

goog.provide('GVol.GeGVolocationProperty');


/**
 * @enum {string}
 */
GVol.GeGVolocationProperty = {
  ACCURACY: 'accuracy',
  ACCURACY_GEOMETRY: 'accuracyGeometry',
  ALTITUDE: 'altitude',
  ALTITUDE_ACCURACY: 'altitudeAccuracy',
  HEADING: 'heading',
  POSITION: 'position',
  PROJECTION: 'projection',
  SPEED: 'speed',
  TRACKING: 'tracking',
  TRACKING_OPTIONS: 'trackingOptions'
};

// FIXME handle geGVolocation not supported

goog.provide('GVol.GeGVolocation');

goog.require('GVol');
goog.require('GVol.GeGVolocationProperty');
goog.require('GVol.Object');
goog.require('GVol.Sphere');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.geom.PGVolygon');
goog.require('GVol.has');
goog.require('GVol.math');
goog.require('GVol.proj');
goog.require('GVol.proj.EPSG4326');


/**
 * @classdesc
 * Helper class for providing HTML5 GeGVolocation capabilities.
 * The [GeGVolocation API](http://www.w3.org/TR/geGVolocation-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 `GVol.GeGVolocation`.
 *
 * Example:
 *
 *     var geGVolocation = new GVol.GeGVolocation({
 *       // take the projection to use from the map's view
 *       projection: view.getProjection()
 *     });
 *     // listen to changes in position
 *     geGVolocation.on('change', function(evt) {
 *       window.consGVole.log(geGVolocation.getPosition());
 *     });
 *
 * @fires error
 * @constructor
 * @extends {GVol.Object}
 * @param {GVolx.GeGVolocationOptions=} opt_options Options.
 * @api
 */
GVol.GeGVolocation = function(opt_options) {

  GVol.Object.call(this);

  var options = opt_options || {};

  /**
   * The unprojected (EPSG:4326) device position.
   * @private
   * @type {GVol.Coordinate}
   */
  this.position_ = null;

  /**
   * @private
   * @type {GVol.TransformFunction}
   */
  this.transform_ = GVol.proj.identityTransform;

  /**
   * @private
   * @type {GVol.Sphere}
   */
  this.sphere_ = new GVol.Sphere(GVol.proj.EPSG4326.RADIUS);

  /**
   * @private
   * @type {number|undefined}
   */
  this.watchId_ = undefined;

  GVol.events.listen(
      this, GVol.Object.getChangeEventType(GVol.GeGVolocationProperty.PROJECTION),
      this.handleProjectionChanged_, this);
  GVol.events.listen(
      this, GVol.Object.getChangeEventType(GVol.GeGVolocationProperty.TRACKING),
      this.handleTrackingChanged_, this);

  if (options.projection !== undefined) {
    this.setProjection(options.projection);
  }
  if (options.trackingOptions !== undefined) {
    this.setTrackingOptions(options.trackingOptions);
  }

  this.setTracking(options.tracking !== undefined ? options.tracking : false);

};
GVol.inherits(GVol.GeGVolocation, GVol.Object);


/**
 * @inheritDoc
 */
GVol.GeGVolocation.prototype.disposeInternal = function() {
  this.setTracking(false);
  GVol.Object.prototype.disposeInternal.call(this);
};


/**
 * @private
 */
GVol.GeGVolocation.prototype.handleProjectionChanged_ = function() {
  var projection = this.getProjection();
  if (projection) {
    this.transform_ = GVol.proj.getTransformFromProjections(
        GVol.proj.get('EPSG:4326'), projection);
    if (this.position_) {
      this.set(
          GVol.GeGVolocationProperty.POSITION, this.transform_(this.position_));
    }
  }
};


/**
 * @private
 */
GVol.GeGVolocation.prototype.handleTrackingChanged_ = function() {
  if (GVol.has.GEOLOCATION) {
    var tracking = this.getTracking();
    if (tracking && this.watchId_ === undefined) {
      this.watchId_ = navigator.geGVolocation.watchPosition(
          this.positionChange_.bind(this),
          this.positionError_.bind(this),
          this.getTrackingOptions());
    } else if (!tracking && this.watchId_ !== undefined) {
      navigator.geGVolocation.clearWatch(this.watchId_);
      this.watchId_ = undefined;
    }
  }
};


/**
 * @private
 * @param {GeGVolocationPosition} position position event.
 */
GVol.GeGVolocation.prototype.positionChange_ = function(position) {
  var coords = position.coords;
  this.set(GVol.GeGVolocationProperty.ACCURACY, coords.accuracy);
  this.set(GVol.GeGVolocationProperty.ALTITUDE,
      coords.altitude === null ? undefined : coords.altitude);
  this.set(GVol.GeGVolocationProperty.ALTITUDE_ACCURACY,
      coords.altitudeAccuracy === null ?
        undefined : coords.altitudeAccuracy);
  this.set(GVol.GeGVolocationProperty.HEADING, coords.heading === null ?
    undefined : GVol.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(GVol.GeGVolocationProperty.POSITION, projectedPosition);
  this.set(GVol.GeGVolocationProperty.SPEED,
      coords.speed === null ? undefined : coords.speed);
  var geometry = GVol.geom.PGVolygon.circular(
      this.sphere_, this.position_, coords.accuracy);
  geometry.applyTransform(this.transform_);
  this.set(GVol.GeGVolocationProperty.ACCURACY_GEOMETRY, geometry);
  this.changed();
};

/**
 * Triggered when the GeGVolocation returns an error.
 * @event error
 * @api
 */

/**
 * @private
 * @param {GeGVolocationPositionError} error error object.
 */
GVol.GeGVolocation.prototype.positionError_ = function(error) {
  error.type = GVol.events.EventType.ERROR;
  this.setTracking(false);
  this.dispatchEvent(/** @type {{type: string, target: undefined}} */ (error));
};


/**
 * Get the accuracy of the position in meters.
 * @return {number|undefined} The accuracy of the position measurement in
 *     meters.
 * @observable
 * @api
 */
GVol.GeGVolocation.prototype.getAccuracy = function() {
  return /** @type {number|undefined} */ (
    this.get(GVol.GeGVolocationProperty.ACCURACY));
};


/**
 * Get a geometry of the position accuracy.
 * @return {?GVol.geom.PGVolygon} A geometry of the position accuracy.
 * @observable
 * @api
 */
GVol.GeGVolocation.prototype.getAccuracyGeometry = function() {
  return /** @type {?GVol.geom.PGVolygon} */ (
    this.get(GVol.GeGVolocationProperty.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
 */
GVol.GeGVolocation.prototype.getAltitude = function() {
  return /** @type {number|undefined} */ (
    this.get(GVol.GeGVolocationProperty.ALTITUDE));
};


/**
 * Get the altitude accuracy of the position.
 * @return {number|undefined} The accuracy of the altitude measurement in
 *     meters.
 * @observable
 * @api
 */
GVol.GeGVolocation.prototype.getAltitudeAccuracy = function() {
  return /** @type {number|undefined} */ (
    this.get(GVol.GeGVolocationProperty.ALTITUDE_ACCURACY));
};


/**
 * Get the heading as radians clockwise from North.
 * @return {number|undefined} The heading of the device in radians from north.
 * @observable
 * @api
 */
GVol.GeGVolocation.prototype.getHeading = function() {
  return /** @type {number|undefined} */ (
    this.get(GVol.GeGVolocationProperty.HEADING));
};


/**
 * Get the position of the device.
 * @return {GVol.Coordinate|undefined} The current position of the device reported
 *     in the current projection.
 * @observable
 * @api
 */
GVol.GeGVolocation.prototype.getPosition = function() {
  return /** @type {GVol.Coordinate|undefined} */ (
    this.get(GVol.GeGVolocationProperty.POSITION));
};


/**
 * Get the projection associated with the position.
 * @return {GVol.proj.Projection|undefined} The projection the position is
 *     reported in.
 * @observable
 * @api
 */
GVol.GeGVolocation.prototype.getProjection = function() {
  return /** @type {GVol.proj.Projection|undefined} */ (
    this.get(GVol.GeGVolocationProperty.PROJECTION));
};


/**
 * Get the speed in meters per second.
 * @return {number|undefined} The instantaneous speed of the device in meters
 *     per second.
 * @observable
 * @api
 */
GVol.GeGVolocation.prototype.getSpeed = function() {
  return /** @type {number|undefined} */ (
    this.get(GVol.GeGVolocationProperty.SPEED));
};


/**
 * Determine if the device location is being tracked.
 * @return {boGVolean} The device location is being tracked.
 * @observable
 * @api
 */
GVol.GeGVolocation.prototype.getTracking = function() {
  return /** @type {boGVolean} */ (
    this.get(GVol.GeGVolocationProperty.TRACKING));
};


/**
 * Get the tracking options.
 * @see http://www.w3.org/TR/geGVolocation-API/#position-options
 * @return {GeGVolocationPositionOptions|undefined} PositionOptions as defined by
 *     the [HTML5 GeGVolocation spec
 *     ](http://www.w3.org/TR/geGVolocation-API/#position_options_interface).
 * @observable
 * @api
 */
GVol.GeGVolocation.prototype.getTrackingOptions = function() {
  return /** @type {GeGVolocationPositionOptions|undefined} */ (
    this.get(GVol.GeGVolocationProperty.TRACKING_OPTIONS));
};


/**
 * Set the projection to use for transforming the coordinates.
 * @param {GVol.ProjectionLike} projection The projection the position is
 *     reported in.
 * @observable
 * @api
 */
GVol.GeGVolocation.prototype.setProjection = function(projection) {
  this.set(GVol.GeGVolocationProperty.PROJECTION, GVol.proj.get(projection));
};


/**
 * Enable or disable tracking.
 * @param {boGVolean} tracking Enable tracking.
 * @observable
 * @api
 */
GVol.GeGVolocation.prototype.setTracking = function(tracking) {
  this.set(GVol.GeGVolocationProperty.TRACKING, tracking);
};


/**
 * Set the tracking options.
 * @see http://www.w3.org/TR/geGVolocation-API/#position-options
 * @param {GeGVolocationPositionOptions} options PositionOptions as defined by the
 *     [HTML5 GeGVolocation spec
 *     ](http://www.w3.org/TR/geGVolocation-API/#position_options_interface).
 * @observable
 * @api
 */
GVol.GeGVolocation.prototype.setTrackingOptions = function(options) {
  this.set(GVol.GeGVolocationProperty.TRACKING_OPTIONS, options);
};

goog.provide('GVol.geom.Circle');

goog.require('GVol');
goog.require('GVol.extent');
goog.require('GVol.geom.GeometryLayout');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.geom.SimpleGeometry');
goog.require('GVol.geom.flat.deflate');


/**
 * @classdesc
 * Circle geometry.
 *
 * @constructor
 * @extends {GVol.geom.SimpleGeometry}
 * @param {GVol.Coordinate} center Center.
 * @param {number=} opt_radius Radius.
 * @param {GVol.geom.GeometryLayout=} opt_layout Layout.
 * @api
 */
GVol.geom.Circle = function(center, opt_radius, opt_layout) {
  GVol.geom.SimpleGeometry.call(this);
  var radius = opt_radius ? opt_radius : 0;
  this.setCenterAndRadius(center, radius, opt_layout);
};
GVol.inherits(GVol.geom.Circle, GVol.geom.SimpleGeometry);


/**
 * Make a complete copy of the geometry.
 * @return {!GVol.geom.Circle} Clone.
 * @override
 * @api
 */
GVol.geom.Circle.prototype.clone = function() {
  var circle = new GVol.geom.Circle(null);
  circle.setFlatCoordinates(this.layout, this.flatCoordinates.slice());
  return circle;
};


/**
 * @inheritDoc
 */
GVol.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
 */
GVol.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 GVol.Coordinate coordinate}.
 * @return {GVol.Coordinate} Center.
 * @api
 */
GVol.geom.Circle.prototype.getCenter = function() {
  return this.flatCoordinates.slice(0, this.stride);
};


/**
 * @inheritDoc
 */
GVol.geom.Circle.prototype.computeExtent = function(extent) {
  var flatCoordinates = this.flatCoordinates;
  var radius = flatCoordinates[this.stride] - flatCoordinates[0];
  return GVol.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
 */
GVol.geom.Circle.prototype.getRadius = function() {
  return Math.sqrt(this.getRadiusSquared_());
};


/**
 * @private
 * @return {number} Radius squared.
 */
GVol.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
 */
GVol.geom.Circle.prototype.getType = function() {
  return GVol.geom.GeometryType.CIRCLE;
};


/**
 * @inheritDoc
 * @api
 */
GVol.geom.Circle.prototype.intersectsExtent = function(extent) {
  var circleExtent = this.getExtent();
  if (GVol.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 GVol.extent.forEachCorner(extent, this.intersectsCoordinate, this);
  }
  return false;

};


/**
 * Set the center of the circle as {@link GVol.Coordinate coordinate}.
 * @param {GVol.Coordinate} center Center.
 * @api
 */
GVol.geom.Circle.prototype.setCenter = function(center) {
  var stride = this.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 GVol.Coordinate coordinate}) and the radius (as
 * number) of the circle.
 * @param {GVol.Coordinate} center Center.
 * @param {number} radius Radius.
 * @param {GVol.geom.GeometryLayout=} opt_layout Layout.
 * @api
 */
GVol.geom.Circle.prototype.setCenterAndRadius = function(center, radius, opt_layout) {
  if (!center) {
    this.setFlatCoordinates(GVol.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 = GVol.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();
  }
};


/**
 * @inheritDoc
 */
GVol.geom.Circle.prototype.getCoordinates = function() {};


/**
 * @inheritDoc
 */
GVol.geom.Circle.prototype.setCoordinates = function(coordinates, opt_layout) {};


/**
 * @param {GVol.geom.GeometryLayout} layout Layout.
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 */
GVol.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
 */
GVol.geom.Circle.prototype.setRadius = function(radius) {
  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 {GVol.ProjectionLike} source The current projection.  Can be a
 *     string identifier or a {@link GVol.proj.Projection} object.
 * @param {GVol.ProjectionLike} destination The desired projection.  Can be a
 *     string identifier or a {@link GVol.proj.Projection} object.
 * @return {GVol.geom.Circle} This geometry.  Note that original geometry is
 *     modified in place.
 * @function
 * @api
 */
GVol.geom.Circle.prototype.transform;

goog.provide('GVol.geom.flat.geodesic');

goog.require('GVol.math');
goog.require('GVol.proj');


/**
 * @private
 * @param {function(number): GVol.Coordinate} interpGVolate InterpGVolate function.
 * @param {GVol.TransformFunction} transform Transform from longitude/latitude to
 *     projected coordinates.
 * @param {number} squaredTGVolerance Squared tGVolerance.
 * @return {Array.<number>} Flat coordinates.
 */
GVol.geom.flat.geodesic.line_ = function(interpGVolate, transform, squaredTGVolerance) {
  // FIXME reduce garbage generation
  // FIXME optimize stack operations

  /** @type {Array.<number>} */
  var flatCoordinates = [];

  var geoA = interpGVolate(0);
  var geoB = interpGVolate(1);

  var a = transform(geoA);
  var b = transform(geoB);

  /** @type {Array.<GVol.Coordinate>} */
  var geoStack = [geoB, geoA];
  /** @type {Array.<GVol.Coordinate>} */
  var stack = [b, a];
  /** @type {Array.<number>} */
  var fractionStack = [1, 0];

  /** @type {Object.<string, boGVolean>} */
  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 = interpGVolate(fracM);
    m = transform(geoM);
    if (GVol.math.squaredSegmentDistance(m[0], m[1], a[0], a[1],
        b[0], b[1]) < squaredTGVolerance) {
      // 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();
      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);
    }
  }

  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 {GVol.proj.Projection} projection Projection.
* @param {number} squaredTGVolerance Squared tGVolerance.
* @return {Array.<number>} Flat coordinates.
*/
GVol.geom.flat.geodesic.greatCircleArc = function(
    lon1, lat1, lon2, lat2, projection, squaredTGVolerance) {

  var geoProjection = GVol.proj.get('EPSG:4326');

  var cosLat1 = Math.cos(GVol.math.toRadians(lat1));
  var sinLat1 = Math.sin(GVol.math.toRadians(lat1));
  var cosLat2 = Math.cos(GVol.math.toRadians(lat2));
  var sinLat2 = Math.sin(GVol.math.toRadians(lat2));
  var cosDeltaLon = Math.cos(GVol.math.toRadians(lon2 - lon1));
  var sinDeltaLon = Math.sin(GVol.math.toRadians(lon2 - lon1));
  var d = sinLat1 * sinLat2 + cosLat1 * cosLat2 * cosDeltaLon;

  return GVol.geom.flat.geodesic.line_(
      /**
       * @param {number} frac Fraction.
       * @return {GVol.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 = GVol.math.toRadians(lon1) +
            Math.atan2(Math.sin(theta) * sinD * cosLat1,
                cosD - sinLat1 * Math.sin(lat));
        return [GVol.math.toDegrees(lon), GVol.math.toDegrees(lat)];
      }, GVol.proj.getTransform(geoProjection, projection), squaredTGVolerance);
};


/**
 * Generate a meridian (line at constant longitude).
 * @param {number} lon Longitude.
 * @param {number} lat1 Latitude 1.
 * @param {number} lat2 Latitude 2.
 * @param {GVol.proj.Projection} projection Projection.
 * @param {number} squaredTGVolerance Squared tGVolerance.
 * @return {Array.<number>} Flat coordinates.
 */
GVol.geom.flat.geodesic.meridian = function(lon, lat1, lat2, projection, squaredTGVolerance) {
  var epsg4326Projection = GVol.proj.get('EPSG:4326');
  return GVol.geom.flat.geodesic.line_(
      /**
       * @param {number} frac Fraction.
       * @return {GVol.Coordinate} Coordinate.
       */
      function(frac) {
        return [lon, lat1 + ((lat2 - lat1) * frac)];
      },
      GVol.proj.getTransform(epsg4326Projection, projection), squaredTGVolerance);
};


/**
 * Generate a parallel (line at constant latitude).
 * @param {number} lat Latitude.
 * @param {number} lon1 Longitude 1.
 * @param {number} lon2 Longitude 2.
 * @param {GVol.proj.Projection} projection Projection.
 * @param {number} squaredTGVolerance Squared tGVolerance.
 * @return {Array.<number>} Flat coordinates.
 */
GVol.geom.flat.geodesic.parallel = function(lat, lon1, lon2, projection, squaredTGVolerance) {
  var epsg4326Projection = GVol.proj.get('EPSG:4326');
  return GVol.geom.flat.geodesic.line_(
      /**
       * @param {number} frac Fraction.
       * @return {GVol.Coordinate} Coordinate.
       */
      function(frac) {
        return [lon1 + ((lon2 - lon1) * frac), lat];
      },
      GVol.proj.getTransform(epsg4326Projection, projection), squaredTGVolerance);
};

goog.provide('GVol.Graticule');

goog.require('GVol.coordinate');
goog.require('GVol.extent');
goog.require('GVol.geom.GeometryLayout');
goog.require('GVol.geom.LineString');
goog.require('GVol.geom.Point');
goog.require('GVol.geom.flat.geodesic');
goog.require('GVol.math');
goog.require('GVol.proj');
goog.require('GVol.render.EventType');
goog.require('GVol.style.Fill');
goog.require('GVol.style.Stroke');
goog.require('GVol.style.Text');


/**
 * Render a grid for a coordinate system on a map.
 * @constructor
 * @param {GVolx.GraticuleOptions=} opt_options Options.
 * @api
 */
GVol.Graticule = function(opt_options) {
  var options = opt_options || {};

  /**
   * @type {GVol.Map}
   * @private
   */
  this.map_ = null;

  /**
   * @type {GVol.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;

  /**
   * @type {Array.<GVol.geom.LineString>}
   * @private
   */
  this.meridians_ = [];

  /**
   * @type {Array.<GVol.geom.LineString>}
   * @private
   */
  this.parallels_ = [];

  /**
   * @type {GVol.style.Stroke}
   * @private
   */
  this.strokeStyle_ = options.strokeStyle !== undefined ?
    options.strokeStyle : GVol.Graticule.DEFAULT_STROKE_STYLE_;

  /**
   * @type {GVol.TransformFunction|undefined}
   * @private
   */
  this.fromLonLatTransform_ = undefined;

  /**
   * @type {GVol.TransformFunction|undefined}
   * @private
   */
  this.toLonLatTransform_ = undefined;

  /**
   * @type {GVol.Coordinate}
   * @private
   */
  this.projectionCenterLonLat_ = null;

  /**
   * @type {Array.<GVol.GraticuleLabelDataType>}
   * @private
   */
  this.meridiansLabels_ = null;

  /**
   * @type {Array.<GVol.GraticuleLabelDataType>}
   * @private
   */
  this.parallelsLabels_ = null;

  if (options.showLabels == true) {
    var degreesToString = GVol.coordinate.degreesToStringHDMS;

    /**
     * @type {null|function(number):string}
     * @private
     */
    this.lonLabelFormatter_ = options.lonLabelFormatter == undefined ?
      degreesToString.bind(this, 'EW') : options.lonLabelFormatter;

    /**
     * @type {function(number):string}
     * @private
     */
    this.latLabelFormatter_ = options.latLabelFormatter == undefined ?
      degreesToString.bind(this, 'NS') : options.latLabelFormatter;

    /**
     * Longitude label position in fractions (0..1) of view extent. 0 means
     * bottom, 1 means top.
     * @type {number}
     * @private
     */
    this.lonLabelPosition_ = options.lonLabelPosition == undefined ? 0 :
      options.lonLabelPosition;

    /**
     * Latitude Label position in fractions (0..1) of view extent. 0 means left, 1
     * means right.
     * @type {number}
     * @private
     */
    this.latLabelPosition_ = options.latLabelPosition == undefined ? 1 :
      options.latLabelPosition;

    /**
     * @type {GVol.style.Text}
     * @private
     */
    this.lonLabelStyle_ = options.lonLabelStyle !== undefined ? options.lonLabelStyle :
      new GVol.style.Text({
        font: '12px Calibri,sans-serif',
        textBaseline: 'bottom',
        fill: new GVol.style.Fill({
          cGVolor: 'rgba(0,0,0,1)'
        }),
        stroke: new GVol.style.Stroke({
          cGVolor: 'rgba(255,255,255,1)',
          width: 3
        })
      });

    /**
     * @type {GVol.style.Text}
     * @private
     */
    this.latLabelStyle_ = options.latLabelStyle !== undefined ? options.latLabelStyle :
      new GVol.style.Text({
        font: '12px Calibri,sans-serif',
        textAlign: 'end',
        fill: new GVol.style.Fill({
          cGVolor: 'rgba(0,0,0,1)'
        }),
        stroke: new GVol.style.Stroke({
          cGVolor: 'rgba(255,255,255,1)',
          width: 3
        })
      });

    this.meridiansLabels_ = [];
    this.parallelsLabels_ = [];
  }

  this.setMap(options.map !== undefined ? options.map : null);
};


/**
 * @type {GVol.style.Stroke}
 * @private
 * @const
 */
GVol.Graticule.DEFAULT_STROKE_STYLE_ = new GVol.style.Stroke({
  cGVolor: 'rgba(0,0,0,0.2)'
});


/**
 * TODO can be configurable
 * @type {Array.<number>}
 * @private
 */
GVol.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} squaredTGVolerance Squared tGVolerance.
 * @param {GVol.Extent} extent Extent.
 * @param {number} index Index.
 * @return {number} Index.
 * @private
 */
GVol.Graticule.prototype.addMeridian_ = function(lon, minLat, maxLat, squaredTGVolerance, extent, index) {
  var lineString = this.getMeridian_(lon, minLat, maxLat,
      squaredTGVolerance, index);
  if (GVol.extent.intersects(lineString.getExtent(), extent)) {
    if (this.meridiansLabels_) {
      var textPoint = this.getMeridianPoint_(lineString, extent, index);
      this.meridiansLabels_[index] = {
        geom: textPoint,
        text: this.lonLabelFormatter_(lon)
      };
    }
    this.meridians_[index++] = lineString;
  }
  return index;
};

/**
 * @param {GVol.geom.LineString} lineString Meridian
 * @param {GVol.Extent} extent Extent.
 * @param {number} index Index.
 * @return {GVol.geom.Point} Meridian point.
 * @private
 */
GVol.Graticule.prototype.getMeridianPoint_ = function(lineString, extent, index) {
  var flatCoordinates = lineString.getFlatCoordinates();
  var clampedBottom = Math.max(extent[1], flatCoordinates[1]);
  var clampedTop = Math.min(extent[3], flatCoordinates[flatCoordinates.length - 1]);
  var lat = GVol.math.clamp(
      extent[1] + Math.abs(extent[1] - extent[3]) * this.lonLabelPosition_,
      clampedBottom, clampedTop);
  var coordinate = [flatCoordinates[0], lat];
  var point = this.meridiansLabels_[index] !== undefined ?
    this.meridiansLabels_[index].geom : new GVol.geom.Point(null);
  point.setCoordinates(coordinate);
  return point;
};


/**
 * @param {number} lat Latitude.
 * @param {number} minLon Minimal longitude.
 * @param {number} maxLon Maximal longitude.
 * @param {number} squaredTGVolerance Squared tGVolerance.
 * @param {GVol.Extent} extent Extent.
 * @param {number} index Index.
 * @return {number} Index.
 * @private
 */
GVol.Graticule.prototype.addParallel_ = function(lat, minLon, maxLon, squaredTGVolerance, extent, index) {
  var lineString = this.getParallel_(lat, minLon, maxLon, squaredTGVolerance,
      index);
  if (GVol.extent.intersects(lineString.getExtent(), extent)) {
    if (this.parallelsLabels_) {
      var textPoint = this.getParallelPoint_(lineString, extent, index);
      this.parallelsLabels_[index] = {
        geom: textPoint,
        text: this.latLabelFormatter_(lat)
      };
    }
    this.parallels_[index++] = lineString;
  }
  return index;
};


/**
 * @param {GVol.geom.LineString} lineString Parallels.
 * @param {GVol.Extent} extent Extent.
 * @param {number} index Index.
 * @return {GVol.geom.Point} Parallel point.
 * @private
 */
GVol.Graticule.prototype.getParallelPoint_ = function(lineString, extent, index) {
  var flatCoordinates = lineString.getFlatCoordinates();
  var clampedLeft = Math.max(extent[0], flatCoordinates[0]);
  var clampedRight = Math.min(extent[2], flatCoordinates[flatCoordinates.length - 2]);
  var lon = GVol.math.clamp(
      extent[0] + Math.abs(extent[0] - extent[2]) * this.latLabelPosition_,
      clampedLeft, clampedRight);
  var coordinate = [lon, flatCoordinates[1]];
  var point = this.parallelsLabels_[index] !== undefined ?
    this.parallelsLabels_[index].geom : new GVol.geom.Point(null);
  point.setCoordinates(coordinate);
  return point;
};


/**
 * @param {GVol.Extent} extent Extent.
 * @param {GVol.Coordinate} center Center.
 * @param {number} resGVolution ResGVolution.
 * @param {number} squaredTGVolerance Squared tGVolerance.
 * @private
 */
GVol.Graticule.prototype.createGraticule_ = function(extent, center, resGVolution, squaredTGVolerance) {

  var interval = this.getInterval_(resGVolution);
  if (interval == -1) {
    this.meridians_.length = this.parallels_.length = 0;
    if (this.meridiansLabels_) {
      this.meridiansLabels_.length = 0;
    }
    if (this.parallelsLabels_) {
      this.parallelsLabels_.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 = GVol.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 = GVol.math.clamp(centerLon, this.minLon_, this.maxLon_);

  idx = this.addMeridian_(lon, minLat, maxLat, squaredTGVolerance, extent, 0);

  cnt = 0;
  while (lon != this.minLon_ && cnt++ < maxLines) {
    lon = Math.max(lon - interval, this.minLon_);
    idx = this.addMeridian_(lon, minLat, maxLat, squaredTGVolerance, extent, idx);
  }

  lon = GVol.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, squaredTGVolerance, extent, idx);
  }

  this.meridians_.length = idx;
  if (this.meridiansLabels_) {
    this.meridiansLabels_.length = idx;
  }

  // Create parallels

  centerLat = Math.floor(centerLat / interval) * interval;
  lat = GVol.math.clamp(centerLat, this.minLat_, this.maxLat_);

  idx = this.addParallel_(lat, minLon, maxLon, squaredTGVolerance, extent, 0);

  cnt = 0;
  while (lat != this.minLat_ && cnt++ < maxLines) {
    lat = Math.max(lat - interval, this.minLat_);
    idx = this.addParallel_(lat, minLon, maxLon, squaredTGVolerance, extent, idx);
  }

  lat = GVol.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, squaredTGVolerance, extent, idx);
  }

  this.parallels_.length = idx;
  if (this.parallelsLabels_) {
    this.parallelsLabels_.length = idx;
  }

};


/**
 * @param {number} resGVolution ResGVolution.
 * @return {number} The interval in degrees.
 * @private
 */
GVol.Graticule.prototype.getInterval_ = function(resGVolution) {
  var centerLon = this.projectionCenterLonLat_[0];
  var centerLat = this.projectionCenterLonLat_[1];
  var interval = -1;
  var i, ii, delta, dist;
  var target = Math.pow(this.targetSize_ * resGVolution, 2);
  /** @type {Array.<number>} **/
  var p1 = [];
  /** @type {Array.<number>} **/
  var p2 = [];
  for (i = 0, ii = GVol.Graticule.intervals_.length; i < ii; ++i) {
    delta = GVol.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 = GVol.Graticule.intervals_[i];
  }
  return interval;
};


/**
 * Get the map associated with this graticule.
 * @return {GVol.Map} The map.
 * @api
 */
GVol.Graticule.prototype.getMap = function() {
  return this.map_;
};


/**
 * @param {number} lon Longitude.
 * @param {number} minLat Minimal latitude.
 * @param {number} maxLat Maximal latitude.
 * @param {number} squaredTGVolerance Squared tGVolerance.
 * @return {GVol.geom.LineString} The meridian line string.
 * @param {number} index Index.
 * @private
 */
GVol.Graticule.prototype.getMeridian_ = function(lon, minLat, maxLat,
    squaredTGVolerance, index) {
  var flatCoordinates = GVol.geom.flat.geodesic.meridian(lon,
      minLat, maxLat, this.projection_, squaredTGVolerance);
  var lineString = this.meridians_[index] !== undefined ?
    this.meridians_[index] : new GVol.geom.LineString(null);
  lineString.setFlatCoordinates(GVol.geom.GeometryLayout.XY, flatCoordinates);
  return lineString;
};


/**
 * Get the list of meridians.  Meridians are lines of equal longitude.
 * @return {Array.<GVol.geom.LineString>} The meridians.
 * @api
 */
GVol.Graticule.prototype.getMeridians = function() {
  return this.meridians_;
};


/**
 * @param {number} lat Latitude.
 * @param {number} minLon Minimal longitude.
 * @param {number} maxLon Maximal longitude.
 * @param {number} squaredTGVolerance Squared tGVolerance.
 * @return {GVol.geom.LineString} The parallel line string.
 * @param {number} index Index.
 * @private
 */
GVol.Graticule.prototype.getParallel_ = function(lat, minLon, maxLon,
    squaredTGVolerance, index) {
  var flatCoordinates = GVol.geom.flat.geodesic.parallel(lat,
      minLon, maxLon, this.projection_, squaredTGVolerance);
  var lineString = this.parallels_[index] !== undefined ?
    this.parallels_[index] : new GVol.geom.LineString(null);
  lineString.setFlatCoordinates(GVol.geom.GeometryLayout.XY, flatCoordinates);
  return lineString;
};


/**
 * Get the list of parallels.  Parallels are lines of equal latitude.
 * @return {Array.<GVol.geom.LineString>} The parallels.
 * @api
 */
GVol.Graticule.prototype.getParallels = function() {
  return this.parallels_;
};


/**
 * @param {GVol.render.Event} e Event.
 * @private
 */
GVol.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 resGVolution = viewState.resGVolution;
  var pixelRatio = frameState.pixelRatio;
  var squaredTGVolerance =
      resGVolution * resGVolution / (4 * pixelRatio * pixelRatio);

  var updateProjectionInfo = !this.projection_ ||
      !GVol.proj.equivalent(this.projection_, projection);

  if (updateProjectionInfo) {
    this.updateProjectionInfo_(projection);
  }

  this.createGraticule_(extent, center, resGVolution, squaredTGVolerance);

  // 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.drawGeometry(line);
  }
  for (i = 0, l = this.parallels_.length; i < l; ++i) {
    line = this.parallels_[i];
    vectorContext.drawGeometry(line);
  }
  var labelData;
  if (this.meridiansLabels_) {
    for (i = 0, l = this.meridiansLabels_.length; i < l; ++i) {
      labelData = this.meridiansLabels_[i];
      this.lonLabelStyle_.setText(labelData.text);
      vectorContext.setTextStyle(this.lonLabelStyle_);
      vectorContext.drawGeometry(labelData.geom);
    }
  }
  if (this.parallelsLabels_) {
    for (i = 0, l = this.parallelsLabels_.length; i < l; ++i) {
      labelData = this.parallelsLabels_[i];
      this.latLabelStyle_.setText(labelData.text);
      vectorContext.setTextStyle(this.latLabelStyle_);
      vectorContext.drawGeometry(labelData.geom);
    }
  }
};


/**
 * @param {GVol.proj.Projection} projection Projection.
 * @private
 */
GVol.Graticule.prototype.updateProjectionInfo_ = function(projection) {
  var epsg4326Projection = GVol.proj.get('EPSG:4326');

  var extent = projection.getExtent();
  var worldExtent = projection.getWorldExtent();
  var worldExtentP = GVol.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];

  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_ = GVol.proj.getTransform(
      epsg4326Projection, projection);

  this.toLonLatTransform_ = GVol.proj.getTransform(
      projection, epsg4326Projection);

  this.projectionCenterLonLat_ = this.toLonLatTransform_(
      GVol.extent.getCenter(extent));

  this.projection_ = projection;
};


/**
 * Set the map for this graticule.  The graticule will be rendered on the
 * provided map.
 * @param {GVol.Map} map Map.
 * @api
 */
GVol.Graticule.prototype.setMap = function(map) {
  if (this.map_) {
    this.map_.un(GVol.render.EventType.POSTCOMPOSE,
        this.handlePostCompose_, this);
    this.map_.render();
  }
  if (map) {
    map.on(GVol.render.EventType.POSTCOMPOSE,
        this.handlePostCompose_, this);
    map.render();
  }
  this.map_ = map;
};

goog.provide('GVol.ImageBase');

goog.require('GVol');
goog.require('GVol.events.EventTarget');
goog.require('GVol.events.EventType');


/**
 * @constructor
 * @abstract
 * @extends {GVol.events.EventTarget}
 * @param {GVol.Extent} extent Extent.
 * @param {number|undefined} resGVolution ResGVolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {GVol.ImageState} state State.
 * @param {Array.<GVol.Attribution>} attributions Attributions.
 */
GVol.ImageBase = function(extent, resGVolution, pixelRatio, state, attributions) {

  GVol.events.EventTarget.call(this);

  /**
   * @private
   * @type {Array.<GVol.Attribution>}
   */
  this.attributions_ = attributions;

  /**
   * @protected
   * @type {GVol.Extent}
   */
  this.extent = extent;

  /**
   * @private
   * @type {number}
   */
  this.pixelRatio_ = pixelRatio;

  /**
   * @protected
   * @type {number|undefined}
   */
  this.resGVolution = resGVolution;

  /**
   * @protected
   * @type {GVol.ImageState}
   */
  this.state = state;

};
GVol.inherits(GVol.ImageBase, GVol.events.EventTarget);


/**
 * @protected
 */
GVol.ImageBase.prototype.changed = function() {
  this.dispatchEvent(GVol.events.EventType.CHANGE);
};


/**
 * @return {Array.<GVol.Attribution>} Attributions.
 */
GVol.ImageBase.prototype.getAttributions = function() {
  return this.attributions_;
};


/**
 * @return {GVol.Extent} Extent.
 */
GVol.ImageBase.prototype.getExtent = function() {
  return this.extent;
};


/**
 * @abstract
 * @param {Object=} opt_context Object.
 * @return {HTMLCanvasElement|Image|HTMLVideoElement} Image.
 */
GVol.ImageBase.prototype.getImage = function(opt_context) {};


/**
 * @return {number} PixelRatio.
 */
GVol.ImageBase.prototype.getPixelRatio = function() {
  return this.pixelRatio_;
};


/**
 * @return {number} ResGVolution.
 */
GVol.ImageBase.prototype.getResGVolution = function() {
  return /** @type {number} */ (this.resGVolution);
};


/**
 * @return {GVol.ImageState} State.
 */
GVol.ImageBase.prototype.getState = function() {
  return this.state;
};


/**
 * Load not yet loaded URI.
 * @abstract
 */
GVol.ImageBase.prototype.load = function() {};

goog.provide('GVol.Image');

goog.require('GVol');
goog.require('GVol.ImageBase');
goog.require('GVol.ImageState');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.extent');
goog.require('GVol.obj');


/**
 * @constructor
 * @extends {GVol.ImageBase}
 * @param {GVol.Extent} extent Extent.
 * @param {number|undefined} resGVolution ResGVolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {Array.<GVol.Attribution>} attributions Attributions.
 * @param {string} src Image source URI.
 * @param {?string} crossOrigin Cross origin.
 * @param {GVol.ImageLoadFunctionType} imageLoadFunction Image load function.
 */
GVol.Image = function(extent, resGVolution, pixelRatio, attributions, src,
    crossOrigin, imageLoadFunction) {

  GVol.ImageBase.call(this, extent, resGVolution, pixelRatio, GVol.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.<GVol.EventsKey>}
   */
  this.imageListenerKeys_ = null;

  /**
   * @protected
   * @type {GVol.ImageState}
   */
  this.state = GVol.ImageState.IDLE;

  /**
   * @private
   * @type {GVol.ImageLoadFunctionType}
   */
  this.imageLoadFunction_ = imageLoadFunction;

};
GVol.inherits(GVol.Image, GVol.ImageBase);


/**
 * @inheritDoc
 * @api
 */
GVol.Image.prototype.getImage = function(opt_context) {
  if (opt_context !== undefined) {
    var image;
    var key = GVol.getUid(opt_context);
    if (key in this.imageByContext_) {
      return this.imageByContext_[key];
    } else if (GVol.obj.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
 */
GVol.Image.prototype.handleImageError_ = function() {
  this.state = GVol.ImageState.ERROR;
  this.unlistenImage_();
  this.changed();
};


/**
 * Tracks successful image load.
 *
 * @private
 */
GVol.Image.prototype.handleImageLoad_ = function() {
  if (this.resGVolution === undefined) {
    this.resGVolution = GVol.extent.getHeight(this.extent) / this.image_.height;
  }
  this.state = GVol.ImageState.LOADED;
  this.unlistenImage_();
  this.changed();
};


/**
 * Load the image or retry if loading previously failed.
 * Loading is taken care of by the tile queue, and calling this method is
 * only needed for preloading or for reloading in case of an error.
 * @override
 * @api
 */
GVol.Image.prototype.load = function() {
  if (this.state == GVol.ImageState.IDLE || this.state == GVol.ImageState.ERROR) {
    this.state = GVol.ImageState.LOADING;
    this.changed();
    this.imageListenerKeys_ = [
      GVol.events.listenOnce(this.image_, GVol.events.EventType.ERROR,
          this.handleImageError_, this),
      GVol.events.listenOnce(this.image_, GVol.events.EventType.LOAD,
          this.handleImageLoad_, this)
    ];
    this.imageLoadFunction_(this, this.src_);
  }
};


/**
 * @param {HTMLCanvasElement|Image|HTMLVideoElement} image Image.
 */
GVol.Image.prototype.setImage = function(image) {
  this.image_ = image;
};


/**
 * Discards event handlers which listen for load completion or errors.
 *
 * @private
 */
GVol.Image.prototype.unlistenImage_ = function() {
  this.imageListenerKeys_.forEach(GVol.events.unlistenByKey);
  this.imageListenerKeys_ = null;
};

goog.provide('GVol.ImageCanvas');

goog.require('GVol');
goog.require('GVol.ImageBase');
goog.require('GVol.ImageState');


/**
 * @constructor
 * @extends {GVol.ImageBase}
 * @param {GVol.Extent} extent Extent.
 * @param {number} resGVolution ResGVolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {Array.<GVol.Attribution>} attributions Attributions.
 * @param {HTMLCanvasElement} canvas Canvas.
 * @param {GVol.ImageCanvasLoader=} opt_loader Optional loader function to
 *     support asynchronous canvas drawing.
 */
GVol.ImageCanvas = function(extent, resGVolution, pixelRatio, attributions,
    canvas, opt_loader) {

  /**
   * Optional canvas loader function.
   * @type {?GVol.ImageCanvasLoader}
   * @private
   */
  this.loader_ = opt_loader !== undefined ? opt_loader : null;

  var state = opt_loader !== undefined ?
    GVol.ImageState.IDLE : GVol.ImageState.LOADED;

  GVol.ImageBase.call(this, extent, resGVolution, pixelRatio, state, attributions);

  /**
   * @private
   * @type {HTMLCanvasElement}
   */
  this.canvas_ = canvas;

  /**
   * @private
   * @type {Error}
   */
  this.error_ = null;

};
GVol.inherits(GVol.ImageCanvas, GVol.ImageBase);


/**
 * Get any error associated with asynchronous rendering.
 * @return {Error} Any error that occurred during rendering.
 */
GVol.ImageCanvas.prototype.getError = function() {
  return this.error_;
};


/**
 * Handle async drawing complete.
 * @param {Error} err Any error during drawing.
 * @private
 */
GVol.ImageCanvas.prototype.handleLoad_ = function(err) {
  if (err) {
    this.error_ = err;
    this.state = GVol.ImageState.ERROR;
  } else {
    this.state = GVol.ImageState.LOADED;
  }
  this.changed();
};


/**
 * @inheritDoc
 */
GVol.ImageCanvas.prototype.load = function() {
  if (this.state == GVol.ImageState.IDLE) {
    this.state = GVol.ImageState.LOADING;
    this.changed();
    this.loader_(this.handleLoad_.bind(this));
  }
};


/**
 * @inheritDoc
 */
GVol.ImageCanvas.prototype.getImage = function(opt_context) {
  return this.canvas_;
};

goog.provide('GVol.Tile');

goog.require('GVol');
goog.require('GVol.TileState');
goog.require('GVol.events.EventTarget');
goog.require('GVol.events.EventType');


/**
 * @classdesc
 * Base class for tiles.
 *
 * @constructor
 * @abstract
 * @extends {GVol.events.EventTarget}
 * @param {GVol.TileCoord} tileCoord Tile coordinate.
 * @param {GVol.TileState} state State.
 */
GVol.Tile = function(tileCoord, state) {

  GVol.events.EventTarget.call(this);

  /**
   * @type {GVol.TileCoord}
   */
  this.tileCoord = tileCoord;

  /**
   * @protected
   * @type {GVol.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 {GVol.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 = '';

};
GVol.inherits(GVol.Tile, GVol.events.EventTarget);


/**
 * @protected
 */
GVol.Tile.prototype.changed = function() {
  this.dispatchEvent(GVol.events.EventType.CHANGE);
};


/**
 * @return {string} Key.
 */
GVol.Tile.prototype.getKey = function() {
  return this.key + '/' + this.tileCoord;
};

/**
 * Get the interim tile most suitable for rendering using the chain of interim
 * tiles. This corresponds to the  most recent tile that has been loaded, if no
 * such tile exists, the original tile is returned.
 * @return {!GVol.Tile} Best tile for rendering.
 */
GVol.Tile.prototype.getInterimTile = function() {
  if (!this.interimTile) {
    //empty chain
    return this;
  }
  var tile = this.interimTile;

  // find the first loaded tile and return it. Since the chain is sorted in
  // decreasing order of creation time, there is no need to search the remainder
  // of the list (all those tiles correspond to GVolder requests and will be
  // cleaned up by refreshInterimChain)
  do {
    if (tile.getState() == GVol.TileState.LOADED) {
      return tile;
    }
    tile = tile.interimTile;
  } while (tile);

  // we can not find a better tile
  return this;
};

/**
 * Goes through the chain of interim tiles and discards sections of the chain
 * that are no longer relevant.
 */
GVol.Tile.prototype.refreshInterimChain = function() {
  if (!this.interimTile) {
    return;
  }

  var tile = this.interimTile;
  var prev = this;

  do {
    if (tile.getState() == GVol.TileState.LOADED) {
      //we have a loaded tile, we can discard the rest of the list
      //we would could abort any LOADING tile request
      //GVolder than this tile (i.e. any LOADING tile fGVollowing this entry in the chain)
      tile.interimTile = null;
      break;
    } else if (tile.getState() == GVol.TileState.LOADING) {
      //keep this LOADING tile any loaded tiles later in the chain are
      //GVolder than this tile, so we're still interested in the request
      prev = tile;
    } else if (tile.getState() == GVol.TileState.IDLE) {
      //the head of the list is the most current tile, we don't need
      //to start any other requests for this chain
      prev.interimTile = tile.interimTile;
    } else {
      prev = tile;
    }
    tile = prev.interimTile;
  } while (tile);
};

/**
 * Get the tile coordinate for this tile.
 * @return {GVol.TileCoord} The tile coordinate.
 * @api
 */
GVol.Tile.prototype.getTileCoord = function() {
  return this.tileCoord;
};


/**
 * @return {GVol.TileState} State.
 */
GVol.Tile.prototype.getState = function() {
  return this.state;
};

/**
 * @param {GVol.TileState} state State.
 */
GVol.Tile.prototype.setState = function(state) {
  this.state = state;
  this.changed();
};

/**
 * Load the image or retry if loading previously failed.
 * Loading is taken care of by the tile queue, and calling this method is
 * only needed for preloading or for reloading in case of an error.
 * @abstract
 * @api
 */
GVol.Tile.prototype.load = function() {};

goog.provide('GVol.ImageTile');

goog.require('GVol');
goog.require('GVol.Tile');
goog.require('GVol.TileState');
goog.require('GVol.dom');
goog.require('GVol.events');
goog.require('GVol.events.EventType');


/**
 * @constructor
 * @extends {GVol.Tile}
 * @param {GVol.TileCoord} tileCoord Tile coordinate.
 * @param {GVol.TileState} state State.
 * @param {string} src Image source URI.
 * @param {?string} crossOrigin Cross origin.
 * @param {GVol.TileLoadFunctionType} tileLoadFunction Tile load function.
 */
GVol.ImageTile = function(tileCoord, state, src, crossOrigin, tileLoadFunction) {

  GVol.Tile.call(this, tileCoord, state);

  /**
   * Image URI
   *
   * @private
   * @type {string}
   */
  this.src_ = src;

  /**
   * @private
   * @type {Image|HTMLCanvasElement}
   */
  this.image_ = new Image();
  if (crossOrigin !== null) {
    this.image_.crossOrigin = crossOrigin;
  }

  /**
   * @private
   * @type {Array.<GVol.EventsKey>}
   */
  this.imageListenerKeys_ = null;

  /**
   * @private
   * @type {GVol.TileLoadFunctionType}
   */
  this.tileLoadFunction_ = tileLoadFunction;

};
GVol.inherits(GVol.ImageTile, GVol.Tile);


/**
 * @inheritDoc
 */
GVol.ImageTile.prototype.disposeInternal = function() {
  if (this.state == GVol.TileState.LOADING) {
    this.unlistenImage_();
    this.image_.src = GVol.ImageTile.blankImage.toDataURL('image/png');
  }
  if (this.interimTile) {
    this.interimTile.dispose();
  }
  this.state = GVol.TileState.ABORT;
  this.changed();
  GVol.Tile.prototype.disposeInternal.call(this);
};


/**
 * Get the HTML image element for this tile (may be a Canvas, Image, or Video).
 * @return {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement} Image.
 * @api
 */
GVol.ImageTile.prototype.getImage = function() {
  return this.image_;
};


/**
 * @inheritDoc
 */
GVol.ImageTile.prototype.getKey = function() {
  return this.src_;
};


/**
 * Tracks loading or read errors.
 *
 * @private
 */
GVol.ImageTile.prototype.handleImageError_ = function() {
  this.state = GVol.TileState.ERROR;
  this.unlistenImage_();
  this.image_ = GVol.ImageTile.blankImage;
  this.changed();
};


/**
 * Tracks successful image load.
 *
 * @private
 */
GVol.ImageTile.prototype.handleImageLoad_ = function() {
  if (this.image_.naturalWidth && this.image_.naturalHeight) {
    this.state = GVol.TileState.LOADED;
  } else {
    this.state = GVol.TileState.EMPTY;
  }
  this.unlistenImage_();
  this.changed();
};


/**
 * @inheritDoc
 * @api
 */
GVol.ImageTile.prototype.load = function() {
  if (this.state == GVol.TileState.IDLE || this.state == GVol.TileState.ERROR) {
    this.state = GVol.TileState.LOADING;
    this.changed();
    this.imageListenerKeys_ = [
      GVol.events.listenOnce(this.image_, GVol.events.EventType.ERROR,
          this.handleImageError_, this),
      GVol.events.listenOnce(this.image_, GVol.events.EventType.LOAD,
          this.handleImageLoad_, this)
    ];
    this.tileLoadFunction_(this, this.src_);
  }
};


/**
 * Discards event handlers which listen for load completion or errors.
 *
 * @private
 */
GVol.ImageTile.prototype.unlistenImage_ = function() {
  this.imageListenerKeys_.forEach(GVol.events.unlistenByKey);
  this.imageListenerKeys_ = null;
};


/**
 * A blank image.
 * @type {HTMLCanvasElement}
 */
GVol.ImageTile.blankImage = (function() {
  var ctx = GVol.dom.createCanvasContext2D(1, 1);
  ctx.fillStyle = 'rgba(0,0,0,0)';
  ctx.fillRect(0, 0, 1, 1);
  return ctx.canvas;
})();

// FIXME should handle all geo-referenced data, not just vector data

goog.provide('GVol.interaction.DragAndDrop');

goog.require('GVol');
goog.require('GVol.functions');
goog.require('GVol.events');
goog.require('GVol.events.Event');
goog.require('GVol.events.EventType');
goog.require('GVol.interaction.Interaction');
goog.require('GVol.proj');


/**
 * @classdesc
 * Handles input of vector data by drag and drop.
 *
 * @constructor
 * @extends {GVol.interaction.Interaction}
 * @fires GVol.interaction.DragAndDrop.Event
 * @param {GVolx.interaction.DragAndDropOptions=} opt_options Options.
 * @api
 */
GVol.interaction.DragAndDrop = function(opt_options) {

  var options = opt_options ? opt_options : {};

  GVol.interaction.Interaction.call(this, {
    handleEvent: GVol.interaction.DragAndDrop.handleEvent
  });

  /**
   * @private
   * @type {Array.<function(new: GVol.format.Feature)>}
   */
  this.formatConstructors_ = options.formatConstructors ?
    options.formatConstructors : [];

  /**
   * @private
   * @type {GVol.proj.Projection}
   */
  this.projection_ = options.projection ?
    GVol.proj.get(options.projection) : null;

  /**
   * @private
   * @type {Array.<GVol.EventsKey>}
   */
  this.dropListenKeys_ = null;

  /**
   * @private
   * @type {GVol.source.Vector}
   */
  this.source_ = options.source || null;

  /**
   * @private
   * @type {Element}
   */
  this.target = options.target ? options.target : null;

};
GVol.inherits(GVol.interaction.DragAndDrop, GVol.interaction.Interaction);


/**
 * @param {Event} event Event.
 * @this {GVol.interaction.DragAndDrop}
 * @private
 */
GVol.interaction.DragAndDrop.handleDrop_ = function(event) {
  var files = event.dataTransfer.files;
  var i, ii, file;
  for (i = 0, ii = files.length; i < ii; ++i) {
    file = files.item(i);
    var reader = new FileReader();
    reader.addEventListener(GVol.events.EventType.LOAD,
        this.handleResult_.bind(this, file));
    reader.readAsText(file);
  }
};


/**
 * @param {Event} event Event.
 * @private
 */
GVol.interaction.DragAndDrop.handleStop_ = function(event) {
  event.stopPropagation();
  event.preventDefault();
  event.dataTransfer.dropEffect = 'copy';
};


/**
 * @param {File} file File.
 * @param {Event} event Load event.
 * @private
 */
GVol.interaction.DragAndDrop.prototype.handleResult_ = function(file, event) {
  var result = event.target.result;
  var map = this.getMap();
  var projection = this.projection_;
  if (!projection) {
    var view = map.getView();
    projection = view.getProjection();
  }

  var formatConstructors = this.formatConstructors_;
  var features = [];
  var i, ii;
  for (i = 0, ii = formatConstructors.length; i < ii; ++i) {
    /**
     * Avoid "cannot instantiate abstract class" error.
     * @type {Function}
     */
    var formatConstructor = formatConstructors[i];
    /**
     * @type {GVol.format.Feature}
     */
    var format = new formatConstructor();
    features = this.tryReadFeatures_(format, result, {
      featureProjection: projection
    });
    if (features && features.length > 0) {
      break;
    }
  }
  if (this.source_) {
    this.source_.clear();
    this.source_.addFeatures(features);
  }
  this.dispatchEvent(
      new GVol.interaction.DragAndDrop.Event(
          GVol.interaction.DragAndDrop.EventType_.ADD_FEATURES, file,
          features, projection));
};


/**
 * Handles the {@link GVol.MapBrowserEvent map browser event} unconditionally and
 * neither prevents the browser default nor stops event propagation.
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} `false` to stop event propagation.
 * @this {GVol.interaction.DragAndDrop}
 * @api
 */
GVol.interaction.DragAndDrop.handleEvent = GVol.functions.TRUE;


/**
 * @private
 */
GVol.interaction.DragAndDrop.prototype.registerListeners_ = function() {
  var map = this.getMap();
  if (map) {
    var dropArea = this.target ? this.target : map.getViewport();
    this.dropListenKeys_ = [
      GVol.events.listen(dropArea, GVol.events.EventType.DROP,
          GVol.interaction.DragAndDrop.handleDrop_, this),
      GVol.events.listen(dropArea, GVol.events.EventType.DRAGENTER,
          GVol.interaction.DragAndDrop.handleStop_, this),
      GVol.events.listen(dropArea, GVol.events.EventType.DRAGOVER,
          GVol.interaction.DragAndDrop.handleStop_, this),
      GVol.events.listen(dropArea, GVol.events.EventType.DROP,
          GVol.interaction.DragAndDrop.handleStop_, this)
    ];
  }
};


/**
 * @inheritDoc
 */
GVol.interaction.DragAndDrop.prototype.setActive = function(active) {
  GVol.interaction.Interaction.prototype.setActive.call(this, active);
  if (active) {
    this.registerListeners_();
  } else {
    this.unregisterListeners_();
  }
};


/**
 * @inheritDoc
 */
GVol.interaction.DragAndDrop.prototype.setMap = function(map) {
  this.unregisterListeners_();
  GVol.interaction.Interaction.prototype.setMap.call(this, map);
  if (this.getActive()) {
    this.registerListeners_();
  }
};


/**
 * @param {GVol.format.Feature} format Format.
 * @param {string} text Text.
 * @param {GVolx.format.ReadOptions} options Read options.
 * @private
 * @return {Array.<GVol.Feature>} Features.
 */
GVol.interaction.DragAndDrop.prototype.tryReadFeatures_ = function(format, text, options) {
  try {
    return format.readFeatures(text, options);
  } catch (e) {
    return null;
  }
};


/**
 * @private
 */
GVol.interaction.DragAndDrop.prototype.unregisterListeners_ = function() {
  if (this.dropListenKeys_) {
    this.dropListenKeys_.forEach(GVol.events.unlistenByKey);
    this.dropListenKeys_ = null;
  }
};


/**
 * @enum {string}
 * @private
 */
GVol.interaction.DragAndDrop.EventType_ = {
  /**
   * Triggered when features are added
   * @event GVol.interaction.DragAndDrop.Event#addfeatures
   * @api
   */
  ADD_FEATURES: 'addfeatures'
};


/**
 * @classdesc
 * Events emitted by {@link GVol.interaction.DragAndDrop} instances are instances
 * of this type.
 *
 * @constructor
 * @extends {GVol.events.Event}
 * @implements {GVoli.interaction.DragAndDropEvent}
 * @param {GVol.interaction.DragAndDrop.EventType_} type Type.
 * @param {File} file File.
 * @param {Array.<GVol.Feature>=} opt_features Features.
 * @param {GVol.proj.Projection=} opt_projection Projection.
 */
GVol.interaction.DragAndDrop.Event = function(type, file, opt_features, opt_projection) {

  GVol.events.Event.call(this, type);

  /**
   * The features parsed from dropped data.
   * @type {Array.<GVol.Feature>|undefined}
   * @api
   */
  this.features = opt_features;

  /**
   * The dropped file.
   * @type {File}
   * @api
   */
  this.file = file;

  /**
   * The feature projection.
   * @type {GVol.proj.Projection|undefined}
   * @api
   */
  this.projection = opt_projection;

};
GVol.inherits(GVol.interaction.DragAndDrop.Event, GVol.events.Event);

goog.provide('GVol.interaction.DragRotateAndZoom');

goog.require('GVol');
goog.require('GVol.RotationConstraint');
goog.require('GVol.ViewHint');
goog.require('GVol.events.condition');
goog.require('GVol.interaction.Interaction');
goog.require('GVol.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 {GVol.interaction.Pointer}
 * @param {GVolx.interaction.DragRotateAndZoomOptions=} opt_options Options.
 * @api
 */
GVol.interaction.DragRotateAndZoom = function(opt_options) {

  var options = opt_options ? opt_options : {};

  GVol.interaction.Pointer.call(this, {
    handleDownEvent: GVol.interaction.DragRotateAndZoom.handleDownEvent_,
    handleDragEvent: GVol.interaction.DragRotateAndZoom.handleDragEvent_,
    handleUpEvent: GVol.interaction.DragRotateAndZoom.handleUpEvent_
  });

  /**
   * @private
   * @type {GVol.EventsConditionType}
   */
  this.condition_ = options.condition ?
    options.condition : GVol.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;

};
GVol.inherits(GVol.interaction.DragRotateAndZoom, GVol.interaction.Pointer);


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @this {GVol.interaction.DragRotateAndZoom}
 * @private
 */
GVol.interaction.DragRotateAndZoom.handleDragEvent_ = function(mapBrowserEvent) {
  if (!GVol.events.condition.mouseOnly(mapBrowserEvent)) {
    return;
  }

  var map = mapBrowserEvent.map;
  var size = map.getSize();
  var offset = mapBrowserEvent.pixel;
  var deltaX = offset[0] - size[0] / 2;
  var deltaY = size[1] / 2 - offset[1];
  var theta = Math.atan2(deltaY, deltaX);
  var magnitude = Math.sqrt(deltaX * deltaX + deltaY * deltaY);
  var view = map.getView();
  if (view.getConstraints().rotation !== GVol.RotationConstraint.disable && this.lastAngle_ !== undefined) {
    var angleDelta = theta - this.lastAngle_;
    GVol.interaction.Interaction.rotateWithoutConstraints(
        view, view.getRotation() - angleDelta);
  }
  this.lastAngle_ = theta;
  if (this.lastMagnitude_ !== undefined) {
    var resGVolution = this.lastMagnitude_ * (view.getResGVolution() / magnitude);
    GVol.interaction.Interaction.zoomWithoutConstraints(view, resGVolution);
  }
  if (this.lastMagnitude_ !== undefined) {
    this.lastScaleDelta_ = this.lastMagnitude_ / magnitude;
  }
  this.lastMagnitude_ = magnitude;
};


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boGVolean} Stop drag sequence?
 * @this {GVol.interaction.DragRotateAndZoom}
 * @private
 */
GVol.interaction.DragRotateAndZoom.handleUpEvent_ = function(mapBrowserEvent) {
  if (!GVol.events.condition.mouseOnly(mapBrowserEvent)) {
    return true;
  }

  var map = mapBrowserEvent.map;
  var view = map.getView();
  view.setHint(GVol.ViewHint.INTERACTING, -1);
  var direction = this.lastScaleDelta_ - 1;
  GVol.interaction.Interaction.rotate(view, view.getRotation());
  GVol.interaction.Interaction.zoom(view, view.getResGVolution(),
      undefined, this.duration_, direction);
  this.lastScaleDelta_ = 0;
  return false;
};


/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boGVolean} Start drag sequence?
 * @this {GVol.interaction.DragRotateAndZoom}
 * @private
 */
GVol.interaction.DragRotateAndZoom.handleDownEvent_ = function(mapBrowserEvent) {
  if (!GVol.events.condition.mouseOnly(mapBrowserEvent)) {
    return false;
  }

  if (this.condition_(mapBrowserEvent)) {
    mapBrowserEvent.map.getView().setHint(GVol.ViewHint.INTERACTING, 1);
    this.lastAngle_ = undefined;
    this.lastMagnitude_ = undefined;
    return true;
  } else {
    return false;
  }
};

goog.provide('GVol.interaction.DrawEventType');


/**
 * @enum {string}
 */
GVol.interaction.DrawEventType = {
  /**
   * Triggered upon feature draw start
   * @event GVol.interaction.Draw.Event#drawstart
   * @api
   */
  DRAWSTART: 'drawstart',
  /**
   * Triggered upon feature draw end
   * @event GVol.interaction.Draw.Event#drawend
   * @api
   */
  DRAWEND: 'drawend'
};

goog.provide('GVol.render.canvas.Instruction');

/**
 * @enum {number}
 */
GVol.render.canvas.Instruction = {
  BEGIN_GEOMETRY: 0,
  BEGIN_PATH: 1,
  CIRCLE: 2,
  CLOSE_PATH: 3,
  CUSTOM: 4,
  DRAW_IMAGE: 5,
  DRAW_TEXT: 6,
  END_GEOMETRY: 7,
  FILL: 8,
  MOVE_TO_LINE_TO: 9,
  SET_FILL_STYLE: 10,
  SET_STROKE_STYLE: 11,
  SET_TEXT_STYLE: 12,
  STROKE: 13
};

goog.provide('GVol.render.canvas.Replay');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.extent');
goog.require('GVol.extent.Relationship');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.geom.flat.inflate');
goog.require('GVol.geom.flat.transform');
goog.require('GVol.has');
goog.require('GVol.obj');
goog.require('GVol.render.VectorContext');
goog.require('GVol.render.canvas.Instruction');
goog.require('GVol.transform');


/**
 * @constructor
 * @extends {GVol.render.VectorContext}
 * @param {number} tGVolerance TGVolerance.
 * @param {GVol.Extent} maxExtent Maximum extent.
 * @param {number} resGVolution ResGVolution.
 * @param {boGVolean} overlaps The replay can have overlapping geometries.
 * @struct
 */
GVol.render.canvas.Replay = function(tGVolerance, maxExtent, resGVolution, overlaps) {
  GVol.render.VectorContext.call(this);

  /**
   * @protected
   * @type {number}
   */
  this.tGVolerance = tGVolerance;

  /**
   * @protected
   * @const
   * @type {GVol.Extent}
   */
  this.maxExtent = maxExtent;

  /**
   * @protected
   * @type {boGVolean}
   */
  this.overlaps = overlaps;

  /**
   * @protected
   * @type {number}
   */
  this.maxLineWidth = 0;

  /**
   * @protected
   * @const
   * @type {number}
   */
  this.resGVolution = resGVolution;

  /**
   * @private
   * @type {GVol.Coordinate}
   */
  this.fillOrigin_;

  /**
   * @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 {Object.<number,GVol.Coordinate|Array.<GVol.Coordinate>|Array.<Array.<GVol.Coordinate>>>}
   */
  this.coordinateCache_ = {};

  /**
   * @private
   * @type {!GVol.Transform}
   */
  this.renderedTransform_ = GVol.transform.create();

  /**
   * @protected
   * @type {Array.<*>}
   */
  this.hitDetectionInstructions = [];

  /**
   * @private
   * @type {Array.<number>}
   */
  this.pixelCoordinates_ = null;

  /**
   * @private
   * @type {!GVol.Transform}
   */
  this.tmpLocalTransform_ = GVol.transform.create();

  /**
   * @private
   * @type {!GVol.Transform}
   */
  this.resetTransform_ = GVol.transform.create();
};
GVol.inherits(GVol.render.canvas.Replay, GVol.render.VectorContext);


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {number} end End.
 * @param {number} stride Stride.
 * @param {boGVolean} closed Last input coordinate equals first.
 * @param {boGVolean} skipFirst Skip first coordinate.
 * @protected
 * @return {number} My end.
 */
GVol.render.canvas.Replay.prototype.appendFlatCoordinates = function(flatCoordinates, offset, end, stride, closed, skipFirst) {

  var myEnd = this.coordinates.length;
  var extent = this.getBufferedMaxExtent();
  if (skipFirst) {
    offset += stride;
  }
  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 = GVol.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 === GVol.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;
  }

  // Last coordinate equals first or only one point to append:
  if ((closed && skipped) || i === offset + stride) {
    this.coordinates[myEnd++] = lastCoord[0];
    this.coordinates[myEnd++] = lastCoord[1];
  }
  return myEnd;
};


/**
 * @param {Array.<number>} flatCoordinates Flat coordinates.
 * @param {number} offset Offset.
 * @param {Array.<number>} ends Ends.
 * @param {number} stride Stride.
 * @param {Array.<number>} replayEnds Replay ends.
 * @return {number} Offset.
 */
GVol.render.canvas.Replay.prototype.drawCustomCoordinates_ = function(flatCoordinates, offset, ends, stride, replayEnds) {
  for (var i = 0, ii = ends.length; i < ii; ++i) {
    var end = ends[i];
    var replayEnd = this.appendFlatCoordinates(flatCoordinates, offset, end, stride, false, false);
    replayEnds.push(replayEnd);
    offset = end;
  }
  return offset;
};


/**
 * @inheritDoc.
 */
GVol.render.canvas.Replay.prototype.drawCustom = function(geometry, feature, renderer) {
  this.beginGeometry(geometry, feature);
  var type = geometry.getType();
  var stride = geometry.getStride();
  var replayBegin = this.coordinates.length;
  var flatCoordinates, replayEnd, replayEnds, replayEndss;
  var offset;
  if (type == GVol.geom.GeometryType.MULTI_POLYGON) {
    geometry = /** @type {GVol.geom.MultiPGVolygon} */ (geometry);
    flatCoordinates = geometry.getOrientedFlatCoordinates();
    replayEndss = [];
    var endss = geometry.getEndss();
    offset = 0;
    for (var i = 0, ii = endss.length; i < ii; ++i) {
      var myEnds = [];
      offset = this.drawCustomCoordinates_(flatCoordinates, offset, endss[i], stride, myEnds);
      replayEndss.push(myEnds);
    }
    this.instructions.push([GVol.render.canvas.Instruction.CUSTOM,
      replayBegin, replayEndss, geometry, renderer, GVol.geom.flat.inflate.coordinatesss]);
  } else if (type == GVol.geom.GeometryType.POLYGON || type == GVol.geom.GeometryType.MULTI_LINE_STRING) {
    replayEnds = [];
    flatCoordinates = (type == GVol.geom.GeometryType.POLYGON) ?
      /** @type {GVol.geom.PGVolygon} */ (geometry).getOrientedFlatCoordinates() :
      geometry.getFlatCoordinates();
    offset = this.drawCustomCoordinates_(flatCoordinates, 0,
        /** @type {GVol.geom.PGVolygon|GVol.geom.MultiLineString} */ (geometry).getEnds(),
        stride, replayEnds);
    this.instructions.push([GVol.render.canvas.Instruction.CUSTOM,
      replayBegin, replayEnds, geometry, renderer, GVol.geom.flat.inflate.coordinatess]);
  } else if (type == GVol.geom.GeometryType.LINE_STRING || type == GVol.geom.GeometryType.MULTI_POINT) {
    flatCoordinates = geometry.getFlatCoordinates();
    replayEnd = this.appendFlatCoordinates(
        flatCoordinates, 0, flatCoordinates.length, stride, false, false);
    this.instructions.push([GVol.render.canvas.Instruction.CUSTOM,
      replayBegin, replayEnd, geometry, renderer, GVol.geom.flat.inflate.coordinates]);
  } else if (type == GVol.geom.GeometryType.POINT) {
    flatCoordinates = geometry.getFlatCoordinates();
    this.coordinates.push(flatCoordinates[0], flatCoordinates[1]);
    replayEnd = this.coordinates.length;
    this.instructions.push([GVol.render.canvas.Instruction.CUSTOM,
      replayBegin, replayEnd, geometry, renderer]);
  }
  this.endGeometry(geometry, feature);
};


/**
 * @protected
 * @param {GVol.geom.Geometry|GVol.render.Feature} geometry Geometry.
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 */
GVol.render.canvas.Replay.prototype.beginGeometry = function(geometry, feature) {
  this.beginGeometryInstruction1_ =
      [GVol.render.canvas.Instruction.BEGIN_GEOMETRY, feature, 0];
  this.instructions.push(this.beginGeometryInstruction1_);
  this.beginGeometryInstruction2_ =
      [GVol.render.canvas.Instruction.BEGIN_GEOMETRY, feature, 0];
  this.hitDetectionInstructions.push(this.beginGeometryInstruction2_);
};


/**
 * @private
 * @param {CanvasRenderingContext2D} context Context.
 * @param {number} rotation Rotation.
 */
GVol.render.canvas.Replay.prototype.fill_ = function(context, rotation) {
  if (this.fillOrigin_) {
    var origin = GVol.transform.apply(this.renderedTransform_, this.fillOrigin_.slice());
    context.translate(origin[0], origin[1]);
    context.rotate(rotation);
  }
  context.fill();
  if (this.fillOrigin_) {
    context.setTransform.apply(context, this.resetTransform_);
  }
};


/**
 * @private
 * @param {CanvasRenderingContext2D} context Context.
 * @param {number} pixelRatio Pixel ratio.
 * @param {GVol.Transform} transform Transform.
 * @param {number} viewRotation View rotation.
 * @param {Object.<string, boGVolean>} skippedFeaturesHash Ids of features
 *     to skip.
 * @param {Array.<*>} instructions Instructions array.
 * @param {function((GVol.Feature|GVol.render.Feature)): T|undefined}
 *     featureCallback Feature callback.
 * @param {GVol.Extent=} opt_hitExtent Only check features that intersect this
 *     extent.
 * @return {T|undefined} Callback result.
 * @template T
 */
GVol.render.canvas.Replay.prototype.replay_ = function(
    context, pixelRatio, transform, viewRotation, skippedFeaturesHash,
    instructions, featureCallback, opt_hitExtent) {
  /** @type {Array.<number>} */
  var pixelCoordinates;
  if (this.pixelCoordinates_ && GVol.array.equals(transform, this.renderedTransform_)) {
    pixelCoordinates = this.pixelCoordinates_;
  } else {
    if (!this.pixelCoordinates_) {
      this.pixelCoordinates_ = [];
    }
    pixelCoordinates = GVol.geom.flat.transform.transform2D(
        this.coordinates, 0, this.coordinates.length, 2,
        transform, this.pixelCoordinates_);
    GVol.transform.setFromArray(this.renderedTransform_, transform);
  }
  var skipFeatures = !GVol.obj.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 resetTransform = this.resetTransform_;
  var prevX, prevY, roundX, roundY;
  var pendingFill = 0;
  var pendingStroke = 0;
  var coordinateCache = this.coordinateCache_;

  var state = /** @type {GVolx.render.State} */ ({
    context: context,
    pixelRatio: pixelRatio,
    resGVolution: this.resGVolution,
    rotation: viewRotation
  });

  // When the batch size gets too big, performance decreases. 200 is a good
  // balance between batch size and number of fill/stroke instructions.
  var batchSize =
      this.instructions != instructions || this.overlaps ? 0 : 200;
  while (i < ii) {
    var instruction = instructions[i];
    var type = /** @type {GVol.render.canvas.Instruction} */ (instruction[0]);
    var /** @type {GVol.Feature|GVol.render.Feature} */ feature, fill, stroke, text, x, y;
    switch (type) {
      case GVol.render.canvas.Instruction.BEGIN_GEOMETRY:
        feature = /** @type {GVol.Feature|GVol.render.Feature} */ (instruction[1]);
        if ((skipFeatures &&
            skippedFeaturesHash[GVol.getUid(feature).toString()]) ||
            !feature.getGeometry()) {
          i = /** @type {number} */ (instruction[2]);
        } else if (opt_hitExtent !== undefined && !GVol.extent.intersects(
            opt_hitExtent, feature.getGeometry().getExtent())) {
          i = /** @type {number} */ (instruction[2]) + 1;
        } else {
          ++i;
        }
        break;
      case GVol.render.canvas.Instruction.BEGIN_PATH:
        if (pendingFill > batchSize) {
          this.fill_(context, viewRotation);
          pendingFill = 0;
        }
        if (pendingStroke > batchSize) {
          context.stroke();
          pendingStroke = 0;
        }
        if (!pendingFill && !pendingStroke) {
          context.beginPath();
          prevX = prevY = NaN;
        }
        ++i;
        break;
      case GVol.render.canvas.Instruction.CIRCLE:
        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.moveTo(x1 + r, y1);
        context.arc(x1, y1, r, 0, 2 * Math.PI, true);
        ++i;
        break;
      case GVol.render.canvas.Instruction.CLOSE_PATH:
        context.closePath();
        ++i;
        break;
      case GVol.render.canvas.Instruction.CUSTOM:
        d = /** @type {number} */ (instruction[1]);
        dd = instruction[2];
        var geometry = /** @type {GVol.geom.SimpleGeometry} */ (instruction[3]);
        var renderer = instruction[4];
        var fn = instruction.length == 6 ? instruction[5] : undefined;
        state.geometry = geometry;
        state.feature = feature;
        if (!(i in coordinateCache)) {
          coordinateCache[i] = [];
        }
        var coords = coordinateCache[i];
        if (fn) {
          fn(pixelCoordinates, d, dd, 2, coords);
        } else {
          coords[0] = pixelCoordinates[d];
          coords[1] = pixelCoordinates[d + 1];
          coords.length = 2;
        }
        renderer(coords, state);
        ++i;
        break;
      case GVol.render.canvas.Instruction.DRAW_IMAGE:
        d = /** @type {number} */ (instruction[1]);
        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 {boGVolean} */ (instruction[10]);
        var rotation = /** @type {number} */ (instruction[11]);
        var scale = /** @type {number} */ (instruction[12]);
        var snapToPixel = /** @type {boGVolean} */ (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;
            GVol.transform.compose(localTransform,
                centerX, centerY, scale, scale, rotation, -centerX, -centerY);
            context.setTransform.apply(context, localTransform);
          }
          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) {
            context.setTransform.apply(context, resetTransform);
          }
        }
        ++i;
        break;
      case GVol.render.canvas.Instruction.DRAW_TEXT:
        d = /** @type {number} */ (instruction[1]);
        dd = /** @type {number} */ (instruction[2]);
        text = /** @type {string} */ (instruction[3]);
        var offsetX = /** @type {number} */ (instruction[4]) * pixelRatio;
        var offsetY = /** @type {number} */ (instruction[5]) * pixelRatio;
        rotation = /** @type {number} */ (instruction[6]);
        scale = /** @type {number} */ (instruction[7]) * pixelRatio;
        fill = /** @type {boGVolean} */ (instruction[8]);
        stroke = /** @type {boGVolean} */ (instruction[9]);
        rotateWithView = /** @type {boGVolean} */ (instruction[10]);
        if (rotateWithView) {
          rotation += viewRotation;
        }
        for (; d < dd; d += 2) {
          x = pixelCoordinates[d] + offsetX;
          y = pixelCoordinates[d + 1] + offsetY;
          if (scale != 1 || rotation !== 0) {
            GVol.transform.compose(localTransform, x, y, scale, scale, rotation, -x, -y);
            context.setTransform.apply(context, localTransform);
          }

          // 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) {
            context.setTransform.apply(context, resetTransform);
          }
        }
        ++i;
        break;
      case GVol.render.canvas.Instruction.END_GEOMETRY:
        if (featureCallback !== undefined) {
          feature = /** @type {GVol.Feature|GVol.render.Feature} */ (instruction[1]);
          var result = featureCallback(feature);
          if (result) {
            return result;
          }
        }
        ++i;
        break;
      case GVol.render.canvas.Instruction.FILL:
        if (batchSize) {
          pendingFill++;
        } else {
          this.fill_(context, viewRotation);
        }
        ++i;
        break;
      case GVol.render.canvas.Instruction.MOVE_TO_LINE_TO:
        d = /** @type {number} */ (instruction[1]);
        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 (d == dd - 2 || roundX !== prevX || roundY !== prevY) {
            context.lineTo(x, y);
            prevX = roundX;
            prevY = roundY;
          }
        }
        ++i;
        break;
      case GVol.render.canvas.Instruction.SET_FILL_STYLE:
        this.fillOrigin_ = instruction[2];

        if (pendingFill) {
          this.fill_(context, viewRotation);
          pendingFill = 0;
          if (pendingStroke) {
            context.stroke();
            pendingStroke = 0;
          }
        }

        context.fillStyle = /** @type {GVol.CGVolorLike} */ (instruction[1]);
        ++i;
        break;
      case GVol.render.canvas.Instruction.SET_STROKE_STYLE:
        var usePixelRatio = instruction[8] !== undefined ?
          instruction[8] : true;
        var renderedPixelRatio = instruction[9];

        var lineWidth = /** @type {number} */ (instruction[2]);
        if (pendingStroke) {
          context.stroke();
          pendingStroke = 0;
        }
        context.strokeStyle = /** @type {GVol.CGVolorLike} */ (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 (GVol.has.CANVAS_LINE_DASH) {
          var lineDash = /** @type {Array.<number>} */ (instruction[6]);
          var lineDashOffset = /** @type {number} */ (instruction[7]);
          if (usePixelRatio && pixelRatio !== renderedPixelRatio) {
            lineDash = lineDash.map(function(dash) {
              return dash * pixelRatio / renderedPixelRatio;
            });
            lineDashOffset *= pixelRatio / renderedPixelRatio;
            instruction[6] = lineDash;
            instruction[7] = lineDashOffset;
            instruction[9] = pixelRatio;
          }
          context.lineDashOffset = lineDashOffset;
          context.setLineDash(lineDash);
        }
        ++i;
        break;
      case GVol.render.canvas.Instruction.SET_TEXT_STYLE:
        context.font = /** @type {string} */ (instruction[1]);
        context.textAlign = /** @type {string} */ (instruction[2]);
        context.textBaseline = /** @type {string} */ (instruction[3]);
        ++i;
        break;
      case GVol.render.canvas.Instruction.STROKE:
        if (batchSize) {
          pendingStroke++;
        } else {
          context.stroke();
        }
        ++i;
        break;
      default:
        ++i; // consume the instruction anyway, to avoid an infinite loop
        break;
    }
  }
  if (pendingFill) {
    this.fill_(context, viewRotation);
  }
  if (pendingStroke) {
    context.stroke();
  }
  return undefined;
};


/**
 * @param {CanvasRenderingContext2D} context Context.
 * @param {number} pixelRatio Pixel ratio.
 * @param {GVol.Transform} transform Transform.
 * @param {number} viewRotation View rotation.
 * @param {Object.<string, boGVolean>} skippedFeaturesHash Ids of features
 *     to skip.
 */
GVol.render.canvas.Replay.prototype.replay = function(
    context, pixelRatio, transform, viewRotation, skippedFeaturesHash) {
  var instructions = this.instructions;
  this.replay_(context, pixelRatio, transform, viewRotation,
      skippedFeaturesHash, instructions, undefined, undefined);
};


/**
 * @param {CanvasRenderingContext2D} context Context.
 * @param {GVol.Transform} transform Transform.
 * @param {number} viewRotation View rotation.
 * @param {Object.<string, boGVolean>} skippedFeaturesHash Ids of features
 *     to skip.
 * @param {function((GVol.Feature|GVol.render.Feature)): T=} opt_featureCallback
 *     Feature callback.
 * @param {GVol.Extent=} opt_hitExtent Only check features that intersect this
 *     extent.
 * @return {T|undefined} Callback result.
 * @template T
 */
GVol.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);
};


/**
 * Reverse the hit detection instructions.
 */
GVol.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 {GVol.render.canvas.Instruction} */ (instruction[0]);
    if (type == GVol.render.canvas.Instruction.END_GEOMETRY) {
      begin = i;
    } else if (type == GVol.render.canvas.Instruction.BEGIN_GEOMETRY) {
      instruction[2] = i;
      GVol.array.reverseSubArray(this.hitDetectionInstructions, begin, i);
      begin = -1;
    }
  }
};


/**
 * @param {GVol.geom.Geometry|GVol.render.Feature} geometry Geometry.
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 */
GVol.render.canvas.Replay.prototype.endGeometry = function(geometry, feature) {
  this.beginGeometryInstruction1_[2] = this.instructions.length;
  this.beginGeometryInstruction1_ = null;
  this.beginGeometryInstruction2_[2] = this.hitDetectionInstructions.length;
  this.beginGeometryInstruction2_ = null;
  var endGeometryInstruction =
      [GVol.render.canvas.Instruction.END_GEOMETRY, feature];
  this.instructions.push(endGeometryInstruction);
  this.hitDetectionInstructions.push(endGeometryInstruction);
};


/**
 * FIXME empty description for jsdoc
 */
GVol.render.canvas.Replay.prototype.finish = GVol.nullFunction;


/**
 * Get the buffered rendering extent.  Rendering will be clipped to the extent
 * provided to the constructor.  To account for symbGVolizers that may intersect
 * this extent, we calculate a buffered extent (e.g. based on stroke width).
 * @return {GVol.Extent} The buffered rendering extent.
 * @protected
 */
GVol.render.canvas.Replay.prototype.getBufferedMaxExtent = function() {
  return this.maxExtent;
};

goog.provide('GVol.render.canvas.ImageReplay');

goog.require('GVol');
goog.require('GVol.render.canvas.Instruction');
goog.require('GVol.render.canvas.Replay');


/**
 * @constructor
 * @extends {GVol.render.canvas.Replay}
 * @param {number} tGVolerance TGVolerance.
 * @param {GVol.Extent} maxExtent Maximum extent.
 * @param {number} resGVolution ResGVolution.
 * @param {boGVolean} overlaps The replay can have overlapping geometries.
 * @struct
 */
GVol.render.canvas.ImageReplay = function(tGVolerance, maxExtent, resGVolution, overlaps) {
  GVol.render.canvas.Replay.call(this, tGVolerance, maxExtent, resGVolution, overlaps);

  /**
   * @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 {boGVolean|undefined}
   */
  this.rotateWithView_ = undefined;

  /**
   * @private
   * @type {number|undefined}
   */
  this.rotation_ = undefined;

  /**
   * @private
   * @type {number|undefined}
   */
  this.scale_ = undefined;

  /**
   * @private
   * @type {boGVolean|undefined}
   */
  this.snapToPixel_ = undefined;

  /**
   * @private
   * @type {number|undefined}
   */
  this.width_ = undefined;

};
GVol.inherits(GVol.render.canvas.ImageReplay, GVol.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.
 */
GVol.render.canvas.ImageReplay.prototype.drawCoordinates_ = function(flatCoordinates, offset, end, stride) {
  return this.appendFlatCoordinates(
      flatCoordinates, offset, end, stride, false, false);
};


/**
 * @inheritDoc
 */
GVol.render.canvas.ImageReplay.prototype.drawPoint = function(pointGeometry, feature) {
  if (!this.image_) {
    return;
  }
  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([
    GVol.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([
    GVol.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
 */
GVol.render.canvas.ImageReplay.prototype.drawMultiPoint = function(multiPointGeometry, feature) {
  if (!this.image_) {
    return;
  }
  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([
    GVol.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([
    GVol.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
 */
GVol.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
 */
GVol.render.canvas.ImageReplay.prototype.setImageStyle = function(imageStyle) {
  var anchor = imageStyle.getAnchor();
  var size = imageStyle.getSize();
  var hitDetectionImage = imageStyle.getHitDetectionImage(1);
  var image = imageStyle.getImage(1);
  var origin = imageStyle.getOrigin();
  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];
};

goog.provide('GVol.render.canvas.LineStringReplay');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.cGVolorlike');
goog.require('GVol.extent');
goog.require('GVol.render.canvas');
goog.require('GVol.render.canvas.Instruction');
goog.require('GVol.render.canvas.Replay');


/**
 * @constructor
 * @extends {GVol.render.canvas.Replay}
 * @param {number} tGVolerance TGVolerance.
 * @param {GVol.Extent} maxExtent Maximum extent.
 * @param {number} resGVolution ResGVolution.
 * @param {boGVolean} overlaps The replay can have overlapping geometries.
 * @struct
 */
GVol.render.canvas.LineStringReplay = function(tGVolerance, maxExtent, resGVolution, overlaps) {

  GVol.render.canvas.Replay.call(this, tGVolerance, maxExtent, resGVolution, overlaps);

  /**
   * @private
   * @type {GVol.Extent}
   */
  this.bufferedMaxExtent_ = null;

  /**
   * @private
   * @type {{currentStrokeStyle: (GVol.CGVolorLike|undefined),
   *         currentLineCap: (string|undefined),
   *         currentLineDash: Array.<number>,
   *         currentLineDashOffset: (number|undefined),
   *         currentLineJoin: (string|undefined),
   *         currentLineWidth: (number|undefined),
   *         currentMiterLimit: (number|undefined),
   *         lastStroke: (number|undefined),
   *         strokeStyle: (GVol.CGVolorLike|undefined),
   *         lineCap: (string|undefined),
   *         lineDash: Array.<number>,
   *         lineDashOffset: (number|undefined),
   *         lineJoin: (string|undefined),
   *         lineWidth: (number|undefined),
   *         miterLimit: (number|undefined)}|null}
   */
  this.state_ = {
    currentStrokeStyle: undefined,
    currentLineCap: undefined,
    currentLineDash: null,
    currentLineDashOffset: undefined,
    currentLineJoin: undefined,
    currentLineWidth: undefined,
    currentMiterLimit: undefined,
    lastStroke: undefined,
    strokeStyle: undefined,
    lineCap: undefined,
    lineDash: null,
    lineDashOffset: undefined,
    lineJoin: undefined,
    lineWidth: undefined,
    miterLimit: undefined
  };

};
GVol.inherits(GVol.render.canvas.LineStringReplay, GVol.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.
 */
GVol.render.canvas.LineStringReplay.prototype.drawFlatCoordinates_ = function(flatCoordinates, offset, end, stride) {
  var myBegin = this.coordinates.length;
  var myEnd = this.appendFlatCoordinates(
      flatCoordinates, offset, end, stride, false, false);
  var moveToLineToInstruction =
      [GVol.render.canvas.Instruction.MOVE_TO_LINE_TO, myBegin, myEnd];
  this.instructions.push(moveToLineToInstruction);
  this.hitDetectionInstructions.push(moveToLineToInstruction);
  return end;
};


/**
 * @inheritDoc
 */
GVol.render.canvas.LineStringReplay.prototype.getBufferedMaxExtent = function() {
  if (!this.bufferedMaxExtent_) {
    this.bufferedMaxExtent_ = GVol.extent.clone(this.maxExtent);
    if (this.maxLineWidth > 0) {
      var width = this.resGVolution * (this.maxLineWidth + 1) / 2;
      GVol.extent.buffer(this.bufferedMaxExtent_, width, this.bufferedMaxExtent_);
    }
  }
  return this.bufferedMaxExtent_;
};


/**
 * @private
 */
GVol.render.canvas.LineStringReplay.prototype.setStrokeStyle_ = function() {
  var state = this.state_;
  var strokeStyle = state.strokeStyle;
  var lineCap = state.lineCap;
  var lineDash = state.lineDash;
  var lineDashOffset = state.lineDashOffset;
  var lineJoin = state.lineJoin;
  var lineWidth = state.lineWidth;
  var miterLimit = state.miterLimit;
  if (state.currentStrokeStyle != strokeStyle ||
      state.currentLineCap != lineCap ||
      !GVol.array.equals(state.currentLineDash, lineDash) ||
      state.currentLineDashOffset != lineDashOffset ||
      state.currentLineJoin != lineJoin ||
      state.currentLineWidth != lineWidth ||
      state.currentMiterLimit != miterLimit) {
    if (state.lastStroke != undefined && state.lastStroke != this.coordinates.length) {
      this.instructions.push([GVol.render.canvas.Instruction.STROKE]);
      state.lastStroke = this.coordinates.length;
    }
    state.lastStroke = 0;
    this.instructions.push([
      GVol.render.canvas.Instruction.SET_STROKE_STYLE,
      strokeStyle, lineWidth, lineCap, lineJoin, miterLimit, lineDash, lineDashOffset, true, 1
    ], [
      GVol.render.canvas.Instruction.BEGIN_PATH
    ]);
    state.currentStrokeStyle = strokeStyle;
    state.currentLineCap = lineCap;
    state.currentLineDash = lineDash;
    state.currentLineDashOffset = lineDashOffset;
    state.currentLineJoin = lineJoin;
    state.currentLineWidth = lineWidth;
    state.currentMiterLimit = miterLimit;
  }
};


/**
 * @inheritDoc
 */
GVol.render.canvas.LineStringReplay.prototype.drawLineString = function(lineStringGeometry, feature) {
  var state = this.state_;
  var strokeStyle = state.strokeStyle;
  var lineWidth = state.lineWidth;
  if (strokeStyle === undefined || lineWidth === undefined) {
    return;
  }
  this.setStrokeStyle_();
  this.beginGeometry(lineStringGeometry, feature);
  this.hitDetectionInstructions.push([
    GVol.render.canvas.Instruction.SET_STROKE_STYLE,
    state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin,
    state.miterLimit, state.lineDash, state.lineDashOffset, true, 1
  ], [
    GVol.render.canvas.Instruction.BEGIN_PATH
  ]);
  var flatCoordinates = lineStringGeometry.getFlatCoordinates();
  var stride = lineStringGeometry.getStride();
  this.drawFlatCoordinates_(flatCoordinates, 0, flatCoordinates.length, stride);
  this.hitDetectionInstructions.push([GVol.render.canvas.Instruction.STROKE]);
  this.endGeometry(lineStringGeometry, feature);
};


/**
 * @inheritDoc
 */
GVol.render.canvas.LineStringReplay.prototype.drawMultiLineString = function(multiLineStringGeometry, feature) {
  var state = this.state_;
  var strokeStyle = state.strokeStyle;
  var lineWidth = state.lineWidth;
  if (strokeStyle === undefined || lineWidth === undefined) {
    return;
  }
  this.setStrokeStyle_();
  this.beginGeometry(multiLineStringGeometry, feature);
  this.hitDetectionInstructions.push([
    GVol.render.canvas.Instruction.SET_STROKE_STYLE,
    state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin,
    state.miterLimit, state.lineDash, state.lineDashOffset, true, 1
  ], [
    GVol.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([GVol.render.canvas.Instruction.STROKE]);
  this.endGeometry(multiLineStringGeometry, feature);
};


/**
 * @inheritDoc
 */
GVol.render.canvas.LineStringReplay.prototype.finish = function() {
  var state = this.state_;
  if (state.lastStroke != undefined && state.lastStroke != this.coordinates.length) {
    this.instructions.push([GVol.render.canvas.Instruction.STROKE]);
  }
  this.reverseHitDetectionInstructions();
  this.state_ = null;
};


/**
 * @inheritDoc
 */
GVol.render.canvas.LineStringReplay.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) {
  var strokeStyleCGVolor = strokeStyle.getCGVolor();
  this.state_.strokeStyle = GVol.cGVolorlike.asCGVolorLike(strokeStyleCGVolor ?
    strokeStyleCGVolor : GVol.render.canvas.defaultStrokeStyle);
  var strokeStyleLineCap = strokeStyle.getLineCap();
  this.state_.lineCap = strokeStyleLineCap !== undefined ?
    strokeStyleLineCap : GVol.render.canvas.defaultLineCap;
  var strokeStyleLineDash = strokeStyle.getLineDash();
  this.state_.lineDash = strokeStyleLineDash ?
    strokeStyleLineDash : GVol.render.canvas.defaultLineDash;
  var strokeStyleLineDashOffset = strokeStyle.getLineDashOffset();
  this.state_.lineDashOffset = strokeStyleLineDashOffset ?
    strokeStyleLineDashOffset : GVol.render.canvas.defaultLineDashOffset;
  var strokeStyleLineJoin = strokeStyle.getLineJoin();
  this.state_.lineJoin = strokeStyleLineJoin !== undefined ?
    strokeStyleLineJoin : GVol.render.canvas.defaultLineJoin;
  var strokeStyleWidth = strokeStyle.getWidth();
  this.state_.lineWidth = strokeStyleWidth !== undefined ?
    strokeStyleWidth : GVol.render.canvas.defaultLineWidth;
  var strokeStyleMiterLimit = strokeStyle.getMiterLimit();
  this.state_.miterLimit = strokeStyleMiterLimit !== undefined ?
    strokeStyleMiterLimit : GVol.render.canvas.defaultMiterLimit;

  if (this.state_.lineWidth > this.maxLineWidth) {
    this.maxLineWidth = this.state_.lineWidth;
    // invalidate the buffered max extent cache
    this.bufferedMaxExtent_ = null;
  }
};

goog.provide('GVol.render.canvas.PGVolygonReplay');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.cGVolor');
goog.require('GVol.cGVolorlike');
goog.require('GVol.extent');
goog.require('GVol.geom.flat.simplify');
goog.require('GVol.render.canvas');
goog.require('GVol.render.canvas.Instruction');
goog.require('GVol.render.canvas.Replay');


/**
 * @constructor
 * @extends {GVol.render.canvas.Replay}
 * @param {number} tGVolerance TGVolerance.
 * @param {GVol.Extent} maxExtent Maximum extent.
 * @param {number} resGVolution ResGVolution.
 * @param {boGVolean} overlaps The replay can have overlapping geometries.
 * @struct
 */
GVol.render.canvas.PGVolygonReplay = function(tGVolerance, maxExtent, resGVolution, overlaps) {

  GVol.render.canvas.Replay.call(this, tGVolerance, maxExtent, resGVolution, overlaps);

  /**
   * @private
   * @type {GVol.Extent}
   */
  this.bufferedMaxExtent_ = null;

  /**
   * @private
   * @type {{currentFillStyle: (GVol.CGVolorLike|undefined),
   *         currentStrokeStyle: (GVol.CGVolorLike|undefined),
   *         currentLineCap: (string|undefined),
   *         currentLineDash: Array.<number>,
   *         currentLineDashOffset: (number|undefined),
   *         currentLineJoin: (string|undefined),
   *         currentLineWidth: (number|undefined),
   *         currentMiterLimit: (number|undefined),
   *         fillStyle: (GVol.CGVolorLike|undefined),
   *         strokeStyle: (GVol.CGVolorLike|undefined),
   *         lineCap: (string|undefined),
   *         lineDash: Array.<number>,
   *         lineDashOffset: (number|undefined),
   *         lineJoin: (string|undefined),
   *         lineWidth: (number|undefined),
   *         miterLimit: (number|undefined)}|null}
   */
  this.state_ = {
    currentFillStyle: undefined,
    currentStrokeStyle: undefined,
    currentLineCap: undefined,
    currentLineDash: null,
    currentLineDashOffset: undefined,
    currentLineJoin: undefined,
    currentLineWidth: undefined,
    currentMiterLimit: undefined,
    fillStyle: undefined,
    strokeStyle: undefined,
    lineCap: undefined,
    lineDash: null,
    lineDashOffset: undefined,
    lineJoin: undefined,
    lineWidth: undefined,
    miterLimit: undefined
  };

};
GVol.inherits(GVol.render.canvas.PGVolygonReplay, GVol.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.
 */
GVol.render.canvas.PGVolygonReplay.prototype.drawFlatCoordinatess_ = function(flatCoordinates, offset, ends, stride) {
  var state = this.state_;
  var fill = state.fillStyle !== undefined;
  var stroke = state.strokeStyle != undefined;
  var numEnds = ends.length;
  var beginPathInstruction = [GVol.render.canvas.Instruction.BEGIN_PATH];
  this.instructions.push(beginPathInstruction);
  this.hitDetectionInstructions.push(beginPathInstruction);
  for (var i = 0; i < numEnds; ++i) {
    var end = ends[i];
    var myBegin = this.coordinates.length;
    var myEnd = this.appendFlatCoordinates(
        flatCoordinates, offset, end, stride, true, !stroke);
    var moveToLineToInstruction =
        [GVol.render.canvas.Instruction.MOVE_TO_LINE_TO, myBegin, myEnd];
    this.instructions.push(moveToLineToInstruction);
    this.hitDetectionInstructions.push(moveToLineToInstruction);
    if (stroke) {
      // Performance optimization: only call closePath() when we have a stroke.
      // Otherwise the ring is closed already (see appendFlatCoordinates above).
      var closePathInstruction = [GVol.render.canvas.Instruction.CLOSE_PATH];
      this.instructions.push(closePathInstruction);
      this.hitDetectionInstructions.push(closePathInstruction);
    }
    offset = end;
  }
  var fillInstruction = [GVol.render.canvas.Instruction.FILL];
  this.hitDetectionInstructions.push(fillInstruction);
  if (fill) {
    this.instructions.push(fillInstruction);
  }
  if (stroke) {
    var strokeInstruction = [GVol.render.canvas.Instruction.STROKE];
    this.instructions.push(strokeInstruction);
    this.hitDetectionInstructions.push(strokeInstruction);
  }
  return offset;
};


/**
 * @inheritDoc
 */
GVol.render.canvas.PGVolygonReplay.prototype.drawCircle = function(circleGeometry, feature) {
  var state = this.state_;
  var fillStyle = state.fillStyle;
  var strokeStyle = state.strokeStyle;
  if (fillStyle === undefined && strokeStyle === undefined) {
    return;
  }
  this.setFillStrokeStyles_(circleGeometry);
  this.beginGeometry(circleGeometry, feature);
  // always fill the circle for hit detection
  this.hitDetectionInstructions.push([
    GVol.render.canvas.Instruction.SET_FILL_STYLE,
    GVol.cGVolor.asString(GVol.render.canvas.defaultFillStyle)
  ]);
  if (state.strokeStyle !== undefined) {
    this.hitDetectionInstructions.push([
      GVol.render.canvas.Instruction.SET_STROKE_STYLE,
      state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin,
      state.miterLimit, state.lineDash, state.lineDashOffset, true, 1
    ]);
  }
  var flatCoordinates = circleGeometry.getFlatCoordinates();
  var stride = circleGeometry.getStride();
  var myBegin = this.coordinates.length;
  this.appendFlatCoordinates(
      flatCoordinates, 0, flatCoordinates.length, stride, false, false);
  var beginPathInstruction = [GVol.render.canvas.Instruction.BEGIN_PATH];
  var circleInstruction = [GVol.render.canvas.Instruction.CIRCLE, myBegin];
  this.instructions.push(beginPathInstruction, circleInstruction);
  this.hitDetectionInstructions.push(beginPathInstruction, circleInstruction);
  var fillInstruction = [GVol.render.canvas.Instruction.FILL];
  this.hitDetectionInstructions.push(fillInstruction);
  if (state.fillStyle !== undefined) {
    this.instructions.push(fillInstruction);
  }
  if (state.strokeStyle !== undefined) {
    var strokeInstruction = [GVol.render.canvas.Instruction.STROKE];
    this.instructions.push(strokeInstruction);
    this.hitDetectionInstructions.push(strokeInstruction);
  }
  this.endGeometry(circleGeometry, feature);
};


/**
 * @inheritDoc
 */
GVol.render.canvas.PGVolygonReplay.prototype.drawPGVolygon = function(pGVolygonGeometry, feature) {
  var state = this.state_;
  this.setFillStrokeStyles_(pGVolygonGeometry);
  this.beginGeometry(pGVolygonGeometry, feature);
  // always fill the pGVolygon for hit detection
  this.hitDetectionInstructions.push([
    GVol.render.canvas.Instruction.SET_FILL_STYLE,
    GVol.cGVolor.asString(GVol.render.canvas.defaultFillStyle)]
  );
  if (state.strokeStyle !== undefined) {
    this.hitDetectionInstructions.push([
      GVol.render.canvas.Instruction.SET_STROKE_STYLE,
      state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin,
      state.miterLimit, state.lineDash, state.lineDashOffset, true, 1
    ]);
  }
  var ends = pGVolygonGeometry.getEnds();
  var flatCoordinates = pGVolygonGeometry.getOrientedFlatCoordinates();
  var stride = pGVolygonGeometry.getStride();
  this.drawFlatCoordinatess_(flatCoordinates, 0, ends, stride);
  this.endGeometry(pGVolygonGeometry, feature);
};


/**
 * @inheritDoc
 */
GVol.render.canvas.PGVolygonReplay.prototype.drawMultiPGVolygon = function(multiPGVolygonGeometry, feature) {
  var state = this.state_;
  var fillStyle = state.fillStyle;
  var strokeStyle = state.strokeStyle;
  if (fillStyle === undefined && strokeStyle === undefined) {
    return;
  }
  this.setFillStrokeStyles_(multiPGVolygonGeometry);
  this.beginGeometry(multiPGVolygonGeometry, feature);
  // always fill the multi-pGVolygon for hit detection
  this.hitDetectionInstructions.push([
    GVol.render.canvas.Instruction.SET_FILL_STYLE,
    GVol.cGVolor.asString(GVol.render.canvas.defaultFillStyle)
  ]);
  if (state.strokeStyle !== undefined) {
    this.hitDetectionInstructions.push([
      GVol.render.canvas.Instruction.SET_STROKE_STYLE,
      state.strokeStyle, state.lineWidth, state.lineCap, state.lineJoin,
      state.miterLimit, state.lineDash, state.lineDashOffset, true, 1
    ]);
  }
  var endss = multiPGVolygonGeometry.getEndss();
  var flatCoordinates = multiPGVolygonGeometry.getOrientedFlatCoordinates();
  var stride = multiPGVolygonGeometry.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(multiPGVolygonGeometry, feature);
};


/**
 * @inheritDoc
 */
GVol.render.canvas.PGVolygonReplay.prototype.finish = function() {
  this.reverseHitDetectionInstructions();
  this.state_ = null;
  // We want to preserve topGVology when drawing pGVolygons.  PGVolygons 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 tGVolerance = this.tGVolerance;
  if (tGVolerance !== 0) {
    var coordinates = this.coordinates;
    var i, ii;
    for (i = 0, ii = coordinates.length; i < ii; ++i) {
      coordinates[i] = GVol.geom.flat.simplify.snap(coordinates[i], tGVolerance);
    }
  }
};


/**
 * @inheritDoc
 */
GVol.render.canvas.PGVolygonReplay.prototype.getBufferedMaxExtent = function() {
  if (!this.bufferedMaxExtent_) {
    this.bufferedMaxExtent_ = GVol.extent.clone(this.maxExtent);
    if (this.maxLineWidth > 0) {
      var width = this.resGVolution * (this.maxLineWidth + 1) / 2;
      GVol.extent.buffer(this.bufferedMaxExtent_, width, this.bufferedMaxExtent_);
    }
  }
  return this.bufferedMaxExtent_;
};


/**
 * @inheritDoc
 */
GVol.render.canvas.PGVolygonReplay.prototype.setFillStrokeStyle = function(fillStyle, strokeStyle) {
  var state = this.state_;
  if (fillStyle) {
    var fillStyleCGVolor = fillStyle.getCGVolor();
    state.fillStyle = GVol.cGVolorlike.asCGVolorLike(fillStyleCGVolor ?
      fillStyleCGVolor : GVol.render.canvas.defaultFillStyle);
  } else {
    state.fillStyle = undefined;
  }
  if (strokeStyle) {
    var strokeStyleCGVolor = strokeStyle.getCGVolor();
    state.strokeStyle = GVol.cGVolorlike.asCGVolorLike(strokeStyleCGVolor ?
      strokeStyleCGVolor : GVol.render.canvas.defaultStrokeStyle);
    var strokeStyleLineCap = strokeStyle.getLineCap();
    state.lineCap = strokeStyleLineCap !== undefined ?
      strokeStyleLineCap : GVol.render.canvas.defaultLineCap;
    var strokeStyleLineDash = strokeStyle.getLineDash();
    state.lineDash = strokeStyleLineDash ?
      strokeStyleLineDash.slice() : GVol.render.canvas.defaultLineDash;
    var strokeStyleLineDashOffset = strokeStyle.getLineDashOffset();
    state.lineDashOffset = strokeStyleLineDashOffset ?
      strokeStyleLineDashOffset : GVol.render.canvas.defaultLineDashOffset;
    var strokeStyleLineJoin = strokeStyle.getLineJoin();
    state.lineJoin = strokeStyleLineJoin !== undefined ?
      strokeStyleLineJoin : GVol.render.canvas.defaultLineJoin;
    var strokeStyleWidth = strokeStyle.getWidth();
    state.lineWidth = strokeStyleWidth !== undefined ?
      strokeStyleWidth : GVol.render.canvas.defaultLineWidth;
    var strokeStyleMiterLimit = strokeStyle.getMiterLimit();
    state.miterLimit = strokeStyleMiterLimit !== undefined ?
      strokeStyleMiterLimit : GVol.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.lineDashOffset = undefined;
    state.lineJoin = undefined;
    state.lineWidth = undefined;
    state.miterLimit = undefined;
  }
};


/**
 * @private
 * @param {GVol.geom.Geometry|GVol.render.Feature} geometry Geometry.
 */
GVol.render.canvas.PGVolygonReplay.prototype.setFillStrokeStyles_ = function(geometry) {
  var state = this.state_;
  var fillStyle = state.fillStyle;
  var strokeStyle = state.strokeStyle;
  var lineCap = state.lineCap;
  var lineDash = state.lineDash;
  var lineDashOffset = state.lineDashOffset;
  var lineJoin = state.lineJoin;
  var lineWidth = state.lineWidth;
  var miterLimit = state.miterLimit;
  if (fillStyle !== undefined && (typeof fillStyle !== 'string' || state.currentFillStyle != fillStyle)) {
    var fillInstruction = [GVol.render.canvas.Instruction.SET_FILL_STYLE, fillStyle];
    if (typeof fillStyle !== 'string') {
      var fillExtent = geometry.getExtent();
      fillInstruction.push([fillExtent[0], fillExtent[3]]);
    }
    this.instructions.push(fillInstruction);
    state.currentFillStyle = state.fillStyle;
  }
  if (strokeStyle !== undefined) {
    if (state.currentStrokeStyle != strokeStyle ||
        state.currentLineCap != lineCap ||
        !GVol.array.equals(state.currentLineDash, lineDash) ||
        state.currentLineDashOffset != lineDashOffset ||
        state.currentLineJoin != lineJoin ||
        state.currentLineWidth != lineWidth ||
        state.currentMiterLimit != miterLimit) {
      this.instructions.push([
        GVol.render.canvas.Instruction.SET_STROKE_STYLE,
        strokeStyle, lineWidth, lineCap, lineJoin, miterLimit, lineDash, lineDashOffset, true, 1
      ]);
      state.currentStrokeStyle = strokeStyle;
      state.currentLineCap = lineCap;
      state.currentLineDash = lineDash;
      state.currentLineDashOffset = lineDashOffset;
      state.currentLineJoin = lineJoin;
      state.currentLineWidth = lineWidth;
      state.currentMiterLimit = miterLimit;
    }
  }
};

goog.provide('GVol.render.canvas.TextReplay');

goog.require('GVol');
goog.require('GVol.cGVolorlike');
goog.require('GVol.render.canvas');
goog.require('GVol.render.canvas.Instruction');
goog.require('GVol.render.canvas.Replay');


/**
 * @constructor
 * @extends {GVol.render.canvas.Replay}
 * @param {number} tGVolerance TGVolerance.
 * @param {GVol.Extent} maxExtent Maximum extent.
 * @param {number} resGVolution ResGVolution.
 * @param {boGVolean} overlaps The replay can have overlapping geometries.
 * @struct
 */
GVol.render.canvas.TextReplay = function(tGVolerance, maxExtent, resGVolution, overlaps) {

  GVol.render.canvas.Replay.call(this, tGVolerance, maxExtent, resGVolution, overlaps);

  /**
   * @private
   * @type {?GVol.CanvasFillState}
   */
  this.replayFillState_ = null;

  /**
   * @private
   * @type {?GVol.CanvasStrokeState}
   */
  this.replayStrokeState_ = null;

  /**
   * @private
   * @type {?GVol.CanvasTextState}
   */
  this.replayTextState_ = null;

  /**
   * @private
   * @type {string}
   */
  this.text_ = '';

  /**
   * @private
   * @type {number}
   */
  this.textOffsetX_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.textOffsetY_ = 0;

  /**
   * @private
   * @type {boGVolean|undefined}
   */
  this.textRotateWithView_ = undefined;

  /**
   * @private
   * @type {number}
   */
  this.textRotation_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.textScale_ = 0;

  /**
   * @private
   * @type {?GVol.CanvasFillState}
   */
  this.textFillState_ = null;

  /**
   * @private
   * @type {?GVol.CanvasStrokeState}
   */
  this.textStrokeState_ = null;

  /**
   * @private
   * @type {?GVol.CanvasTextState}
   */
  this.textState_ = null;

};
GVol.inherits(GVol.render.canvas.TextReplay, GVol.render.canvas.Replay);


/**
 * @inheritDoc
 */
GVol.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, false);
  var fill = !!this.textFillState_;
  var stroke = !!this.textStrokeState_;
  var drawTextInstruction = [
    GVol.render.canvas.Instruction.DRAW_TEXT, myBegin, myEnd, this.text_,
    this.textOffsetX_, this.textOffsetY_, this.textRotation_, this.textScale_,
    fill, stroke, this.textRotateWithView_];
  this.instructions.push(drawTextInstruction);
  this.hitDetectionInstructions.push(drawTextInstruction);
  this.endGeometry(geometry, feature);
};


/**
 * @param {GVol.CanvasFillState} fillState Fill state.
 * @private
 */
GVol.render.canvas.TextReplay.prototype.setReplayFillState_ = function(fillState) {
  var replayFillState = this.replayFillState_;
  if (replayFillState &&
      replayFillState.fillStyle == fillState.fillStyle) {
    return;
  }
  var setFillStyleInstruction =
      [GVol.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 {GVol.CanvasStrokeState} strokeState Stroke state.
 * @private
 */
GVol.render.canvas.TextReplay.prototype.setReplayStrokeState_ = function(strokeState) {
  var replayStrokeState = this.replayStrokeState_;
  if (replayStrokeState &&
      replayStrokeState.lineCap == strokeState.lineCap &&
      replayStrokeState.lineDash == strokeState.lineDash &&
      replayStrokeState.lineDashOffset == strokeState.lineDashOffset &&
      replayStrokeState.lineJoin == strokeState.lineJoin &&
      replayStrokeState.lineWidth == strokeState.lineWidth &&
      replayStrokeState.miterLimit == strokeState.miterLimit &&
      replayStrokeState.strokeStyle == strokeState.strokeStyle) {
    return;
  }
  var setStrokeStyleInstruction = [
    GVol.render.canvas.Instruction.SET_STROKE_STYLE, strokeState.strokeStyle,
    strokeState.lineWidth, strokeState.lineCap, strokeState.lineJoin,
    strokeState.miterLimit, strokeState.lineDash, strokeState.lineDashOffset, false, 1
  ];
  this.instructions.push(setStrokeStyleInstruction);
  this.hitDetectionInstructions.push(setStrokeStyleInstruction);
  if (!replayStrokeState) {
    this.replayStrokeState_ = {
      lineCap: strokeState.lineCap,
      lineDash: strokeState.lineDash,
      lineDashOffset: strokeState.lineDashOffset,
      lineJoin: strokeState.lineJoin,
      lineWidth: strokeState.lineWidth,
      miterLimit: strokeState.miterLimit,
      strokeStyle: strokeState.strokeStyle
    };
  } else {
    replayStrokeState.lineCap = strokeState.lineCap;
    replayStrokeState.lineDash = strokeState.lineDash;
    replayStrokeState.lineDashOffset = strokeState.lineDashOffset;
    replayStrokeState.lineJoin = strokeState.lineJoin;
    replayStrokeState.lineWidth = strokeState.lineWidth;
    replayStrokeState.miterLimit = strokeState.miterLimit;
    replayStrokeState.strokeStyle = strokeState.strokeStyle;
  }
};


/**
 * @param {GVol.CanvasTextState} textState Text state.
 * @private
 */
GVol.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 = [GVol.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
 */
GVol.render.canvas.TextReplay.prototype.setTextStyle = function(textStyle) {
  if (!textStyle) {
    this.text_ = '';
  } else {
    var textFillStyle = textStyle.getFill();
    if (!textFillStyle) {
      this.textFillState_ = null;
    } else {
      var textFillStyleCGVolor = textFillStyle.getCGVolor();
      var fillStyle = GVol.cGVolorlike.asCGVolorLike(textFillStyleCGVolor ?
        textFillStyleCGVolor : GVol.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 textStrokeStyleCGVolor = textStrokeStyle.getCGVolor();
      var textStrokeStyleLineCap = textStrokeStyle.getLineCap();
      var textStrokeStyleLineDash = textStrokeStyle.getLineDash();
      var textStrokeStyleLineDashOffset = textStrokeStyle.getLineDashOffset();
      var textStrokeStyleLineJoin = textStrokeStyle.getLineJoin();
      var textStrokeStyleWidth = textStrokeStyle.getWidth();
      var textStrokeStyleMiterLimit = textStrokeStyle.getMiterLimit();
      var lineCap = textStrokeStyleLineCap !== undefined ?
        textStrokeStyleLineCap : GVol.render.canvas.defaultLineCap;
      var lineDash = textStrokeStyleLineDash ?
        textStrokeStyleLineDash.slice() : GVol.render.canvas.defaultLineDash;
      var lineDashOffset = textStrokeStyleLineDashOffset !== undefined ?
        textStrokeStyleLineDashOffset : GVol.render.canvas.defaultLineDashOffset;
      var lineJoin = textStrokeStyleLineJoin !== undefined ?
        textStrokeStyleLineJoin : GVol.render.canvas.defaultLineJoin;
      var lineWidth = textStrokeStyleWidth !== undefined ?
        textStrokeStyleWidth : GVol.render.canvas.defaultLineWidth;
      var miterLimit = textStrokeStyleMiterLimit !== undefined ?
        textStrokeStyleMiterLimit : GVol.render.canvas.defaultMiterLimit;
      var strokeStyle = GVol.cGVolorlike.asCGVolorLike(textStrokeStyleCGVolor ?
        textStrokeStyleCGVolor : GVol.render.canvas.defaultStrokeStyle);
      if (!this.textStrokeState_) {
        this.textStrokeState_ = {
          lineCap: lineCap,
          lineDash: lineDash,
          lineDashOffset: lineDashOffset,
          lineJoin: lineJoin,
          lineWidth: lineWidth,
          miterLimit: miterLimit,
          strokeStyle: strokeStyle
        };
      } else {
        var textStrokeState = this.textStrokeState_;
        textStrokeState.lineCap = lineCap;
        textStrokeState.lineDash = lineDash;
        textStrokeState.lineDashOffset = lineDashOffset;
        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 textRotateWithView = textStyle.getRotateWithView();
    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 : GVol.render.canvas.defaultFont;
    var textAlign = textTextAlign !== undefined ?
      textTextAlign : GVol.render.canvas.defaultTextAlign;
    var textBaseline = textTextBaseline !== undefined ?
      textTextBaseline : GVol.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.textRotateWithView_ = textRotateWithView !== undefined ? textRotateWithView : false;
    this.textRotation_ = textRotation !== undefined ? textRotation : 0;
    this.textScale_ = textScale !== undefined ? textScale : 1;
  }
};

goog.provide('GVol.render.canvas.ReplayGroup');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.dom');
goog.require('GVol.extent');
goog.require('GVol.geom.flat.transform');
goog.require('GVol.obj');
goog.require('GVol.render.ReplayGroup');
goog.require('GVol.render.canvas.Replay');
goog.require('GVol.render.canvas.ImageReplay');
goog.require('GVol.render.canvas.LineStringReplay');
goog.require('GVol.render.canvas.PGVolygonReplay');
goog.require('GVol.render.canvas.TextReplay');
goog.require('GVol.render.replay');
goog.require('GVol.transform');


/**
 * @constructor
 * @extends {GVol.render.ReplayGroup}
 * @param {number} tGVolerance TGVolerance.
 * @param {GVol.Extent} maxExtent Max extent.
 * @param {number} resGVolution ResGVolution.
 * @param {boGVolean} overlaps The replay group can have overlapping geometries.
 * @param {number=} opt_renderBuffer Optional rendering buffer.
 * @struct
 */
GVol.render.canvas.ReplayGroup = function(
    tGVolerance, maxExtent, resGVolution, overlaps, opt_renderBuffer) {
  GVol.render.ReplayGroup.call(this);

  /**
   * @private
   * @type {number}
   */
  this.tGVolerance_ = tGVolerance;

  /**
   * @private
   * @type {GVol.Extent}
   */
  this.maxExtent_ = maxExtent;

  /**
   * @private
   * @type {boGVolean}
   */
  this.overlaps_ = overlaps;

  /**
   * @private
   * @type {number}
   */
  this.resGVolution_ = resGVolution;

  /**
   * @private
   * @type {number|undefined}
   */
  this.renderBuffer_ = opt_renderBuffer;

  /**
   * @private
   * @type {!Object.<string,
   *        Object.<GVol.render.ReplayType, GVol.render.canvas.Replay>>}
   */
  this.replaysByZIndex_ = {};

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.hitDetectionContext_ = GVol.dom.createCanvasContext2D(1, 1);

  /**
   * @private
   * @type {GVol.Transform}
   */
  this.hitDetectionTransform_ = GVol.transform.create();
};
GVol.inherits(GVol.render.canvas.ReplayGroup, GVol.render.ReplayGroup);


/**
 * This cache is used for storing calculated pixel circles for increasing performance.
 * It is a static property to allow each Replaygroup to access it.
 * @type {Object.<number, Array.<Array.<(boGVolean|undefined)>>>}
 * @private
 */
GVol.render.canvas.ReplayGroup.circleArrayCache_ = {
  0: [[true]]
};


/**
 * This method fills a row in the array from the given coordinate to the
 * middle with `true`.
 * @param {Array.<Array.<(boGVolean|undefined)>>} array The array that will be altered.
 * @param {number} x X coordinate.
 * @param {number} y Y coordinate.
 * @private
 */
GVol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_ = function(array, x, y) {
  var i;
  var radius = Math.floor(array.length / 2);
  if (x >= radius) {
    for (i = radius; i < x; i++) {
      array[i][y] = true;
    }
  } else if (x < radius) {
    for (i = x + 1; i < radius; i++) {
      array[i][y] = true;
    }
  }
};


/**
 * This methods creates a circle inside a fitting array. Points inside the
 * circle are marked by true, points on the outside are undefined.
 * It uses the midpoint circle algorithm.
 * A cache is used to increase performance.
 * @param {number} radius Radius.
 * @returns {Array.<Array.<(boGVolean|undefined)>>} An array with marked circle points.
 * @private
 */
GVol.render.canvas.ReplayGroup.getCircleArray_ = function(radius) {
  if (GVol.render.canvas.ReplayGroup.circleArrayCache_[radius] !== undefined) {
    return GVol.render.canvas.ReplayGroup.circleArrayCache_[radius];
  }

  var arraySize = radius * 2 + 1;
  var arr = new Array(arraySize);
  for (var i = 0; i < arraySize; i++) {
    arr[i] = new Array(arraySize);
  }

  var x = radius;
  var y = 0;
  var error = 0;

  while (x >= y) {
    GVol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius + x, radius + y);
    GVol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius + y, radius + x);
    GVol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius - y, radius + x);
    GVol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius - x, radius + y);
    GVol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius - x, radius - y);
    GVol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius - y, radius - x);
    GVol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius + y, radius - x);
    GVol.render.canvas.ReplayGroup.fillCircleArrayRowToMiddle_(arr, radius + x, radius - y);

    y++;
    error += 1 + 2 * y;
    if (2 * (error - x) + 1 > 0) {
      x -= 1;
      error += 1 - 2 * x;
    }
  }

  GVol.render.canvas.ReplayGroup.circleArrayCache_[radius] = arr;
  return arr;
};


/**
 * @param {Array.<GVol.render.ReplayType>} replays Replays.
 * @return {boGVolean} Has replays of the provided types.
 */
GVol.render.canvas.ReplayGroup.prototype.hasReplays = function(replays) {
  for (var zIndex in this.replaysByZIndex_) {
    var candidates = this.replaysByZIndex_[zIndex];
    for (var i = 0, ii = replays.length; i < ii; ++i) {
      if (replays[i] in candidates) {
        return true;
      }
    }
  }
  return false;
};


/**
 * FIXME empty description for jsdoc
 */
GVol.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 {GVol.Coordinate} coordinate Coordinate.
 * @param {number} resGVolution ResGVolution.
 * @param {number} rotation Rotation.
 * @param {number} hitTGVolerance Hit tGVolerance in pixels.
 * @param {Object.<string, boGVolean>} skippedFeaturesHash Ids of features
 *     to skip.
 * @param {function((GVol.Feature|GVol.render.Feature)): T} callback Feature
 *     callback.
 * @return {T|undefined} Callback result.
 * @template T
 */
GVol.render.canvas.ReplayGroup.prototype.forEachFeatureAtCoordinate = function(
    coordinate, resGVolution, rotation, hitTGVolerance, skippedFeaturesHash, callback) {

  hitTGVolerance = Math.round(hitTGVolerance);
  var contextSize = hitTGVolerance * 2 + 1;
  var transform = GVol.transform.compose(this.hitDetectionTransform_,
      hitTGVolerance + 0.5, hitTGVolerance + 0.5,
      1 / resGVolution, -1 / resGVolution,
      -rotation,
      -coordinate[0], -coordinate[1]);
  var context = this.hitDetectionContext_;

  if (context.canvas.width !== contextSize || context.canvas.height !== contextSize) {
    context.canvas.width = contextSize;
    context.canvas.height = contextSize;
  } else {
    context.clearRect(0, 0, contextSize, contextSize);
  }

  /**
   * @type {GVol.Extent}
   */
  var hitExtent;
  if (this.renderBuffer_ !== undefined) {
    hitExtent = GVol.extent.createEmpty();
    GVol.extent.extendCoordinate(hitExtent, coordinate);
    GVol.extent.buffer(hitExtent, resGVolution * (this.renderBuffer_ + hitTGVolerance), hitExtent);
  }

  var mask = GVol.render.canvas.ReplayGroup.getCircleArray_(hitTGVolerance);

  return this.replayHitDetection_(context, transform, rotation,
      skippedFeaturesHash,
      /**
       * @param {GVol.Feature|GVol.render.Feature} feature Feature.
       * @return {?} Callback result.
       */
      function(feature) {
        var imageData = context.getImageData(0, 0, contextSize, contextSize).data;
        for (var i = 0; i < contextSize; i++) {
          for (var j = 0; j < contextSize; j++) {
            if (mask[i][j]) {
              if (imageData[(j * contextSize + i) * 4 + 3] > 0) {
                var result = callback(feature);
                if (result) {
                  return result;
                } else {
                  context.clearRect(0, 0, contextSize, contextSize);
                  return undefined;
                }
              }
            }
          }
        }
      }, hitExtent);
};


/**
 * @param {GVol.Transform} transform Transform.
 * @return {Array.<number>} Clip coordinates.
 */
GVol.render.canvas.ReplayGroup.prototype.getClipCoords = function(transform) {
  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];
  GVol.geom.flat.transform.transform2D(
      flatClipCoords, 0, 8, 2, transform, flatClipCoords);
  return flatClipCoords;
};


/**
 * @inheritDoc
 */
GVol.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 = GVol.render.canvas.ReplayGroup.BATCH_CONSTRUCTORS_[replayType];
    replay = new Constructor(this.tGVolerance_, this.maxExtent_,
        this.resGVolution_, this.overlaps_);
    replays[replayType] = replay;
  }
  return replay;
};


/**
 * @inheritDoc
 */
GVol.render.canvas.ReplayGroup.prototype.isEmpty = function() {
  return GVol.obj.isEmpty(this.replaysByZIndex_);
};


/**
 * @param {CanvasRenderingContext2D} context Context.
 * @param {number} pixelRatio Pixel ratio.
 * @param {GVol.Transform} transform Transform.
 * @param {number} viewRotation View rotation.
 * @param {Object.<string, boGVolean>} skippedFeaturesHash Ids of features
 *     to skip.
 * @param {Array.<GVol.render.ReplayType>=} opt_replayTypes Ordered replay types
 *     to replay. Default is {@link GVol.render.replay.ORDER}
 */
GVol.render.canvas.ReplayGroup.prototype.replay = function(context, pixelRatio,
    transform, viewRotation, skippedFeaturesHash, opt_replayTypes) {

  /** @type {Array.<number>} */
  var zs = Object.keys(this.replaysByZIndex_).map(Number);
  zs.sort(GVol.array.numberSafeCompareFunction);

  // setup clipping so that the parts of over-simplified geometries are not
  // visible outside the current extent when panning
  var flatClipCoords = this.getClipCoords(transform);
  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.clip();

  var replayTypes = opt_replayTypes ? opt_replayTypes : GVol.render.replay.ORDER;
  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 = replayTypes.length; j < jj; ++j) {
      replay = replays[replayTypes[j]];
      if (replay !== undefined) {
        replay.replay(context, pixelRatio, transform, viewRotation,
            skippedFeaturesHash);
      }
    }
  }

  context.restore();
};


/**
 * @private
 * @param {CanvasRenderingContext2D} context Context.
 * @param {GVol.Transform} transform Transform.
 * @param {number} viewRotation View rotation.
 * @param {Object.<string, boGVolean>} skippedFeaturesHash Ids of features
 *     to skip.
 * @param {function((GVol.Feature|GVol.render.Feature)): T} featureCallback
 *     Feature callback.
 * @param {GVol.Extent=} opt_hitExtent Only check features that intersect this
 *     extent.
 * @return {T|undefined} Callback result.
 * @template T
 */
GVol.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 = GVol.render.replay.ORDER.length - 1; j >= 0; --j) {
      replay = replays[GVol.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.<GVol.render.ReplayType,
 *                function(new: GVol.render.canvas.Replay, number, GVol.Extent,
 *                number, boGVolean)>}
 */
GVol.render.canvas.ReplayGroup.BATCH_CONSTRUCTORS_ = {
  'Circle': GVol.render.canvas.PGVolygonReplay,
  'Default': GVol.render.canvas.Replay,
  'Image': GVol.render.canvas.ImageReplay,
  'LineString': GVol.render.canvas.LineStringReplay,
  'PGVolygon': GVol.render.canvas.PGVolygonReplay,
  'Text': GVol.render.canvas.TextReplay
};

goog.provide('GVol.renderer.Layer');

goog.require('GVol');
goog.require('GVol.ImageState');
goog.require('GVol.Observable');
goog.require('GVol.TileState');
goog.require('GVol.asserts');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.functions');
goog.require('GVol.source.State');


/**
 * @constructor
 * @extends {GVol.Observable}
 * @param {GVol.layer.Layer} layer Layer.
 * @struct
 */
GVol.renderer.Layer = function(layer) {

  GVol.Observable.call(this);

  /**
   * @private
   * @type {GVol.layer.Layer}
   */
  this.layer_ = layer;


};
GVol.inherits(GVol.renderer.Layer, GVol.Observable);


/**
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @param {GVolx.FrameState} frameState Frame state.
 * @param {number} hitTGVolerance Hit tGVolerance in pixels.
 * @param {function(this: S, (GVol.Feature|GVol.render.Feature), GVol.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
 */
GVol.renderer.Layer.prototype.forEachFeatureAtCoordinate = GVol.nullFunction;


/**
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @param {GVolx.FrameState} frameState Frame state.
 * @return {boGVolean} Is there a feature at the given coordinate?
 */
GVol.renderer.Layer.prototype.hasFeatureAtCoordinate = GVol.functions.FALSE;


/**
 * Create a function that adds loaded tiles to the tile lookup.
 * @param {GVol.source.Tile} source Tile source.
 * @param {GVol.proj.Projection} projection Projection of the tiles.
 * @param {Object.<number, Object.<string, GVol.Tile>>} tiles Lookup of loaded
 *     tiles by zoom level.
 * @return {function(number, GVol.TileRange):boGVolean} A function that can be
 *     called with a zoom level and a tile range to add loaded tiles to the
 *     lookup.
 * @protected
 */
GVol.renderer.Layer.prototype.createLoadedTileFinder = function(source, projection, tiles) {
  return (
    /**
     * @param {number} zoom Zoom level.
     * @param {GVol.TileRange} tileRange Tile range.
     * @return {boGVolean} 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 {GVol.layer.Layer} Layer.
 */
GVol.renderer.Layer.prototype.getLayer = function() {
  return this.layer_;
};


/**
 * Handle changes in image state.
 * @param {GVol.events.Event} event Image change event.
 * @private
 */
GVol.renderer.Layer.prototype.handleImageChange_ = function(event) {
  var image = /** @type {GVol.Image} */ (event.target);
  if (image.getState() === GVol.ImageState.LOADED) {
    this.renderIfReadyAndVisible();
  }
};


/**
 * Load the image if not already loaded, and register the image change
 * listener if needed.
 * @param {GVol.ImageBase} image Image.
 * @return {boGVolean} `true` if the image is already loaded, `false`
 *     otherwise.
 * @protected
 */
GVol.renderer.Layer.prototype.loadImage = function(image) {
  var imageState = image.getState();
  if (imageState != GVol.ImageState.LOADED &&
      imageState != GVol.ImageState.ERROR) {
    GVol.events.listen(image, GVol.events.EventType.CHANGE,
        this.handleImageChange_, this);
  }
  if (imageState == GVol.ImageState.IDLE) {
    image.load();
    imageState = image.getState();
  }
  return imageState == GVol.ImageState.LOADED;
};


/**
 * @protected
 */
GVol.renderer.Layer.prototype.renderIfReadyAndVisible = function() {
  var layer = this.getLayer();
  if (layer.getVisible() && layer.getSourceState() == GVol.source.State.READY) {
    this.changed();
  }
};


/**
 * @param {GVolx.FrameState} frameState Frame state.
 * @param {GVol.source.Tile} tileSource Tile source.
 * @protected
 */
GVol.renderer.Layer.prototype.scheduleExpireCache = function(frameState, tileSource) {
  if (tileSource.canExpireCache()) {
    /**
     * @param {GVol.source.Tile} tileSource Tile source.
     * @param {GVol.Map} map Map.
     * @param {GVolx.FrameState} frameState Frame state.
     */
    var postRenderFunction = function(tileSource, map, frameState) {
      var tileSourceKey = GVol.getUid(tileSource).toString();
      if (tileSourceKey in frameState.usedTiles) {
        tileSource.expireCache(frameState.viewState.projection,
            frameState.usedTiles[tileSourceKey]);
      }
    }.bind(null, tileSource);

    frameState.postRenderFunctions.push(
        /** @type {GVol.PostRenderFunction} */ (postRenderFunction)
    );
  }
};


/**
 * @param {Object.<string, GVol.Attribution>} attributionsSet Attributions
 *     set (target).
 * @param {Array.<GVol.Attribution>} attributions Attributions (source).
 * @protected
 */
GVol.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[GVol.getUid(attribution).toString()] = attribution;
    }
  }
};


/**
 * @param {GVolx.FrameState} frameState Frame state.
 * @param {GVol.source.Source} source Source.
 * @protected
 */
GVol.renderer.Layer.prototype.updateLogos = function(frameState, source) {
  var logo = source.getLogo();
  if (logo !== undefined) {
    if (typeof logo === 'string') {
      frameState.logos[logo] = '';
    } else if (logo) {
      GVol.asserts.assert(typeof logo.href == 'string', 44); // `logo.href` should be a string.
      GVol.asserts.assert(typeof logo.src == 'string', 45); // `logo.src` should be a string.
      frameState.logos[logo.src] = logo.href;
    }
  }
};


/**
 * @param {Object.<string, Object.<string, GVol.TileRange>>} usedTiles Used tiles.
 * @param {GVol.source.Tile} tileSource Tile source.
 * @param {number} z Z.
 * @param {GVol.TileRange} tileRange Tile range.
 * @protected
 */
GVol.renderer.Layer.prototype.updateUsedTiles = function(usedTiles, tileSource, z, tileRange) {
  // FIXME should we use tilesToDrawByZ instead?
  var tileSourceKey = GVol.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;
  }
};


/**
 * 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 {GVolx.FrameState} frameState Frame state.
 * @param {GVol.source.Tile} tileSource Tile source.
 * @param {GVol.tilegrid.TileGrid} tileGrid Tile grid.
 * @param {number} pixelRatio Pixel ratio.
 * @param {GVol.proj.Projection} projection Projection.
 * @param {GVol.Extent} extent Extent.
 * @param {number} currentZ Current Z.
 * @param {number} preload Load low resGVolution tiles up to 'preload' levels.
 * @param {function(this: T, GVol.Tile)=} opt_tileCallback Tile callback.
 * @param {T=} opt_this Object to use as `this` in `opt_tileCallback`.
 * @protected
 * @template T
 */
GVol.renderer.Layer.prototype.manageTilePyramid = function(
    frameState, tileSource, tileGrid, pixelRatio, projection, extent,
    currentZ, preload, opt_tileCallback, opt_this) {
  var tileSourceKey = GVol.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, tileResGVolution, x, y, z;
  for (z = currentZ; z >= minZoom; --z) {
    tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z, tileRange);
    tileResGVolution = tileGrid.getResGVolution(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() == GVol.TileState.IDLE) {
            wantedTiles[tile.getKey()] = true;
            if (!tileQueue.isKeyQueued(tile.getKey())) {
              tileQueue.enqueue([tile, tileSourceKey,
                tileGrid.getTileCoordCenter(tile.tileCoord), tileResGVolution]);
            }
          }
          if (opt_tileCallback !== undefined) {
            opt_tileCallback.call(opt_this, tile);
          }
        } else {
          tileSource.useTile(z, x, y, projection);
        }
      }
    }
  }
};

goog.provide('GVol.renderer.canvas.Layer');

goog.require('GVol');
goog.require('GVol.extent');
goog.require('GVol.functions');
goog.require('GVol.render.Event');
goog.require('GVol.render.EventType');
goog.require('GVol.render.canvas');
goog.require('GVol.render.canvas.Immediate');
goog.require('GVol.renderer.Layer');
goog.require('GVol.transform');


/**
 * @constructor
 * @abstract
 * @extends {GVol.renderer.Layer}
 * @param {GVol.layer.Layer} layer Layer.
 */
GVol.renderer.canvas.Layer = function(layer) {

  GVol.renderer.Layer.call(this, layer);

  /**
   * @protected
   * @type {number}
   */
  this.renderedResGVolution;

  /**
   * @private
   * @type {GVol.Transform}
   */
  this.transform_ = GVol.transform.create();

};
GVol.inherits(GVol.renderer.canvas.Layer, GVol.renderer.Layer);


/**
 * @param {CanvasRenderingContext2D} context Context.
 * @param {GVolx.FrameState} frameState Frame state.
 * @param {GVol.Extent} extent Clip extent.
 * @protected
 */
GVol.renderer.canvas.Layer.prototype.clip = function(context, frameState, extent) {
  var pixelRatio = frameState.pixelRatio;
  var width = frameState.size[0] * pixelRatio;
  var height = frameState.size[1] * pixelRatio;
  var rotation = frameState.viewState.rotation;
  var topLeft = GVol.extent.getTopLeft(/** @type {GVol.Extent} */ (extent));
  var topRight = GVol.extent.getTopRight(/** @type {GVol.Extent} */ (extent));
  var bottomRight = GVol.extent.getBottomRight(/** @type {GVol.Extent} */ (extent));
  var bottomLeft = GVol.extent.getBottomLeft(/** @type {GVol.Extent} */ (extent));

  GVol.transform.apply(frameState.coordinateToPixelTransform, topLeft);
  GVol.transform.apply(frameState.coordinateToPixelTransform, topRight);
  GVol.transform.apply(frameState.coordinateToPixelTransform, bottomRight);
  GVol.transform.apply(frameState.coordinateToPixelTransform, bottomLeft);

  context.save();
  GVol.render.canvas.rotateAtOffset(context, -rotation, width / 2, height / 2);
  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();
  GVol.render.canvas.rotateAtOffset(context, rotation, width / 2, height / 2);
};


/**
 * @param {GVol.render.EventType} type Event type.
 * @param {CanvasRenderingContext2D} context Context.
 * @param {GVolx.FrameState} frameState Frame state.
 * @param {GVol.Transform=} opt_transform Transform.
 * @private
 */
GVol.renderer.canvas.Layer.prototype.dispatchComposeEvent_ = function(type, context, frameState, opt_transform) {
  var layer = this.getLayer();
  if (layer.hasListener(type)) {
    var width = frameState.size[0] * frameState.pixelRatio;
    var height = frameState.size[1] * frameState.pixelRatio;
    var rotation = frameState.viewState.rotation;
    GVol.render.canvas.rotateAtOffset(context, -rotation, width / 2, height / 2);
    var transform = opt_transform !== undefined ?
      opt_transform : this.getTransform(frameState, 0);
    var render = new GVol.render.canvas.Immediate(
        context, frameState.pixelRatio, frameState.extent, transform,
        frameState.viewState.rotation);
    var composeEvent = new GVol.render.Event(type, render, frameState,
        context, null);
    layer.dispatchEvent(composeEvent);
    GVol.render.canvas.rotateAtOffset(context, rotation, width / 2, height / 2);
  }
};


/**
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @param {GVolx.FrameState} frameState FrameState.
 * @param {function(this: S, GVol.layer.Layer, (Uint8ClampedArray|Uint8Array)): T} callback Layer
 *     callback.
 * @param {S} thisArg Value to use as `this` when executing `callback`.
 * @return {T|undefined} Callback result.
 * @template S,T,U
 */
GVol.renderer.canvas.Layer.prototype.forEachLayerAtCoordinate = function(coordinate, frameState, callback, thisArg) {
  var hasFeature = this.forEachFeatureAtCoordinate(
      coordinate, frameState, 0, GVol.functions.TRUE, this);

  if (hasFeature) {
    return callback.call(thisArg, this.getLayer(), null);
  } else {
    return undefined;
  }
};


/**
 * @param {CanvasRenderingContext2D} context Context.
 * @param {GVolx.FrameState} frameState Frame state.
 * @param {GVol.LayerState} layerState Layer state.
 * @param {GVol.Transform=} opt_transform Transform.
 * @protected
 */
GVol.renderer.canvas.Layer.prototype.postCompose = function(context, frameState, layerState, opt_transform) {
  this.dispatchComposeEvent_(GVol.render.EventType.POSTCOMPOSE, context,
      frameState, opt_transform);
};


/**
 * @param {CanvasRenderingContext2D} context Context.
 * @param {GVolx.FrameState} frameState Frame state.
 * @param {GVol.Transform=} opt_transform Transform.
 * @protected
 */
GVol.renderer.canvas.Layer.prototype.preCompose = function(context, frameState, opt_transform) {
  this.dispatchComposeEvent_(GVol.render.EventType.PRECOMPOSE, context,
      frameState, opt_transform);
};


/**
 * @param {CanvasRenderingContext2D} context Context.
 * @param {GVolx.FrameState} frameState Frame state.
 * @param {GVol.Transform=} opt_transform Transform.
 * @protected
 */
GVol.renderer.canvas.Layer.prototype.dispatchRenderEvent = function(context, frameState, opt_transform) {
  this.dispatchComposeEvent_(GVol.render.EventType.RENDER, context,
      frameState, opt_transform);
};


/**
 * @param {GVolx.FrameState} frameState Frame state.
 * @param {number} offsetX Offset on the x-axis in view coordinates.
 * @protected
 * @return {!GVol.Transform} Transform.
 */
GVol.renderer.canvas.Layer.prototype.getTransform = function(frameState, offsetX) {
  var viewState = frameState.viewState;
  var pixelRatio = frameState.pixelRatio;
  var dx1 = pixelRatio * frameState.size[0] / 2;
  var dy1 = pixelRatio * frameState.size[1] / 2;
  var sx = pixelRatio / viewState.resGVolution;
  var sy = -sx;
  var angle = -viewState.rotation;
  var dx2 = -viewState.center[0] + offsetX;
  var dy2 = -viewState.center[1];
  return GVol.transform.compose(this.transform_, dx1, dy1, sx, sy, angle, dx2, dy2);
};


/**
 * @abstract
 * @param {GVolx.FrameState} frameState Frame state.
 * @param {GVol.LayerState} layerState Layer state.
 * @param {CanvasRenderingContext2D} context Context.
 */
GVol.renderer.canvas.Layer.prototype.composeFrame = function(frameState, layerState, context) {};

/**
 * @abstract
 * @param {GVolx.FrameState} frameState Frame state.
 * @param {GVol.LayerState} layerState Layer state.
 * @return {boGVolean} whether composeFrame should be called.
 */
GVol.renderer.canvas.Layer.prototype.prepareFrame = function(frameState, layerState) {};

goog.provide('GVol.renderer.vector');

goog.require('GVol');
goog.require('GVol.ImageState');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.render.ReplayType');


/**
 * @param {GVol.Feature|GVol.render.Feature} feature1 Feature 1.
 * @param {GVol.Feature|GVol.render.Feature} feature2 Feature 2.
 * @return {number} Order.
 */
GVol.renderer.vector.defaultOrder = function(feature1, feature2) {
  return GVol.getUid(feature1) - GVol.getUid(feature2);
};


/**
 * @param {number} resGVolution ResGVolution.
 * @param {number} pixelRatio Pixel ratio.
 * @return {number} Squared pixel tGVolerance.
 */
GVol.renderer.vector.getSquaredTGVolerance = function(resGVolution, pixelRatio) {
  var tGVolerance = GVol.renderer.vector.getTGVolerance(resGVolution, pixelRatio);
  return tGVolerance * tGVolerance;
};


/**
 * @param {number} resGVolution ResGVolution.
 * @param {number} pixelRatio Pixel ratio.
 * @return {number} Pixel tGVolerance.
 */
GVol.renderer.vector.getTGVolerance = function(resGVolution, pixelRatio) {
  return GVol.SIMPLIFY_TOLERANCE * resGVolution / pixelRatio;
};


/**
 * @param {GVol.render.ReplayGroup} replayGroup Replay group.
 * @param {GVol.geom.Circle} geometry Geometry.
 * @param {GVol.style.Style} style Style.
 * @param {GVol.Feature} feature Feature.
 * @private
 */
GVol.renderer.vector.renderCircleGeometry_ = function(replayGroup, geometry, style, feature) {
  var fillStyle = style.getFill();
  var strokeStyle = style.getStroke();
  if (fillStyle || strokeStyle) {
    var circleReplay = replayGroup.getReplay(
        style.getZIndex(), GVol.render.ReplayType.CIRCLE);
    circleReplay.setFillStrokeStyle(fillStyle, strokeStyle);
    circleReplay.drawCircle(geometry, feature);
  }
  var textStyle = style.getText();
  if (textStyle) {
    var textReplay = replayGroup.getReplay(
        style.getZIndex(), GVol.render.ReplayType.TEXT);
    textReplay.setTextStyle(textStyle);
    textReplay.drawText(geometry.getCenter(), 0, 2, 2, geometry, feature);
  }
};


/**
 * @param {GVol.render.ReplayGroup} replayGroup Replay group.
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 * @param {GVol.style.Style} style Style.
 * @param {number} squaredTGVolerance Squared tGVolerance.
 * @param {function(this: T, GVol.events.Event)} listener Listener function.
 * @param {T} thisArg Value to use as `this` when executing `listener`.
 * @return {boGVolean} `true` if style is loading.
 * @template T
 */
GVol.renderer.vector.renderFeature = function(
    replayGroup, feature, style, squaredTGVolerance, listener, thisArg) {
  var loading = false;
  var imageStyle, imageState;
  imageStyle = style.getImage();
  if (imageStyle) {
    imageState = imageStyle.getImageState();
    if (imageState == GVol.ImageState.LOADED ||
        imageState == GVol.ImageState.ERROR) {
      imageStyle.unlistenImageChange(listener, thisArg);
    } else {
      if (imageState == GVol.ImageState.IDLE) {
        imageStyle.load();
      }
      imageState = imageStyle.getImageState();
      imageStyle.listenImageChange(listener, thisArg);
      loading = true;
    }
  }
  GVol.renderer.vector.renderFeature_(replayGroup, feature, style,
      squaredTGVolerance);
  return loading;
};


/**
 * @param {GVol.render.ReplayGroup} replayGroup Replay group.
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 * @param {GVol.style.Style} style Style.
 * @param {number} squaredTGVolerance Squared tGVolerance.
 * @private
 */
GVol.renderer.vector.renderFeature_ = function(
    replayGroup, feature, style, squaredTGVolerance) {
  var geometry = style.getGeometryFunction()(feature);
  if (!geometry) {
    return;
  }
  var simplifiedGeometry = geometry.getSimplifiedGeometry(squaredTGVolerance);
  var renderer = style.getRenderer();
  if (renderer) {
    GVol.renderer.vector.renderGeometry_(replayGroup, simplifiedGeometry, style, feature);
  } else {
    var geometryRenderer =
        GVol.renderer.vector.GEOMETRY_RENDERERS_[simplifiedGeometry.getType()];
    geometryRenderer(replayGroup, simplifiedGeometry, style, feature);
  }
};


/**
 * @param {GVol.render.ReplayGroup} replayGroup Replay group.
 * @param {GVol.geom.Geometry} geometry Geometry.
 * @param {GVol.style.Style} style Style.
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 * @private
 */
GVol.renderer.vector.renderGeometry_ = function(replayGroup, geometry, style, feature) {
  if (geometry.getType() == GVol.geom.GeometryType.GEOMETRY_COLLECTION) {
    var geometries = /** @type {GVol.geom.GeometryCGVollection} */ (geometry).getGeometries();
    for (var i = 0, ii = geometries.length; i < ii; ++i) {
      GVol.renderer.vector.renderGeometry_(replayGroup, geometries[i], style, feature);
    }
    return;
  }
  var replay = replayGroup.getReplay(style.getZIndex(), GVol.render.ReplayType.DEFAULT);
  replay.drawCustom(/** @type {GVol.geom.SimpleGeometry} */ (geometry), feature, style.getRenderer());
};


/**
 * @param {GVol.render.ReplayGroup} replayGroup Replay group.
 * @param {GVol.geom.GeometryCGVollection} geometry Geometry.
 * @param {GVol.style.Style} style Style.
 * @param {GVol.Feature} feature Feature.
 * @private
 */
GVol.renderer.vector.renderGeometryCGVollectionGeometry_ = function(replayGroup, geometry, style, feature) {
  var geometries = geometry.getGeometriesArray();
  var i, ii;
  for (i = 0, ii = geometries.length; i < ii; ++i) {
    var geometryRenderer =
        GVol.renderer.vector.GEOMETRY_RENDERERS_[geometries[i].getType()];
    geometryRenderer(replayGroup, geometries[i], style, feature);
  }
};


/**
 * @param {GVol.render.ReplayGroup} replayGroup Replay group.
 * @param {GVol.geom.LineString|GVol.render.Feature} geometry Geometry.
 * @param {GVol.style.Style} style Style.
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 * @private
 */
GVol.renderer.vector.renderLineStringGeometry_ = function(replayGroup, geometry, style, feature) {
  var strokeStyle = style.getStroke();
  if (strokeStyle) {
    var lineStringReplay = replayGroup.getReplay(
        style.getZIndex(), GVol.render.ReplayType.LINE_STRING);
    lineStringReplay.setFillStrokeStyle(null, strokeStyle);
    lineStringReplay.drawLineString(geometry, feature);
  }
  var textStyle = style.getText();
  if (textStyle) {
    var textReplay = replayGroup.getReplay(
        style.getZIndex(), GVol.render.ReplayType.TEXT);
    textReplay.setTextStyle(textStyle);
    textReplay.drawText(geometry.getFlatMidpoint(), 0, 2, 2, geometry, feature);
  }
};


/**
 * @param {GVol.render.ReplayGroup} replayGroup Replay group.
 * @param {GVol.geom.MultiLineString|GVol.render.Feature} geometry Geometry.
 * @param {GVol.style.Style} style Style.
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 * @private
 */
GVol.renderer.vector.renderMultiLineStringGeometry_ = function(replayGroup, geometry, style, feature) {
  var strokeStyle = style.getStroke();
  if (strokeStyle) {
    var lineStringReplay = replayGroup.getReplay(
        style.getZIndex(), GVol.render.ReplayType.LINE_STRING);
    lineStringReplay.setFillStrokeStyle(null, strokeStyle);
    lineStringReplay.drawMultiLineString(geometry, feature);
  }
  var textStyle = style.getText();
  if (textStyle) {
    var textReplay = replayGroup.getReplay(
        style.getZIndex(), GVol.render.ReplayType.TEXT);
    textReplay.setTextStyle(textStyle);
    var flatMidpointCoordinates = geometry.getFlatMidpoints();
    textReplay.drawText(flatMidpointCoordinates, 0,
        flatMidpointCoordinates.length, 2, geometry, feature);
  }
};


/**
 * @param {GVol.render.ReplayGroup} replayGroup Replay group.
 * @param {GVol.geom.MultiPGVolygon} geometry Geometry.
 * @param {GVol.style.Style} style Style.
 * @param {GVol.Feature} feature Feature.
 * @private
 */
GVol.renderer.vector.renderMultiPGVolygonGeometry_ = function(replayGroup, geometry, style, feature) {
  var fillStyle = style.getFill();
  var strokeStyle = style.getStroke();
  if (strokeStyle || fillStyle) {
    var pGVolygonReplay = replayGroup.getReplay(
        style.getZIndex(), GVol.render.ReplayType.POLYGON);
    pGVolygonReplay.setFillStrokeStyle(fillStyle, strokeStyle);
    pGVolygonReplay.drawMultiPGVolygon(geometry, feature);
  }
  var textStyle = style.getText();
  if (textStyle) {
    var textReplay = replayGroup.getReplay(
        style.getZIndex(), GVol.render.ReplayType.TEXT);
    textReplay.setTextStyle(textStyle);
    var flatInteriorPointCoordinates = geometry.getFlatInteriorPoints();
    textReplay.drawText(flatInteriorPointCoordinates, 0,
        flatInteriorPointCoordinates.length, 2, geometry, feature);
  }
};


/**
 * @param {GVol.render.ReplayGroup} replayGroup Replay group.
 * @param {GVol.geom.Point|GVol.render.Feature} geometry Geometry.
 * @param {GVol.style.Style} style Style.
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 * @private
 */
GVol.renderer.vector.renderPointGeometry_ = function(replayGroup, geometry, style, feature) {
  var imageStyle = style.getImage();
  if (imageStyle) {
    if (imageStyle.getImageState() != GVol.ImageState.LOADED) {
      return;
    }
    var imageReplay = replayGroup.getReplay(
        style.getZIndex(), GVol.render.ReplayType.IMAGE);
    imageReplay.setImageStyle(imageStyle);
    imageReplay.drawPoint(geometry, feature);
  }
  var textStyle = style.getText();
  if (textStyle) {
    var textReplay = replayGroup.getReplay(
        style.getZIndex(), GVol.render.ReplayType.TEXT);
    textReplay.setTextStyle(textStyle);
    textReplay.drawText(geometry.getFlatCoordinates(), 0, 2, 2, geometry,
        feature);
  }
};


/**
 * @param {GVol.render.ReplayGroup} replayGroup Replay group.
 * @param {GVol.geom.MultiPoint|GVol.render.Feature} geometry Geometry.
 * @param {GVol.style.Style} style Style.
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 * @private
 */
GVol.renderer.vector.renderMultiPointGeometry_ = function(replayGroup, geometry, style, feature) {
  var imageStyle = style.getImage();
  if (imageStyle) {
    if (imageStyle.getImageState() != GVol.ImageState.LOADED) {
      return;
    }
    var imageReplay = replayGroup.getReplay(
        style.getZIndex(), GVol.render.ReplayType.IMAGE);
    imageReplay.setImageStyle(imageStyle);
    imageReplay.drawMultiPoint(geometry, feature);
  }
  var textStyle = style.getText();
  if (textStyle) {
    var textReplay = replayGroup.getReplay(
        style.getZIndex(), GVol.render.ReplayType.TEXT);
    textReplay.setTextStyle(textStyle);
    var flatCoordinates = geometry.getFlatCoordinates();
    textReplay.drawText(flatCoordinates, 0, flatCoordinates.length,
        geometry.getStride(), geometry, feature);
  }
};


/**
 * @param {GVol.render.ReplayGroup} replayGroup Replay group.
 * @param {GVol.geom.PGVolygon|GVol.render.Feature} geometry Geometry.
 * @param {GVol.style.Style} style Style.
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 * @private
 */
GVol.renderer.vector.renderPGVolygonGeometry_ = function(replayGroup, geometry, style, feature) {
  var fillStyle = style.getFill();
  var strokeStyle = style.getStroke();
  if (fillStyle || strokeStyle) {
    var pGVolygonReplay = replayGroup.getReplay(
        style.getZIndex(), GVol.render.ReplayType.POLYGON);
    pGVolygonReplay.setFillStrokeStyle(fillStyle, strokeStyle);
    pGVolygonReplay.drawPGVolygon(geometry, feature);
  }
  var textStyle = style.getText();
  if (textStyle) {
    var textReplay = replayGroup.getReplay(
        style.getZIndex(), GVol.render.ReplayType.TEXT);
    textReplay.setTextStyle(textStyle);
    textReplay.drawText(
        geometry.getFlatInteriorPoint(), 0, 2, 2, geometry, feature);
  }
};


/**
 * @const
 * @private
 * @type {Object.<GVol.geom.GeometryType,
 *                function(GVol.render.ReplayGroup, GVol.geom.Geometry,
 *                         GVol.style.Style, Object)>}
 */
GVol.renderer.vector.GEOMETRY_RENDERERS_ = {
  'Point': GVol.renderer.vector.renderPointGeometry_,
  'LineString': GVol.renderer.vector.renderLineStringGeometry_,
  'PGVolygon': GVol.renderer.vector.renderPGVolygonGeometry_,
  'MultiPoint': GVol.renderer.vector.renderMultiPointGeometry_,
  'MultiLineString': GVol.renderer.vector.renderMultiLineStringGeometry_,
  'MultiPGVolygon': GVol.renderer.vector.renderMultiPGVolygonGeometry_,
  'GeometryCGVollection': GVol.renderer.vector.renderGeometryCGVollectionGeometry_,
  'Circle': GVol.renderer.vector.renderCircleGeometry_
};

goog.provide('GVol.renderer.canvas.VectorLayer');

goog.require('GVol');
goog.require('GVol.ViewHint');
goog.require('GVol.dom');
goog.require('GVol.extent');
goog.require('GVol.render.EventType');
goog.require('GVol.render.canvas');
goog.require('GVol.render.canvas.ReplayGroup');
goog.require('GVol.renderer.canvas.Layer');
goog.require('GVol.renderer.vector');


/**
 * @constructor
 * @extends {GVol.renderer.canvas.Layer}
 * @param {GVol.layer.Vector} vectorLayer Vector layer.
 */
GVol.renderer.canvas.VectorLayer = function(vectorLayer) {

  GVol.renderer.canvas.Layer.call(this, vectorLayer);

  /**
   * @private
   * @type {boGVolean}
   */
  this.dirty_ = false;

  /**
   * @private
   * @type {number}
   */
  this.renderedRevision_ = -1;

  /**
   * @private
   * @type {number}
   */
  this.renderedResGVolution_ = NaN;

  /**
   * @private
   * @type {GVol.Extent}
   */
  this.renderedExtent_ = GVol.extent.createEmpty();

  /**
   * @private
   * @type {function(GVol.Feature, GVol.Feature): number|null}
   */
  this.renderedRenderOrder_ = null;

  /**
   * @private
   * @type {GVol.render.canvas.ReplayGroup}
   */
  this.replayGroup_ = null;

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.context_ = GVol.dom.createCanvasContext2D();

};
GVol.inherits(GVol.renderer.canvas.VectorLayer, GVol.renderer.canvas.Layer);


/**
 * @inheritDoc
 */
GVol.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 = /** @type {GVol.source.Vector} */ (this.getLayer().getSource());

  var transform = this.getTransform(frameState, 0);

  this.preCompose(context, frameState, transform);

  // clipped rendering if layer extent is set
  var clipExtent = layerState.extent;
  var clipped = clipExtent !== undefined;
  if (clipped) {
    this.clip(context, frameState,  /** @type {GVol.Extent} */ (clipExtent));
  }
  var replayGroup = this.replayGroup_;
  if (replayGroup && !replayGroup.isEmpty()) {
    var layer = this.getLayer();
    var drawOffsetX = 0;
    var drawOffsetY = 0;
    var replayContext;
    var transparentLayer = layerState.opacity !== 1;
    var hasRenderListeners = layer.hasListener(GVol.render.EventType.RENDER);
    if (transparentLayer || hasRenderListeners) {
      var drawWidth = context.canvas.width;
      var drawHeight = context.canvas.height;
      if (rotation) {
        var drawSize = Math.round(Math.sqrt(drawWidth * drawWidth + drawHeight * drawHeight));
        drawOffsetX = (drawSize - drawWidth) / 2;
        drawOffsetY = (drawSize - drawHeight) / 2;
        drawWidth = drawHeight = drawSize;
      }
      // resize and clear
      this.context_.canvas.width = drawWidth;
      this.context_.canvas.height = drawHeight;
      replayContext = this.context_;
    } else {
      replayContext = context;
    }

    var alpha = replayContext.globalAlpha;
    if (!transparentLayer) {
      // 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
      replayContext.globalAlpha = layerState.opacity;
    }

    if (replayContext != context) {
      replayContext.translate(drawOffsetX, drawOffsetY);
    }

    var width = frameState.size[0] * pixelRatio;
    var height = frameState.size[1] * pixelRatio;
    GVol.render.canvas.rotateAtOffset(replayContext, -rotation,
        width / 2, height / 2);
    replayGroup.replay(replayContext, pixelRatio, transform, rotation,
        skippedFeatureUids);
    if (vectorSource.getWrapX() && projection.canWrapX() &&
        !GVol.extent.containsExtent(projectionExtent, extent)) {
      var startX = extent[0];
      var worldWidth = GVol.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);
    }
    GVol.render.canvas.rotateAtOffset(replayContext, rotation,
        width / 2, height / 2);

    if (replayContext != context) {
      if (hasRenderListeners) {
        this.dispatchRenderEvent(replayContext, frameState, transform);
      }
      if (transparentLayer) {
        var mainContextAlpha = context.globalAlpha;
        context.globalAlpha = layerState.opacity;
        context.drawImage(replayContext.canvas, -drawOffsetX, -drawOffsetY);
        context.globalAlpha = mainContextAlpha;
      } else {
        context.drawImage(replayContext.canvas, -drawOffsetX, -drawOffsetY);
      }
      replayContext.translate(-drawOffsetX, -drawOffsetY);
    }

    if (!transparentLayer) {
      replayContext.globalAlpha = alpha;
    }
  }

  if (clipped) {
    context.restore();
  }
  this.postCompose(context, frameState, layerState, transform);

};


/**
 * @inheritDoc
 */
GVol.renderer.canvas.VectorLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTGVolerance, callback, thisArg) {
  if (!this.replayGroup_) {
    return undefined;
  } else {
    var resGVolution = frameState.viewState.resGVolution;
    var rotation = frameState.viewState.rotation;
    var layer = this.getLayer();
    /** @type {Object.<string, boGVolean>} */
    var features = {};
    return this.replayGroup_.forEachFeatureAtCoordinate(coordinate, resGVolution,
        rotation, hitTGVolerance, {},
        /**
         * @param {GVol.Feature|GVol.render.Feature} feature Feature.
         * @return {?} Callback result.
         */
        function(feature) {
          var key = GVol.getUid(feature).toString();
          if (!(key in features)) {
            features[key] = true;
            return callback.call(thisArg, feature, layer);
          }
        });
  }
};


/**
 * Handle changes in image style state.
 * @param {GVol.events.Event} event Image style change event.
 * @private
 */
GVol.renderer.canvas.VectorLayer.prototype.handleStyleImageChange_ = function(event) {
  this.renderIfReadyAndVisible();
};


/**
 * @inheritDoc
 */
GVol.renderer.canvas.VectorLayer.prototype.prepareFrame = function(frameState, layerState) {

  var vectorLayer = /** @type {GVol.layer.Vector} */ (this.getLayer());
  var vectorSource = vectorLayer.getSource();

  this.updateAttributions(
      frameState.attributions, vectorSource.getAttributions());
  this.updateLogos(frameState, vectorSource);

  var animating = frameState.viewHints[GVol.ViewHint.ANIMATING];
  var interacting = frameState.viewHints[GVol.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 resGVolution = viewState.resGVolution;
  var pixelRatio = frameState.pixelRatio;
  var vectorLayerRevision = vectorLayer.getRevision();
  var vectorLayerRenderBuffer = vectorLayer.getRenderBuffer();
  var vectorLayerRenderOrder = vectorLayer.getRenderOrder();

  if (vectorLayerRenderOrder === undefined) {
    vectorLayerRenderOrder = GVol.renderer.vector.defaultOrder;
  }

  var extent = GVol.extent.buffer(frameStateExtent,
      vectorLayerRenderBuffer * resGVolution);
  var projectionExtent = viewState.projection.getExtent();

  if (vectorSource.getWrapX() && viewState.projection.canWrapX() &&
      !GVol.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 whGVole viewport.
    var worldWidth = GVol.extent.getWidth(projectionExtent);
    var buffer = Math.max(GVol.extent.getWidth(extent) / 2, worldWidth);
    extent[0] = projectionExtent[0] - buffer;
    extent[2] = projectionExtent[2] + buffer;
  }

  if (!this.dirty_ &&
      this.renderedResGVolution_ == resGVolution &&
      this.renderedRevision_ == vectorLayerRevision &&
      this.renderedRenderOrder_ == vectorLayerRenderOrder &&
      GVol.extent.containsExtent(this.renderedExtent_, extent)) {
    return true;
  }

  this.replayGroup_ = null;

  this.dirty_ = false;

  var replayGroup = new GVol.render.canvas.ReplayGroup(
      GVol.renderer.vector.getTGVolerance(resGVolution, pixelRatio), extent,
      resGVolution, vectorSource.getOverlaps(), vectorLayer.getRenderBuffer());
  vectorSource.loadFeatures(extent, resGVolution, projection);
  /**
   * @param {GVol.Feature} feature Feature.
   * @this {GVol.renderer.canvas.VectorLayer}
   */
  var renderFeature = function(feature) {
    var styles;
    var styleFunction = feature.getStyleFunction();
    if (styleFunction) {
      styles = styleFunction.call(feature, resGVolution);
    } else {
      styleFunction = vectorLayer.getStyleFunction();
      if (styleFunction) {
        styles = styleFunction(feature, resGVolution);
      }
    }
    if (styles) {
      var dirty = this.renderFeature(
          feature, resGVolution, pixelRatio, styles, replayGroup);
      this.dirty_ = this.dirty_ || dirty;
    }
  }.bind(this);
  if (vectorLayerRenderOrder) {
    /** @type {Array.<GVol.Feature>} */
    var features = [];
    vectorSource.forEachFeatureInExtent(extent,
        /**
         * @param {GVol.Feature} feature Feature.
         */
        function(feature) {
          features.push(feature);
        }, this);
    features.sort(vectorLayerRenderOrder);
    for (var i = 0, ii = features.length; i < ii; ++i) {
      renderFeature(features[i]);
    }
  } else {
    vectorSource.forEachFeatureInExtent(extent, renderFeature, this);
  }
  replayGroup.finish();

  this.renderedResGVolution_ = resGVolution;
  this.renderedRevision_ = vectorLayerRevision;
  this.renderedRenderOrder_ = vectorLayerRenderOrder;
  this.renderedExtent_ = extent;
  this.replayGroup_ = replayGroup;

  return true;
};


/**
 * @param {GVol.Feature} feature Feature.
 * @param {number} resGVolution ResGVolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {(GVol.style.Style|Array.<GVol.style.Style>)} styles The style or array of
 *     styles.
 * @param {GVol.render.canvas.ReplayGroup} replayGroup Replay group.
 * @return {boGVolean} `true` if an image is loading.
 */
GVol.renderer.canvas.VectorLayer.prototype.renderFeature = function(feature, resGVolution, pixelRatio, styles, replayGroup) {
  if (!styles) {
    return false;
  }
  var loading = false;
  if (Array.isArray(styles)) {
    for (var i = 0, ii = styles.length; i < ii; ++i) {
      loading = GVol.renderer.vector.renderFeature(
          replayGroup, feature, styles[i],
          GVol.renderer.vector.getSquaredTGVolerance(resGVolution, pixelRatio),
          this.handleStyleImageChange_, this) || loading;
    }
  } else {
    loading = GVol.renderer.vector.renderFeature(
        replayGroup, feature, styles,
        GVol.renderer.vector.getSquaredTGVolerance(resGVolution, pixelRatio),
        this.handleStyleImageChange_, this) || loading;
  }
  return loading;
};

// This file is automatically generated, do not edit
/* eslint openlayers-internal/no-missing-requires: 0 */
goog.provide('GVol.renderer.webgl.defaultmapshader');

goog.require('GVol');
goog.require('GVol.webgl.Fragment');
goog.require('GVol.webgl.Vertex');

if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @extends {GVol.webgl.Fragment}
   * @struct
   */
  GVol.renderer.webgl.defaultmapshader.Fragment = function() {
    GVol.webgl.Fragment.call(this, GVol.renderer.webgl.defaultmapshader.Fragment.SOURCE);
  };
  GVol.inherits(GVol.renderer.webgl.defaultmapshader.Fragment, GVol.webgl.Fragment);


  /**
   * @const
   * @type {string}
   */
  GVol.renderer.webgl.defaultmapshader.Fragment.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 texCGVolor = texture2D(u_texture, v_texCoord);\n  gl_FragCGVolor.rgb = texCGVolor.rgb;\n  gl_FragCGVolor.a = texCGVolor.a * u_opacity;\n}\n';


  /**
   * @const
   * @type {string}
   */
  GVol.renderer.webgl.defaultmapshader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;uniform float f;uniform sampler2D g;void main(void){vec4 texCGVolor=texture2D(g,a);gl_FragCGVolor.rgb=texCGVolor.rgb;gl_FragCGVolor.a=texCGVolor.a*f;}';


  /**
   * @const
   * @type {string}
   */
  GVol.renderer.webgl.defaultmapshader.Fragment.SOURCE = GVol.DEBUG_WEBGL ?
    GVol.renderer.webgl.defaultmapshader.Fragment.DEBUG_SOURCE :
    GVol.renderer.webgl.defaultmapshader.Fragment.OPTIMIZED_SOURCE;


  GVol.renderer.webgl.defaultmapshader.fragment = new GVol.renderer.webgl.defaultmapshader.Fragment();


  /**
   * @constructor
   * @extends {GVol.webgl.Vertex}
   * @struct
   */
  GVol.renderer.webgl.defaultmapshader.Vertex = function() {
    GVol.webgl.Vertex.call(this, GVol.renderer.webgl.defaultmapshader.Vertex.SOURCE);
  };
  GVol.inherits(GVol.renderer.webgl.defaultmapshader.Vertex, GVol.webgl.Vertex);


  /**
   * @const
   * @type {string}
   */
  GVol.renderer.webgl.defaultmapshader.Vertex.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}
   */
  GVol.renderer.webgl.defaultmapshader.Vertex.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}
   */
  GVol.renderer.webgl.defaultmapshader.Vertex.SOURCE = GVol.DEBUG_WEBGL ?
    GVol.renderer.webgl.defaultmapshader.Vertex.DEBUG_SOURCE :
    GVol.renderer.webgl.defaultmapshader.Vertex.OPTIMIZED_SOURCE;


  GVol.renderer.webgl.defaultmapshader.vertex = new GVol.renderer.webgl.defaultmapshader.Vertex();


  /**
   * @constructor
   * @param {WebGLRenderingContext} gl GL.
   * @param {WebGLProgram} program Program.
   * @struct
   */
  GVol.renderer.webgl.defaultmapshader.Locations = function(gl, program) {

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_opacity = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_opacity' : 'f');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_projectionMatrix = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_projectionMatrix' : 'e');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_texCoordMatrix = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_texCoordMatrix' : 'd');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_texture = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_texture' : 'g');

    /**
     * @type {number}
     */
    this.a_position = gl.getAttribLocation(
        program, GVol.DEBUG_WEBGL ? 'a_position' : 'b');

    /**
     * @type {number}
     */
    this.a_texCoord = gl.getAttribLocation(
        program, GVol.DEBUG_WEBGL ? 'a_texCoord' : 'c');
  };

}

goog.provide('GVol.renderer.webgl.Layer');

goog.require('GVol');
goog.require('GVol.render.Event');
goog.require('GVol.render.EventType');
goog.require('GVol.render.webgl.Immediate');
goog.require('GVol.renderer.Layer');
goog.require('GVol.renderer.webgl.defaultmapshader');
goog.require('GVol.transform');
goog.require('GVol.vec.Mat4');
goog.require('GVol.webgl');
goog.require('GVol.webgl.Buffer');
goog.require('GVol.webgl.Context');


if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @abstract
   * @extends {GVol.renderer.Layer}
   * @param {GVol.renderer.webgl.Map} mapRenderer Map renderer.
   * @param {GVol.layer.Layer} layer Layer.
   */
  GVol.renderer.webgl.Layer = function(mapRenderer, layer) {

    GVol.renderer.Layer.call(this, layer);

    /**
     * @protected
     * @type {GVol.renderer.webgl.Map}
     */
    this.mapRenderer = mapRenderer;

    /**
     * @private
     * @type {GVol.webgl.Buffer}
     */
    this.arrayBuffer_ = new GVol.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 {GVol.Transform}
     */
    this.texCoordMatrix = GVol.transform.create();

    /**
     * @protected
     * @type {GVol.Transform}
     */
    this.projectionMatrix = GVol.transform.create();

    /**
     * @type {Array.<number>}
     * @private
     */
    this.tmpMat4_ = GVol.vec.Mat4.create();

    /**
     * @private
     * @type {GVol.renderer.webgl.defaultmapshader.Locations}
     */
    this.defaultLocations_ = null;

  };
  GVol.inherits(GVol.renderer.webgl.Layer, GVol.renderer.Layer);


  /**
   * @param {GVolx.FrameState} frameState Frame state.
   * @param {number} framebufferDimension Framebuffer dimension.
   * @protected
   */
  GVol.renderer.webgl.Layer.prototype.bindFramebuffer = function(frameState, framebufferDimension) {

    var gl = this.mapRenderer.getGL();

    if (this.framebufferDimension === undefined ||
        this.framebufferDimension != framebufferDimension) {
      /**
       * @param {WebGLRenderingContext} gl GL.
       * @param {WebGLFramebuffer} framebuffer Framebuffer.
       * @param {WebGLTexture} texture Texture.
       */
      var postRenderFunction = function(gl, framebuffer, texture) {
        if (!gl.isContextLost()) {
          gl.deleteFramebuffer(framebuffer);
          gl.deleteTexture(texture);
        }
      }.bind(null, gl, this.framebuffer, this.texture);

      frameState.postRenderFunctions.push(
          /** @type {GVol.PostRenderFunction} */ (postRenderFunction)
      );

      var texture = GVol.webgl.Context.createEmptyTexture(
          gl, framebufferDimension, framebufferDimension);

      var framebuffer = gl.createFramebuffer();
      gl.bindFramebuffer(GVol.webgl.FRAMEBUFFER, framebuffer);
      gl.framebufferTexture2D(GVol.webgl.FRAMEBUFFER,
          GVol.webgl.COLOR_ATTACHMENT0, GVol.webgl.TEXTURE_2D, texture, 0);

      this.texture = texture;
      this.framebuffer = framebuffer;
      this.framebufferDimension = framebufferDimension;

    } else {
      gl.bindFramebuffer(GVol.webgl.FRAMEBUFFER, this.framebuffer);
    }

  };


  /**
   * @param {GVolx.FrameState} frameState Frame state.
   * @param {GVol.LayerState} layerState Layer state.
   * @param {GVol.webgl.Context} context Context.
   */
  GVol.renderer.webgl.Layer.prototype.composeFrame = function(frameState, layerState, context) {

    this.dispatchComposeEvent_(
        GVol.render.EventType.PRECOMPOSE, context, frameState);

    context.bindBuffer(GVol.webgl.ARRAY_BUFFER, this.arrayBuffer_);

    var gl = context.getGL();

    var fragmentShader = GVol.renderer.webgl.defaultmapshader.fragment;
    var vertexShader = GVol.renderer.webgl.defaultmapshader.vertex;

    var program = context.getProgram(fragmentShader, vertexShader);

    var locations;
    if (!this.defaultLocations_) {
      // eslint-disable-next-line openlayers-internal/no-missing-requires
      locations = new GVol.renderer.webgl.defaultmapshader.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, GVol.webgl.FLOAT, false, 16, 0);
      gl.enableVertexAttribArray(locations.a_texCoord);
      gl.vertexAttribPointer(
          locations.a_texCoord, 2, GVol.webgl.FLOAT, false, 16, 8);
      gl.uniform1i(locations.u_texture, 0);
    }

    gl.uniformMatrix4fv(locations.u_texCoordMatrix, false,
        GVol.vec.Mat4.fromTransform(this.tmpMat4_, this.getTexCoordMatrix()));
    gl.uniformMatrix4fv(locations.u_projectionMatrix, false,
        GVol.vec.Mat4.fromTransform(this.tmpMat4_, this.getProjectionMatrix()));
    gl.uniform1f(locations.u_opacity, layerState.opacity);
    gl.bindTexture(GVol.webgl.TEXTURE_2D, this.getTexture());
    gl.drawArrays(GVol.webgl.TRIANGLE_STRIP, 0, 4);

    this.dispatchComposeEvent_(
        GVol.render.EventType.POSTCOMPOSE, context, frameState);

  };


  /**
   * @param {GVol.render.EventType} type Event type.
   * @param {GVol.webgl.Context} context WebGL context.
   * @param {GVolx.FrameState} frameState Frame state.
   * @private
   */
  GVol.renderer.webgl.Layer.prototype.dispatchComposeEvent_ = function(type, context, frameState) {
    var layer = this.getLayer();
    if (layer.hasListener(type)) {
      var viewState = frameState.viewState;
      var resGVolution = viewState.resGVolution;
      var pixelRatio = frameState.pixelRatio;
      var extent = frameState.extent;
      var center = viewState.center;
      var rotation = viewState.rotation;
      var size = frameState.size;

      var render = new GVol.render.webgl.Immediate(
          context, center, resGVolution, rotation, size, extent, pixelRatio);
      var composeEvent = new GVol.render.Event(
          type, render, frameState, null, context);
      layer.dispatchEvent(composeEvent);
    }
  };


  /**
   * @return {!GVol.Transform} Matrix.
   */
  GVol.renderer.webgl.Layer.prototype.getTexCoordMatrix = function() {
    return this.texCoordMatrix;
  };


  /**
   * @return {WebGLTexture} Texture.
   */
  GVol.renderer.webgl.Layer.prototype.getTexture = function() {
    return this.texture;
  };


  /**
   * @return {!GVol.Transform} Matrix.
   */
  GVol.renderer.webgl.Layer.prototype.getProjectionMatrix = function() {
    return this.projectionMatrix;
  };


  /**
   * Handle webglcontextlost.
   */
  GVol.renderer.webgl.Layer.prototype.handleWebGLContextLost = function() {
    this.texture = null;
    this.framebuffer = null;
    this.framebufferDimension = undefined;
  };


  /**
   * @abstract
   * @param {GVolx.FrameState} frameState Frame state.
   * @param {GVol.LayerState} layerState Layer state.
   * @param {GVol.webgl.Context} context Context.
   * @return {boGVolean} whether composeFrame should be called.
   */
  GVol.renderer.webgl.Layer.prototype.prepareFrame = function(frameState, layerState, context) {};


  /**
   * @abstract
   * @param {GVol.Pixel} pixel Pixel.
   * @param {GVolx.FrameState} frameState FrameState.
   * @param {function(this: S, GVol.layer.Layer, (Uint8ClampedArray|Uint8Array)): T} callback Layer
   *     callback.
   * @param {S} thisArg Value to use as `this` when executing `callback`.
   * @return {T|undefined} Callback result.
   * @template S,T,U
   */
  GVol.renderer.webgl.Layer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) {};

}

goog.provide('GVol.renderer.webgl.VectorLayer');

goog.require('GVol');
goog.require('GVol.ViewHint');
goog.require('GVol.extent');
goog.require('GVol.render.webgl.ReplayGroup');
goog.require('GVol.renderer.vector');
goog.require('GVol.renderer.webgl.Layer');
goog.require('GVol.transform');


if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @extends {GVol.renderer.webgl.Layer}
   * @param {GVol.renderer.webgl.Map} mapRenderer Map renderer.
   * @param {GVol.layer.Vector} vectorLayer Vector layer.
   */
  GVol.renderer.webgl.VectorLayer = function(mapRenderer, vectorLayer) {

    GVol.renderer.webgl.Layer.call(this, mapRenderer, vectorLayer);

    /**
     * @private
     * @type {boGVolean}
     */
    this.dirty_ = false;

    /**
     * @private
     * @type {number}
     */
    this.renderedRevision_ = -1;

    /**
     * @private
     * @type {number}
     */
    this.renderedResGVolution_ = NaN;

    /**
     * @private
     * @type {GVol.Extent}
     */
    this.renderedExtent_ = GVol.extent.createEmpty();

    /**
     * @private
     * @type {function(GVol.Feature, GVol.Feature): number|null}
     */
    this.renderedRenderOrder_ = null;

    /**
     * @private
     * @type {GVol.render.webgl.ReplayGroup}
     */
    this.replayGroup_ = null;

    /**
     * The last layer state.
     * @private
     * @type {?GVol.LayerState}
     */
    this.layerState_ = null;

  };
  GVol.inherits(GVol.renderer.webgl.VectorLayer, GVol.renderer.webgl.Layer);


  /**
   * @inheritDoc
   */
  GVol.renderer.webgl.VectorLayer.prototype.composeFrame = function(frameState, layerState, context) {
    this.layerState_ = layerState;
    var viewState = frameState.viewState;
    var replayGroup = this.replayGroup_;
    var size = frameState.size;
    var pixelRatio = frameState.pixelRatio;
    var gl = this.mapRenderer.getGL();
    if (replayGroup && !replayGroup.isEmpty()) {
      gl.enable(gl.SCISSOR_TEST);
      gl.scissor(0, 0, size[0] * pixelRatio, size[1] * pixelRatio);
      replayGroup.replay(context,
          viewState.center, viewState.resGVolution, viewState.rotation,
          size, pixelRatio, layerState.opacity,
          layerState.managed ? frameState.skippedFeatureUids : {});
      gl.disable(gl.SCISSOR_TEST);
    }

  };


  /**
   * @inheritDoc
   */
  GVol.renderer.webgl.VectorLayer.prototype.disposeInternal = function() {
    var replayGroup = this.replayGroup_;
    if (replayGroup) {
      var context = this.mapRenderer.getContext();
      replayGroup.getDeleteResourcesFunction(context)();
      this.replayGroup_ = null;
    }
    GVol.renderer.webgl.Layer.prototype.disposeInternal.call(this);
  };


  /**
   * @inheritDoc
   */
  GVol.renderer.webgl.VectorLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTGVolerance, 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, boGVolean>} */
      var features = {};
      return this.replayGroup_.forEachFeatureAtCoordinate(coordinate,
          context, viewState.center, viewState.resGVolution, viewState.rotation,
          frameState.size, frameState.pixelRatio, layerState.opacity,
          {},
          /**
           * @param {GVol.Feature|GVol.render.Feature} feature Feature.
           * @return {?} Callback result.
           */
          function(feature) {
            var key = GVol.getUid(feature).toString();
            if (!(key in features)) {
              features[key] = true;
              return callback.call(thisArg, feature, layer);
            }
          });
    }
  };


  /**
   * @inheritDoc
   */
  GVol.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.resGVolution, viewState.rotation,
          frameState.size, frameState.pixelRatio, layerState.opacity,
          frameState.skippedFeatureUids);
    }
  };


  /**
   * @inheritDoc
   */
  GVol.renderer.webgl.VectorLayer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) {
    var coordinate = GVol.transform.apply(
        frameState.pixelToCoordinateTransform, pixel.slice());
    var hasFeature = this.hasFeatureAtCoordinate(coordinate, frameState);

    if (hasFeature) {
      return callback.call(thisArg, this.getLayer(), null);
    } else {
      return undefined;
    }
  };


  /**
   * Handle changes in image style state.
   * @param {GVol.events.Event} event Image style change event.
   * @private
   */
  GVol.renderer.webgl.VectorLayer.prototype.handleStyleImageChange_ = function(event) {
    this.renderIfReadyAndVisible();
  };


  /**
   * @inheritDoc
   */
  GVol.renderer.webgl.VectorLayer.prototype.prepareFrame = function(frameState, layerState, context) {

    var vectorLayer = /** @type {GVol.layer.Vector} */ (this.getLayer());
    var vectorSource = vectorLayer.getSource();

    this.updateAttributions(
        frameState.attributions, vectorSource.getAttributions());
    this.updateLogos(frameState, vectorSource);

    var animating = frameState.viewHints[GVol.ViewHint.ANIMATING];
    var interacting = frameState.viewHints[GVol.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 resGVolution = viewState.resGVolution;
    var pixelRatio = frameState.pixelRatio;
    var vectorLayerRevision = vectorLayer.getRevision();
    var vectorLayerRenderBuffer = vectorLayer.getRenderBuffer();
    var vectorLayerRenderOrder = vectorLayer.getRenderOrder();

    if (vectorLayerRenderOrder === undefined) {
      vectorLayerRenderOrder = GVol.renderer.vector.defaultOrder;
    }

    var extent = GVol.extent.buffer(frameStateExtent,
        vectorLayerRenderBuffer * resGVolution);

    if (!this.dirty_ &&
        this.renderedResGVolution_ == resGVolution &&
        this.renderedRevision_ == vectorLayerRevision &&
        this.renderedRenderOrder_ == vectorLayerRenderOrder &&
        GVol.extent.containsExtent(this.renderedExtent_, extent)) {
      return true;
    }

    if (this.replayGroup_) {
      frameState.postRenderFunctions.push(
          this.replayGroup_.getDeleteResourcesFunction(context));
    }

    this.dirty_ = false;

    var replayGroup = new GVol.render.webgl.ReplayGroup(
        GVol.renderer.vector.getTGVolerance(resGVolution, pixelRatio),
        extent, vectorLayer.getRenderBuffer());
    vectorSource.loadFeatures(extent, resGVolution, projection);
    /**
     * @param {GVol.Feature} feature Feature.
     * @this {GVol.renderer.webgl.VectorLayer}
     */
    var renderFeature = function(feature) {
      var styles;
      var styleFunction = feature.getStyleFunction();
      if (styleFunction) {
        styles = styleFunction.call(feature, resGVolution);
      } else {
        styleFunction = vectorLayer.getStyleFunction();
        if (styleFunction) {
          styles = styleFunction(feature, resGVolution);
        }
      }
      if (styles) {
        var dirty = this.renderFeature(
            feature, resGVolution, pixelRatio, styles, replayGroup);
        this.dirty_ = this.dirty_ || dirty;
      }
    };
    if (vectorLayerRenderOrder) {
      /** @type {Array.<GVol.Feature>} */
      var features = [];
      vectorSource.forEachFeatureInExtent(extent,
          /**
           * @param {GVol.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.renderedResGVolution_ = resGVolution;
    this.renderedRevision_ = vectorLayerRevision;
    this.renderedRenderOrder_ = vectorLayerRenderOrder;
    this.renderedExtent_ = extent;
    this.replayGroup_ = replayGroup;

    return true;
  };


  /**
   * @param {GVol.Feature} feature Feature.
   * @param {number} resGVolution ResGVolution.
   * @param {number} pixelRatio Pixel ratio.
   * @param {(GVol.style.Style|Array.<GVol.style.Style>)} styles The style or array of
   *     styles.
   * @param {GVol.render.webgl.ReplayGroup} replayGroup Replay group.
   * @return {boGVolean} `true` if an image is loading.
   */
  GVol.renderer.webgl.VectorLayer.prototype.renderFeature = function(feature, resGVolution, pixelRatio, styles, replayGroup) {
    if (!styles) {
      return false;
    }
    var loading = false;
    if (Array.isArray(styles)) {
      for (var i = styles.length - 1, ii = 0; i >= ii; --i) {
        loading = GVol.renderer.vector.renderFeature(
            replayGroup, feature, styles[i],
            GVol.renderer.vector.getSquaredTGVolerance(resGVolution, pixelRatio),
            this.handleStyleImageChange_, this) || loading;
      }
    } else {
      loading = GVol.renderer.vector.renderFeature(
          replayGroup, feature, styles,
          GVol.renderer.vector.getSquaredTGVolerance(resGVolution, pixelRatio),
          this.handleStyleImageChange_, this) || loading;
    }
    return loading;
  };

}

goog.provide('GVol.layer.Vector');

goog.require('GVol');
goog.require('GVol.layer.Layer');
goog.require('GVol.obj');
goog.require('GVol.renderer.Type');
goog.require('GVol.renderer.canvas.VectorLayer');
goog.require('GVol.renderer.webgl.VectorLayer');
goog.require('GVol.style.Style');


/**
 * @classdesc
 * Vector data that is rendered client-side.
 * Note that any property set in the options is set as a {@link GVol.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 {GVol.layer.Layer}
 * @fires GVol.render.Event
 * @param {GVolx.layer.VectorOptions=} opt_options Options.
 * @api
 */
GVol.layer.Vector = function(opt_options) {
  var options = opt_options ?
    opt_options : /** @type {GVolx.layer.VectorOptions} */ ({});

  var baseOptions = GVol.obj.assign({}, options);

  delete baseOptions.style;
  delete baseOptions.renderBuffer;
  delete baseOptions.updateWhileAnimating;
  delete baseOptions.updateWhileInteracting;
  GVol.layer.Layer.call(this, /** @type {GVolx.layer.LayerOptions} */ (baseOptions));

  /**
   * @type {number}
   * @private
   */
  this.renderBuffer_ = options.renderBuffer !== undefined ?
    options.renderBuffer : 100;

  /**
   * User provided style.
   * @type {GVol.style.Style|Array.<GVol.style.Style>|GVol.StyleFunction}
   * @private
   */
  this.style_ = null;

  /**
   * Style function for use within the library.
   * @type {GVol.StyleFunction|undefined}
   * @private
   */
  this.styleFunction_ = undefined;

  this.setStyle(options.style);

  /**
   * @type {boGVolean}
   * @private
   */
  this.updateWhileAnimating_ = options.updateWhileAnimating !== undefined ?
    options.updateWhileAnimating : false;

  /**
   * @type {boGVolean}
   * @private
   */
  this.updateWhileInteracting_ = options.updateWhileInteracting !== undefined ?
    options.updateWhileInteracting : false;
};
GVol.inherits(GVol.layer.Vector, GVol.layer.Layer);


/**
 * @inheritDoc
 */
GVol.layer.Vector.prototype.createRenderer = function(mapRenderer) {
  var renderer = null;
  var type = mapRenderer.getType();
  if (GVol.ENABLE_CANVAS && type === GVol.renderer.Type.CANVAS) {
    renderer = new GVol.renderer.canvas.VectorLayer(this);
  } else if (GVol.ENABLE_WEBGL && type === GVol.renderer.Type.WEBGL) {
    renderer = new GVol.renderer.webgl.VectorLayer(/** @type {GVol.renderer.webgl.Map} */ (mapRenderer), this);
  }
  return renderer;
};


/**
 * @return {number|undefined} Render buffer.
 */
GVol.layer.Vector.prototype.getRenderBuffer = function() {
  return this.renderBuffer_;
};


/**
 * @return {function(GVol.Feature, GVol.Feature): number|null|undefined} Render
 *     order.
 */
GVol.layer.Vector.prototype.getRenderOrder = function() {
  return /** @type {GVol.RenderOrderFunction|null|undefined} */ (
    this.get(GVol.layer.Vector.Property_.RENDER_ORDER));
};


/**
 * Return the associated {@link GVol.source.Vector vectorsource} of the layer.
 * @function
 * @return {GVol.source.Vector} Source.
 * @api
 */
GVol.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 {GVol.style.Style|Array.<GVol.style.Style>|GVol.StyleFunction}
 *     Layer style.
 * @api
 */
GVol.layer.Vector.prototype.getStyle = function() {
  return this.style_;
};


/**
 * Get the style function.
 * @return {GVol.StyleFunction|undefined} Layer style function.
 * @api
 */
GVol.layer.Vector.prototype.getStyleFunction = function() {
  return this.styleFunction_;
};


/**
 * @return {boGVolean} Whether the rendered layer should be updated while
 *     animating.
 */
GVol.layer.Vector.prototype.getUpdateWhileAnimating = function() {
  return this.updateWhileAnimating_;
};


/**
 * @return {boGVolean} Whether the rendered layer should be updated while
 *     interacting.
 */
GVol.layer.Vector.prototype.getUpdateWhileInteracting = function() {
  return this.updateWhileInteracting_;
};


/**
 * @param {GVol.RenderOrderFunction|null|undefined} renderOrder
 *     Render order.
 */
GVol.layer.Vector.prototype.setRenderOrder = function(renderOrder) {
  this.set(GVol.layer.Vector.Property_.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 resGVolution 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 GVol.style} for information on the default style.
 * @param {GVol.style.Style|Array.<GVol.style.Style>|GVol.StyleFunction|null|undefined}
 *     style Layer style.
 * @api
 */
GVol.layer.Vector.prototype.setStyle = function(style) {
  this.style_ = style !== undefined ? style : GVol.style.Style.defaultFunction;
  this.styleFunction_ = style === null ?
    undefined : GVol.style.Style.createFunction(this.style_);
  this.changed();
};


/**
 * @enum {string}
 * @private
 */
GVol.layer.Vector.Property_ = {
  RENDER_ORDER: 'renderOrder'
};

goog.provide('GVol.loadingstrategy');


/**
 * Strategy function for loading all features with a single request.
 * @param {GVol.Extent} extent Extent.
 * @param {number} resGVolution ResGVolution.
 * @return {Array.<GVol.Extent>} Extents.
 * @api
 */
GVol.loadingstrategy.all = function(extent, resGVolution) {
  return [[-Infinity, -Infinity, Infinity, Infinity]];
};


/**
 * Strategy function for loading features based on the view's extent and
 * resGVolution.
 * @param {GVol.Extent} extent Extent.
 * @param {number} resGVolution ResGVolution.
 * @return {Array.<GVol.Extent>} Extents.
 * @api
 */
GVol.loadingstrategy.bbox = function(extent, resGVolution) {
  return [extent];
};


/**
 * Creates a strategy function for loading features based on a tile grid.
 * @param {GVol.tilegrid.TileGrid} tileGrid Tile grid.
 * @return {function(GVol.Extent, number): Array.<GVol.Extent>} Loading strategy.
 * @api
 */
GVol.loadingstrategy.tile = function(tileGrid) {
  return (
  /**
       * @param {GVol.Extent} extent Extent.
       * @param {number} resGVolution ResGVolution.
       * @return {Array.<GVol.Extent>} Extents.
       */
    function(extent, resGVolution) {
      var z = tileGrid.getZForResGVolution(resGVolution);
      var tileRange = tileGrid.getTileRangeForExtentAndZ(extent, z);
      /** @type {Array.<GVol.Extent>} */
      var extents = [];
      /** @type {GVol.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('GVol.source.Source');

goog.require('GVol');
goog.require('GVol.Attribution');
goog.require('GVol.Object');
goog.require('GVol.proj');
goog.require('GVol.source.State');


/**
 * @classdesc
 * Abstract base class; normally only used for creating subclasses and not
 * instantiated in apps.
 * Base class for {@link GVol.layer.Layer} sources.
 *
 * A generic `change` event is triggered when the state of the source changes.
 *
 * @constructor
 * @abstract
 * @extends {GVol.Object}
 * @param {GVol.SourceSourceOptions} options Source options.
 * @api
 */
GVol.source.Source = function(options) {

  GVol.Object.call(this);

  /**
   * @private
   * @type {GVol.proj.Projection}
   */
  this.projection_ = GVol.proj.get(options.projection);

  /**
   * @private
   * @type {Array.<GVol.Attribution>}
   */
  this.attributions_ = GVol.source.Source.toAttributionsArray_(options.attributions);

  /**
   * @private
   * @type {string|GVolx.LogoOptions|undefined}
   */
  this.logo_ = options.logo;

  /**
   * @private
   * @type {GVol.source.State}
   */
  this.state_ = options.state !== undefined ?
    options.state : GVol.source.State.READY;

  /**
   * @private
   * @type {boGVolean}
   */
  this.wrapX_ = options.wrapX !== undefined ? options.wrapX : false;

};
GVol.inherits(GVol.source.Source, GVol.Object);

/**
 * Turns various ways of defining an attribution to an array of `GVol.Attributions`.
 *
 * @param {GVol.AttributionLike|undefined}
 *     attributionLike The attributions as string, array of strings,
 *     `GVol.Attribution`, array of `GVol.Attribution` or undefined.
 * @return {Array.<GVol.Attribution>} The array of `GVol.Attribution` or null if
 *     `undefined` was given.
 */
GVol.source.Source.toAttributionsArray_ = function(attributionLike) {
  if (typeof attributionLike === 'string') {
    return [new GVol.Attribution({html: attributionLike})];
  } else if (attributionLike instanceof GVol.Attribution) {
    return [attributionLike];
  } else if (Array.isArray(attributionLike)) {
    var len = attributionLike.length;
    var attributions = new Array(len);
    for (var i = 0; i < len; i++) {
      var item = attributionLike[i];
      if (typeof item === 'string') {
        attributions[i] = new GVol.Attribution({html: item});
      } else {
        attributions[i] = item;
      }
    }
    return attributions;
  } else {
    return null;
  }
};


/**
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @param {number} resGVolution ResGVolution.
 * @param {number} rotation Rotation.
 * @param {number} hitTGVolerance Hit tGVolerance in pixels.
 * @param {Object.<string, boGVolean>} skippedFeatureUids Skipped feature uids.
 * @param {function((GVol.Feature|GVol.render.Feature)): T} callback Feature
 *     callback.
 * @return {T|undefined} Callback result.
 * @template T
 */
GVol.source.Source.prototype.forEachFeatureAtCoordinate = GVol.nullFunction;


/**
 * Get the attributions of the source.
 * @return {Array.<GVol.Attribution>} Attributions.
 * @api
 */
GVol.source.Source.prototype.getAttributions = function() {
  return this.attributions_;
};


/**
 * Get the logo of the source.
 * @return {string|GVolx.LogoOptions|undefined} Logo.
 * @api
 */
GVol.source.Source.prototype.getLogo = function() {
  return this.logo_;
};


/**
 * Get the projection of the source.
 * @return {GVol.proj.Projection} Projection.
 * @api
 */
GVol.source.Source.prototype.getProjection = function() {
  return this.projection_;
};


/**
 * @abstract
 * @return {Array.<number>|undefined} ResGVolutions.
 */
GVol.source.Source.prototype.getResGVolutions = function() {};


/**
 * Get the state of the source, see {@link GVol.source.State} for possible states.
 * @return {GVol.source.State} State.
 * @api
 */
GVol.source.Source.prototype.getState = function() {
  return this.state_;
};


/**
 * @return {boGVolean|undefined} Wrap X.
 */
GVol.source.Source.prototype.getWrapX = function() {
  return this.wrapX_;
};


/**
 * Refreshes the source and finally dispatches a 'change' event.
 * @api
 */
GVol.source.Source.prototype.refresh = function() {
  this.changed();
};


/**
 * Set the attributions of the source.
 * @param {GVol.AttributionLike|undefined} attributions Attributions.
 *     Can be passed as `string`, `Array<string>`, `{@link GVol.Attribution}`,
 *     `Array<{@link GVol.Attribution}>` or `undefined`.
 * @api
 */
GVol.source.Source.prototype.setAttributions = function(attributions) {
  this.attributions_ = GVol.source.Source.toAttributionsArray_(attributions);
  this.changed();
};


/**
 * Set the logo of the source.
 * @param {string|GVolx.LogoOptions|undefined} logo Logo.
 */
GVol.source.Source.prototype.setLogo = function(logo) {
  this.logo_ = logo;
};


/**
 * Set the state of the source.
 * @param {GVol.source.State} state State.
 * @protected
 */
GVol.source.Source.prototype.setState = function(state) {
  this.state_ = state;
  this.changed();
};

goog.provide('GVol.source.VectorEventType');

/**
 * @enum {string}
 */
GVol.source.VectorEventType = {
  /**
   * Triggered when a feature is added to the source.
   * @event GVol.source.Vector.Event#addfeature
   * @api
   */
  ADDFEATURE: 'addfeature',

  /**
   * Triggered when a feature is updated.
   * @event GVol.source.Vector.Event#changefeature
   * @api
   */
  CHANGEFEATURE: 'changefeature',

  /**
   * Triggered when the clear method is called on the source.
   * @event GVol.source.Vector.Event#clear
   * @api
   */
  CLEAR: 'clear',

  /**
   * Triggered when a feature is removed from the source.
   * See {@link GVol.source.Vector#clear source.clear()} for exceptions.
   * @event GVol.source.Vector.Event#removefeature
   * @api
   */
  REMOVEFEATURE: 'removefeature'
};

// FIXME bulk feature upload - suppress events
// FIXME make change-detection more refined (notably, geometry hint)

goog.provide('GVol.source.Vector');

goog.require('GVol');
goog.require('GVol.CGVollection');
goog.require('GVol.CGVollectionEventType');
goog.require('GVol.ObjectEventType');
goog.require('GVol.array');
goog.require('GVol.asserts');
goog.require('GVol.events');
goog.require('GVol.events.Event');
goog.require('GVol.events.EventType');
goog.require('GVol.extent');
goog.require('GVol.featureloader');
goog.require('GVol.functions');
goog.require('GVol.loadingstrategy');
goog.require('GVol.obj');
goog.require('GVol.source.Source');
goog.require('GVol.source.State');
goog.require('GVol.source.VectorEventType');
goog.require('GVol.structs.RBush');


/**
 * @classdesc
 * Provides a source of features for vector layers. Vector features provided
 * by this source are suitable for editing. See {@link GVol.source.VectorTile} for
 * vector data that is optimized for rendering.
 *
 * @constructor
 * @extends {GVol.source.Source}
 * @fires GVol.source.Vector.Event
 * @param {GVolx.source.VectorOptions=} opt_options Vector source options.
 * @api
 */
GVol.source.Vector = function(opt_options) {

  var options = opt_options || {};

  GVol.source.Source.call(this, {
    attributions: options.attributions,
    logo: options.logo,
    projection: undefined,
    state: GVol.source.State.READY,
    wrapX: options.wrapX !== undefined ? options.wrapX : true
  });

  /**
   * @private
   * @type {GVol.FeatureLoader}
   */
  this.loader_ = GVol.nullFunction;

  /**
   * @private
   * @type {GVol.format.Feature|undefined}
   */
  this.format_ = options.format;

  /**
   * @private
   * @type {boGVolean}
   */
  this.overlaps_ = options.overlaps == undefined ? true : options.overlaps;

  /**
   * @private
   * @type {string|GVol.FeatureUrlFunction|undefined}
   */
  this.url_ = options.url;

  if (options.loader !== undefined) {
    this.loader_ = options.loader;
  } else if (this.url_ !== undefined) {
    GVol.asserts.assert(this.format_, 7); // `format` must be set when `url` is set
    // create a XHR feature loader for "url" and "format"
    this.loader_ = GVol.featureloader.xhr(this.url_, /** @type {GVol.format.Feature} */ (this.format_));
  }

  /**
   * @private
   * @type {GVol.LoadingStrategy}
   */
  this.strategy_ = options.strategy !== undefined ? options.strategy :
    GVol.loadingstrategy.all;

  var useSpatialIndex =
      options.useSpatialIndex !== undefined ? options.useSpatialIndex : true;

  /**
   * @private
   * @type {GVol.structs.RBush.<GVol.Feature>}
   */
  this.featuresRtree_ = useSpatialIndex ? new GVol.structs.RBush() : null;

  /**
   * @private
   * @type {GVol.structs.RBush.<{extent: GVol.Extent}>}
   */
  this.loadedExtentsRtree_ = new GVol.structs.RBush();

  /**
   * @private
   * @type {Object.<string, GVol.Feature>}
   */
  this.nullGeometryFeatures_ = {};

  /**
   * A lookup of features by id (the return from feature.getId()).
   * @private
   * @type {Object.<string, GVol.Feature>}
   */
  this.idIndex_ = {};

  /**
   * A lookup of features without id (keyed by GVol.getUid(feature)).
   * @private
   * @type {Object.<string, GVol.Feature>}
   */
  this.undefIdIndex_ = {};

  /**
   * @private
   * @type {Object.<string, Array.<GVol.EventsKey>>}
   */
  this.featureChangeKeys_ = {};

  /**
   * @private
   * @type {GVol.CGVollection.<GVol.Feature>}
   */
  this.featuresCGVollection_ = null;

  var cGVollection, features;
  if (options.features instanceof GVol.CGVollection) {
    cGVollection = options.features;
    features = cGVollection.getArray();
  } else if (Array.isArray(options.features)) {
    features = options.features;
  }
  if (!useSpatialIndex && cGVollection === undefined) {
    cGVollection = new GVol.CGVollection(features);
  }
  if (features !== undefined) {
    this.addFeaturesInternal(features);
  }
  if (cGVollection !== undefined) {
    this.bindFeaturesCGVollection_(cGVollection);
  }

};
GVol.inherits(GVol.source.Vector, GVol.source.Source);


/**
 * Add a single feature to the source.  If you want to add a batch of features
 * at once, call {@link GVol.source.Vector#addFeatures source.addFeatures()}
 * instead. A feature will not be added to the source if feature with
 * the same id is already there. The reason for this behavior is to avoid
 * feature duplication when using bbox or tile loading strategies.
 * @param {GVol.Feature} feature Feature to add.
 * @api
 */
GVol.source.Vector.prototype.addFeature = function(feature) {
  this.addFeatureInternal(feature);
  this.changed();
};


/**
 * Add a feature without firing a `change` event.
 * @param {GVol.Feature} feature Feature.
 * @protected
 */
GVol.source.Vector.prototype.addFeatureInternal = function(feature) {
  var featureKey = GVol.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 GVol.source.Vector.Event(GVol.source.VectorEventType.ADDFEATURE, feature));
};


/**
 * @param {string} featureKey Unique identifier for the feature.
 * @param {GVol.Feature} feature The feature.
 * @private
 */
GVol.source.Vector.prototype.setupChangeEvents_ = function(featureKey, feature) {
  this.featureChangeKeys_[featureKey] = [
    GVol.events.listen(feature, GVol.events.EventType.CHANGE,
        this.handleFeatureChange_, this),
    GVol.events.listen(feature, GVol.ObjectEventType.PROPERTYCHANGE,
        this.handleFeatureChange_, this)
  ];
};


/**
 * @param {string} featureKey Unique identifier for the feature.
 * @param {GVol.Feature} feature The feature.
 * @return {boGVolean} The feature is "valid", in the sense that it is also a
 *     candidate for insertion into the Rtree.
 * @private
 */
GVol.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 {
    GVol.asserts.assert(!(featureKey in this.undefIdIndex_),
        30); // The passed `feature` was already added to the source
    this.undefIdIndex_[featureKey] = feature;
  }
  return valid;
};


/**
 * Add a batch of features to the source.
 * @param {Array.<GVol.Feature>} features Features to add.
 * @api
 */
GVol.source.Vector.prototype.addFeatures = function(features) {
  this.addFeaturesInternal(features);
  this.changed();
};


/**
 * Add features without firing a `change` event.
 * @param {Array.<GVol.Feature>} features Features.
 * @protected
 */
GVol.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 = GVol.getUid(feature).toString();
    if (this.addToIndex_(featureKey, feature)) {
      newFeatures.push(feature);
    }
  }

  for (i = 0, length = newFeatures.length; i < length; i++) {
    feature = newFeatures[i];
    featureKey = GVol.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 GVol.source.Vector.Event(
        GVol.source.VectorEventType.ADDFEATURE, newFeatures[i]));
  }
};


/**
 * @param {!GVol.CGVollection.<GVol.Feature>} cGVollection CGVollection.
 * @private
 */
GVol.source.Vector.prototype.bindFeaturesCGVollection_ = function(cGVollection) {
  var modifyingCGVollection = false;
  GVol.events.listen(this, GVol.source.VectorEventType.ADDFEATURE,
      function(evt) {
        if (!modifyingCGVollection) {
          modifyingCGVollection = true;
          cGVollection.push(evt.feature);
          modifyingCGVollection = false;
        }
      });
  GVol.events.listen(this, GVol.source.VectorEventType.REMOVEFEATURE,
      function(evt) {
        if (!modifyingCGVollection) {
          modifyingCGVollection = true;
          cGVollection.remove(evt.feature);
          modifyingCGVollection = false;
        }
      });
  GVol.events.listen(cGVollection, GVol.CGVollectionEventType.ADD,
      function(evt) {
        if (!modifyingCGVollection) {
          modifyingCGVollection = true;
          this.addFeature(/** @type {GVol.Feature} */ (evt.element));
          modifyingCGVollection = false;
        }
      }, this);
  GVol.events.listen(cGVollection, GVol.CGVollectionEventType.REMOVE,
      function(evt) {
        if (!modifyingCGVollection) {
          modifyingCGVollection = true;
          this.removeFeature(/** @type {GVol.Feature} */ (evt.element));
          modifyingCGVollection = false;
        }
      }, this);
  this.featuresCGVollection_ = cGVollection;
};


/**
 * Remove all features from the source.
 * @param {boGVolean=} opt_fast Skip dispatching of {@link removefeature} events.
 * @api
 */
GVol.source.Vector.prototype.clear = function(opt_fast) {
  if (opt_fast) {
    for (var featureId in this.featureChangeKeys_) {
      var keys = this.featureChangeKeys_[featureId];
      keys.forEach(GVol.events.unlistenByKey);
    }
    if (!this.featuresCGVollection_) {
      this.featureChangeKeys_ = {};
      this.idIndex_ = {};
      this.undefIdIndex_ = {};
    }
  } else {
    if (this.featuresRtree_) {
      this.featuresRtree_.forEach(this.removeFeatureInternal, this);
      for (var id in this.nullGeometryFeatures_) {
        this.removeFeatureInternal(this.nullGeometryFeatures_[id]);
      }
    }
  }
  if (this.featuresCGVollection_) {
    this.featuresCGVollection_.clear();
  }

  if (this.featuresRtree_) {
    this.featuresRtree_.clear();
  }
  this.loadedExtentsRtree_.clear();
  this.nullGeometryFeatures_ = {};

  var clearEvent = new GVol.source.Vector.Event(GVol.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, GVol.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
 */
GVol.source.Vector.prototype.forEachFeature = function(callback, opt_this) {
  if (this.featuresRtree_) {
    return this.featuresRtree_.forEach(callback, opt_this);
  } else if (this.featuresCGVollection_) {
    return this.featuresCGVollection_.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 {GVol.Coordinate} coordinate Coordinate.
 * @param {function(this: T, GVol.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
 */
GVol.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();
    if (geometry.intersectsCoordinate(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 GVol.source.Vector#forEachFeatureIntersectingExtent
 * source.forEachFeatureIntersectingExtent()} method instead.
 *
 * When `useSpatialIndex` is set to false, this method will loop through all
 * features, equivalent to {@link GVol.source.Vector#forEachFeature}.
 *
 * @param {GVol.Extent} extent Extent.
 * @param {function(this: T, GVol.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
 */
GVol.source.Vector.prototype.forEachFeatureInExtent = function(extent, callback, opt_this) {
  if (this.featuresRtree_) {
    return this.featuresRtree_.forEachInExtent(extent, callback, opt_this);
  } else if (this.featuresCGVollection_) {
    return this.featuresCGVollection_.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 GVol.source.Vector#forEachFeatureInExtent
 * source.forEachFeatureInExtent()} method instead.
 *
 * @param {GVol.Extent} extent Extent.
 * @param {function(this: T, GVol.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
 */
GVol.source.Vector.prototype.forEachFeatureIntersectingExtent = function(extent, callback, opt_this) {
  return this.forEachFeatureInExtent(extent,
      /**
       * @param {GVol.Feature} feature Feature.
       * @return {S|undefined} The return value from the last call to the callback.
       * @template S
       */
      function(feature) {
        var geometry = feature.getGeometry();
        if (geometry.intersectsExtent(extent)) {
          var result = callback.call(opt_this, feature);
          if (result) {
            return result;
          }
        }
      });
};


/**
 * Get the features cGVollection associated with this source. Will be `null`
 * unless the source was configured with `useSpatialIndex` set to `false`, or
 * with an {@link GVol.CGVollection} as `features`.
 * @return {GVol.CGVollection.<GVol.Feature>} The cGVollection of features.
 * @api
 */
GVol.source.Vector.prototype.getFeaturesCGVollection = function() {
  return this.featuresCGVollection_;
};


/**
 * Get all features on the source in random order.
 * @return {Array.<GVol.Feature>} Features.
 * @api
 */
GVol.source.Vector.prototype.getFeatures = function() {
  var features;
  if (this.featuresCGVollection_) {
    features = this.featuresCGVollection_.getArray();
  } else if (this.featuresRtree_) {
    features = this.featuresRtree_.getAll();
    if (!GVol.obj.isEmpty(this.nullGeometryFeatures_)) {
      GVol.array.extend(
          features, GVol.obj.getValues(this.nullGeometryFeatures_));
    }
  }
  return /** @type {Array.<GVol.Feature>} */ (features);
};


/**
 * Get all features whose geometry intersects the provided coordinate.
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @return {Array.<GVol.Feature>} Features.
 * @api
 */
GVol.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 an array of
 * all features intersecting the given extent in random order (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 {GVol.Extent} extent Extent.
 * @return {Array.<GVol.Feature>} Features.
 * @api
 */
GVol.source.Vector.prototype.getFeaturesInExtent = function(extent) {
  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 {GVol.Coordinate} coordinate Coordinate.
 * @param {function(GVol.Feature):boGVolean=} opt_filter Feature filter function.
 *     The filter function will receive one argument, the {@link GVol.Feature feature}
 *     and it should return a boGVolean value. By default, no filtering is made.
 * @return {GVol.Feature} Closest feature.
 * @api
 */
GVol.source.Vector.prototype.getClosestFeatureToCoordinate = function(coordinate, opt_filter) {
  // 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];
  var filter = opt_filter ? opt_filter : GVol.functions.TRUE;
  this.featuresRtree_.forEachInExtent(extent,
      /**
       * @param {GVol.Feature} feature Feature.
       */
      function(feature) {
        if (filter(feature)) {
          var geometry = feature.getGeometry();
          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 GVold 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`.
 * @param {GVol.Extent=} opt_extent Destination extent. If provided, no new extent
 *     will be created. Instead, that extent's coordinates will be overwritten.
 * @return {GVol.Extent} Extent.
 * @api
 */
GVol.source.Vector.prototype.getExtent = function(opt_extent) {
  return this.featuresRtree_.getExtent(opt_extent);
};


/**
 * 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 {GVol.Feature} The feature (or `null` if not found).
 * @api
 */
GVol.source.Vector.prototype.getFeatureById = function(id) {
  var feature = this.idIndex_[id.toString()];
  return feature !== undefined ? feature : null;
};


/**
 * Get the format associated with this source.
 *
 * @return {GVol.format.Feature|undefined} The feature format.
 * @api
 */
GVol.source.Vector.prototype.getFormat = function() {
  return this.format_;
};


/**
 * @return {boGVolean} The source can have overlapping geometries.
 */
GVol.source.Vector.prototype.getOverlaps = function() {
  return this.overlaps_;
};


/**
 * @override
 */
GVol.source.Vector.prototype.getResGVolutions = function() {};


/**
 * Get the url associated with this source.
 *
 * @return {string|GVol.FeatureUrlFunction|undefined} The url.
 * @api
 */
GVol.source.Vector.prototype.getUrl = function() {
  return this.url_;
};


/**
 * @param {GVol.events.Event} event Event.
 * @private
 */
GVol.source.Vector.prototype.handleFeatureChange_ = function(event) {
  var feature = /** @type {GVol.Feature} */ (event.target);
  var featureKey = GVol.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();
  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) {
        this.removeFromIdIndex_(feature);
        this.idIndex_[sid] = feature;
      }
    }
  } else {
    if (!(featureKey in this.undefIdIndex_)) {
      this.removeFromIdIndex_(feature);
      this.undefIdIndex_[featureKey] = feature;
    }
  }
  this.changed();
  this.dispatchEvent(new GVol.source.Vector.Event(
      GVol.source.VectorEventType.CHANGEFEATURE, feature));
};


/**
 * @return {boGVolean} Is empty.
 */
GVol.source.Vector.prototype.isEmpty = function() {
  return this.featuresRtree_.isEmpty() &&
      GVol.obj.isEmpty(this.nullGeometryFeatures_);
};


/**
 * @param {GVol.Extent} extent Extent.
 * @param {number} resGVolution ResGVolution.
 * @param {GVol.proj.Projection} projection Projection.
 */
GVol.source.Vector.prototype.loadFeatures = function(
    extent, resGVolution, projection) {
  var loadedExtentsRtree = this.loadedExtentsRtree_;
  var extentsToLoad = this.strategy_(extent, resGVolution);
  var i, ii;
  for (i = 0, ii = extentsToLoad.length; i < ii; ++i) {
    var extentToLoad = extentsToLoad[i];
    var alreadyLoaded = loadedExtentsRtree.forEachInExtent(extentToLoad,
        /**
         * @param {{extent: GVol.Extent}} object Object.
         * @return {boGVolean} Contains.
         */
        function(object) {
          return GVol.extent.containsExtent(object.extent, extentToLoad);
        });
    if (!alreadyLoaded) {
      this.loader_.call(this, extentToLoad, resGVolution, 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 GVol.source.Vector#clear source.clear()} method
 * instead.
 * @param {GVol.Feature} feature Feature to remove.
 * @api
 */
GVol.source.Vector.prototype.removeFeature = function(feature) {
  var featureKey = GVol.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 {GVol.Feature} feature Feature.
 * @protected
 */
GVol.source.Vector.prototype.removeFeatureInternal = function(feature) {
  var featureKey = GVol.getUid(feature).toString();
  this.featureChangeKeys_[featureKey].forEach(GVol.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 GVol.source.Vector.Event(
      GVol.source.VectorEventType.REMOVEFEATURE, feature));
};


/**
 * Remove a feature from the id index.  Called internally when the feature id
 * may have changed.
 * @param {GVol.Feature} feature The feature.
 * @return {boGVolean} Removed the feature from the index.
 * @private
 */
GVol.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 GVol.source.Vector} instances are instances of this
 * type.
 *
 * @constructor
 * @extends {GVol.events.Event}
 * @implements {GVoli.source.Vector.Event}
 * @param {string} type Type.
 * @param {GVol.Feature=} opt_feature Feature.
 */
GVol.source.Vector.Event = function(type, opt_feature) {

  GVol.events.Event.call(this, type);

  /**
   * The feature being added or removed.
   * @type {GVol.Feature|undefined}
   * @api
   */
  this.feature = opt_feature;

};
GVol.inherits(GVol.source.Vector.Event, GVol.events.Event);

goog.provide('GVol.interaction.Draw');

goog.require('GVol');
goog.require('GVol.Feature');
goog.require('GVol.MapBrowserEventType');
goog.require('GVol.Object');
goog.require('GVol.coordinate');
goog.require('GVol.events');
goog.require('GVol.events.Event');
goog.require('GVol.events.condition');
goog.require('GVol.extent');
goog.require('GVol.functions');
goog.require('GVol.geom.Circle');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.geom.LineString');
goog.require('GVol.geom.MultiLineString');
goog.require('GVol.geom.MultiPoint');
goog.require('GVol.geom.MultiPGVolygon');
goog.require('GVol.geom.Point');
goog.require('GVol.geom.PGVolygon');
goog.require('GVol.interaction.DrawEventType');
goog.require('GVol.interaction.Pointer');
goog.require('GVol.interaction.Property');
goog.require('GVol.layer.Vector');
goog.require('GVol.source.Vector');
goog.require('GVol.style.Style');


/**
 * @classdesc
 * Interaction for drawing feature geometries.
 *
 * @constructor
 * @extends {GVol.interaction.Pointer}
 * @fires GVol.interaction.Draw.Event
 * @param {GVolx.interaction.DrawOptions} options Options.
 * @api
 */
GVol.interaction.Draw = function(options) {

  GVol.interaction.Pointer.call(this, {
    handleDownEvent: GVol.interaction.Draw.handleDownEvent_,
    handleEvent: GVol.interaction.Draw.handleEvent,
    handleUpEvent: GVol.interaction.Draw.handleUpEvent_
  });

  /**
   * @type {boGVolean}
   * @private
   */
  this.shouldHandle_ = false;

  /**
   * @type {GVol.Pixel}
   * @private
   */
  this.downPx_ = null;

  /**
   * @type {boGVolean}
   * @private
   */
  this.freehand_ = false;

  /**
   * Target source for drawn features.
   * @type {GVol.source.Vector}
   * @private
   */
  this.source_ = options.source ? options.source : null;

  /**
   * Target cGVollection for drawn features.
   * @type {GVol.CGVollection.<GVol.Feature>}
   * @private
   */
  this.features_ = options.features ? options.features : null;

  /**
   * Pixel distance for snapping.
   * @type {number}
   * @private
   */
  this.snapTGVolerance_ = options.snapTGVolerance ? options.snapTGVolerance : 12;

  /**
   * Geometry type.
   * @type {GVol.geom.GeometryType}
   * @private
   */
  this.type_ = options.type;

  /**
   * Drawing mode (derived from geometry type.
   * @type {GVol.interaction.Draw.Mode_}
   * @private
   */
  this.mode_ = GVol.interaction.Draw.getMode_(this.type_);

  /**
   * The number of points that must be drawn before a pGVolygon ring or line
   * string can be finished.  The default is 3 for pGVolygon rings and 2 for
   * line strings.
   * @type {number}
   * @private
   */
  this.minPoints_ = options.minPoints ?
    options.minPoints :
    (this.mode_ === GVol.interaction.Draw.Mode_.POLYGON ? 3 : 2);

  /**
   * The number of points that can be drawn before a pGVolygon ring or line string
   * is finished. The default is no restriction.
   * @type {number}
   * @private
   */
  this.maxPoints_ = options.maxPoints ? options.maxPoints : Infinity;

  /**
   * A function to decide if a potential finish coordinate is permissible
   * @private
   * @type {GVol.EventsConditionType}
   */
  this.finishCondition_ = options.finishCondition ? options.finishCondition : GVol.functions.TRUE;

  var geometryFunction = options.geometryFunction;
  if (!geometryFunction) {
    if (this.type_ === GVol.geom.GeometryType.CIRCLE) {
      /**
       * @param {!Array.<GVol.Coordinate>} coordinates
       *     The coordinates.
       * @param {GVol.geom.SimpleGeometry=} opt_geometry Optional geometry.
       * @return {GVol.geom.SimpleGeometry} A geometry.
       */
      geometryFunction = function(coordinates, opt_geometry) {
        var circle = opt_geometry ? /** @type {GVol.geom.Circle} */ (opt_geometry) :
          new GVol.geom.Circle([NaN, NaN]);
        var squaredLength = GVol.coordinate.squaredDistance(
            coordinates[0], coordinates[1]);
        circle.setCenterAndRadius(coordinates[0], Math.sqrt(squaredLength));
        return circle;
      };
    } else {
      var Constructor;
      var mode = this.mode_;
      if (mode === GVol.interaction.Draw.Mode_.POINT) {
        Constructor = GVol.geom.Point;
      } else if (mode === GVol.interaction.Draw.Mode_.LINE_STRING) {
        Constructor = GVol.geom.LineString;
      } else if (mode === GVol.interaction.Draw.Mode_.POLYGON) {
        Constructor = GVol.geom.PGVolygon;
      }
      /**
       * @param {!Array.<GVol.Coordinate>} coordinates
       *     The coordinates.
       * @param {GVol.geom.SimpleGeometry=} opt_geometry Optional geometry.
       * @return {GVol.geom.SimpleGeometry} A geometry.
       */
      geometryFunction = function(coordinates, opt_geometry) {
        var geometry = opt_geometry;
        if (geometry) {
          if (mode === GVol.interaction.Draw.Mode_.POLYGON) {
            geometry.setCoordinates([coordinates[0].concat([coordinates[0][0]])]);
          } else {
            geometry.setCoordinates(coordinates);
          }
        } else {
          geometry = new Constructor(coordinates);
        }
        return geometry;
      };
    }
  }

  /**
   * @type {GVol.DrawGeometryFunctionType}
   * @private
   */
  this.geometryFunction_ = geometryFunction;

  /**
   * Finish coordinate for the feature (first point for pGVolygons, last point for
   * linestrings).
   * @type {GVol.Coordinate}
   * @private
   */
  this.finishCoordinate_ = null;

  /**
   * Sketch feature.
   * @type {GVol.Feature}
   * @private
   */
  this.sketchFeature_ = null;

  /**
   * Sketch point.
   * @type {GVol.Feature}
   * @private
   */
  this.sketchPoint_ = null;

  /**
   * Sketch coordinates. Used when drawing a line or pGVolygon.
   * @type {GVol.Coordinate|Array.<GVol.Coordinate>|Array.<Array.<GVol.Coordinate>>}
   * @private
   */
  this.sketchCoords_ = null;

  /**
   * Sketch line. Used when drawing pGVolygon.
   * @type {GVol.Feature}
   * @private
   */
  this.sketchLine_ = null;

  /**
   * Sketch line coordinates. Used when drawing a pGVolygon or circle.
   * @type {Array.<GVol.Coordinate>}
   * @private
   */
  this.sketchLineCoords_ = null;

  /**
   * Squared tGVolerance for handling up events.  If the squared distance
   * between a down and up event is greater than this tGVolerance, up events
   * will not be handled.
   * @type {number}
   * @private
   */
  this.squaredClickTGVolerance_ = options.clickTGVolerance ?
    options.clickTGVolerance * options.clickTGVolerance : 36;

  /**
   * Draw overlay where our sketch features are drawn.
   * @type {GVol.layer.Vector}
   * @private
   */
  this.overlay_ = new GVol.layer.Vector({
    source: new GVol.source.Vector({
      useSpatialIndex: false,
      wrapX: options.wrapX ? options.wrapX : false
    }),
    style: options.style ? options.style :
      GVol.interaction.Draw.getDefaultStyleFunction()
  });

  /**
   * Name of the geometry attribute for newly created features.
   * @type {string|undefined}
   * @private
   */
  this.geometryName_ = options.geometryName;

  /**
   * @private
   * @type {GVol.EventsConditionType}
   */
  this.condition_ = options.condition ?
    options.condition : GVol.events.condition.noModifierKeys;

  /**
   * @private
   * @type {GVol.EventsConditionType}
   */
  this.freehandCondition_;
  if (options.freehand) {
    this.freehandCondition_ = GVol.events.condition.always;
  } else {
    this.freehandCondition_ = options.freehandCondition ?
      options.freehandCondition : GVol.events.condition.shiftKeyOnly;
  }

  GVol.events.listen(this,
      GVol.Object.getChangeEventType(GVol.interaction.Property.ACTIVE),
      this.updateState_, this);

};
GVol.inherits(GVol.interaction.Draw, GVol.interaction.Pointer);


/**
 * @return {GVol.StyleFunction} Styles.
 */
GVol.interaction.Draw.getDefaultStyleFunction = function() {
  var styles = GVol.style.Style.createDefaultEditing();
  return function(feature, resGVolution) {
    return styles[feature.getGeometry().getType()];
  };
};


/**
 * @inheritDoc
 */
GVol.interaction.Draw.prototype.setMap = function(map) {
  GVol.interaction.Pointer.prototype.setMap.call(this, map);
  this.updateState_();
};


/**
 * Handles the {@link GVol.MapBrowserEvent map browser event} and may actually
 * draw or finish the drawing.
 * @param {GVol.MapBrowserEvent} event Map browser event.
 * @return {boGVolean} `false` to stop event propagation.
 * @this {GVol.interaction.Draw}
 * @api
 */
GVol.interaction.Draw.handleEvent = function(event) {
  this.freehand_ = this.mode_ !== GVol.interaction.Draw.Mode_.POINT && this.freehandCondition_(event);
  var pass = !this.freehand_;
  if (this.freehand_ &&
      event.type === GVol.MapBrowserEventType.POINTERDRAG && this.sketchFeature_ !== null) {
    this.addToDrawing_(event);
    pass = false;
  } else if (event.type ===
      GVol.MapBrowserEventType.POINTERMOVE) {
    pass = this.handlePointerMove_(event);
  } else if (event.type === GVol.MapBrowserEventType.DBLCLICK) {
    pass = false;
  }
  return GVol.interaction.Pointer.handleEvent.call(this, event) && pass;
};


/**
 * @param {GVol.MapBrowserPointerEvent} event Event.
 * @return {boGVolean} Start drag sequence?
 * @this {GVol.interaction.Draw}
 * @private
 */
GVol.interaction.Draw.handleDownEvent_ = function(event) {
  this.shouldHandle_ = !this.freehand_;

  if (this.freehand_) {
    this.downPx_ = event.pixel;
    if (!this.finishCoordinate_) {
      this.startDrawing_(event);
    }
    return true;
  } else if (this.condition_(event)) {
    this.downPx_ = event.pixel;
    return true;
  } else {
    return false;
  }
};


/**
 * @param {GVol.MapBrowserPointerEvent} event Event.
 * @return {boGVolean} Stop drag sequence?
 * @this {GVol.interaction.Draw}
 * @private
 */
GVol.interaction.Draw.handleUpEvent_ = function(event) {
  var pass = true;

  this.handlePointerMove_(event);

  var circleMode = this.mode_ === GVol.interaction.Draw.Mode_.CIRCLE;

  if (this.shouldHandle_) {
    if (!this.finishCoordinate_) {
      this.startDrawing_(event);
      if (this.mode_ === GVol.interaction.Draw.Mode_.POINT) {
        this.finishDrawing();
      }
    } else if (this.freehand_ || circleMode) {
      this.finishDrawing();
    } else if (this.atFinish_(event)) {
      if (this.finishCondition_(event)) {
        this.finishDrawing();
      }
    } else {
      this.addToDrawing_(event);
    }
    pass = false;
  } else if (this.freehand_) {
    this.finishCoordinate_ = null;
    this.abortDrawing_();
  }
  return pass;
};


/**
 * Handle move events.
 * @param {GVol.MapBrowserEvent} event A move event.
 * @return {boGVolean} Pass the event to other interactions.
 * @private
 */
GVol.interaction.Draw.prototype.handlePointerMove_ = function(event) {
  if (this.downPx_ &&
      ((!this.freehand_ && this.shouldHandle_) ||
      (this.freehand_ && !this.shouldHandle_))) {
    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;
    this.shouldHandle_ = this.freehand_ ?
      squaredDistance > this.squaredClickTGVolerance_ :
      squaredDistance <= this.squaredClickTGVolerance_;
  }

  if (this.finishCoordinate_) {
    this.modifyDrawing_(event);
  } else {
    this.createOrUpdateSketchPoint_(event);
  }
  return true;
};


/**
 * Determine if an event is within the snapping tGVolerance of the start coord.
 * @param {GVol.MapBrowserEvent} event Event.
 * @return {boGVolean} The event is within the snapping tGVolerance of the start.
 * @private
 */
GVol.interaction.Draw.prototype.atFinish_ = function(event) {
  var at = false;
  if (this.sketchFeature_) {
    var potentiallyDone = false;
    var potentiallyFinishCoordinates = [this.finishCoordinate_];
    if (this.mode_ === GVol.interaction.Draw.Mode_.LINE_STRING) {
      potentiallyDone = this.sketchCoords_.length > this.minPoints_;
    } else if (this.mode_ === GVol.interaction.Draw.Mode_.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 snapTGVolerance = this.freehand_ ? 1 : this.snapTGVolerance_;
        at = Math.sqrt(dx * dx + dy * dy) <= snapTGVolerance;
        if (at) {
          this.finishCoordinate_ = finishCoordinate;
          break;
        }
      }
    }
  }
  return at;
};


/**
 * @param {GVol.MapBrowserEvent} event Event.
 * @private
 */
GVol.interaction.Draw.prototype.createOrUpdateSketchPoint_ = function(event) {
  var coordinates = event.coordinate.slice();
  if (!this.sketchPoint_) {
    this.sketchPoint_ = new GVol.Feature(new GVol.geom.Point(coordinates));
    this.updateSketchFeatures_();
  } else {
    var sketchPointGeom = /** @type {GVol.geom.Point} */ (this.sketchPoint_.getGeometry());
    sketchPointGeom.setCoordinates(coordinates);
  }
};


/**
 * Start the drawing.
 * @param {GVol.MapBrowserEvent} event Event.
 * @private
 */
GVol.interaction.Draw.prototype.startDrawing_ = function(event) {
  var start = event.coordinate;
  this.finishCoordinate_ = start;
  if (this.mode_ === GVol.interaction.Draw.Mode_.POINT) {
    this.sketchCoords_ = start.slice();
  } else if (this.mode_ === GVol.interaction.Draw.Mode_.POLYGON) {
    this.sketchCoords_ = [[start.slice(), start.slice()]];
    this.sketchLineCoords_ = this.sketchCoords_[0];
  } else {
    this.sketchCoords_ = [start.slice(), start.slice()];
    if (this.mode_ === GVol.interaction.Draw.Mode_.CIRCLE) {
      this.sketchLineCoords_ = this.sketchCoords_;
    }
  }
  if (this.sketchLineCoords_) {
    this.sketchLine_ = new GVol.Feature(
        new GVol.geom.LineString(this.sketchLineCoords_));
  }
  var geometry = this.geometryFunction_(this.sketchCoords_);
  this.sketchFeature_ = new GVol.Feature();
  if (this.geometryName_) {
    this.sketchFeature_.setGeometryName(this.geometryName_);
  }
  this.sketchFeature_.setGeometry(geometry);
  this.updateSketchFeatures_();
  this.dispatchEvent(new GVol.interaction.Draw.Event(
      GVol.interaction.DrawEventType.DRAWSTART, this.sketchFeature_));
};


/**
 * Modify the drawing.
 * @param {GVol.MapBrowserEvent} event Event.
 * @private
 */
GVol.interaction.Draw.prototype.modifyDrawing_ = function(event) {
  var coordinate = event.coordinate;
  var geometry = /** @type {GVol.geom.SimpleGeometry} */ (this.sketchFeature_.getGeometry());
  var coordinates, last;
  if (this.mode_ === GVol.interaction.Draw.Mode_.POINT) {
    last = this.sketchCoords_;
  } else if (this.mode_ === GVol.interaction.Draw.Mode_.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];
  this.geometryFunction_(/** @type {!Array.<GVol.Coordinate>} */ (this.sketchCoords_), geometry);
  if (this.sketchPoint_) {
    var sketchPointGeom = /** @type {GVol.geom.Point} */ (this.sketchPoint_.getGeometry());
    sketchPointGeom.setCoordinates(coordinate);
  }
  var sketchLineGeom;
  if (geometry instanceof GVol.geom.PGVolygon &&
      this.mode_ !== GVol.interaction.Draw.Mode_.POLYGON) {
    if (!this.sketchLine_) {
      this.sketchLine_ = new GVol.Feature(new GVol.geom.LineString(null));
    }
    var ring = geometry.getLinearRing(0);
    sketchLineGeom = /** @type {GVol.geom.LineString} */ (this.sketchLine_.getGeometry());
    sketchLineGeom.setFlatCoordinates(
        ring.getLayout(), ring.getFlatCoordinates());
  } else if (this.sketchLineCoords_) {
    sketchLineGeom = /** @type {GVol.geom.LineString} */ (this.sketchLine_.getGeometry());
    sketchLineGeom.setCoordinates(this.sketchLineCoords_);
  }
  this.updateSketchFeatures_();
};


/**
 * Add a new coordinate to the drawing.
 * @param {GVol.MapBrowserEvent} event Event.
 * @private
 */
GVol.interaction.Draw.prototype.addToDrawing_ = function(event) {
  var coordinate = event.coordinate;
  var geometry = /** @type {GVol.geom.SimpleGeometry} */ (this.sketchFeature_.getGeometry());
  var done;
  var coordinates;
  if (this.mode_ === GVol.interaction.Draw.Mode_.LINE_STRING) {
    this.finishCoordinate_ = coordinate.slice();
    coordinates = this.sketchCoords_;
    if (coordinates.length >= this.maxPoints_) {
      if (this.freehand_) {
        coordinates.pop();
      } else {
        done = true;
      }
    }
    coordinates.push(coordinate.slice());
    this.geometryFunction_(coordinates, geometry);
  } else if (this.mode_ === GVol.interaction.Draw.Mode_.POLYGON) {
    coordinates = this.sketchCoords_[0];
    if (coordinates.length >= this.maxPoints_) {
      if (this.freehand_) {
        coordinates.pop();
      } else {
        done = true;
      }
    }
    coordinates.push(coordinate.slice());
    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
 */
GVol.interaction.Draw.prototype.removeLastPoint = function() {
  if (!this.sketchFeature_) {
    return;
  }
  var geometry = /** @type {GVol.geom.SimpleGeometry} */ (this.sketchFeature_.getGeometry());
  var coordinates, sketchLineGeom;
  if (this.mode_ === GVol.interaction.Draw.Mode_.LINE_STRING) {
    coordinates = this.sketchCoords_;
    coordinates.splice(-2, 1);
    this.geometryFunction_(coordinates, geometry);
    if (coordinates.length >= 2) {
      this.finishCoordinate_ = coordinates[coordinates.length - 2].slice();
    }
  } else if (this.mode_ === GVol.interaction.Draw.Mode_.POLYGON) {
    coordinates = this.sketchCoords_[0];
    coordinates.splice(-2, 1);
    sketchLineGeom = /** @type {GVol.geom.LineString} */ (this.sketchLine_.getGeometry());
    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 GVol.interaction.DrawEventType.DRAWEND} event is dispatched before
 * inserting the feature.
 * @api
 */
GVol.interaction.Draw.prototype.finishDrawing = function() {
  var sketchFeature = this.abortDrawing_();
  var coordinates = this.sketchCoords_;
  var geometry = /** @type {GVol.geom.SimpleGeometry} */ (sketchFeature.getGeometry());
  if (this.mode_ === GVol.interaction.Draw.Mode_.LINE_STRING) {
    // remove the redundant last point
    coordinates.pop();
    this.geometryFunction_(coordinates, geometry);
  } else if (this.mode_ === GVol.interaction.Draw.Mode_.POLYGON) {
    // remove the redundant last point in ring
    coordinates[0].pop();
    this.geometryFunction_(coordinates, geometry);
    coordinates = geometry.getCoordinates();
  }

  // cast multi-part geometries
  if (this.type_ === GVol.geom.GeometryType.MULTI_POINT) {
    sketchFeature.setGeometry(new GVol.geom.MultiPoint([coordinates]));
  } else if (this.type_ === GVol.geom.GeometryType.MULTI_LINE_STRING) {
    sketchFeature.setGeometry(new GVol.geom.MultiLineString([coordinates]));
  } else if (this.type_ === GVol.geom.GeometryType.MULTI_POLYGON) {
    sketchFeature.setGeometry(new GVol.geom.MultiPGVolygon([coordinates]));
  }

  // First dispatch event to allow full set up of feature
  this.dispatchEvent(new GVol.interaction.Draw.Event(
      GVol.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 {GVol.Feature} The sketch feature (or null if none).
 * @private
 */
GVol.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 {!GVol.Feature} feature Feature to be extended.
 * @api
 */
GVol.interaction.Draw.prototype.extend = function(feature) {
  var geometry = feature.getGeometry();
  var lineString = /** @type {GVol.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 GVol.interaction.Draw.Event(
      GVol.interaction.DrawEventType.DRAWSTART, this.sketchFeature_));
};


/**
 * @inheritDoc
 */
GVol.interaction.Draw.prototype.shouldStopEvent = GVol.functions.FALSE;


/**
 * Redraw the sketch features.
 * @private
 */
GVol.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
 */
GVol.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 `type: 'Circle'` that will create a regular
 * pGVolygon with a user specified number of sides and start angle instead of an
 * `GVol.geom.Circle` geometry.
 * @param {number=} opt_sides Number of sides of the regular pGVolygon. 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 pGVolygon to the current pointer position.
 * @return {GVol.DrawGeometryFunctionType} Function that draws a
 *     pGVolygon.
 * @api
 */
GVol.interaction.Draw.createRegularPGVolygon = function(opt_sides, opt_angle) {
  return (
  /**
       * @param {GVol.Coordinate|Array.<GVol.Coordinate>|Array.<Array.<GVol.Coordinate>>} coordinates
       * @param {GVol.geom.SimpleGeometry=} opt_geometry
       * @return {GVol.geom.SimpleGeometry}
       */
    function(coordinates, opt_geometry) {
      var center = coordinates[0];
      var end = coordinates[1];
      var radius = Math.sqrt(
          GVol.coordinate.squaredDistance(center, end));
      var geometry = opt_geometry ? /** @type {GVol.geom.PGVolygon} */ (opt_geometry) :
        GVol.geom.PGVolygon.fromCircle(new GVol.geom.Circle(center), opt_sides);
      var angle = opt_angle ? opt_angle :
        Math.atan((end[1] - center[1]) / (end[0] - center[0]));
      GVol.geom.PGVolygon.makeRegular(geometry, center, radius, angle);
      return geometry;
    }
  );
};


/**
 * Create a `geometryFunction` that will create a box-shaped pGVolygon (aligned
 * with the coordinate system axes).  Use this with the draw interaction and
 * `type: 'Circle'` to return a box instead of a circle geometry.
 * @return {GVol.DrawGeometryFunctionType} Function that draws a box-shaped pGVolygon.
 * @api
 */
GVol.interaction.Draw.createBox = function() {
  return (
    /**
     * @param {Array.<GVol.Coordinate>} coordinates
     * @param {GVol.geom.SimpleGeometry=} opt_geometry
     * @return {GVol.geom.SimpleGeometry}
     */
    function(coordinates, opt_geometry) {
      var extent = GVol.extent.boundingExtent(coordinates);
      var geometry = opt_geometry || new GVol.geom.PGVolygon(null);
      geometry.setCoordinates([[
        GVol.extent.getBottomLeft(extent),
        GVol.extent.getBottomRight(extent),
        GVol.extent.getTopRight(extent),
        GVol.extent.getTopLeft(extent),
        GVol.extent.getBottomLeft(extent)
      ]]);
      return geometry;
    }
  );
};


/**
 * Get the drawing mode.  The mode for mult-part geometries is the same as for
 * their single-part cousins.
 * @param {GVol.geom.GeometryType} type Geometry type.
 * @return {GVol.interaction.Draw.Mode_} Drawing mode.
 * @private
 */
GVol.interaction.Draw.getMode_ = function(type) {
  var mode;
  if (type === GVol.geom.GeometryType.POINT ||
      type === GVol.geom.GeometryType.MULTI_POINT) {
    mode = GVol.interaction.Draw.Mode_.POINT;
  } else if (type === GVol.geom.GeometryType.LINE_STRING ||
      type === GVol.geom.GeometryType.MULTI_LINE_STRING) {
    mode = GVol.interaction.Draw.Mode_.LINE_STRING;
  } else if (type === GVol.geom.GeometryType.POLYGON ||
      type === GVol.geom.GeometryType.MULTI_POLYGON) {
    mode = GVol.interaction.Draw.Mode_.POLYGON;
  } else if (type === GVol.geom.GeometryType.CIRCLE) {
    mode = GVol.interaction.Draw.Mode_.CIRCLE;
  }
  return /** @type {!GVol.interaction.Draw.Mode_} */ (mode);
};


/**
 * Draw mode.  This cGVollapses multi-part geometry types with their single-part
 * cousins.
 * @enum {string}
 * @private
 */
GVol.interaction.Draw.Mode_ = {
  POINT: 'Point',
  LINE_STRING: 'LineString',
  POLYGON: 'PGVolygon',
  CIRCLE: 'Circle'
};

/**
 * @classdesc
 * Events emitted by {@link GVol.interaction.Draw} instances are instances of
 * this type.
 *
 * @constructor
 * @extends {GVol.events.Event}
 * @implements {GVoli.DrawEvent}
 * @param {GVol.interaction.DrawEventType} type Type.
 * @param {GVol.Feature} feature The feature drawn.
 */
GVol.interaction.Draw.Event = function(type, feature) {

  GVol.events.Event.call(this, type);

  /**
   * The feature being drawn.
   * @type {GVol.Feature}
   * @api
   */
  this.feature = feature;

};
GVol.inherits(GVol.interaction.Draw.Event, GVol.events.Event);

goog.provide('GVol.interaction.ExtentEventType');


/**
 * @enum {string}
 */
GVol.interaction.ExtentEventType = {
  /**
   * Triggered after the extent is changed
   * @event GVol.interaction.Extent.Event#extentchanged
   * @api
   */
  EXTENTCHANGED: 'extentchanged'
};

goog.provide('GVol.interaction.Extent');

goog.require('GVol');
goog.require('GVol.Feature');
goog.require('GVol.MapBrowserEventType');
goog.require('GVol.MapBrowserPointerEvent');
goog.require('GVol.coordinate');
goog.require('GVol.events.Event');
goog.require('GVol.extent');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.geom.Point');
goog.require('GVol.geom.PGVolygon');
goog.require('GVol.interaction.ExtentEventType');
goog.require('GVol.interaction.Pointer');
goog.require('GVol.layer.Vector');
goog.require('GVol.source.Vector');
goog.require('GVol.style.Style');


/**
 * @classdesc
 * Allows the user to draw a vector box by clicking and dragging on the map.
 * Once drawn, the vector box can be modified by dragging its vertices or edges.
 * This interaction is only supported for mouse devices.
 *
 * @constructor
 * @extends {GVol.interaction.Pointer}
 * @fires GVol.interaction.Extent.Event
 * @param {GVolx.interaction.ExtentOptions=} opt_options Options.
 * @api
 */
GVol.interaction.Extent = function(opt_options) {

  /**
   * Extent of the drawn box
   * @type {GVol.Extent}
   * @private
   */
  this.extent_ = null;

  /**
   * Handler for pointer move events
   * @type {function (GVol.Coordinate): GVol.Extent|null}
   * @private
   */
  this.pointerHandler_ = null;

  /**
   * Pixel threshGVold to snap to extent
   * @type {number}
   * @private
   */
  this.pixelTGVolerance_ = 10;

  /**
   * Is the pointer snapped to an extent vertex
   * @type {boGVolean}
   * @private
   */
  this.snappedToVertex_ = false;

  /**
   * Feature for displaying the visible extent
   * @type {GVol.Feature}
   * @private
   */
  this.extentFeature_ = null;

  /**
   * Feature for displaying the visible pointer
   * @type {GVol.Feature}
   * @private
   */
  this.vertexFeature_ = null;

  if (!opt_options) {
    opt_options = {};
  }

  /* Inherit GVol.interaction.Pointer */
  GVol.interaction.Pointer.call(this, {
    handleDownEvent: GVol.interaction.Extent.handleDownEvent_,
    handleDragEvent: GVol.interaction.Extent.handleDragEvent_,
    handleEvent: GVol.interaction.Extent.handleEvent_,
    handleUpEvent: GVol.interaction.Extent.handleUpEvent_
  });

  /**
   * Layer for the extentFeature
   * @type {GVol.layer.Vector}
   * @private
   */
  this.extentOverlay_ = new GVol.layer.Vector({
    source: new GVol.source.Vector({
      useSpatialIndex: false,
      wrapX: !!opt_options.wrapX
    }),
    style: opt_options.boxStyle ? opt_options.boxStyle : GVol.interaction.Extent.getDefaultExtentStyleFunction_(),
    updateWhileAnimating: true,
    updateWhileInteracting: true
  });

  /**
   * Layer for the vertexFeature
   * @type {GVol.layer.Vector}
   * @private
   */
  this.vertexOverlay_ = new GVol.layer.Vector({
    source: new GVol.source.Vector({
      useSpatialIndex: false,
      wrapX: !!opt_options.wrapX
    }),
    style: opt_options.pointerStyle ? opt_options.pointerStyle : GVol.interaction.Extent.getDefaultPointerStyleFunction_(),
    updateWhileAnimating: true,
    updateWhileInteracting: true
  });

  if (opt_options.extent) {
    this.setExtent(opt_options.extent);
  }
};

GVol.inherits(GVol.interaction.Extent, GVol.interaction.Pointer);

/**
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Event.
 * @return {boGVolean} Propagate event?
 * @this {GVol.interaction.Extent}
 * @private
 */
GVol.interaction.Extent.handleEvent_ = function(mapBrowserEvent) {
  if (!(mapBrowserEvent instanceof GVol.MapBrowserPointerEvent)) {
    return true;
  }
  //display pointer (if not dragging)
  if (mapBrowserEvent.type == GVol.MapBrowserEventType.POINTERMOVE && !this.handlingDownUpSequence) {
    this.handlePointerMove_(mapBrowserEvent);
  }
  //call pointer to determine up/down/drag
  GVol.interaction.Pointer.handleEvent.call(this, mapBrowserEvent);
  //return false to stop propagation
  return false;
};

/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boGVolean} Event handled?
 * @this {GVol.interaction.Extent}
 * @private
 */
GVol.interaction.Extent.handleDownEvent_ = function(mapBrowserEvent) {
  var pixel = mapBrowserEvent.pixel;
  var map = mapBrowserEvent.map;

  var extent = this.getExtent();
  var vertex = this.snapToVertex_(pixel, map);

  //find the extent corner opposite the passed corner
  var getOpposingPoint = function(point) {
    var x_ = null;
    var y_ = null;
    if (point[0] == extent[0]) {
      x_ = extent[2];
    } else if (point[0] == extent[2]) {
      x_ = extent[0];
    }
    if (point[1] == extent[1]) {
      y_ = extent[3];
    } else if (point[1] == extent[3]) {
      y_ = extent[1];
    }
    if (x_ !== null && y_ !== null) {
      return [x_, y_];
    }
    return null;
  };
  if (vertex && extent) {
    var x = (vertex[0] == extent[0] || vertex[0] == extent[2]) ? vertex[0] : null;
    var y = (vertex[1] == extent[1] || vertex[1] == extent[3]) ? vertex[1] : null;

    //snap to point
    if (x !== null && y !== null) {
      this.pointerHandler_ = GVol.interaction.Extent.getPointHandler_(getOpposingPoint(vertex));
    //snap to edge
    } else if (x !== null) {
      this.pointerHandler_ = GVol.interaction.Extent.getEdgeHandler_(
          getOpposingPoint([x, extent[1]]),
          getOpposingPoint([x, extent[3]])
      );
    } else if (y !== null) {
      this.pointerHandler_ = GVol.interaction.Extent.getEdgeHandler_(
          getOpposingPoint([extent[0], y]),
          getOpposingPoint([extent[2], y])
      );
    }
  //no snap - new bbox
  } else {
    vertex = map.getCoordinateFromPixel(pixel);
    this.setExtent([vertex[0], vertex[1], vertex[0], vertex[1]]);
    this.pointerHandler_ = GVol.interaction.Extent.getPointHandler_(vertex);
  }
  return true; //event handled; start downup sequence
};

/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boGVolean} Event handled?
 * @this {GVol.interaction.Extent}
 * @private
 */
GVol.interaction.Extent.handleDragEvent_ = function(mapBrowserEvent) {
  if (this.pointerHandler_) {
    var pixelCoordinate = mapBrowserEvent.coordinate;
    this.setExtent(this.pointerHandler_(pixelCoordinate));
    this.createOrUpdatePointerFeature_(pixelCoordinate);
  }
  return true;
};

/**
 * @param {GVol.MapBrowserPointerEvent} mapBrowserEvent Event.
 * @return {boGVolean} Stop drag sequence?
 * @this {GVol.interaction.Extent}
 * @private
 */
GVol.interaction.Extent.handleUpEvent_ = function(mapBrowserEvent) {
  this.pointerHandler_ = null;
  //If bbox is zero area, set to null;
  var extent = this.getExtent();
  if (!extent || GVol.extent.getArea(extent) === 0) {
    this.setExtent(null);
  }
  return false; //Stop handling downup sequence
};

/**
 * Returns the default style for the drawn bbox
 *
 * @return {GVol.StyleFunction} Default Extent style
 * @private
 */
GVol.interaction.Extent.getDefaultExtentStyleFunction_ = function() {
  var style = GVol.style.Style.createDefaultEditing();
  return function(feature, resGVolution) {
    return style[GVol.geom.GeometryType.POLYGON];
  };
};

/**
 * Returns the default style for the pointer
 *
 * @return {GVol.StyleFunction} Default pointer style
 * @private
 */
GVol.interaction.Extent.getDefaultPointerStyleFunction_ = function() {
  var style = GVol.style.Style.createDefaultEditing();
  return function(feature, resGVolution) {
    return style[GVol.geom.GeometryType.POINT];
  };
};

/**
 * @param {GVol.Coordinate} fixedPoint corner that will be unchanged in the new extent
 * @returns {function (GVol.Coordinate): GVol.Extent} event handler
 * @private
 */
GVol.interaction.Extent.getPointHandler_ = function(fixedPoint) {
  return function(point) {
    return GVol.extent.boundingExtent([fixedPoint, point]);
  };
};

/**
 * @param {GVol.Coordinate} fixedP1 first corner that will be unchanged in the new extent
 * @param {GVol.Coordinate} fixedP2 second corner that will be unchanged in the new extent
 * @returns {function (GVol.Coordinate): GVol.Extent|null} event handler
 * @private
 */
GVol.interaction.Extent.getEdgeHandler_ = function(fixedP1, fixedP2) {
  if (fixedP1[0] == fixedP2[0]) {
    return function(point) {
      return GVol.extent.boundingExtent([fixedP1, [point[0], fixedP2[1]]]);
    };
  } else if (fixedP1[1] == fixedP2[1]) {
    return function(point) {
      return GVol.extent.boundingExtent([fixedP1, [fixedP2[0], point[1]]]);
    };
  } else {
    return null;
  }
};

/**
 * @param {GVol.Extent} extent extent
 * @returns {Array<Array<GVol.Coordinate>>} extent line segments
 * @private
 */
GVol.interaction.Extent.getSegments_ = function(extent) {
  return [
    [[extent[0], extent[1]], [extent[0], extent[3]]],
    [[extent[0], extent[3]], [extent[2], extent[3]]],
    [[extent[2], extent[3]], [extent[2], extent[1]]],
    [[extent[2], extent[1]], [extent[0], extent[1]]]
  ];
};

/**
 * @param {GVol.Pixel} pixel cursor location
 * @param {GVol.Map} map map
 * @returns {GVol.Coordinate|null} snapped vertex on extent
 * @private
 */
GVol.interaction.Extent.prototype.snapToVertex_ = function(pixel, map) {
  var pixelCoordinate = map.getCoordinateFromPixel(pixel);
  var sortByDistance = function(a, b) {
    return GVol.coordinate.squaredDistanceToSegment(pixelCoordinate, a) -
        GVol.coordinate.squaredDistanceToSegment(pixelCoordinate, b);
  };
  var extent = this.getExtent();
  if (extent) {
    //convert extents to line segments and find the segment closest to pixelCoordinate
    var segments = GVol.interaction.Extent.getSegments_(extent);
    segments.sort(sortByDistance);
    var closestSegment = segments[0];

    var vertex = (GVol.coordinate.closestOnSegment(pixelCoordinate,
        closestSegment));
    var vertexPixel = map.getPixelFromCoordinate(vertex);

    //if the distance is within tGVolerance, snap to the segment
    if (GVol.coordinate.distance(pixel, vertexPixel) <= this.pixelTGVolerance_) {
      //test if we should further snap to a vertex
      var pixel1 = map.getPixelFromCoordinate(closestSegment[0]);
      var pixel2 = map.getPixelFromCoordinate(closestSegment[1]);
      var squaredDist1 = GVol.coordinate.squaredDistance(vertexPixel, pixel1);
      var squaredDist2 = GVol.coordinate.squaredDistance(vertexPixel, pixel2);
      var dist = Math.sqrt(Math.min(squaredDist1, squaredDist2));
      this.snappedToVertex_ = dist <= this.pixelTGVolerance_;
      if (this.snappedToVertex_) {
        vertex = squaredDist1 > squaredDist2 ?
          closestSegment[1] : closestSegment[0];
      }
      return vertex;
    }
  }
  return null;
};

/**
 * @param {GVol.MapBrowserEvent} mapBrowserEvent pointer move event
 * @private
 */
GVol.interaction.Extent.prototype.handlePointerMove_ = function(mapBrowserEvent) {
  var pixel = mapBrowserEvent.pixel;
  var map = mapBrowserEvent.map;

  var vertex = this.snapToVertex_(pixel, map);
  if (!vertex) {
    vertex = map.getCoordinateFromPixel(pixel);
  }
  this.createOrUpdatePointerFeature_(vertex);
};

/**
 * @param {GVol.Extent} extent extent
 * @returns {GVol.Feature} extent as featrue
 * @private
 */
GVol.interaction.Extent.prototype.createOrUpdateExtentFeature_ = function(extent) {
  var extentFeature = this.extentFeature_;

  if (!extentFeature) {
    if (!extent) {
      extentFeature = new GVol.Feature({});
    } else {
      extentFeature = new GVol.Feature(GVol.geom.PGVolygon.fromExtent(extent));
    }
    this.extentFeature_ = extentFeature;
    this.extentOverlay_.getSource().addFeature(extentFeature);
  } else {
    if (!extent) {
      extentFeature.setGeometry(undefined);
    } else {
      extentFeature.setGeometry(GVol.geom.PGVolygon.fromExtent(extent));
    }
  }
  return extentFeature;
};


/**
 * @param {GVol.Coordinate} vertex location of feature
 * @returns {GVol.Feature} vertex as feature
 * @private
 */
GVol.interaction.Extent.prototype.createOrUpdatePointerFeature_ = function(vertex) {
  var vertexFeature = this.vertexFeature_;
  if (!vertexFeature) {
    vertexFeature = new GVol.Feature(new GVol.geom.Point(vertex));
    this.vertexFeature_ = vertexFeature;
    this.vertexOverlay_.getSource().addFeature(vertexFeature);
  } else {
    var geometry = /** @type {GVol.geom.Point} */ (vertexFeature.getGeometry());
    geometry.setCoordinates(vertex);
  }
  return vertexFeature;
};


/**
 * @inheritDoc
 */
GVol.interaction.Extent.prototype.setMap = function(map) {
  this.extentOverlay_.setMap(map);
  this.vertexOverlay_.setMap(map);
  GVol.interaction.Pointer.prototype.setMap.call(this, map);
};

/**
 * Returns the current drawn extent in the view projection
 *
 * @return {GVol.Extent} Drawn extent in the view projection.
 * @api
 */
GVol.interaction.Extent.prototype.getExtent = function() {
  return this.extent_;
};

/**
 * Manually sets the drawn extent, using the view projection.
 *
 * @param {GVol.Extent} extent Extent
 * @api
 */
GVol.interaction.Extent.prototype.setExtent = function(extent) {
  //Null extent means no bbox
  this.extent_ = extent ? extent : null;
  this.createOrUpdateExtentFeature_(extent);
  this.dispatchEvent(new GVol.interaction.Extent.Event(this.extent_));
};


/**
 * @classdesc
 * Events emitted by {@link GVol.interaction.Extent} instances are instances of
 * this type.
 *
 * @constructor
 * @implements {GVoli.ExtentEvent}
 * @param {GVol.Extent} extent the new extent
 * @extends {GVol.events.Event}
 */
GVol.interaction.Extent.Event = function(extent) {
  GVol.events.Event.call(this, GVol.interaction.ExtentEventType.EXTENTCHANGED);

  /**
   * The current extent.
   * @type {GVol.Extent}
   * @api
   */
  this.extent = extent;

};
GVol.inherits(GVol.interaction.Extent.Event, GVol.events.Event);

goog.provide('GVol.interaction.ModifyEventType');


/**
 * @enum {string}
 */
GVol.interaction.ModifyEventType = {
  /**
   * Triggered upon feature modification start
   * @event GVol.interaction.Modify.Event#modifystart
   * @api
   */
  MODIFYSTART: 'modifystart',
  /**
   * Triggered upon feature modification end
   * @event GVol.interaction.Modify.Event#modifyend
   * @api
   */
  MODIFYEND: 'modifyend'
};

goog.provide('GVol.interaction.Modify');

goog.require('GVol');
goog.require('GVol.CGVollection');
goog.require('GVol.CGVollectionEventType');
goog.require('GVol.Feature');
goog.require('GVol.MapBrowserEventType');
goog.require('GVol.MapBrowserPointerEvent');
goog.require('GVol.ViewHint');
goog.require('GVol.array');
goog.require('GVol.coordinate');
goog.require('GVol.events');
goog.require('GVol.events.Event');
goog.require('GVol.events.EventType');
goog.require('GVol.events.condition');
goog.require('GVol.extent');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.geom.Point');
goog.require('GVol.interaction.ModifyEventType');
goog.require('GVol.interaction.Pointer');
goog.require('GVol.layer.Vector');
goog.require('GVol.source.Vector');
goog.require('GVol.source.VectorEventType');
goog.require('GVol.structs.RBush');
goog.require('GVol.style.Style');

/**
 * @classdesc
 * Interaction for modifying feature geometries.  To modify features that have
 * been added to an existing source, construct the modify interaction with the
 * `source` option.  If you want to modify features in a cGVollection (for example,
 * the cGVollection used by a select interaction), construct the interaction with
 * the `features` option.  The interaction must be constructed with either a
 * `source` or `features` option.
 *
 * By default, the interaction will allow deletion of vertices when the `alt`
 * key is pressed.  To configure the interaction with a different condition
 * for deletion, use the `deleteCondition` option.
 *
 * @constructor
 * @extends {GVol.interaction.Pointer}
 * @param {GVolx.interaction.ModifyOptions} options Options.
 * @fires GVol.interaction.Modify.Event
 * @api
 */
GVol.interaction.Modify = function(options) {

  GVol.interaction.Pointer.call(this, {
    handleDownEvent: GVol.interaction.Modify.handleDownEvent_,
    handleDragEvent: GVol.interaction.Modify.handleDragEvent_,
    handleEvent: GVol.interaction.Modify.handleEvent,
    handleUpEvent: GVol.interaction.Modify.handleUpEvent_
  });

  /**
   * @private
   * @type {GVol.EventsConditionType}
   */
  this.condition_ = options.condition ?
    options.condition : GVol.events.condition.primaryAction;


  /**
   * @private
   * @param {GVol.MapBrowserEvent} mapBrowserEvent Browser event.
   * @return {boGVolean} Combined condition result.
   */
  this.defaultDeleteCondition_ = function(mapBrowserEvent) {
    return GVol.events.condition.altKeyOnly(mapBrowserEvent) &&
      GVol.events.condition.singleClick(mapBrowserEvent);
  };

  /**
   * @type {GVol.EventsConditionType}
   * @private
   */
  this.deleteCondition_ = options.deleteCondition ?
    options.deleteCondition : this.defaultDeleteCondition_;

  /**
   * @type {GVol.EventsConditionType}
   * @private
   */
  this.insertVertexCondition_ = options.insertVertexCondition ?
    options.insertVertexCondition : GVol.events.condition.always;

  /**
   * Editing vertex.
   * @type {GVol.Feature}
   * @private
   */
  this.vertexFeature_ = null;

  /**
   * Segments intersecting {@link this.vertexFeature_} by segment uid.
   * @type {Object.<string, boGVolean>}
   * @private
   */
  this.vertexSegments_ = null;

  /**
   * @type {GVol.Pixel}
   * @private
   */
  this.lastPixel_ = [0, 0];

  /**
   * Tracks if the next `singleclick` event should be ignored to prevent
   * accidental deletion right after vertex creation.
   * @type {boGVolean}
   * @private
   */
  this.ignoreNextSingleClick_ = false;

  /**
   * @type {boGVolean}
   * @private
   */
  this.modified_ = false;

  /**
   * Segment RTree for each layer
   * @type {GVol.structs.RBush.<GVol.ModifySegmentDataType>}
   * @private
   */
  this.rBush_ = new GVol.structs.RBush();

  /**
   * @type {number}
   * @private
   */
  this.pixelTGVolerance_ = options.pixelTGVolerance !== undefined ?
    options.pixelTGVolerance : 10;

  /**
   * @type {boGVolean}
   * @private
   */
  this.snappedToVertex_ = false;

  /**
   * Indicate whether the interaction is currently changing a feature's
   * coordinates.
   * @type {boGVolean}
   * @private
   */
  this.changingFeature_ = false;

  /**
   * @type {Array}
   * @private
   */
  this.dragSegments_ = [];

  /**
   * Draw overlay where sketch features are drawn.
   * @type {GVol.layer.Vector}
   * @private
   */
  this.overlay_ = new GVol.layer.Vector({
    source: new GVol.source.Vector({
      useSpatialIndex: false,
      wrapX: !!options.wrapX
    }),
    style: options.style ? options.style :
      GVol.interaction.Modify.getDefaultStyleFunction(),
    updateWhileAnimating: true,
    updateWhileInteracting: true
  });

  /**
  * @const
  * @private
  * @type {Object.<string, function(GVol.Feature, GVol.geom.Geometry)>}
  */
  this.SEGMENT_WRITERS_ = {
    'Point': this.writePointGeometry_,
    'LineString': this.writeLineStringGeometry_,
    'LinearRing': this.writeLineStringGeometry_,
    'PGVolygon': this.writePGVolygonGeometry_,
    'MultiPoint': this.writeMultiPointGeometry_,
    'MultiLineString': this.writeMultiLineStringGeometry_,
    'MultiPGVolygon': this.writeMultiPGVolygonGeometry_,
    'Circle': this.writeCircleGeometry_,
    'GeometryCGVollection': this.writeGeometryCGVollectionGeometry_
  };


  /**
   * @type {GVol.source.Vector}
   * @private
   */
  this.source_ = null;

  var features;
  if (options.source) {
    this.source_ = options.source;
    features = new GVol.CGVollection(this.source_.getFeatures());
    GVol.events.listen(this.source_, GVol.source.VectorEventType.ADDFEATURE,
        this.handleSourceAdd_, this);
    GVol.events.listen(this.source_, GVol.source.VectorEventType.REMOVEFEATURE,
        this.handleSourceRemove_, this);
  } else {
    features = options.features;
  }
  if (!features) {
    throw new Error('The modify interaction requires features or a source');
  }

  /**
   * @type {GVol.CGVollection.<GVol.Feature>}
   * @private
   */
  this.features_ = features;

  this.features_.forEach(this.addFeature_, this);
  GVol.events.listen(this.features_, GVol.CGVollectionEventType.ADD,
      this.handleFeatureAdd_, this);
  GVol.events.listen(this.features_, GVol.CGVollectionEventType.REMOVE,
      this.handleFeatureRemove_, this);

  /**
   * @type {GVol.MapBrowserPointerEvent}
   * @private
   */
  this.lastPointerEvent_ = null;

};
GVol.inherits(GVol.interaction.Modify, GVol.interaction.Pointer);


/**
 * @define {number} The segment index assigned to a circle's center when
 * breaking up a cicrle into ModifySegmentDataType segments.
 */
GVol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CENTER_INDEX = 0;

/**
 * @define {number} The segment index assigned to a circle's circumference when
 * breaking up a circle into ModifySegmentDataType segments.
 */
GVol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CIRCUMFERENCE_INDEX = 1;


/**
 * @param {GVol.Feature} feature Feature.
 * @private
 */
GVol.interaction.Modify.prototype.addFeature_ = function(feature) {
  var geometry = feature.getGeometry();
  if (geometry && geometry.getType() in this.SEGMENT_WRITERS_) {
    this.SEGMENT_WRITERS_[geometry.getType()].call(this, feature, geometry);
  }
  var map = this.getMap();
  if (map && map.isRendered() && this.getActive()) {
    this.handlePointerAtPixel_(this.lastPixel_, map);
  }
  GVol.events.listen(feature, GVol.events.EventType.CHANGE,
      this.handleFeatureChange_, this);
};


/**
 * @param {GVol.MapBrowserPointerEvent} evt Map browser event
 * @private
 */
GVol.interaction.Modify.prototype.willModifyFeatures_ = function(evt) {
  if (!this.modified_) {
    this.modified_ = true;
    this.dispatchEvent(new GVol.interaction.Modify.Event(
        GVol.interaction.ModifyEventType.MODIFYSTART, this.features_, evt));
  }
};


/**
 * @param {GVol.Feature} feature Feature.
 * @private
 */
GVol.interaction.Modify.prototype.removeFeature_ = function(feature) {
  this.removeFeatureSegmentData_(feature);
  // Remove the vertex feature if the cGVollection of canditate features
  // is empty.
  if (this.vertexFeature_ && this.features_.getLength() === 0) {
    this.overlay_.getSource().removeFeature(this.vertexFeature_);
    this.vertexFeature_ = null;
  }
  GVol.events.unlisten(feature, GVol.events.EventType.CHANGE,
      this.handleFeatureChange_, this);
};


/**
 * @param {GVol.Feature} feature Feature.
 * @private
 */
GVol.interaction.Modify.prototype.removeFeatureSegmentData_ = function(feature) {
  var rBush = this.rBush_;
  var /** @type {Array.<GVol.ModifySegmentDataType>} */ nodesToRemove = [];
  rBush.forEach(
      /**
       * @param {GVol.ModifySegmentDataType} 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
 */
GVol.interaction.Modify.prototype.setActive = function(active) {
  if (this.vertexFeature_ && !active) {
    this.overlay_.getSource().removeFeature(this.vertexFeature_);
    this.vertexFeature_ = null;
  }
  GVol.interaction.Pointer.prototype.setActive.call(this, active);
};


/**
 * @inheritDoc
 */
GVol.interaction.Modify.prototype.setMap = function(map) {
  this.overlay_.setMap(map);
  GVol.interaction.Pointer.prototype.setMap.call(this, map);
};


/**
 * @param {GVol.source.Vector.Event} event Event.
 * @private
 */
GVol.interaction.Modify.prototype.handleSourceAdd_ = function(event) {
  if (event.feature) {
    this.features_.push(event.feature);
  }
};


/**
 * @param {GVol.source.Vector.Event} event Event.
 * @private
 */
GVol.interaction.Modify.prototype.handleSourceRemove_ = function(event) {
  if (event.feature) {
    this.features_.remove(event.feature);
  }
};


/**
 * @param {GVol.CGVollection.Event} evt Event.
 * @private
 */
GVol.interaction.Modify.prototype.handleFeatureAdd_ = function(evt) {
  this.addFeature_(/** @type {GVol.Feature} */ (evt.element));
};


/**
 * @param {GVol.events.Event} evt Event.
 * @private
 */
GVol.interaction.Modify.prototype.handleFeatureChange_ = function(evt) {
  if (!this.changingFeature_) {
    var feature = /** @type {GVol.Feature} */ (evt.target);
    this.removeFeature_(feature);
    this.addFeature_(feature);
  }
};


/**
 * @param {GVol.CGVollection.Event} evt Event.
 * @private
 */
GVol.interaction.Modify.prototype.handleFeatureRemove_ = function(evt) {
  var feature = /** @type {GVol.Feature} */ (evt.element);
  this.removeFeature_(feature);
};


/**
 * @param {GVol.Feature} feature Feature
 * @param {GVol.geom.Point} geometry Geometry.
 * @private
 */
GVol.interaction.Modify.prototype.writePointGeometry_ = function(feature, geometry) {
  var coordinates = geometry.getCoordinates();
  var segmentData = /** @type {GVol.ModifySegmentDataType} */ ({
    feature: feature,
    geometry: geometry,
    segment: [coordinates, coordinates]
  });
  this.rBush_.insert(geometry.getExtent(), segmentData);
};


/**
 * @param {GVol.Feature} feature Feature
 * @param {GVol.geom.MultiPoint} geometry Geometry.
 * @private
 */
GVol.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 {GVol.ModifySegmentDataType} */ ({
      feature: feature,
      geometry: geometry,
      depth: [i],
      index: i,
      segment: [coordinates, coordinates]
    });
    this.rBush_.insert(geometry.getExtent(), segmentData);
  }
};


/**
 * @param {GVol.Feature} feature Feature
 * @param {GVol.geom.LineString} geometry Geometry.
 * @private
 */
GVol.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 {GVol.ModifySegmentDataType} */ ({
      feature: feature,
      geometry: geometry,
      index: i,
      segment: segment
    });
    this.rBush_.insert(GVol.extent.boundingExtent(segment), segmentData);
  }
};


/**
 * @param {GVol.Feature} feature Feature
 * @param {GVol.geom.MultiLineString} geometry Geometry.
 * @private
 */
GVol.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 {GVol.ModifySegmentDataType} */ ({
        feature: feature,
        geometry: geometry,
        depth: [j],
        index: i,
        segment: segment
      });
      this.rBush_.insert(GVol.extent.boundingExtent(segment), segmentData);
    }
  }
};


/**
 * @param {GVol.Feature} feature Feature
 * @param {GVol.geom.PGVolygon} geometry Geometry.
 * @private
 */
GVol.interaction.Modify.prototype.writePGVolygonGeometry_ = 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 {GVol.ModifySegmentDataType} */ ({
        feature: feature,
        geometry: geometry,
        depth: [j],
        index: i,
        segment: segment
      });
      this.rBush_.insert(GVol.extent.boundingExtent(segment), segmentData);
    }
  }
};


/**
 * @param {GVol.Feature} feature Feature
 * @param {GVol.geom.MultiPGVolygon} geometry Geometry.
 * @private
 */
GVol.interaction.Modify.prototype.writeMultiPGVolygonGeometry_ = function(feature, geometry) {
  var pGVolygons = geometry.getCoordinates();
  var coordinates, i, ii, j, jj, k, kk, rings, segment, segmentData;
  for (k = 0, kk = pGVolygons.length; k < kk; ++k) {
    rings = pGVolygons[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 {GVol.ModifySegmentDataType} */ ({
          feature: feature,
          geometry: geometry,
          depth: [j, k],
          index: i,
          segment: segment
        });
        this.rBush_.insert(GVol.extent.boundingExtent(segment), segmentData);
      }
    }
  }
};


/**
 * We convert a circle into two segments.  The segment at index
 * {@link GVol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CENTER_INDEX} is the
 * circle's center (a point).  The segment at index
 * {@link GVol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CIRCUMFERENCE_INDEX} is
 * the circumference, and is not a line segment.
 *
 * @param {GVol.Feature} feature Feature.
 * @param {GVol.geom.Circle} geometry Geometry.
 * @private
 */
GVol.interaction.Modify.prototype.writeCircleGeometry_ = function(feature, geometry) {
  var coordinates = geometry.getCenter();
  var centerSegmentData = /** @type {GVol.ModifySegmentDataType} */ ({
    feature: feature,
    geometry: geometry,
    index: GVol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CENTER_INDEX,
    segment: [coordinates, coordinates]
  });
  var circumferenceSegmentData = /** @type {GVol.ModifySegmentDataType} */ ({
    feature: feature,
    geometry: geometry,
    index: GVol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CIRCUMFERENCE_INDEX,
    segment: [coordinates, coordinates]
  });
  var featureSegments = [centerSegmentData, circumferenceSegmentData];
  centerSegmentData.featureSegments = circumferenceSegmentData.featureSegments = featureSegments;
  this.rBush_.insert(GVol.extent.createOrUpdateFromCoordinate(coordinates), centerSegmentData);
  this.rBush_.insert(geometry.getExtent(), circumferenceSegmentData);
};


/**
 * @param {GVol.Feature} feature Feature
 * @param {GVol.geom.GeometryCGVollection} geometry Geometry.
 * @private
 */
GVol.interaction.Modify.prototype.writeGeometryCGVollectionGeometry_ = 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 {GVol.Coordinate} coordinates Coordinates.
 * @return {GVol.Feature} Vertex feature.
 * @private
 */
GVol.interaction.Modify.prototype.createOrUpdateVertexFeature_ = function(coordinates) {
  var vertexFeature = this.vertexFeature_;
  if (!vertexFeature) {
    vertexFeature = new GVol.Feature(new GVol.geom.Point(coordinates));
    this.vertexFeature_ = vertexFeature;
    this.overlay_.getSource().addFeature(vertexFeature);
  } else {
    var geometry = /** @type {GVol.geom.Point} */ (vertexFeature.getGeometry());
    geometry.setCoordinates(coordinates);
  }
  return vertexFeature;
};


/**
 * @param {GVol.ModifySegmentDataType} a The first segment data.
 * @param {GVol.ModifySegmentDataType} b The second segment data.
 * @return {number} The difference in indexes.
 * @private
 */
GVol.interaction.Modify.compareIndexes_ = function(a, b) {
  return a.index - b.index;
};


/**
 * @param {GVol.MapBrowserPointerEvent} evt Event.
 * @return {boGVolean} Start drag sequence?
 * @this {GVol.interaction.Modify}
 * @private
 */
GVol.interaction.Modify.handleDownEvent_ = function(evt) {
  if (!this.condition_(evt)) {
    return false;
  }
  this.handlePointerAtPixel_(evt.pixel, evt.map);
  var pixelCoordinate = evt.map.getCoordinateFromPixel(evt.pixel);
  this.dragSegments_.length = 0;
  this.modified_ = false;
  var vertexFeature = this.vertexFeature_;
  if (vertexFeature) {
    var insertVertices = [];
    var geometry = /** @type {GVol.geom.Point} */ (vertexFeature.getGeometry());
    var vertex = geometry.getCoordinates();
    var vertexExtent = GVol.extent.boundingExtent([vertex]);
    var segmentDataMatches = this.rBush_.getInExtent(vertexExtent);
    var componentSegments = {};
    segmentDataMatches.sort(GVol.interaction.Modify.compareIndexes_);
    for (var i = 0, ii = segmentDataMatches.length; i < ii; ++i) {
      var segmentDataMatch = segmentDataMatches[i];
      var segment = segmentDataMatch.segment;
      var uid = GVol.getUid(segmentDataMatch.feature);
      var depth = segmentDataMatch.depth;
      if (depth) {
        uid += '-' + depth.join('-'); // separate feature components
      }
      if (!componentSegments[uid]) {
        componentSegments[uid] = new Array(2);
      }
      if (segmentDataMatch.geometry.getType() === GVol.geom.GeometryType.CIRCLE &&
      segmentDataMatch.index === GVol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CIRCUMFERENCE_INDEX) {

        var closestVertex = GVol.interaction.Modify.closestOnSegmentData_(pixelCoordinate, segmentDataMatch);
        if (GVol.coordinate.equals(closestVertex, vertex) && !componentSegments[uid][0]) {
          this.dragSegments_.push([segmentDataMatch, 0]);
          componentSegments[uid][0] = segmentDataMatch;
        }
      } else if (GVol.coordinate.equals(segment[0], vertex) &&
          !componentSegments[uid][0]) {
        this.dragSegments_.push([segmentDataMatch, 0]);
        componentSegments[uid][0] = segmentDataMatch;
      } else if (GVol.coordinate.equals(segment[1], vertex) &&
          !componentSegments[uid][1]) {

        // prevent dragging closed linestrings by the connecting node
        if ((segmentDataMatch.geometry.getType() ===
            GVol.geom.GeometryType.LINE_STRING ||
            segmentDataMatch.geometry.getType() ===
            GVol.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 (this.insertVertexCondition_(evt) && GVol.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 {GVol.MapBrowserPointerEvent} evt Event.
 * @this {GVol.interaction.Modify}
 * @private
 */
GVol.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;
    var segment = segmentData.segment;
    var index = dragSegment[1];

    while (vertex.length < geometry.getStride()) {
      vertex.push(segment[index][vertex.length]);
    }

    switch (geometry.getType()) {
      case GVol.geom.GeometryType.POINT:
        coordinates = vertex;
        segment[0] = segment[1] = vertex;
        break;
      case GVol.geom.GeometryType.MULTI_POINT:
        coordinates = geometry.getCoordinates();
        coordinates[segmentData.index] = vertex;
        segment[0] = segment[1] = vertex;
        break;
      case GVol.geom.GeometryType.LINE_STRING:
        coordinates = geometry.getCoordinates();
        coordinates[segmentData.index + index] = vertex;
        segment[index] = vertex;
        break;
      case GVol.geom.GeometryType.MULTI_LINE_STRING:
        coordinates = geometry.getCoordinates();
        coordinates[depth[0]][segmentData.index + index] = vertex;
        segment[index] = vertex;
        break;
      case GVol.geom.GeometryType.POLYGON:
        coordinates = geometry.getCoordinates();
        coordinates[depth[0]][segmentData.index + index] = vertex;
        segment[index] = vertex;
        break;
      case GVol.geom.GeometryType.MULTI_POLYGON:
        coordinates = geometry.getCoordinates();
        coordinates[depth[1]][depth[0]][segmentData.index + index] = vertex;
        segment[index] = vertex;
        break;
      case GVol.geom.GeometryType.CIRCLE:
        segment[0] = segment[1] = vertex;
        if (segmentData.index === GVol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CENTER_INDEX) {
          this.changingFeature_ = true;
          geometry.setCenter(vertex);
          this.changingFeature_ = false;
        } else { // We're dragging the circle's circumference:
          this.changingFeature_ = true;
          geometry.setRadius(GVol.coordinate.distance(geometry.getCenter(), vertex));
          this.changingFeature_ = false;
        }
        break;
      default:
        // pass
    }

    if (coordinates) {
      this.setGeometryCoordinates_(geometry, coordinates);
    }
  }
  this.createOrUpdateVertexFeature_(vertex);
};


/**
 * @param {GVol.MapBrowserPointerEvent} evt Event.
 * @return {boGVolean} Stop drag sequence?
 * @this {GVol.interaction.Modify}
 * @private
 */
GVol.interaction.Modify.handleUpEvent_ = function(evt) {
  var segmentData;
  var geometry;
  for (var i = this.dragSegments_.length - 1; i >= 0; --i) {
    segmentData = this.dragSegments_[i][0];
    geometry = segmentData.geometry;
    if (geometry.getType() === GVol.geom.GeometryType.CIRCLE) {
      // Update a circle object in the R* bush:
      var coordinates = geometry.getCenter();
      var centerSegmentData = segmentData.featureSegments[0];
      var circumferenceSegmentData = segmentData.featureSegments[1];
      centerSegmentData.segment[0] = centerSegmentData.segment[1] = coordinates;
      circumferenceSegmentData.segment[0] = circumferenceSegmentData.segment[1] = coordinates;
      this.rBush_.update(GVol.extent.createOrUpdateFromCoordinate(coordinates), centerSegmentData);
      this.rBush_.update(geometry.getExtent(), circumferenceSegmentData);
    } else {
      this.rBush_.update(GVol.extent.boundingExtent(segmentData.segment),
          segmentData);
    }
  }
  if (this.modified_) {
    this.dispatchEvent(new GVol.interaction.Modify.Event(
        GVol.interaction.ModifyEventType.MODIFYEND, this.features_, evt));
    this.modified_ = false;
  }
  return false;
};


/**
 * Handles the {@link GVol.MapBrowserEvent map browser event} and may modify the
 * geometry.
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} `false` to stop event propagation.
 * @this {GVol.interaction.Modify}
 * @api
 */
GVol.interaction.Modify.handleEvent = function(mapBrowserEvent) {
  if (!(mapBrowserEvent instanceof GVol.MapBrowserPointerEvent)) {
    return true;
  }
  this.lastPointerEvent_ = mapBrowserEvent;

  var handled;
  if (!mapBrowserEvent.map.getView().getHints()[GVol.ViewHint.INTERACTING] &&
      mapBrowserEvent.type == GVol.MapBrowserEventType.POINTERMOVE &&
      !this.handlingDownUpSequence) {
    this.handlePointerMove_(mapBrowserEvent);
  }
  if (this.vertexFeature_ && this.deleteCondition_(mapBrowserEvent)) {
    if (mapBrowserEvent.type != GVol.MapBrowserEventType.SINGLECLICK ||
        !this.ignoreNextSingleClick_) {
      handled = this.removePoint();
    } else {
      handled = true;
    }
  }

  if (mapBrowserEvent.type == GVol.MapBrowserEventType.SINGLECLICK) {
    this.ignoreNextSingleClick_ = false;
  }

  return GVol.interaction.Pointer.handleEvent.call(this, mapBrowserEvent) &&
      !handled;
};


/**
 * @param {GVol.MapBrowserEvent} evt Event.
 * @private
 */
GVol.interaction.Modify.prototype.handlePointerMove_ = function(evt) {
  this.lastPixel_ = evt.pixel;
  this.handlePointerAtPixel_(evt.pixel, evt.map);
};


/**
 * @param {GVol.Pixel} pixel Pixel
 * @param {GVol.Map} map Map.
 * @private
 */
GVol.interaction.Modify.prototype.handlePointerAtPixel_ = function(pixel, map) {
  var pixelCoordinate = map.getCoordinateFromPixel(pixel);
  var sortByDistance = function(a, b) {
    return GVol.interaction.Modify.pointDistanceToSegmentDataSquared_(pixelCoordinate, a) -
        GVol.interaction.Modify.pointDistanceToSegmentDataSquared_(pixelCoordinate, b);
  };

  var box = GVol.extent.buffer(
      GVol.extent.createOrUpdateFromCoordinate(pixelCoordinate),
      map.getView().getResGVolution() * this.pixelTGVolerance_);

  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 = GVol.interaction.Modify.closestOnSegmentData_(pixelCoordinate, node);
    var vertexPixel = map.getPixelFromCoordinate(vertex);
    var dist = GVol.coordinate.distance(pixel, vertexPixel);
    if (dist <= this.pixelTGVolerance_) {
      var vertexSegments = {};

      if (node.geometry.getType() === GVol.geom.GeometryType.CIRCLE &&
      node.index === GVol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CIRCUMFERENCE_INDEX) {

        this.snappedToVertex_ = true;
        this.createOrUpdateVertexFeature_(vertex);
      } else {
        var pixel1 = map.getPixelFromCoordinate(closestSegment[0]);
        var pixel2 = map.getPixelFromCoordinate(closestSegment[1]);
        var squaredDist1 = GVol.coordinate.squaredDistance(vertexPixel, pixel1);
        var squaredDist2 = GVol.coordinate.squaredDistance(vertexPixel, pixel2);
        dist = Math.sqrt(Math.min(squaredDist1, squaredDist2));
        this.snappedToVertex_ = dist <= this.pixelTGVolerance_;
        if (this.snappedToVertex_) {
          vertex = squaredDist1 > squaredDist2 ?
            closestSegment[1] : closestSegment[0];
        }
        this.createOrUpdateVertexFeature_(vertex);
        var segment;
        for (var i = 1, ii = nodes.length; i < ii; ++i) {
          segment = nodes[i].segment;
          if ((GVol.coordinate.equals(closestSegment[0], segment[0]) &&
              GVol.coordinate.equals(closestSegment[1], segment[1]) ||
              (GVol.coordinate.equals(closestSegment[0], segment[1]) &&
              GVol.coordinate.equals(closestSegment[1], segment[0])))) {
            vertexSegments[GVol.getUid(segment)] = true;
          } else {
            break;
          }
        }
      }

      vertexSegments[GVol.getUid(closestSegment)] = true;
      this.vertexSegments_ = vertexSegments;
      return;
    }
  }
  if (this.vertexFeature_) {
    this.overlay_.getSource().removeFeature(this.vertexFeature_);
    this.vertexFeature_ = null;
  }
};


/**
 * Returns the distance from a point to a line segment.
 *
 * @param {GVol.Coordinate} pointCoordinates The coordinates of the point from
 *        which to calculate the distance.
 * @param {GVol.ModifySegmentDataType} segmentData The object describing the line
 *        segment we are calculating the distance to.
 * @return {number} The square of the distance between a point and a line segment.
 */
GVol.interaction.Modify.pointDistanceToSegmentDataSquared_ = function(pointCoordinates, segmentData) {
  var geometry = segmentData.geometry;

  if (geometry.getType() === GVol.geom.GeometryType.CIRCLE) {
    var circleGeometry = /** @type {GVol.geom.Circle} */ (geometry);

    if (segmentData.index === GVol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CIRCUMFERENCE_INDEX) {
      var distanceToCenterSquared =
            GVol.coordinate.squaredDistance(circleGeometry.getCenter(), pointCoordinates);
      var distanceToCircumference =
            Math.sqrt(distanceToCenterSquared) - circleGeometry.getRadius();
      return distanceToCircumference * distanceToCircumference;
    }
  }
  return GVol.coordinate.squaredDistanceToSegment(pointCoordinates, segmentData.segment);
};

/**
 * Returns the point closest to a given line segment.
 *
 * @param {GVol.Coordinate} pointCoordinates The point to which a closest point
 *        should be found.
 * @param {GVol.ModifySegmentDataType} segmentData The object describing the line
 *        segment which should contain the closest point.
 * @return {GVol.Coordinate} The point closest to the specified line segment.
 */
GVol.interaction.Modify.closestOnSegmentData_ = function(pointCoordinates, segmentData) {
  var geometry = segmentData.geometry;

  if (geometry.getType() === GVol.geom.GeometryType.CIRCLE &&
  segmentData.index === GVol.interaction.Modify.MODIFY_SEGMENT_CIRCLE_CIRCUMFERENCE_INDEX) {
    return geometry.getClosestPoint(pointCoordinates);
  }
  return GVol.coordinate.closestOnSegment(pointCoordinates, segmentData.segment);
};


/**
 * @param {GVol.ModifySegmentDataType} segmentData Segment data.
 * @param {GVol.Coordinate} vertex Vertex.
 * @private
 */
GVol.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 = /** @type {number} */ (segmentData.index);
  var coordinates;

  while (vertex.length < geometry.getStride()) {
    vertex.push(0);
  }

  switch (geometry.getType()) {
    case GVol.geom.GeometryType.MULTI_LINE_STRING:
      coordinates = geometry.getCoordinates();
      coordinates[depth[0]].splice(index + 1, 0, vertex);
      break;
    case GVol.geom.GeometryType.POLYGON:
      coordinates = geometry.getCoordinates();
      coordinates[depth[0]].splice(index + 1, 0, vertex);
      break;
    case GVol.geom.GeometryType.MULTI_POLYGON:
      coordinates = geometry.getCoordinates();
      coordinates[depth[1]][depth[0]].splice(index + 1, 0, vertex);
      break;
    case GVol.geom.GeometryType.LINE_STRING:
      coordinates = geometry.getCoordinates();
      coordinates.splice(index + 1, 0, vertex);
      break;
    default:
      return;
  }

  this.setGeometryCoordinates_(geometry, coordinates);
  var rTree = this.rBush_;
  rTree.remove(segmentData);
  this.updateSegmentIndices_(geometry, index, depth, 1);
  var newSegmentData = /** @type {GVol.ModifySegmentDataType} */ ({
    segment: [segment[0], vertex],
    feature: feature,
    geometry: geometry,
    depth: depth,
    index: index
  });
  rTree.insert(GVol.extent.boundingExtent(newSegmentData.segment),
      newSegmentData);
  this.dragSegments_.push([newSegmentData, 1]);

  var newSegmentData2 = /** @type {GVol.ModifySegmentDataType} */ ({
    segment: [vertex, segment[1]],
    feature: feature,
    geometry: geometry,
    depth: depth,
    index: index + 1
  });
  rTree.insert(GVol.extent.boundingExtent(newSegmentData2.segment),
      newSegmentData2);
  this.dragSegments_.push([newSegmentData2, 0]);
  this.ignoreNextSingleClick_ = true;
};

/**
 * Removes the vertex currently being pointed.
 * @return {boGVolean} True when a vertex was removed.
 * @api
 */
GVol.interaction.Modify.prototype.removePoint = function() {
  if (this.lastPointerEvent_ && this.lastPointerEvent_.type != GVol.MapBrowserEventType.POINTERDRAG) {
    var evt = this.lastPointerEvent_;
    this.willModifyFeatures_(evt);
    this.removeVertex_();
    this.dispatchEvent(new GVol.interaction.Modify.Event(
        GVol.interaction.ModifyEventType.MODIFYEND, this.features_, evt));
    this.modified_ = false;
    return true;
  }
  return false;
};

/**
 * Removes a vertex from all matching features.
 * @return {boGVolean} True when a vertex was removed.
 * @private
 */
GVol.interaction.Modify.prototype.removeVertex_ = function() {
  var dragSegments = this.dragSegments_;
  var segmentsByFeature = {};
  var deleted = false;
  var component, coordinates, dragSegment, geometry, i, index, left;
  var newIndex, right, segmentData, uid;
  for (i = dragSegments.length - 1; i >= 0; --i) {
    dragSegment = dragSegments[i];
    segmentData = dragSegment[0];
    uid = GVol.getUid(segmentData.feature);
    if (segmentData.depth) {
      // separate feature components
      uid += '-' + segmentData.depth.join('-');
    }
    if (!(uid in segmentsByFeature)) {
      segmentsByFeature[uid] = {};
    }
    if (dragSegment[1] === 0) {
      segmentsByFeature[uid].right = segmentData;
      segmentsByFeature[uid].index = segmentData.index;
    } else if (dragSegment[1] == 1) {
      segmentsByFeature[uid].left = segmentData;
      segmentsByFeature[uid].index = segmentData.index + 1;
    }

  }
  for (uid in segmentsByFeature) {
    right = segmentsByFeature[uid].right;
    left = segmentsByFeature[uid].left;
    index = segmentsByFeature[uid].index;
    newIndex = index - 1;
    if (left !== undefined) {
      segmentData = left;
    } else {
      segmentData = right;
    }
    if (newIndex < 0) {
      newIndex = 0;
    }
    geometry = segmentData.geometry;
    coordinates = geometry.getCoordinates();
    component = coordinates;
    deleted = false;
    switch (geometry.getType()) {
      case GVol.geom.GeometryType.MULTI_LINE_STRING:
        if (coordinates[segmentData.depth[0]].length > 2) {
          coordinates[segmentData.depth[0]].splice(index, 1);
          deleted = true;
        }
        break;
      case GVol.geom.GeometryType.LINE_STRING:
        if (coordinates.length > 2) {
          coordinates.splice(index, 1);
          deleted = true;
        }
        break;
      case GVol.geom.GeometryType.MULTI_POLYGON:
        component = component[segmentData.depth[1]];
        /* falls through */
      case GVol.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.setGeometryCoordinates_(geometry, coordinates);
      var segments = [];
      if (left !== undefined) {
        this.rBush_.remove(left);
        segments.push(left.segment[0]);
      }
      if (right !== undefined) {
        this.rBush_.remove(right);
        segments.push(right.segment[1]);
      }
      if (left !== undefined && right !== undefined) {
        var newSegmentData = /** @type {GVol.ModifySegmentDataType} */ ({
          depth: segmentData.depth,
          feature: segmentData.feature,
          geometry: segmentData.geometry,
          index: newIndex,
          segment: segments
        });
        this.rBush_.insert(GVol.extent.boundingExtent(newSegmentData.segment),
            newSegmentData);
      }
      this.updateSegmentIndices_(geometry, index, segmentData.depth, -1);
      if (this.vertexFeature_) {
        this.overlay_.getSource().removeFeature(this.vertexFeature_);
        this.vertexFeature_ = null;
      }
      dragSegments.length = 0;
    }

  }
  return deleted;
};


/**
 * @param {GVol.geom.SimpleGeometry} geometry Geometry.
 * @param {Array} coordinates Coordinates.
 * @private
 */
GVol.interaction.Modify.prototype.setGeometryCoordinates_ = function(geometry, coordinates) {
  this.changingFeature_ = true;
  geometry.setCoordinates(coordinates);
  this.changingFeature_ = false;
};


/**
 * @param {GVol.geom.SimpleGeometry} geometry Geometry.
 * @param {number} index Index.
 * @param {Array.<number>|undefined} depth Depth.
 * @param {number} delta Delta (1 or -1).
 * @private
 */
GVol.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 ||
        GVol.array.equals(segmentDataMatch.depth, depth)) &&
        segmentDataMatch.index > index) {
      segmentDataMatch.index += delta;
    }
  });
};


/**
 * @return {GVol.StyleFunction} Styles.
 */
GVol.interaction.Modify.getDefaultStyleFunction = function() {
  var style = GVol.style.Style.createDefaultEditing();
  return function(feature, resGVolution) {
    return style[GVol.geom.GeometryType.POINT];
  };
};


/**
 * @classdesc
 * Events emitted by {@link GVol.interaction.Modify} instances are instances of
 * this type.
 *
 * @constructor
 * @extends {GVol.events.Event}
 * @implements {GVoli.ModifyEvent}
 * @param {GVol.interaction.ModifyEventType} type Type.
 * @param {GVol.CGVollection.<GVol.Feature>} features The features modified.
 * @param {GVol.MapBrowserPointerEvent} mapBrowserPointerEvent Associated
 *     {@link GVol.MapBrowserPointerEvent}.
 */
GVol.interaction.Modify.Event = function(type, features, mapBrowserPointerEvent) {

  GVol.events.Event.call(this, type);

  /**
   * The features being modified.
   * @type {GVol.CGVollection.<GVol.Feature>}
   * @api
   */
  this.features = features;

  /**
   * Associated {@link GVol.MapBrowserEvent}.
   * @type {GVol.MapBrowserEvent}
   * @api
   */
  this.mapBrowserEvent = mapBrowserPointerEvent;
};
GVol.inherits(GVol.interaction.Modify.Event, GVol.events.Event);

goog.provide('GVol.interaction.Select');

goog.require('GVol');
goog.require('GVol.CGVollectionEventType');
goog.require('GVol.array');
goog.require('GVol.events');
goog.require('GVol.events.Event');
goog.require('GVol.events.condition');
goog.require('GVol.functions');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.interaction.Interaction');
goog.require('GVol.layer.Vector');
goog.require('GVol.obj');
goog.require('GVol.source.Vector');
goog.require('GVol.style.Style');


/**
 * @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 contrGVolling 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 {GVol.interaction.Interaction}
 * @param {GVolx.interaction.SelectOptions=} opt_options Options.
 * @fires GVol.interaction.Select.Event
 * @api
 */
GVol.interaction.Select = function(opt_options) {

  GVol.interaction.Interaction.call(this, {
    handleEvent: GVol.interaction.Select.handleEvent
  });

  var options = opt_options ? opt_options : {};

  /**
   * @private
   * @type {GVol.EventsConditionType}
   */
  this.condition_ = options.condition ?
    options.condition : GVol.events.condition.singleClick;

  /**
   * @private
   * @type {GVol.EventsConditionType}
   */
  this.addCondition_ = options.addCondition ?
    options.addCondition : GVol.events.condition.never;

  /**
   * @private
   * @type {GVol.EventsConditionType}
   */
  this.removeCondition_ = options.removeCondition ?
    options.removeCondition : GVol.events.condition.never;

  /**
   * @private
   * @type {GVol.EventsConditionType}
   */
  this.toggleCondition_ = options.toggleCondition ?
    options.toggleCondition : GVol.events.condition.shiftKeyOnly;

  /**
   * @private
   * @type {boGVolean}
   */
  this.multi_ = options.multi ? options.multi : false;

  /**
   * @private
   * @type {GVol.SelectFilterFunction}
   */
  this.filter_ = options.filter ? options.filter :
    GVol.functions.TRUE;

  /**
   * @private
   * @type {number}
   */
  this.hitTGVolerance_ = options.hitTGVolerance ? options.hitTGVolerance : 0;

  var featureOverlay = new GVol.layer.Vector({
    source: new GVol.source.Vector({
      useSpatialIndex: false,
      features: options.features,
      wrapX: options.wrapX
    }),
    style: options.style ? options.style :
      GVol.interaction.Select.getDefaultStyleFunction(),
    updateWhileAnimating: true,
    updateWhileInteracting: true
  });

  /**
   * @private
   * @type {GVol.layer.Vector}
   */
  this.featureOverlay_ = featureOverlay;

  /** @type {function(GVol.layer.Layer): boGVolean} */
  var layerFilter;
  if (options.layers) {
    if (typeof options.layers === 'function') {
      layerFilter = options.layers;
    } else {
      var layers = options.layers;
      layerFilter = function(layer) {
        return GVol.array.includes(layers, layer);
      };
    }
  } else {
    layerFilter = GVol.functions.TRUE;
  }

  /**
   * @private
   * @type {function(GVol.layer.Layer): boGVolean}
   */
  this.layerFilter_ = layerFilter;

  /**
   * An association between selected feature (key)
   * and layer (value)
   * @private
   * @type {Object.<number, GVol.layer.Layer>}
   */
  this.featureLayerAssociation_ = {};

  var features = this.featureOverlay_.getSource().getFeaturesCGVollection();
  GVol.events.listen(features, GVol.CGVollectionEventType.ADD,
      this.addFeature_, this);
  GVol.events.listen(features, GVol.CGVollectionEventType.REMOVE,
      this.removeFeature_, this);

};
GVol.inherits(GVol.interaction.Select, GVol.interaction.Interaction);


/**
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 * @param {GVol.layer.Layer} layer Layer.
 * @private
 */
GVol.interaction.Select.prototype.addFeatureLayerAssociation_ = function(feature, layer) {
  var key = GVol.getUid(feature);
  this.featureLayerAssociation_[key] = layer;
};


/**
 * Get the selected features.
 * @return {GVol.CGVollection.<GVol.Feature>} Features cGVollection.
 * @api
 */
GVol.interaction.Select.prototype.getFeatures = function() {
  return this.featureOverlay_.getSource().getFeaturesCGVollection();
};


/**
 * Returns the Hit-detection tGVolerance.
 * @returns {number} Hit tGVolerance in pixels.
 * @api
 */
GVol.interaction.Select.prototype.getHitTGVolerance = function() {
  return this.hitTGVolerance_;
};


/**
 * Returns the associated {@link GVol.layer.Vector vectorlayer} of
 * the (last) selected feature. Note that this will not work with any
 * programmatic method like pushing features to
 * {@link GVol.interaction.Select#getFeatures cGVollection}.
 * @param {GVol.Feature|GVol.render.Feature} feature Feature
 * @return {GVol.layer.Vector} Layer.
 * @api
 */
GVol.interaction.Select.prototype.getLayer = function(feature) {
  var key = GVol.getUid(feature);
  return /** @type {GVol.layer.Vector} */ (this.featureLayerAssociation_[key]);
};


/**
 * Handles the {@link GVol.MapBrowserEvent map browser event} and may change the
 * selected state of features.
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Map browser event.
 * @return {boGVolean} `false` to stop event propagation.
 * @this {GVol.interaction.Select}
 * @api
 */
GVol.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().getFeaturesCGVollection();
  var deselected = [];
  var selected = [];
  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.
    GVol.obj.clear(this.featureLayerAssociation_);
    map.forEachFeatureAtPixel(mapBrowserEvent.pixel,
        (
          /**
           * @param {GVol.Feature|GVol.render.Feature} feature Feature.
           * @param {GVol.layer.Layer} layer Layer.
           * @return {boGVolean|undefined} Continue to iterate over the features.
           */
          function(feature, layer) {
            if (this.filter_(feature, layer)) {
              selected.push(feature);
              this.addFeatureLayerAssociation_(feature, layer);
              return !this.multi_;
            }
          }).bind(this), {
          layerFilter: this.layerFilter_,
          hitTGVolerance: this.hitTGVolerance_
        });
    var i;
    for (i = features.getLength() - 1; i >= 0; --i) {
      var feature = features.item(i);
      var index = selected.indexOf(feature);
      if (index > -1) {
        // feature is already selected
        selected.splice(index, 1);
      } else {
        features.remove(feature);
        deselected.push(feature);
      }
    }
    if (selected.length !== 0) {
      features.extend(selected);
    }
  } else {
    // Modify the currently selected feature(s).
    map.forEachFeatureAtPixel(mapBrowserEvent.pixel,
        (
          /**
           * @param {GVol.Feature|GVol.render.Feature} feature Feature.
           * @param {GVol.layer.Layer} layer Layer.
           * @return {boGVolean|undefined} Continue to iterate over the features.
           */
          function(feature, layer) {
            if (this.filter_(feature, layer)) {
              if ((add || toggle) &&
                !GVol.array.includes(features.getArray(), feature)) {
                selected.push(feature);
                this.addFeatureLayerAssociation_(feature, layer);
              } else if ((remove || toggle) &&
                GVol.array.includes(features.getArray(), feature)) {
                deselected.push(feature);
                this.removeFeatureLayerAssociation_(feature);
              }
              return !this.multi_;
            }
          }).bind(this), {
          layerFilter: this.layerFilter_,
          hitTGVolerance: this.hitTGVolerance_
        });
    var j;
    for (j = deselected.length - 1; j >= 0; --j) {
      features.remove(deselected[j]);
    }
    features.extend(selected);
  }
  if (selected.length > 0 || deselected.length > 0) {
    this.dispatchEvent(
        new GVol.interaction.Select.Event(GVol.interaction.Select.EventType_.SELECT,
            selected, deselected, mapBrowserEvent));
  }
  return GVol.events.condition.pointerMove(mapBrowserEvent);
};


/**
 * Hit-detection tGVolerance. Pixels inside the radius around the given position
 * will be checked for features. This only works for the canvas renderer and
 * not for WebGL.
 * @param {number} hitTGVolerance Hit tGVolerance in pixels.
 * @api
 */
GVol.interaction.Select.prototype.setHitTGVolerance = function(hitTGVolerance) {
  this.hitTGVolerance_ = hitTGVolerance;
};


/**
 * 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 {GVol.Map} map Map.
 * @override
 * @api
 */
GVol.interaction.Select.prototype.setMap = function(map) {
  var currentMap = this.getMap();
  var selectedFeatures =
      this.featureOverlay_.getSource().getFeaturesCGVollection();
  if (currentMap) {
    selectedFeatures.forEach(currentMap.unskipFeature, currentMap);
  }
  GVol.interaction.Interaction.prototype.setMap.call(this, map);
  this.featureOverlay_.setMap(map);
  if (map) {
    selectedFeatures.forEach(map.skipFeature, map);
  }
};


/**
 * @return {GVol.StyleFunction} Styles.
 */
GVol.interaction.Select.getDefaultStyleFunction = function() {
  var styles = GVol.style.Style.createDefaultEditing();
  GVol.array.extend(styles[GVol.geom.GeometryType.POLYGON],
      styles[GVol.geom.GeometryType.LINE_STRING]);
  GVol.array.extend(styles[GVol.geom.GeometryType.GEOMETRY_COLLECTION],
      styles[GVol.geom.GeometryType.LINE_STRING]);

  return function(feature, resGVolution) {
    if (!feature.getGeometry()) {
      return null;
    }
    return styles[feature.getGeometry().getType()];
  };
};


/**
 * @param {GVol.CGVollection.Event} evt Event.
 * @private
 */
GVol.interaction.Select.prototype.addFeature_ = function(evt) {
  var map = this.getMap();
  if (map) {
    map.skipFeature(/** @type {GVol.Feature} */ (evt.element));
  }
};


/**
 * @param {GVol.CGVollection.Event} evt Event.
 * @private
 */
GVol.interaction.Select.prototype.removeFeature_ = function(evt) {
  var map = this.getMap();
  if (map) {
    map.unskipFeature(/** @type {GVol.Feature} */ (evt.element));
  }
};


/**
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 * @private
 */
GVol.interaction.Select.prototype.removeFeatureLayerAssociation_ = function(feature) {
  var key = GVol.getUid(feature);
  delete this.featureLayerAssociation_[key];
};


/**
 * @classdesc
 * Events emitted by {@link GVol.interaction.Select} instances are instances of
 * this type.
 *
 * @param {GVol.interaction.Select.EventType_} type The event type.
 * @param {Array.<GVol.Feature>} selected Selected features.
 * @param {Array.<GVol.Feature>} deselected Deselected features.
 * @param {GVol.MapBrowserEvent} mapBrowserEvent Associated
 *     {@link GVol.MapBrowserEvent}.
 * @implements {GVoli.SelectEvent}
 * @extends {GVol.events.Event}
 * @constructor
 */
GVol.interaction.Select.Event = function(type, selected, deselected, mapBrowserEvent) {
  GVol.events.Event.call(this, type);

  /**
   * Selected features array.
   * @type {Array.<GVol.Feature>}
   * @api
   */
  this.selected = selected;

  /**
   * Deselected features array.
   * @type {Array.<GVol.Feature>}
   * @api
   */
  this.deselected = deselected;

  /**
   * Associated {@link GVol.MapBrowserEvent}.
   * @type {GVol.MapBrowserEvent}
   * @api
   */
  this.mapBrowserEvent = mapBrowserEvent;
};
GVol.inherits(GVol.interaction.Select.Event, GVol.events.Event);


/**
 * @enum {string}
 * @private
 */
GVol.interaction.Select.EventType_ = {
  /**
   * Triggered when feature(s) has been (de)selected.
   * @event GVol.interaction.Select.Event#select
   * @api
   */
  SELECT: 'select'
};

goog.provide('GVol.interaction.Snap');

goog.require('GVol');
goog.require('GVol.CGVollection');
goog.require('GVol.CGVollectionEventType');
goog.require('GVol.coordinate');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.extent');
goog.require('GVol.functions');
goog.require('GVol.geom.GeometryType');
goog.require('GVol.geom.PGVolygon');
goog.require('GVol.interaction.Pointer');
goog.require('GVol.obj');
goog.require('GVol.source.Vector');
goog.require('GVol.source.VectorEventType');
goog.require('GVol.structs.RBush');


/**
 * @classdesc
 * Handles snapping of vector features while modifying or drawing them.  The
 * features can come from a {@link GVol.source.Vector} or {@link GVol.CGVollection}
 * 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 GVol.interaction.Snap({
 *       source: source
 *     });
 *
 * @constructor
 * @extends {GVol.interaction.Pointer}
 * @param {GVolx.interaction.SnapOptions=} opt_options Options.
 * @api
 */
GVol.interaction.Snap = function(opt_options) {

  GVol.interaction.Pointer.call(this, {
    handleEvent: GVol.interaction.Snap.handleEvent_,
    handleDownEvent: GVol.functions.TRUE,
    handleUpEvent: GVol.interaction.Snap.handleUpEvent_
  });

  var options = opt_options ? opt_options : {};

  /**
   * @type {GVol.source.Vector}
   * @private
   */
  this.source_ = options.source ? options.source : null;

  /**
   * @private
   * @type {boGVolean}
   */
  this.vertex_ = options.vertex !== undefined ? options.vertex : true;

  /**
   * @private
   * @type {boGVolean}
   */
  this.edge_ = options.edge !== undefined ? options.edge : true;

  /**
   * @type {GVol.CGVollection.<GVol.Feature>}
   * @private
   */
  this.features_ = options.features ? options.features : null;

  /**
   * @type {Array.<GVol.EventsKey>}
   * @private
   */
  this.featuresListenerKeys_ = [];

  /**
   * @type {Object.<number, GVol.EventsKey>}
   * @private
   */
  this.featureChangeListenerKeys_ = {};

  /**
   * Extents are preserved so indexed segment can be quickly removed
   * when its feature geometry changes
   * @type {Object.<number, GVol.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, GVol.Feature>}
   * @private
   */
  this.pendingFeatures_ = {};

  /**
   * Used for distance sorting in sortByDistance_
   * @type {GVol.Coordinate}
   * @private
   */
  this.pixelCoordinate_ = null;

  /**
   * @type {number}
   * @private
   */
  this.pixelTGVolerance_ = options.pixelTGVolerance !== undefined ?
    options.pixelTGVolerance : 10;

  /**
   * @type {function(GVol.SnapSegmentDataType, GVol.SnapSegmentDataType): number}
   * @private
   */
  this.sortByDistance_ = GVol.interaction.Snap.sortByDistance.bind(this);


  /**
  * Segment RTree for each layer
  * @type {GVol.structs.RBush.<GVol.SnapSegmentDataType>}
  * @private
  */
  this.rBush_ = new GVol.structs.RBush();


  /**
  * @const
  * @private
  * @type {Object.<string, function(GVol.Feature, GVol.geom.Geometry)>}
  */
  this.SEGMENT_WRITERS_ = {
    'Point': this.writePointGeometry_,
    'LineString': this.writeLineStringGeometry_,
    'LinearRing': this.writeLineStringGeometry_,
    'PGVolygon': this.writePGVolygonGeometry_,
    'MultiPoint': this.writeMultiPointGeometry_,
    'MultiLineString': this.writeMultiLineStringGeometry_,
    'MultiPGVolygon': this.writeMultiPGVolygonGeometry_,
    'GeometryCGVollection': this.writeGeometryCGVollectionGeometry_,
    'Circle': this.writeCircleGeometry_
  };
};
GVol.inherits(GVol.interaction.Snap, GVol.interaction.Pointer);


/**
 * Add a feature to the cGVollection of features that we may snap to.
 * @param {GVol.Feature} feature Feature.
 * @param {boGVolean=} opt_listen Whether to listen to the feature change or not
 *     Defaults to `true`.
 * @api
 */
GVol.interaction.Snap.prototype.addFeature = function(feature, opt_listen) {
  var listen = opt_listen !== undefined ? opt_listen : true;
  var feature_uid = GVol.getUid(feature);
  var geometry = feature.getGeometry();
  if (geometry) {
    var segmentWriter = this.SEGMENT_WRITERS_[geometry.getType()];
    if (segmentWriter) {
      this.indexedFeaturesExtents_[feature_uid] = geometry.getExtent(
          GVol.extent.createEmpty());
      segmentWriter.call(this, feature, geometry);
    }
  }

  if (listen) {
    this.featureChangeListenerKeys_[feature_uid] = GVol.events.listen(
        feature,
        GVol.events.EventType.CHANGE,
        this.handleFeatureChange_, this);
  }
};


/**
 * @param {GVol.Feature} feature Feature.
 * @private
 */
GVol.interaction.Snap.prototype.forEachFeatureAdd_ = function(feature) {
  this.addFeature(feature);
};


/**
 * @param {GVol.Feature} feature Feature.
 * @private
 */
GVol.interaction.Snap.prototype.forEachFeatureRemove_ = function(feature) {
  this.removeFeature(feature);
};


/**
 * @return {GVol.CGVollection.<GVol.Feature>|Array.<GVol.Feature>} Features.
 * @private
 */
GVol.interaction.Snap.prototype.getFeatures_ = function() {
  var features;
  if (this.features_) {
    features = this.features_;
  } else if (this.source_) {
    features = this.source_.getFeatures();
  }
  return /** @type {!Array.<GVol.Feature>|!GVol.CGVollection.<GVol.Feature>} */ (features);
};


/**
 * @param {GVol.source.Vector.Event|GVol.CGVollection.Event} evt Event.
 * @private
 */
GVol.interaction.Snap.prototype.handleFeatureAdd_ = function(evt) {
  var feature;
  if (evt instanceof GVol.source.Vector.Event) {
    feature = evt.feature;
  } else if (evt instanceof GVol.CGVollection.Event) {
    feature = evt.element;
  }
  this.addFeature(/** @type {GVol.Feature} */ (feature));
};


/**
 * @param {GVol.source.Vector.Event|GVol.CGVollection.Event} evt Event.
 * @private
 */
GVol.interaction.Snap.prototype.handleFeatureRemove_ = function(evt) {
  var feature;
  if (evt instanceof GVol.source.Vector.Event) {
    feature = evt.feature;
  } else if (evt instanceof GVol.CGVollection.Event) {
    feature = evt.element;
  }
  this.removeFeature(/** @type {GVol.Feature} */ (feature));
};


/**
 * @param {GVol.events.Event} evt Event.
 * @private
 */
GVol.interaction.Snap.prototype.handleFeatureChange_ = function(evt) {
  var feature = /** @type {GVol.Feature} */ (evt.target);
  if (this.handlingDownUpSequence) {
    var uid = GVol.getUid(feature);
    if (!(uid in this.pendingFeatures_)) {
      this.pendingFeatures_[uid] = feature;
    }
  } else {
    this.updateFeature_(feature);
  }
};


/**
 * Remove a feature from the cGVollection of features that we may snap to.
 * @param {GVol.Feature} feature Feature
 * @param {boGVolean=} opt_unlisten Whether to unlisten to the feature change
 *     or not. Defaults to `true`.
 * @api
 */
GVol.interaction.Snap.prototype.removeFeature = function(feature, opt_unlisten) {
  var unlisten = opt_unlisten !== undefined ? opt_unlisten : true;
  var feature_uid = GVol.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) {
    GVol.events.unlistenByKey(this.featureChangeListenerKeys_[feature_uid]);
    delete this.featureChangeListenerKeys_[feature_uid];
  }
};


/**
 * @inheritDoc
 */
GVol.interaction.Snap.prototype.setMap = function(map) {
  var currentMap = this.getMap();
  var keys = this.featuresListenerKeys_;
  var features = this.getFeatures_();

  if (currentMap) {
    keys.forEach(GVol.events.unlistenByKey);
    keys.length = 0;
    features.forEach(this.forEachFeatureRemove_, this);
  }
  GVol.interaction.Pointer.prototype.setMap.call(this, map);

  if (map) {
    if (this.features_) {
      keys.push(
          GVol.events.listen(this.features_, GVol.CGVollectionEventType.ADD,
              this.handleFeatureAdd_, this),
          GVol.events.listen(this.features_, GVol.CGVollectionEventType.REMOVE,
              this.handleFeatureRemove_, this)
      );
    } else if (this.source_) {
      keys.push(
          GVol.events.listen(this.source_, GVol.source.VectorEventType.ADDFEATURE,
              this.handleFeatureAdd_, this),
          GVol.events.listen(this.source_, GVol.source.VectorEventType.REMOVEFEATURE,
              this.handleFeatureRemove_, this)
      );
    }
    features.forEach(this.forEachFeatureAdd_, this);
  }
};


/**
 * @inheritDoc
 */
GVol.interaction.Snap.prototype.shouldStopEvent = GVol.functions.FALSE;


/**
 * @param {GVol.Pixel} pixel Pixel
 * @param {GVol.Coordinate} pixelCoordinate Coordinate
 * @param {GVol.Map} map Map.
 * @return {GVol.SnapResultType} Snap result
 */
GVol.interaction.Snap.prototype.snapTo = function(pixel, pixelCoordinate, map) {

  var lowerLeft = map.getCoordinateFromPixel(
      [pixel[0] - this.pixelTGVolerance_, pixel[1] + this.pixelTGVolerance_]);
  var upperRight = map.getCoordinateFromPixel(
      [pixel[0] + this.pixelTGVolerance_, pixel[1] - this.pixelTGVolerance_]);
  var box = GVol.extent.boundingExtent([lowerLeft, upperRight]);

  var segments = this.rBush_.getInExtent(box);

  // If snapping on vertices only, don't consider circles
  if (this.vertex_ && !this.edge_) {
    segments = segments.filter(function(segment) {
      return segment.feature.getGeometry().getType() !==
          GVol.geom.GeometryType.CIRCLE;
    });
  }

  var snappedToVertex = false;
  var snapped = false;
  var vertex = null;
  var vertexPixel = null;
  var dist, pixel1, pixel2, squaredDist1, squaredDist2;
  if (segments.length > 0) {
    this.pixelCoordinate_ = pixelCoordinate;
    segments.sort(this.sortByDistance_);
    var closestSegment = segments[0].segment;
    var isCircle = segments[0].feature.getGeometry().getType() ===
        GVol.geom.GeometryType.CIRCLE;
    if (this.vertex_ && !this.edge_) {
      pixel1 = map.getPixelFromCoordinate(closestSegment[0]);
      pixel2 = map.getPixelFromCoordinate(closestSegment[1]);
      squaredDist1 = GVol.coordinate.squaredDistance(pixel, pixel1);
      squaredDist2 = GVol.coordinate.squaredDistance(pixel, pixel2);
      dist = Math.sqrt(Math.min(squaredDist1, squaredDist2));
      snappedToVertex = dist <= this.pixelTGVolerance_;
      if (snappedToVertex) {
        snapped = true;
        vertex = squaredDist1 > squaredDist2 ?
          closestSegment[1] : closestSegment[0];
        vertexPixel = map.getPixelFromCoordinate(vertex);
      }
    } else if (this.edge_) {
      if (isCircle) {
        vertex = GVol.coordinate.closestOnCircle(pixelCoordinate,
            /** @type {GVol.geom.Circle} */ (segments[0].feature.getGeometry()));
      } else {
        vertex = (GVol.coordinate.closestOnSegment(pixelCoordinate,
            closestSegment));
      }
      vertexPixel = map.getPixelFromCoordinate(vertex);
      if (GVol.coordinate.distance(pixel, vertexPixel) <= this.pixelTGVolerance_) {
        snapped = true;
        if (this.vertex_ && !isCircle) {
          pixel1 = map.getPixelFromCoordinate(closestSegment[0]);
          pixel2 = map.getPixelFromCoordinate(closestSegment[1]);
          squaredDist1 = GVol.coordinate.squaredDistance(vertexPixel, pixel1);
          squaredDist2 = GVol.coordinate.squaredDistance(vertexPixel, pixel2);
          dist = Math.sqrt(Math.min(squaredDist1, squaredDist2));
          snappedToVertex = dist <= this.pixelTGVolerance_;
          if (snappedToVertex) {
            vertex = squaredDist1 > squaredDist2 ?
              closestSegment[1] : closestSegment[0];
            vertexPixel = map.getPixelFromCoordinate(vertex);
          }
        }
      }
    }
    if (snapped) {
      vertexPixel = [Math.round(vertexPixel[0]), Math.round(vertexPixel[1])];
    }
  }
  return /** @type {GVol.SnapResultType} */ ({
    snapped: snapped,
    vertex: vertex,
    vertexPixel: vertexPixel
  });
};


/**
 * @param {GVol.Feature} feature Feature
 * @private
 */
GVol.interaction.Snap.prototype.updateFeature_ = function(feature) {
  this.removeFeature(feature, false);
  this.addFeature(feature, false);
};


/**
 * @param {GVol.Feature} feature Feature
 * @param {GVol.geom.Circle} geometry Geometry.
 * @private
 */
GVol.interaction.Snap.prototype.writeCircleGeometry_ = function(feature, geometry) {
  var pGVolygon = GVol.geom.PGVolygon.fromCircle(geometry);
  var coordinates = pGVolygon.getCoordinates()[0];
  var i, ii, segment, segmentData;
  for (i = 0, ii = coordinates.length - 1; i < ii; ++i) {
    segment = coordinates.slice(i, i + 2);
    segmentData = /** @type {GVol.SnapSegmentDataType} */ ({
      feature: feature,
      segment: segment
    });
    this.rBush_.insert(GVol.extent.boundingExtent(segment), segmentData);
  }
};


/**
 * @param {GVol.Feature} feature Feature
 * @param {GVol.geom.GeometryCGVollection} geometry Geometry.
 * @private
 */
GVol.interaction.Snap.prototype.writeGeometryCGVollectionGeometry_ = function(feature, geometry) {
  var i, geometries = geometry.getGeometriesArray();
  for (i = 0; i < geometries.length; ++i) {
    var segmentWriter = this.SEGMENT_WRITERS_[geometries[i].getType()];
    if (segmentWriter) {
      segmentWriter.call(this, feature, geometries[i]);
    }
  }
};


/**
 * @param {GVol.Feature} feature Feature
 * @param {GVol.geom.LineString} geometry Geometry.
 * @private
 */
GVol.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 {GVol.SnapSegmentDataType} */ ({
      feature: feature,
      segment: segment
    });
    this.rBush_.insert(GVol.extent.boundingExtent(segment), segmentData);
  }
};


/**
 * @param {GVol.Feature} feature Feature
 * @param {GVol.geom.MultiLineString} geometry Geometry.
 * @private
 */
GVol.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 {GVol.SnapSegmentDataType} */ ({
        feature: feature,
        segment: segment
      });
      this.rBush_.insert(GVol.extent.boundingExtent(segment), segmentData);
    }
  }
};


/**
 * @param {GVol.Feature} feature Feature
 * @param {GVol.geom.MultiPoint} geometry Geometry.
 * @private
 */
GVol.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 {GVol.SnapSegmentDataType} */ ({
      feature: feature,
      segment: [coordinates, coordinates]
    });
    this.rBush_.insert(geometry.getExtent(), segmentData);
  }
};


/**
 * @param {GVol.Feature} feature Feature
 * @param {GVol.geom.MultiPGVolygon} geometry Geometry.
 * @private
 */
GVol.interaction.Snap.prototype.writeMultiPGVolygonGeometry_ = function(feature, geometry) {
  var pGVolygons = geometry.getCoordinates();
  var coordinates, i, ii, j, jj, k, kk, rings, segment, segmentData;
  for (k = 0, kk = pGVolygons.length; k < kk; ++k) {
    rings = pGVolygons[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 {GVol.SnapSegmentDataType} */ ({
          feature: feature,
          segment: segment
        });
        this.rBush_.insert(GVol.extent.boundingExtent(segment), segmentData);
      }
    }
  }
};


/**
 * @param {GVol.Feature} feature Feature
 * @param {GVol.geom.Point} geometry Geometry.
 * @private
 */
GVol.interaction.Snap.prototype.writePointGeometry_ = function(feature, geometry) {
  var coordinates = geometry.getCoordinates();
  var segmentData = /** @type {GVol.SnapSegmentDataType} */ ({
    feature: feature,
    segment: [coordinates, coordinates]
  });
  this.rBush_.insert(geometry.getExtent(), segmentData);
};


/**
 * @param {GVol.Feature} feature Feature
 * @param {GVol.geom.PGVolygon} geometry Geometry.
 * @private
 */
GVol.interaction.Snap.prototype.writePGVolygonGeometry_ = 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 {GVol.SnapSegmentDataType} */ ({
        feature: feature,
        segment: segment
      });
      this.rBush_.insert(GVol.extent.boundingExtent(segment), segmentData);
    }
  }
};


/**
 * Handle all pointer events events.
 * @param {GVol.MapBrowserEvent} evt A move event.
 * @return {boGVolean} Pass the event to other interactions.
 * @this {GVol.interaction.Snap}
 * @private
 */
GVol.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 GVol.interaction.Pointer.handleEvent.call(this, evt);
};


/**
 * @param {GVol.MapBrowserPointerEvent} evt Event.
 * @return {boGVolean} Stop drag sequence?
 * @this {GVol.interaction.Snap}
 * @private
 */
GVol.interaction.Snap.handleUpEvent_ = function(evt) {
  var featuresToUpdate = GVol.obj.getValues(this.pendingFeatures_);
  if (featuresToUpdate.length) {
    featuresToUpdate.forEach(this.updateFeature_, this);
    this.pendingFeatures_ = {};
  }
  return false;
};


/**
 * Sort segments by distance, helper function
 * @param {GVol.SnapSegmentDataType} a The first segment data.
 * @param {GVol.SnapSegmentDataType} b The second segment data.
 * @return {number} The difference in distance.
 * @this {GVol.interaction.Snap}
 */
GVol.interaction.Snap.sortByDistance = function(a, b) {
  return GVol.coordinate.squaredDistanceToSegment(
      this.pixelCoordinate_, a.segment) -
      GVol.coordinate.squaredDistanceToSegment(
          this.pixelCoordinate_, b.segment);
};

goog.provide('GVol.interaction.TranslateEventType');


/**
 * @enum {string}
 */
GVol.interaction.TranslateEventType = {
  /**
   * Triggered upon feature translation start.
   * @event GVol.interaction.Translate.Event#translatestart
   * @api
   */
  TRANSLATESTART: 'translatestart',
  /**
   * Triggered upon feature translation.
   * @event GVol.interaction.Translate.Event#translating
   * @api
   */
  TRANSLATING: 'translating',
  /**
   * Triggered upon feature translation end.
   * @event GVol.interaction.Translate.Event#translateend
   * @api
   */
  TRANSLATEEND: 'translateend'
};

goog.provide('GVol.interaction.Translate');

goog.require('GVol');
goog.require('GVol.CGVollection');
goog.require('GVol.Object');
goog.require('GVol.events');
goog.require('GVol.events.Event');
goog.require('GVol.functions');
goog.require('GVol.array');
goog.require('GVol.interaction.Pointer');
goog.require('GVol.interaction.Property');
goog.require('GVol.interaction.TranslateEventType');


/**
 * @classdesc
 * Interaction for translating (moving) features.
 *
 * @constructor
 * @extends {GVol.interaction.Pointer}
 * @fires GVol.interaction.Translate.Event
 * @param {GVolx.interaction.TranslateOptions=} opt_options Options.
 * @api
 */
GVol.interaction.Translate = function(opt_options) {
  GVol.interaction.Pointer.call(this, {
    handleDownEvent: GVol.interaction.Translate.handleDownEvent_,
    handleDragEvent: GVol.interaction.Translate.handleDragEvent_,
    handleMoveEvent: GVol.interaction.Translate.handleMoveEvent_,
    handleUpEvent: GVol.interaction.Translate.handleUpEvent_
  });

  var options = opt_options ? opt_options : {};

  /**
   * The last position we translated to.
   * @type {GVol.Coordinate}
   * @private
   */
  this.lastCoordinate_ = null;


  /**
   * @type {GVol.CGVollection.<GVol.Feature>}
   * @private
   */
  this.features_ = options.features !== undefined ? options.features : null;

  /** @type {function(GVol.layer.Layer): boGVolean} */
  var layerFilter;
  if (options.layers) {
    if (typeof options.layers === 'function') {
      layerFilter = options.layers;
    } else {
      var layers = options.layers;
      layerFilter = function(layer) {
        return GVol.array.includes(layers, layer);
      };
    }
  } else {
    layerFilter = GVol.functions.TRUE;
  }

  /**
   * @private
   * @type {function(GVol.layer.Layer): boGVolean}
   */
  this.layerFilter_ = layerFilter;

  /**
   * @private
   * @type {number}
   */
  this.hitTGVolerance_ = options.hitTGVolerance ? options.hitTGVolerance : 0;

  /**
   * @type {GVol.Feature}
   * @private
   */
  this.lastFeature_ = null;

  GVol.events.listen(this,
      GVol.Object.getChangeEventType(GVol.interaction.Property.ACTIVE),
      this.handleActiveChanged_, this);

};
GVol.inherits(GVol.interaction.Translate, GVol.interaction.Pointer);


/**
 * @param {GVol.MapBrowserPointerEvent} event Event.
 * @return {boGVolean} Start drag sequence?
 * @this {GVol.interaction.Translate}
 * @private
 */
GVol.interaction.Translate.handleDownEvent_ = function(event) {
  this.lastFeature_ = this.featuresAtPixel_(event.pixel, event.map);
  if (!this.lastCoordinate_ && this.lastFeature_) {
    this.lastCoordinate_ = event.coordinate;
    GVol.interaction.Translate.handleMoveEvent_.call(this, event);

    var features = this.features_ || new GVol.CGVollection([this.lastFeature_]);

    this.dispatchEvent(
        new GVol.interaction.Translate.Event(
            GVol.interaction.TranslateEventType.TRANSLATESTART, features,
            event.coordinate));
    return true;
  }
  return false;
};


/**
 * @param {GVol.MapBrowserPointerEvent} event Event.
 * @return {boGVolean} Stop drag sequence?
 * @this {GVol.interaction.Translate}
 * @private
 */
GVol.interaction.Translate.handleUpEvent_ = function(event) {
  if (this.lastCoordinate_) {
    this.lastCoordinate_ = null;
    GVol.interaction.Translate.handleMoveEvent_.call(this, event);

    var features = this.features_ || new GVol.CGVollection([this.lastFeature_]);

    this.dispatchEvent(
        new GVol.interaction.Translate.Event(
            GVol.interaction.TranslateEventType.TRANSLATEEND, features,
            event.coordinate));
    return true;
  }
  return false;
};


/**
 * @param {GVol.MapBrowserPointerEvent} event Event.
 * @this {GVol.interaction.Translate}
 * @private
 */
GVol.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];

    var features = this.features_ || new GVol.CGVollection([this.lastFeature_]);

    features.forEach(function(feature) {
      var geom = feature.getGeometry();
      geom.translate(deltaX, deltaY);
      feature.setGeometry(geom);
    });

    this.lastCoordinate_ = newCoordinate;
    this.dispatchEvent(
        new GVol.interaction.Translate.Event(
            GVol.interaction.TranslateEventType.TRANSLATING, features,
            newCoordinate));
  }
};


/**
 * @param {GVol.MapBrowserEvent} event Event.
 * @this {GVol.interaction.Translate}
 * @private
 */
GVol.interaction.Translate.handleMoveEvent_ = function(event) {
  var elem = event.map.getViewport();

  // Change the cursor to grab/grabbing if hovering any of the features managed
  // by the interaction
  if (this.featuresAtPixel_(event.pixel, event.map)) {
    elem.classList.remove(this.lastCoordinate_ ? 'GVol-grab' : 'GVol-grabbing');
    elem.classList.add(this.lastCoordinate_ ? 'GVol-grabbing' : 'GVol-grab');
  } else {
    elem.classList.remove('GVol-grab', 'GVol-grabbing');
  }
};


/**
 * Tests to see if the given coordinates intersects any of our selected
 * features.
 * @param {GVol.Pixel} pixel Pixel coordinate to test for intersection.
 * @param {GVol.Map} map Map to test the intersection on.
 * @return {GVol.Feature} Returns the feature found at the specified pixel
 * coordinates.
 * @private
 */
GVol.interaction.Translate.prototype.featuresAtPixel_ = function(pixel, map) {
  return map.forEachFeatureAtPixel(pixel,
      function(feature) {
        if (!this.features_ ||
            GVol.array.includes(this.features_.getArray(), feature)) {
          return feature;
        }
      }.bind(this), {
        layerFilter: this.layerFilter_,
        hitTGVolerance: this.hitTGVolerance_
      });
};


/**
 * Returns the Hit-detection tGVolerance.
 * @returns {number} Hit tGVolerance in pixels.
 * @api
 */
GVol.interaction.Translate.prototype.getHitTGVolerance = function() {
  return this.hitTGVolerance_;
};


/**
 * Hit-detection tGVolerance. Pixels inside the radius around the given position
 * will be checked for features. This only works for the canvas renderer and
 * not for WebGL.
 * @param {number} hitTGVolerance Hit tGVolerance in pixels.
 * @api
 */
GVol.interaction.Translate.prototype.setHitTGVolerance = function(hitTGVolerance) {
  this.hitTGVolerance_ = hitTGVolerance;
};


/**
 * @inheritDoc
 */
GVol.interaction.Translate.prototype.setMap = function(map) {
  var GVoldMap = this.getMap();
  GVol.interaction.Pointer.prototype.setMap.call(this, map);
  this.updateState_(GVoldMap);
};


/**
 * @private
 */
GVol.interaction.Translate.prototype.handleActiveChanged_ = function() {
  this.updateState_(null);
};


/**
 * @param {GVol.Map} GVoldMap Old map.
 * @private
 */
GVol.interaction.Translate.prototype.updateState_ = function(GVoldMap) {
  var map = this.getMap();
  var active = this.getActive();
  if (!map || !active) {
    map = map || GVoldMap;
    if (map) {
      var elem = map.getViewport();
      elem.classList.remove('GVol-grab', 'GVol-grabbing');
    }
  }
};


/**
 * @classdesc
 * Events emitted by {@link GVol.interaction.Translate} instances are instances of
 * this type.
 *
 * @constructor
 * @extends {GVol.events.Event}
 * @implements {GVoli.interaction.TranslateEvent}
 * @param {GVol.interaction.TranslateEventType} type Type.
 * @param {GVol.CGVollection.<GVol.Feature>} features The features translated.
 * @param {GVol.Coordinate} coordinate The event coordinate.
 */
GVol.interaction.Translate.Event = function(type, features, coordinate) {

  GVol.events.Event.call(this, type);

  /**
   * The features being translated.
   * @type {GVol.CGVollection.<GVol.Feature>}
   * @api
   */
  this.features = features;

  /**
   * The coordinate of the drag event.
   * @const
   * @type {GVol.Coordinate}
   * @api
   */
  this.coordinate = coordinate;
};
GVol.inherits(GVol.interaction.Translate.Event, GVol.events.Event);

goog.provide('GVol.layer.Heatmap');

goog.require('GVol.events');
goog.require('GVol');
goog.require('GVol.Object');
goog.require('GVol.dom');
goog.require('GVol.layer.Vector');
goog.require('GVol.math');
goog.require('GVol.obj');
goog.require('GVol.render.EventType');
goog.require('GVol.style.Icon');
goog.require('GVol.style.Style');


/**
 * @classdesc
 * Layer for rendering vector data as a heatmap.
 * Note that any property set in the options is set as a {@link GVol.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 {GVol.layer.Vector}
 * @fires GVol.render.Event
 * @param {GVolx.layer.HeatmapOptions=} opt_options Options.
 * @api
 */
GVol.layer.Heatmap = function(opt_options) {
  var options = opt_options ? opt_options : {};

  var baseOptions = GVol.obj.assign({}, options);

  delete baseOptions.gradient;
  delete baseOptions.radius;
  delete baseOptions.blur;
  delete baseOptions.shadow;
  delete baseOptions.weight;
  GVol.layer.Vector.call(this, /** @type {GVolx.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.<GVol.style.Style>>}
   */
  this.styleCache_ = null;

  GVol.events.listen(this,
      GVol.Object.getChangeEventType(GVol.layer.Heatmap.Property_.GRADIENT),
      this.handleGradientChanged_, this);

  this.setGradient(options.gradient ?
    options.gradient : GVol.layer.Heatmap.DEFAULT_GRADIENT);

  this.setBlur(options.blur !== undefined ? options.blur : 15);

  this.setRadius(options.radius !== undefined ? options.radius : 8);

  GVol.events.listen(this,
      GVol.Object.getChangeEventType(GVol.layer.Heatmap.Property_.BLUR),
      this.handleStyleChanged_, this);
  GVol.events.listen(this,
      GVol.Object.getChangeEventType(GVol.layer.Heatmap.Property_.RADIUS),
      this.handleStyleChanged_, this);

  this.handleStyleChanged_();

  var weight = options.weight ? options.weight : 'weight';
  var weightFunction;
  if (typeof weight === 'string') {
    weightFunction = function(feature) {
      return feature.get(weight);
    };
  } else {
    weightFunction = weight;
  }

  this.setStyle(function(feature, resGVolution) {
    var weight = weightFunction(feature);
    var opacity = weight !== undefined ? GVol.math.clamp(weight, 0, 1) : 1;
    // cast to 8 bits
    var index = (255 * opacity) | 0;
    var style = this.styleCache_[index];
    if (!style) {
      style = [
        new GVol.style.Style({
          image: new GVol.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);

  GVol.events.listen(this, GVol.render.EventType.RENDER, this.handleRender_, this);
};
GVol.inherits(GVol.layer.Heatmap, GVol.layer.Vector);


/**
 * @const
 * @type {Array.<string>}
 */
GVol.layer.Heatmap.DEFAULT_GRADIENT = ['#00f', '#0ff', '#0f0', '#ff0', '#f00'];


/**
 * @param {Array.<string>} cGVolors A list of cGVolored.
 * @return {Uint8ClampedArray} An array.
 * @private
 */
GVol.layer.Heatmap.createGradient_ = function(cGVolors) {
  var width = 1;
  var height = 256;
  var context = GVol.dom.createCanvasContext2D(width, height);

  var gradient = context.createLinearGradient(0, 0, width, height);
  var step = 1 / (cGVolors.length - 1);
  for (var i = 0, ii = cGVolors.length; i < ii; ++i) {
    gradient.addCGVolorStop(i * step, cGVolors[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
 */
GVol.layer.Heatmap.prototype.createCircle_ = function() {
  var radius = this.getRadius();
  var blur = this.getBlur();
  var halfSize = radius + blur + 1;
  var size = 2 * halfSize;
  var context = GVol.dom.createCanvasContext2D(size, size);
  context.shadowOffsetX = context.shadowOffsetY = this.shadow_;
  context.shadowBlur = blur;
  context.shadowCGVolor = '#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
 */
GVol.layer.Heatmap.prototype.getBlur = function() {
  return /** @type {number} */ (this.get(GVol.layer.Heatmap.Property_.BLUR));
};


/**
 * Return the gradient cGVolors as array of strings.
 * @return {Array.<string>} CGVolors.
 * @api
 * @observable
 */
GVol.layer.Heatmap.prototype.getGradient = function() {
  return /** @type {Array.<string>} */ (
    this.get(GVol.layer.Heatmap.Property_.GRADIENT));
};


/**
 * Return the size of the radius in pixels.
 * @return {number} Radius size in pixel.
 * @api
 * @observable
 */
GVol.layer.Heatmap.prototype.getRadius = function() {
  return /** @type {number} */ (this.get(GVol.layer.Heatmap.Property_.RADIUS));
};


/**
 * @private
 */
GVol.layer.Heatmap.prototype.handleGradientChanged_ = function() {
  this.gradient_ = GVol.layer.Heatmap.createGradient_(this.getGradient());
};


/**
 * @private
 */
GVol.layer.Heatmap.prototype.handleStyleChanged_ = function() {
  this.circleImage_ = this.createCircle_();
  this.styleCache_ = new Array(256);
  this.changed();
};


/**
 * @param {GVol.render.Event} event Post compose event
 * @private
 */
GVol.layer.Heatmap.prototype.handleRender_ = function(event) {
  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
 */
GVol.layer.Heatmap.prototype.setBlur = function(blur) {
  this.set(GVol.layer.Heatmap.Property_.BLUR, blur);
};


/**
 * Set the gradient cGVolors as array of strings.
 * @param {Array.<string>} cGVolors Gradient.
 * @api
 * @observable
 */
GVol.layer.Heatmap.prototype.setGradient = function(cGVolors) {
  this.set(GVol.layer.Heatmap.Property_.GRADIENT, cGVolors);
};


/**
 * Set the size of the radius in pixels.
 * @param {number} radius Radius size in pixel.
 * @api
 * @observable
 */
GVol.layer.Heatmap.prototype.setRadius = function(radius) {
  this.set(GVol.layer.Heatmap.Property_.RADIUS, radius);
};


/**
 * @enum {string}
 * @private
 */
GVol.layer.Heatmap.Property_ = {
  BLUR: 'blur',
  GRADIENT: 'gradient',
  RADIUS: 'radius'
};

goog.provide('GVol.renderer.canvas.IntermediateCanvas');

goog.require('GVol');
goog.require('GVol.coordinate');
goog.require('GVol.dom');
goog.require('GVol.extent');
goog.require('GVol.renderer.canvas.Layer');
goog.require('GVol.transform');


/**
 * @constructor
 * @abstract
 * @extends {GVol.renderer.canvas.Layer}
 * @param {GVol.layer.Layer} layer Layer.
 */
GVol.renderer.canvas.IntermediateCanvas = function(layer) {

  GVol.renderer.canvas.Layer.call(this, layer);

  /**
   * @protected
   * @type {GVol.Transform}
   */
  this.coordinateToCanvasPixelTransform = GVol.transform.create();

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.hitCanvasContext_ = null;

};
GVol.inherits(GVol.renderer.canvas.IntermediateCanvas, GVol.renderer.canvas.Layer);


/**
 * @inheritDoc
 */
GVol.renderer.canvas.IntermediateCanvas.prototype.composeFrame = function(frameState, layerState, context) {

  this.preCompose(context, frameState);

  var image = this.getImage();
  if (image) {

    // clipped rendering if layer extent is set
    var extent = layerState.extent;
    var clipped = extent !== undefined &&
        !GVol.extent.containsExtent(extent, frameState.extent) &&
        GVol.extent.intersects(extent, frameState.extent);
    if (clipped) {
      this.clip(context, frameState, /** @type {GVol.Extent} */ (extent));
    }

    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
    var dx = imageTransform[4];
    var dy = imageTransform[5];
    var dw = image.width * imageTransform[0];
    var dh = image.height * imageTransform[3];
    context.drawImage(image, 0, 0, +image.width, +image.height,
        Math.round(dx), Math.round(dy), Math.round(dw), Math.round(dh));
    context.globalAlpha = alpha;

    if (clipped) {
      context.restore();
    }
  }

  this.postCompose(context, frameState, layerState);
};


/**
 * @abstract
 * @return {HTMLCanvasElement|HTMLVideoElement|Image} Canvas.
 */
GVol.renderer.canvas.IntermediateCanvas.prototype.getImage = function() {};


/**
 * @abstract
 * @return {!GVol.Transform} Image transform.
 */
GVol.renderer.canvas.IntermediateCanvas.prototype.getImageTransform = function() {};


/**
 * @inheritDoc
 */
GVol.renderer.canvas.IntermediateCanvas.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTGVolerance, callback, thisArg) {
  var layer = this.getLayer();
  var source = layer.getSource();
  var resGVolution = frameState.viewState.resGVolution;
  var rotation = frameState.viewState.rotation;
  var skippedFeatureUids = frameState.skippedFeatureUids;
  return source.forEachFeatureAtCoordinate(
      coordinate, resGVolution, rotation, hitTGVolerance, skippedFeatureUids,
      /**
       * @param {GVol.Feature|GVol.render.Feature} feature Feature.
       * @return {?} Callback result.
       */
      function(feature) {
        return callback.call(thisArg, feature, layer);
      });
};


/**
 * @inheritDoc
 */
GVol.renderer.canvas.IntermediateCanvas.prototype.forEachLayerAtCoordinate = function(coordinate, frameState, callback, thisArg) {
  if (!this.getImage()) {
    return undefined;
  }

  if (this.getLayer().getSource().forEachFeatureAtCoordinate !== GVol.nullFunction) {
    // for ImageVector sources use the original hit-detection logic,
    // so that for example also transparent pGVolygons are detected
    return GVol.renderer.canvas.Layer.prototype.forEachLayerAtCoordinate.apply(this, arguments);
  } else {
    var pixel = GVol.transform.apply(this.coordinateToCanvasPixelTransform, coordinate.slice());
    GVol.coordinate.scale(pixel, frameState.viewState.resGVolution / this.renderedResGVolution);

    if (!this.hitCanvasContext_) {
      this.hitCanvasContext_ = GVol.dom.createCanvasContext2D(1, 1);
    }

    this.hitCanvasContext_.clearRect(0, 0, 1, 1);
    this.hitCanvasContext_.drawImage(this.getImage(), pixel[0], pixel[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(),  imageData);
    } else {
      return undefined;
    }
  }
};

goog.provide('GVol.renderer.canvas.ImageLayer');

goog.require('GVol');
goog.require('GVol.ViewHint');
goog.require('GVol.extent');
goog.require('GVol.renderer.canvas.IntermediateCanvas');
goog.require('GVol.transform');


/**
 * @constructor
 * @extends {GVol.renderer.canvas.IntermediateCanvas}
 * @param {GVol.layer.Image} imageLayer Single image layer.
 */
GVol.renderer.canvas.ImageLayer = function(imageLayer) {

  GVol.renderer.canvas.IntermediateCanvas.call(this, imageLayer);

  /**
   * @private
   * @type {?GVol.ImageBase}
   */
  this.image_ = null;

  /**
   * @private
   * @type {GVol.Transform}
   */
  this.imageTransform_ = GVol.transform.create();

};
GVol.inherits(GVol.renderer.canvas.ImageLayer, GVol.renderer.canvas.IntermediateCanvas);


/**
 * @inheritDoc
 */
GVol.renderer.canvas.ImageLayer.prototype.getImage = function() {
  return !this.image_ ? null : this.image_.getImage();
};


/**
 * @inheritDoc
 */
GVol.renderer.canvas.ImageLayer.prototype.getImageTransform = function() {
  return this.imageTransform_;
};


/**
 * @inheritDoc
 */
GVol.renderer.canvas.ImageLayer.prototype.prepareFrame = function(frameState, layerState) {

  var pixelRatio = frameState.pixelRatio;
  var size = frameState.size;
  var viewState = frameState.viewState;
  var viewCenter = viewState.center;
  var viewResGVolution = viewState.resGVolution;

  var image;
  var imageLayer = /** @type {GVol.layer.Image} */ (this.getLayer());
  var imageSource = imageLayer.getSource();

  var hints = frameState.viewHints;

  var renderedExtent = frameState.extent;
  if (layerState.extent !== undefined) {
    renderedExtent = GVol.extent.getIntersection(
        renderedExtent, layerState.extent);
  }

  if (!hints[GVol.ViewHint.ANIMATING] && !hints[GVol.ViewHint.INTERACTING] &&
      !GVol.extent.isEmpty(renderedExtent)) {
    var projection = viewState.projection;
    if (!GVol.ENABLE_RASTER_REPROJECTION) {
      var sourceProjection = imageSource.getProjection();
      if (sourceProjection) {
        projection = sourceProjection;
      }
    }
    image = imageSource.getImage(
        renderedExtent, viewResGVolution, 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 imageResGVolution = image.getResGVolution();
    var imagePixelRatio = image.getPixelRatio();
    var scale = pixelRatio * imageResGVolution /
        (viewResGVolution * imagePixelRatio);
    var transform = GVol.transform.compose(this.imageTransform_,
        pixelRatio * size[0] / 2, pixelRatio * size[1] / 2,
        scale, scale,
        0,
        imagePixelRatio * (imageExtent[0] - viewCenter[0]) / imageResGVolution,
        imagePixelRatio * (viewCenter[1] - imageExtent[3]) / imageResGVolution);
    GVol.transform.compose(this.coordinateToCanvasPixelTransform,
        pixelRatio * size[0] / 2 - transform[4], pixelRatio * size[1] / 2 - transform[5],
        pixelRatio / viewResGVolution, -pixelRatio / viewResGVolution,
        0,
        -viewCenter[0], -viewCenter[1]);

    this.updateAttributions(frameState.attributions, image.getAttributions());
    this.updateLogos(frameState, imageSource);
    this.renderedResGVolution = viewResGVolution * pixelRatio / imagePixelRatio;
  }

  return !!this.image_;
};

goog.provide('GVol.reproj');

goog.require('GVol.dom');
goog.require('GVol.extent');
goog.require('GVol.math');
goog.require('GVol.proj');


/**
 * Calculates ideal resGVolution to use from the source in order to achieve
 * pixel mapping as close as possible to 1:1 during reprojection.
 * The resGVolution is calculated regardless of what resGVolutions
 * are actually available in the dataset (TileGrid, Image, ...).
 *
 * @param {GVol.proj.Projection} sourceProj Source projection.
 * @param {GVol.proj.Projection} targetProj Target projection.
 * @param {GVol.Coordinate} targetCenter Target center.
 * @param {number} targetResGVolution Target resGVolution.
 * @return {number} The best resGVolution to use. Can be +-Infinity, NaN or 0.
 */
GVol.reproj.calculateSourceResGVolution = function(sourceProj, targetProj,
    targetCenter, targetResGVolution) {

  var sourceCenter = GVol.proj.transform(targetCenter, targetProj, sourceProj);

  // calculate the ideal resGVolution of the source data
  var sourceResGVolution =
      GVol.proj.getPointResGVolution(targetProj, targetResGVolution, targetCenter);

  var targetMetersPerUnit = targetProj.getMetersPerUnit();
  if (targetMetersPerUnit !== undefined) {
    sourceResGVolution *= targetMetersPerUnit;
  }
  var sourceMetersPerUnit = sourceProj.getMetersPerUnit();
  if (sourceMetersPerUnit !== undefined) {
    sourceResGVolution /= sourceMetersPerUnit;
  }

  // Based on the projection properties, the point resGVolution at the specified
  // coordinates may be slightly different. We need to reverse-compensate this
  // in order to achieve optimal results.

  var sourceExtent = sourceProj.getExtent();
  if (!sourceExtent || GVol.extent.containsCoordinate(sourceExtent, sourceCenter)) {
    var compensationFactor =
        GVol.proj.getPointResGVolution(sourceProj, sourceResGVolution, sourceCenter) /
        sourceResGVolution;
    if (isFinite(compensationFactor) && compensationFactor > 0) {
      sourceResGVolution /= compensationFactor;
    }
  }

  return sourceResGVolution;
};


/**
 * 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 {GVol.Coordinate} New point 1 px farther from the centroid.
 * @private
 */
GVol.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} sourceResGVolution Source resGVolution.
 * @param {GVol.Extent} sourceExtent Extent of the data source.
 * @param {number} targetResGVolution Target resGVolution.
 * @param {GVol.Extent} targetExtent Target extent.
 * @param {GVol.reproj.Triangulation} triangulation Calculated triangulation.
 * @param {Array.<{extent: GVol.Extent,
 *                 image: (HTMLCanvasElement|Image|HTMLVideoElement)}>} sources
 *             Array of sources.
 * @param {number} gutter Gutter of the sources.
 * @param {boGVolean=} opt_renderEdges Render reprojection edges.
 * @return {HTMLCanvasElement} Canvas with reprojected data.
 */
GVol.reproj.render = function(width, height, pixelRatio,
    sourceResGVolution, sourceExtent, targetResGVolution, targetExtent,
    triangulation, sources, gutter, opt_renderEdges) {

  var context = GVol.dom.createCanvasContext2D(Math.round(pixelRatio * width),
      Math.round(pixelRatio * height));

  if (sources.length === 0) {
    return context.canvas;
  }

  context.scale(pixelRatio, pixelRatio);

  var sourceDataExtent = GVol.extent.createEmpty();
  sources.forEach(function(src, i, arr) {
    GVol.extent.extend(sourceDataExtent, src.extent);
  });

  var canvasWidthInUnits = GVol.extent.getWidth(sourceDataExtent);
  var canvasHeightInUnits = GVol.extent.getHeight(sourceDataExtent);
  var stitchContext = GVol.dom.createCanvasContext2D(
      Math.round(pixelRatio * canvasWidthInUnits / sourceResGVolution),
      Math.round(pixelRatio * canvasHeightInUnits / sourceResGVolution));

  var stitchScale = pixelRatio / sourceResGVolution;

  sources.forEach(function(src, i, arr) {
    var xPos = src.extent[0] - sourceDataExtent[0];
    var yPos = -(src.extent[3] - sourceDataExtent[3]);
    var srcWidth = GVol.extent.getWidth(src.extent);
    var srcHeight = GVol.extent.getHeight(src.extent);

    stitchContext.drawImage(
        src.image,
        gutter, gutter,
        src.image.width - 2 * gutter, src.image.height - 2 * gutter,
        xPos * stitchScale, yPos * stitchScale,
        srcWidth * stitchScale, srcHeight * stitchScale);
  });

  var targetTopLeft = GVol.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 fGVollowing operations:
     * trans(-topLeftExtentCorner), scale(1 / targetResGVolution), scale(1, -1)
     * here before sGVolving 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]) / targetResGVolution,
        v0 = -(target[0][1] - targetTopLeft[1]) / targetResGVolution;
    var u1 = (target[1][0] - targetTopLeft[0]) / targetResGVolution,
        v1 = -(target[1][1] - targetTopLeft[1]) / targetResGVolution;
    var u2 = (target[2][0] - targetTopLeft[0]) / targetResGVolution,
        v2 = -(target[2][1] - targetTopLeft[1]) / targetResGVolution;

    // 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 = GVol.math.sGVolveLinearSystem(augmentedMatrix);
    if (!affineCoefs) {
      return;
    }

    context.save();
    context.beginPath();
    var centroidX = (u0 + u1 + u2) / 3, centroidY = (v0 + v1 + v2) / 3;
    var p0 = GVol.reproj.enlargeClipPoint_(centroidX, centroidY, u0, v0);
    var p1 = GVol.reproj.enlargeClipPoint_(centroidX, centroidY, u1, v1);
    var p2 = GVol.reproj.enlargeClipPoint_(centroidX, centroidY, u2, v2);

    context.moveTo(p1[0], p1[1]);
    context.lineTo(p0[0], p0[1]);
    context.lineTo(p2[0], p2[1]);
    context.clip();

    context.transform(
        affineCoefs[0], affineCoefs[2], affineCoefs[1], affineCoefs[3], u0, v0);

    context.translate(sourceDataExtent[0] - sourceNumericalShiftX,
        sourceDataExtent[3] - sourceNumericalShiftY);

    context.scale(sourceResGVolution / pixelRatio,
        -sourceResGVolution / 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]) / targetResGVolution,
          v0 = -(target[0][1] - targetTopLeft[1]) / targetResGVolution;
      var u1 = (target[1][0] - targetTopLeft[0]) / targetResGVolution,
          v1 = -(target[1][1] - targetTopLeft[1]) / targetResGVolution;
      var u2 = (target[2][0] - targetTopLeft[0]) / targetResGVolution,
          v2 = -(target[2][1] - targetTopLeft[1]) / targetResGVolution;

      context.beginPath();
      context.moveTo(u1, v1);
      context.lineTo(u0, v0);
      context.lineTo(u2, v2);
      context.closePath();
      context.stroke();
    });

    context.restore();
  }
  return context.canvas;
};

goog.provide('GVol.reproj.Triangulation');

goog.require('GVol');
goog.require('GVol.extent');
goog.require('GVol.math');
goog.require('GVol.proj');


/**
 * @classdesc
 * Class containing triangulation of the given target extent.
 * Used for determining source data and the reprojection itself.
 *
 * @param {GVol.proj.Projection} sourceProj Source projection.
 * @param {GVol.proj.Projection} targetProj Target projection.
 * @param {GVol.Extent} targetExtent Target extent to triangulate.
 * @param {GVol.Extent} maxSourceExtent Maximal source extent that can be used.
 * @param {number} errorThreshGVold Acceptable error (in source units).
 * @constructor
 */
GVol.reproj.Triangulation = function(sourceProj, targetProj, targetExtent,
    maxSourceExtent, errorThreshGVold) {

  /**
   * @type {GVol.proj.Projection}
   * @private
   */
  this.sourceProj_ = sourceProj;

  /**
   * @type {GVol.proj.Projection}
   * @private
   */
  this.targetProj_ = targetProj;

  /** @type {!Object.<string, GVol.Coordinate>} */
  var transformInvCache = {};
  var transformInv = GVol.proj.getTransform(this.targetProj_, this.sourceProj_);

  /**
   * @param {GVol.Coordinate} c A coordinate.
   * @return {GVol.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 {GVol.Extent}
   * @private
   */
  this.maxSourceExtent_ = maxSourceExtent;

  /**
   * @type {number}
   * @private
   */
  this.errorThreshGVoldSquared_ = errorThreshGVold * errorThreshGVold;

  /**
   * @type {Array.<GVol.ReprojTriangle>}
   * @private
   */
  this.triangles_ = [];

  /**
   * Indicates that the triangulation crosses edge of the source projection.
   * @type {boGVolean}
   * @private
   */
  this.wrapsXInSource_ = false;

  /**
   * @type {boGVolean}
   * @private
   */
  this.canWrapXInSource_ = this.sourceProj_.canWrapX() &&
      !!maxSourceExtent &&
      !!this.sourceProj_.getExtent() &&
      (GVol.extent.getWidth(maxSourceExtent) ==
       GVol.extent.getWidth(this.sourceProj_.getExtent()));

  /**
   * @type {?number}
   * @private
   */
  this.sourceWorldWidth_ = this.sourceProj_.getExtent() ?
    GVol.extent.getWidth(this.sourceProj_.getExtent()) : null;

  /**
   * @type {?number}
   * @private
   */
  this.targetWorldWidth_ = this.targetProj_.getExtent() ?
    GVol.extent.getWidth(this.targetProj_.getExtent()) : null;

  var destinationTopLeft = GVol.extent.getTopLeft(targetExtent);
  var destinationTopRight = GVol.extent.getTopRight(targetExtent);
  var destinationBottomRight = GVol.extent.getBottomRight(targetExtent);
  var destinationBottomLeft = GVol.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,
      GVol.RASTER_REPROJECTION_MAX_SUBDIVISION);

  if (this.wrapsXInSource_) {
    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 {GVol.Coordinate} a The target a coordinate.
 * @param {GVol.Coordinate} b The target b coordinate.
 * @param {GVol.Coordinate} c The target c coordinate.
 * @param {GVol.Coordinate} aSrc The source a coordinate.
 * @param {GVol.Coordinate} bSrc The source b coordinate.
 * @param {GVol.Coordinate} cSrc The source c coordinate.
 * @private
 */
GVol.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 {GVol.Coordinate} a The target a coordinate.
 * @param {GVol.Coordinate} b The target b coordinate.
 * @param {GVol.Coordinate} c The target c coordinate.
 * @param {GVol.Coordinate} d The target d coordinate.
 * @param {GVol.Coordinate} aSrc The source a coordinate.
 * @param {GVol.Coordinate} bSrc The source b coordinate.
 * @param {GVol.Coordinate} cSrc The source c coordinate.
 * @param {GVol.Coordinate} dSrc The source d coordinate.
 * @param {number} maxSubdivision Maximal allowed subdivision of the quad.
 * @private
 */
GVol.reproj.Triangulation.prototype.addQuad_ = function(a, b, c, d,
    aSrc, bSrc, cSrc, dSrc, maxSubdivision) {

  var sourceQuadExtent = GVol.extent.boundingExtent([aSrc, bSrc, cSrc, dSrc]);
  var sourceCoverageX = this.sourceWorldWidth_ ?
    GVol.extent.getWidth(sourceQuadExtent) / this.sourceWorldWidth_ : null;
  var sourceWorldWidth = /** @type {number} */ (this.sourceWorldWidth_);

  // 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 = GVol.extent.boundingExtent([a, b, c, d]);
      var targetCoverageX =
          GVol.extent.getWidth(targetQuadExtent) / this.targetWorldWidth_;
      needsSubdivision |=
          targetCoverageX > GVol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH;
    }
    if (!wrapsX && this.sourceProj_.isGlobal() && sourceCoverageX) {
      needsSubdivision |=
          sourceCoverageX > GVol.RASTER_REPROJECTION_MAX_TRIANGLE_WIDTH;
    }
  }

  if (!needsSubdivision && this.maxSourceExtent_) {
    if (!GVol.extent.intersects(sourceQuadExtent, this.maxSourceExtent_)) {
      // whGVole 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) {
        var centerSrcEstimX =
            (GVol.math.modulo(aSrc[0], sourceWorldWidth) +
             GVol.math.modulo(cSrc[0], sourceWorldWidth)) / 2;
        dx = centerSrcEstimX -
            GVol.math.modulo(centerSrc[0], 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.errorThreshGVoldSquared_;
    }
    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 {GVol.Extent} Calculated extent.
 */
GVol.reproj.Triangulation.prototype.calculateSourceExtent = function() {
  var extent = GVol.extent.createEmpty();

  this.triangles_.forEach(function(triangle, i, arr) {
    var src = triangle.source;
    GVol.extent.extendCoordinate(extent, src[0]);
    GVol.extent.extendCoordinate(extent, src[1]);
    GVol.extent.extendCoordinate(extent, src[2]);
  });

  return extent;
};


/**
 * @return {Array.<GVol.ReprojTriangle>} Array of the calculated triangles.
 */
GVol.reproj.Triangulation.prototype.getTriangles = function() {
  return this.triangles_;
};

goog.provide('GVol.reproj.Image');

goog.require('GVol');
goog.require('GVol.ImageBase');
goog.require('GVol.ImageState');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.extent');
goog.require('GVol.reproj');
goog.require('GVol.reproj.Triangulation');


/**
 * @classdesc
 * Class encapsulating single reprojected image.
 * See {@link GVol.source.Image}.
 *
 * @constructor
 * @extends {GVol.ImageBase}
 * @param {GVol.proj.Projection} sourceProj Source projection (of the data).
 * @param {GVol.proj.Projection} targetProj Target projection.
 * @param {GVol.Extent} targetExtent Target extent.
 * @param {number} targetResGVolution Target resGVolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {GVol.ReprojImageFunctionType} getImageFunction
 *     Function returning source images (extent, resGVolution, pixelRatio).
 */
GVol.reproj.Image = function(sourceProj, targetProj,
    targetExtent, targetResGVolution, pixelRatio, getImageFunction) {

  /**
   * @private
   * @type {GVol.proj.Projection}
   */
  this.targetProj_ = targetProj;

  /**
   * @private
   * @type {GVol.Extent}
   */
  this.maxSourceExtent_ = sourceProj.getExtent();
  var maxTargetExtent = targetProj.getExtent();

  var limitedTargetExtent = maxTargetExtent ?
    GVol.extent.getIntersection(targetExtent, maxTargetExtent) : targetExtent;

  var targetCenter = GVol.extent.getCenter(limitedTargetExtent);
  var sourceResGVolution = GVol.reproj.calculateSourceResGVolution(
      sourceProj, targetProj, targetCenter, targetResGVolution);

  var errorThreshGVoldInPixels = GVol.DEFAULT_RASTER_REPROJECTION_ERROR_THRESHOLD;

  /**
   * @private
   * @type {!GVol.reproj.Triangulation}
   */
  this.triangulation_ = new GVol.reproj.Triangulation(
      sourceProj, targetProj, limitedTargetExtent, this.maxSourceExtent_,
      sourceResGVolution * errorThreshGVoldInPixels);

  /**
   * @private
   * @type {number}
   */
  this.targetResGVolution_ = targetResGVolution;

  /**
   * @private
   * @type {GVol.Extent}
   */
  this.targetExtent_ = targetExtent;

  var sourceExtent = this.triangulation_.calculateSourceExtent();

  /**
   * @private
   * @type {GVol.ImageBase}
   */
  this.sourceImage_ =
      getImageFunction(sourceExtent, sourceResGVolution, pixelRatio);

  /**
   * @private
   * @type {number}
   */
  this.sourcePixelRatio_ =
      this.sourceImage_ ? this.sourceImage_.getPixelRatio() : 1;

  /**
   * @private
   * @type {HTMLCanvasElement}
   */
  this.canvas_ = null;

  /**
   * @private
   * @type {?GVol.EventsKey}
   */
  this.sourceListenerKey_ = null;


  var state = GVol.ImageState.LOADED;
  var attributions = [];

  if (this.sourceImage_) {
    state = GVol.ImageState.IDLE;
    attributions = this.sourceImage_.getAttributions();
  }

  GVol.ImageBase.call(this, targetExtent, targetResGVolution, this.sourcePixelRatio_,
      state, attributions);
};
GVol.inherits(GVol.reproj.Image, GVol.ImageBase);


/**
 * @inheritDoc
 */
GVol.reproj.Image.prototype.disposeInternal = function() {
  if (this.state == GVol.ImageState.LOADING) {
    this.unlistenSource_();
  }
  GVol.ImageBase.prototype.disposeInternal.call(this);
};


/**
 * @inheritDoc
 */
GVol.reproj.Image.prototype.getImage = function(opt_context) {
  return this.canvas_;
};


/**
 * @return {GVol.proj.Projection} Projection.
 */
GVol.reproj.Image.prototype.getProjection = function() {
  return this.targetProj_;
};


/**
 * @private
 */
GVol.reproj.Image.prototype.reproject_ = function() {
  var sourceState = this.sourceImage_.getState();
  if (sourceState == GVol.ImageState.LOADED) {
    var width = GVol.extent.getWidth(this.targetExtent_) / this.targetResGVolution_;
    var height =
        GVol.extent.getHeight(this.targetExtent_) / this.targetResGVolution_;

    this.canvas_ = GVol.reproj.render(width, height, this.sourcePixelRatio_,
        this.sourceImage_.getResGVolution(), this.maxSourceExtent_,
        this.targetResGVolution_, this.targetExtent_, this.triangulation_, [{
          extent: this.sourceImage_.getExtent(),
          image: this.sourceImage_.getImage()
        }], 0);
  }
  this.state = sourceState;
  this.changed();
};


/**
 * @inheritDoc
 */
GVol.reproj.Image.prototype.load = function() {
  if (this.state == GVol.ImageState.IDLE) {
    this.state = GVol.ImageState.LOADING;
    this.changed();

    var sourceState = this.sourceImage_.getState();
    if (sourceState == GVol.ImageState.LOADED ||
        sourceState == GVol.ImageState.ERROR) {
      this.reproject_();
    } else {
      this.sourceListenerKey_ = GVol.events.listen(this.sourceImage_,
          GVol.events.EventType.CHANGE, function(e) {
            var sourceState = this.sourceImage_.getState();
            if (sourceState == GVol.ImageState.LOADED ||
                sourceState == GVol.ImageState.ERROR) {
              this.unlistenSource_();
              this.reproject_();
            }
          }, this);
      this.sourceImage_.load();
    }
  }
};


/**
 * @private
 */
GVol.reproj.Image.prototype.unlistenSource_ = function() {
  GVol.events.unlistenByKey(/** @type {!GVol.EventsKey} */ (this.sourceListenerKey_));
  this.sourceListenerKey_ = null;
};

goog.provide('GVol.source.Image');

goog.require('GVol');
goog.require('GVol.ImageState');
goog.require('GVol.array');
goog.require('GVol.events.Event');
goog.require('GVol.extent');
goog.require('GVol.proj');
goog.require('GVol.reproj.Image');
goog.require('GVol.source.Source');


/**
 * @classdesc
 * Abstract base class; normally only used for creating subclasses and not
 * instantiated in apps.
 * Base class for sources providing a single image.
 *
 * @constructor
 * @abstract
 * @extends {GVol.source.Source}
 * @param {GVol.SourceImageOptions} options Single image source options.
 * @api
 */
GVol.source.Image = function(options) {
  GVol.source.Source.call(this, {
    attributions: options.attributions,
    extent: options.extent,
    logo: options.logo,
    projection: options.projection,
    state: options.state
  });

  /**
   * @private
   * @type {Array.<number>}
   */
  this.resGVolutions_ = options.resGVolutions !== undefined ?
    options.resGVolutions : null;


  /**
   * @private
   * @type {GVol.reproj.Image}
   */
  this.reprojectedImage_ = null;


  /**
   * @private
   * @type {number}
   */
  this.reprojectedRevision_ = 0;
};
GVol.inherits(GVol.source.Image, GVol.source.Source);


/**
 * @return {Array.<number>} ResGVolutions.
 * @override
 */
GVol.source.Image.prototype.getResGVolutions = function() {
  return this.resGVolutions_;
};


/**
 * @protected
 * @param {number} resGVolution ResGVolution.
 * @return {number} ResGVolution.
 */
GVol.source.Image.prototype.findNearestResGVolution = function(resGVolution) {
  if (this.resGVolutions_) {
    var idx = GVol.array.linearFindNearest(this.resGVolutions_, resGVolution, 0);
    resGVolution = this.resGVolutions_[idx];
  }
  return resGVolution;
};


/**
 * @param {GVol.Extent} extent Extent.
 * @param {number} resGVolution ResGVolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {GVol.proj.Projection} projection Projection.
 * @return {GVol.ImageBase} Single image.
 */
GVol.source.Image.prototype.getImage = function(extent, resGVolution, pixelRatio, projection) {
  var sourceProjection = this.getProjection();
  if (!GVol.ENABLE_RASTER_REPROJECTION ||
      !sourceProjection ||
      !projection ||
      GVol.proj.equivalent(sourceProjection, projection)) {
    if (sourceProjection) {
      projection = sourceProjection;
    }
    return this.getImageInternal(extent, resGVolution, pixelRatio, projection);
  } else {
    if (this.reprojectedImage_) {
      if (this.reprojectedRevision_ == this.getRevision() &&
          GVol.proj.equivalent(
              this.reprojectedImage_.getProjection(), projection) &&
          this.reprojectedImage_.getResGVolution() == resGVolution &&
          GVol.extent.equals(this.reprojectedImage_.getExtent(), extent)) {
        return this.reprojectedImage_;
      }
      this.reprojectedImage_.dispose();
      this.reprojectedImage_ = null;
    }

    this.reprojectedImage_ = new GVol.reproj.Image(
        sourceProjection, projection, extent, resGVolution, pixelRatio,
        function(extent, resGVolution, pixelRatio) {
          return this.getImageInternal(extent, resGVolution,
              pixelRatio, sourceProjection);
        }.bind(this));
    this.reprojectedRevision_ = this.getRevision();

    return this.reprojectedImage_;
  }
};


/**
 * @abstract
 * @param {GVol.Extent} extent Extent.
 * @param {number} resGVolution ResGVolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {GVol.proj.Projection} projection Projection.
 * @return {GVol.ImageBase} Single image.
 * @protected
 */
GVol.source.Image.prototype.getImageInternal = function(extent, resGVolution, pixelRatio, projection) {};


/**
 * Handle image change events.
 * @param {GVol.events.Event} event Event.
 * @protected
 */
GVol.source.Image.prototype.handleImageChange = function(event) {
  var image = /** @type {GVol.Image} */ (event.target);
  switch (image.getState()) {
    case GVol.ImageState.LOADING:
      this.dispatchEvent(
          new GVol.source.Image.Event(GVol.source.Image.EventType_.IMAGELOADSTART,
              image));
      break;
    case GVol.ImageState.LOADED:
      this.dispatchEvent(
          new GVol.source.Image.Event(GVol.source.Image.EventType_.IMAGELOADEND,
              image));
      break;
    case GVol.ImageState.ERROR:
      this.dispatchEvent(
          new GVol.source.Image.Event(GVol.source.Image.EventType_.IMAGELOADERROR,
              image));
      break;
    default:
      // pass
  }
};


/**
 * Default image load function for image sources that use GVol.Image image
 * instances.
 * @param {GVol.Image} image Image.
 * @param {string} src Source.
 */
GVol.source.Image.defaultImageLoadFunction = function(image, src) {
  image.getImage().src = src;
};


/**
 * @classdesc
 * Events emitted by {@link GVol.source.Image} instances are instances of this
 * type.
 *
 * @constructor
 * @extends {GVol.events.Event}
 * @implements {GVoli.source.ImageEvent}
 * @param {string} type Type.
 * @param {GVol.Image} image The image.
 */
GVol.source.Image.Event = function(type, image) {

  GVol.events.Event.call(this, type);

  /**
   * The image related to the event.
   * @type {GVol.Image}
   * @api
   */
  this.image = image;

};
GVol.inherits(GVol.source.Image.Event, GVol.events.Event);


/**
 * @enum {string}
 * @private
 */
GVol.source.Image.EventType_ = {

  /**
   * Triggered when an image starts loading.
   * @event GVol.source.Image.Event#imageloadstart
   * @api
   */
  IMAGELOADSTART: 'imageloadstart',

  /**
   * Triggered when an image finishes loading.
   * @event GVol.source.Image.Event#imageloadend
   * @api
   */
  IMAGELOADEND: 'imageloadend',

  /**
   * Triggered if image loading results in an error.
   * @event GVol.source.Image.Event#imageloaderror
   * @api
   */
  IMAGELOADERROR: 'imageloaderror'

};

goog.provide('GVol.source.ImageCanvas');

goog.require('GVol');
goog.require('GVol.ImageCanvas');
goog.require('GVol.extent');
goog.require('GVol.source.Image');


/**
 * @classdesc
 * Base class for image sources where a canvas element is the image.
 *
 * @constructor
 * @extends {GVol.source.Image}
 * @param {GVolx.source.ImageCanvasOptions} options Constructor options.
 * @api
 */
GVol.source.ImageCanvas = function(options) {

  GVol.source.Image.call(this, {
    attributions: options.attributions,
    logo: options.logo,
    projection: options.projection,
    resGVolutions: options.resGVolutions,
    state: options.state
  });

  /**
   * @private
   * @type {GVol.CanvasFunctionType}
   */
  this.canvasFunction_ = options.canvasFunction;

  /**
   * @private
   * @type {GVol.ImageCanvas}
   */
  this.canvas_ = null;

  /**
   * @private
   * @type {number}
   */
  this.renderedRevision_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.ratio_ = options.ratio !== undefined ?
    options.ratio : 1.5;

};
GVol.inherits(GVol.source.ImageCanvas, GVol.source.Image);


/**
 * @inheritDoc
 */
GVol.source.ImageCanvas.prototype.getImageInternal = function(extent, resGVolution, pixelRatio, projection) {
  resGVolution = this.findNearestResGVolution(resGVolution);

  var canvas = this.canvas_;
  if (canvas &&
      this.renderedRevision_ == this.getRevision() &&
      canvas.getResGVolution() == resGVolution &&
      canvas.getPixelRatio() == pixelRatio &&
      GVol.extent.containsExtent(canvas.getExtent(), extent)) {
    return canvas;
  }

  extent = extent.slice();
  GVol.extent.scaleFromCenter(extent, this.ratio_);
  var width = GVol.extent.getWidth(extent) / resGVolution;
  var height = GVol.extent.getHeight(extent) / resGVolution;
  var size = [width * pixelRatio, height * pixelRatio];

  var canvasElement = this.canvasFunction_(
      extent, resGVolution, pixelRatio, size, projection);
  if (canvasElement) {
    canvas = new GVol.ImageCanvas(extent, resGVolution, pixelRatio,
        this.getAttributions(), canvasElement);
  }
  this.canvas_ = canvas;
  this.renderedRevision_ = this.getRevision();

  return canvas;
};

goog.provide('GVol.source.ImageVector');

goog.require('GVol');
goog.require('GVol.dom');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.extent');
goog.require('GVol.render.canvas.ReplayGroup');
goog.require('GVol.renderer.vector');
goog.require('GVol.source.ImageCanvas');
goog.require('GVol.style.Style');
goog.require('GVol.transform');


/**
 * @classdesc
 * An image source whose images are canvas elements into which vector features
 * read from a vector source (`GVol.source.Vector`) are drawn. An
 * `GVol.source.ImageVector` object is to be used as the `source` of an image
 * layer (`GVol.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
 * `GVol.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 {GVol.source.ImageCanvas}
 * @param {GVolx.source.ImageVectorOptions} options Options.
 * @api
 */
GVol.source.ImageVector = function(options) {

  /**
   * @private
   * @type {GVol.source.Vector}
   */
  this.source_ = options.source;

  /**
   * @private
   * @type {GVol.Transform}
   */
  this.transform_ = GVol.transform.create();

  /**
   * @private
   * @type {CanvasRenderingContext2D}
   */
  this.canvasContext_ = GVol.dom.createCanvasContext2D();

  /**
   * @private
   * @type {GVol.Size}
   */
  this.canvasSize_ = [0, 0];

  /**
   * @private
   * @type {number}
   */
  this.renderBuffer_ = options.renderBuffer == undefined ? 100 : options.renderBuffer;

  /**
   * @private
   * @type {GVol.render.canvas.ReplayGroup}
   */
  this.replayGroup_ = null;

  GVol.source.ImageCanvas.call(this, {
    attributions: options.attributions,
    canvasFunction: this.canvasFunctionInternal_.bind(this),
    logo: options.logo,
    projection: options.projection,
    ratio: options.ratio,
    resGVolutions: options.resGVolutions,
    state: this.source_.getState()
  });

  /**
   * User provided style.
   * @type {GVol.style.Style|Array.<GVol.style.Style>|GVol.StyleFunction}
   * @private
   */
  this.style_ = null;

  /**
   * Style function for use within the library.
   * @type {GVol.StyleFunction|undefined}
   * @private
   */
  this.styleFunction_ = undefined;

  this.setStyle(options.style);

  GVol.events.listen(this.source_, GVol.events.EventType.CHANGE,
      this.handleSourceChange_, this);

};
GVol.inherits(GVol.source.ImageVector, GVol.source.ImageCanvas);


/**
 * @param {GVol.Extent} extent Extent.
 * @param {number} resGVolution ResGVolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {GVol.Size} size Size.
 * @param {GVol.proj.Projection} projection Projection;
 * @return {HTMLCanvasElement} Canvas element.
 * @private
 */
GVol.source.ImageVector.prototype.canvasFunctionInternal_ = function(extent, resGVolution, pixelRatio, size, projection) {

  var replayGroup = new GVol.render.canvas.ReplayGroup(
      GVol.renderer.vector.getTGVolerance(resGVolution, pixelRatio), extent,
      resGVolution, this.source_.getOverlaps(), this.renderBuffer_);

  this.source_.loadFeatures(extent, resGVolution, projection);

  var loading = false;
  this.source_.forEachFeatureInExtent(extent,
      /**
       * @param {GVol.Feature} feature Feature.
       */
      function(feature) {
        loading = loading ||
            this.renderFeature_(feature, resGVolution, 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_(GVol.extent.getCenter(extent),
      resGVolution, pixelRatio, size);
  replayGroup.replay(this.canvasContext_, pixelRatio, transform, 0, {});

  this.replayGroup_ = replayGroup;

  return this.canvasContext_.canvas;
};


/**
 * @inheritDoc
 */
GVol.source.ImageVector.prototype.forEachFeatureAtCoordinate = function(
    coordinate, resGVolution, rotation, hitTGVolerance, skippedFeatureUids, callback) {
  if (!this.replayGroup_) {
    return undefined;
  } else {
    /** @type {Object.<string, boGVolean>} */
    var features = {};
    return this.replayGroup_.forEachFeatureAtCoordinate(
        coordinate, resGVolution, 0, hitTGVolerance, skippedFeatureUids,
        /**
         * @param {GVol.Feature|GVol.render.Feature} feature Feature.
         * @return {?} Callback result.
         */
        function(feature) {
          var key = GVol.getUid(feature).toString();
          if (!(key in features)) {
            features[key] = true;
            return callback(feature);
          }
        });
  }
};


/**
 * Get a reference to the wrapped source.
 * @return {GVol.source.Vector} Source.
 * @api
 */
GVol.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 {GVol.style.Style|Array.<GVol.style.Style>|GVol.StyleFunction}
 *     Layer style.
 * @api
 */
GVol.source.ImageVector.prototype.getStyle = function() {
  return this.style_;
};


/**
 * Get the style function.
 * @return {GVol.StyleFunction|undefined} Layer style function.
 * @api
 */
GVol.source.ImageVector.prototype.getStyleFunction = function() {
  return this.styleFunction_;
};


/**
 * @param {GVol.Coordinate} center Center.
 * @param {number} resGVolution ResGVolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {GVol.Size} size Size.
 * @return {!GVol.Transform} Transform.
 * @private
 */
GVol.source.ImageVector.prototype.getTransform_ = function(center, resGVolution, pixelRatio, size) {
  var dx1 = size[0] / 2;
  var dy1 = size[1] / 2;
  var sx = pixelRatio / resGVolution;
  var sy = -sx;
  var dx2 = -center[0];
  var dy2 = -center[1];

  return GVol.transform.compose(this.transform_, dx1, dy1, sx, sy, 0, dx2, dy2);
};


/**
 * Handle changes in image style state.
 * @param {GVol.events.Event} event Image style change event.
 * @private
 */
GVol.source.ImageVector.prototype.handleImageChange_ = function(event) {
  this.changed();
};


/**
 * @private
 */
GVol.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 {GVol.Feature} feature Feature.
 * @param {number} resGVolution ResGVolution.
 * @param {number} pixelRatio Pixel ratio.
 * @param {GVol.render.canvas.ReplayGroup} replayGroup Replay group.
 * @return {boGVolean} `true` if an image is loading.
 * @private
 */
GVol.source.ImageVector.prototype.renderFeature_ = function(feature, resGVolution, pixelRatio, replayGroup) {
  var styles;
  var styleFunction = feature.getStyleFunction();
  if (styleFunction) {
    styles = styleFunction.call(feature, resGVolution);
  } else if (this.styleFunction_) {
    styles = this.styleFunction_(feature, resGVolution);
  }
  if (!styles) {
    return false;
  }
  var i, ii, loading = false;
  if (!Array.isArray(styles)) {
    styles = [styles];
  }
  for (i = 0, ii = styles.length; i < ii; ++i) {
    loading = GVol.renderer.vector.renderFeature(
        replayGroup, feature, styles[i],
        GVol.renderer.vector.getSquaredTGVolerance(resGVolution, 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 resGVolution 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 GVol.style} for information on the default style.
 * @param {GVol.style.Style|Array.<GVol.style.Style>|GVol.StyleFunction|undefined}
 *     style Layer style.
 * @api
 */
GVol.source.ImageVector.prototype.setStyle = function(style) {
  this.style_ = style !== undefined ? style : GVol.style.Style.defaultFunction;
  this.styleFunction_ = !style ?
    undefined : GVol.style.Style.createFunction(this.style_);
  this.changed();
};

goog.provide('GVol.renderer.webgl.ImageLayer');

goog.require('GVol');
goog.require('GVol.ViewHint');
goog.require('GVol.dom');
goog.require('GVol.extent');
goog.require('GVol.functions');
goog.require('GVol.renderer.webgl.Layer');
goog.require('GVol.source.ImageVector');
goog.require('GVol.transform');
goog.require('GVol.webgl');
goog.require('GVol.webgl.Context');


if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @extends {GVol.renderer.webgl.Layer}
   * @param {GVol.renderer.webgl.Map} mapRenderer Map renderer.
   * @param {GVol.layer.Image} imageLayer Tile layer.
   */
  GVol.renderer.webgl.ImageLayer = function(mapRenderer, imageLayer) {

    GVol.renderer.webgl.Layer.call(this, mapRenderer, imageLayer);

    /**
     * The last rendered image.
     * @private
     * @type {?GVol.ImageBase}
     */
    this.image_ = null;

    /**
     * @private
     * @type {CanvasRenderingContext2D}
     */
    this.hitCanvasContext_ = null;

    /**
     * @private
     * @type {?GVol.Transform}
     */
    this.hitTransformationMatrix_ = null;

  };
  GVol.inherits(GVol.renderer.webgl.ImageLayer, GVol.renderer.webgl.Layer);


  /**
   * @param {GVol.ImageBase} image Image.
   * @private
   * @return {WebGLTexture} Texture.
   */
  GVol.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 GVol.webgl.Context.createTexture(
        gl, imageElement, GVol.webgl.CLAMP_TO_EDGE, GVol.webgl.CLAMP_TO_EDGE);
  };


  /**
   * @inheritDoc
   */
  GVol.renderer.webgl.ImageLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTGVolerance, callback, thisArg) {
    var layer = this.getLayer();
    var source = layer.getSource();
    var resGVolution = frameState.viewState.resGVolution;
    var rotation = frameState.viewState.rotation;
    var skippedFeatureUids = frameState.skippedFeatureUids;
    return source.forEachFeatureAtCoordinate(
        coordinate, resGVolution, rotation, hitTGVolerance, skippedFeatureUids,

        /**
         * @param {GVol.Feature|GVol.render.Feature} feature Feature.
         * @return {?} Callback result.
         */
        function(feature) {
          return callback.call(thisArg, feature, layer);
        });
  };


  /**
   * @inheritDoc
   */
  GVol.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 viewResGVolution = viewState.resGVolution;
    var viewRotation = viewState.rotation;

    var image = this.image_;
    var texture = this.texture;
    var imageLayer = /** @type {GVol.layer.Image} */ (this.getLayer());
    var imageSource = imageLayer.getSource();

    var hints = frameState.viewHints;

    var renderedExtent = frameState.extent;
    if (layerState.extent !== undefined) {
      renderedExtent = GVol.extent.getIntersection(
          renderedExtent, layerState.extent);
    }
    if (!hints[GVol.ViewHint.ANIMATING] && !hints[GVol.ViewHint.INTERACTING] &&
        !GVol.extent.isEmpty(renderedExtent)) {
      var projection = viewState.projection;
      if (!GVol.ENABLE_RASTER_REPROJECTION) {
        var sourceProjection = imageSource.getProjection();
        if (sourceProjection) {
          projection = sourceProjection;
        }
      }
      var image_ = imageSource.getImage(renderedExtent, viewResGVolution,
          pixelRatio, projection);
      if (image_) {
        var loaded = this.loadImage(image_);
        if (loaded) {
          image = image_;
          texture = this.createTexture_(image_);
          if (this.texture) {
            /**
             * @param {WebGLRenderingContext} gl GL.
             * @param {WebGLTexture} texture Texture.
             */
            var postRenderFunction = function(gl, texture) {
              if (!gl.isContextLost()) {
                gl.deleteTexture(texture);
              }
            }.bind(null, gl, this.texture);
            frameState.postRenderFunctions.push(
                /** @type {GVol.PostRenderFunction} */ (postRenderFunction)
            );
          }
        }
      }
    }

    if (image) {
      var canvas = this.mapRenderer.getContext().getCanvas();

      this.updateProjectionMatrix_(canvas.width, canvas.height,
          pixelRatio, viewCenter, viewResGVolution, viewRotation,
          image.getExtent());
      this.hitTransformationMatrix_ = null;

      // Translate and scale to flip the Y coord.
      var texCoordMatrix = this.texCoordMatrix;
      GVol.transform.reset(texCoordMatrix);
      GVol.transform.scale(texCoordMatrix, 1, -1);
      GVol.transform.translate(texCoordMatrix, 0, -1);

      this.image_ = image;
      this.texture = texture;

      this.updateAttributions(frameState.attributions, image.getAttributions());
      this.updateLogos(frameState, imageSource);
    }

    return !!image;
  };


  /**
   * @param {number} canvasWidth Canvas width.
   * @param {number} canvasHeight Canvas height.
   * @param {number} pixelRatio Pixel ratio.
   * @param {GVol.Coordinate} viewCenter View center.
   * @param {number} viewResGVolution View resGVolution.
   * @param {number} viewRotation View rotation.
   * @param {GVol.Extent} imageExtent Image extent.
   * @private
   */
  GVol.renderer.webgl.ImageLayer.prototype.updateProjectionMatrix_ = function(canvasWidth, canvasHeight, pixelRatio,
      viewCenter, viewResGVolution, viewRotation, imageExtent) {

    var canvasExtentWidth = canvasWidth * viewResGVolution;
    var canvasExtentHeight = canvasHeight * viewResGVolution;

    var projectionMatrix = this.projectionMatrix;
    GVol.transform.reset(projectionMatrix);
    GVol.transform.scale(projectionMatrix,
        pixelRatio * 2 / canvasExtentWidth,
        pixelRatio * 2 / canvasExtentHeight);
    GVol.transform.rotate(projectionMatrix, -viewRotation);
    GVol.transform.translate(projectionMatrix,
        imageExtent[0] - viewCenter[0],
        imageExtent[1] - viewCenter[1]);
    GVol.transform.scale(projectionMatrix,
        (imageExtent[2] - imageExtent[0]) / 2,
        (imageExtent[3] - imageExtent[1]) / 2);
    GVol.transform.translate(projectionMatrix, 1, 1);

  };


  /**
   * @inheritDoc
   */
  GVol.renderer.webgl.ImageLayer.prototype.hasFeatureAtCoordinate = function(coordinate, frameState) {
    var hasFeature = this.forEachFeatureAtCoordinate(
        coordinate, frameState, 0, GVol.functions.TRUE, this);
    return hasFeature !== undefined;
  };


  /**
   * @inheritDoc
   */
  GVol.renderer.webgl.ImageLayer.prototype.forEachLayerAtPixel = function(pixel, frameState, callback, thisArg) {
    if (!this.image_ || !this.image_.getImage()) {
      return undefined;
    }

    if (this.getLayer().getSource() instanceof GVol.source.ImageVector) {
      // for ImageVector sources use the original hit-detection logic,
      // so that for example also transparent pGVolygons are detected
      var coordinate = GVol.transform.apply(
          frameState.pixelToCoordinateTransform, pixel.slice());
      var hasFeature = this.forEachFeatureAtCoordinate(
          coordinate, frameState, 0, GVol.functions.TRUE, this);

      if (hasFeature) {
        return callback.call(thisArg, this.getLayer(), null);
      } 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 = GVol.transform.apply(
          this.hitTransformationMatrix_, pixel.slice());

      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_ = GVol.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(),  imageData);
      } else {
        return undefined;
      }
    }
  };


  /**
   * The transformation matrix to get the pixel on the image for a
   * pixel on the map.
   * @param {GVol.Size} mapSize The map size.
   * @param {GVol.Size} imageSize The image size.
   * @return {GVol.Transform} The transformation matrix.
   * @private
   */
  GVol.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 mapCoordTransform = GVol.transform.create();
    GVol.transform.translate(mapCoordTransform, -1, -1);
    GVol.transform.scale(mapCoordTransform, 2 / mapSize[0], 2 / mapSize[1]);
    GVol.transform.translate(mapCoordTransform, 0, mapSize[1]);
    GVol.transform.scale(mapCoordTransform, 1, -1);

    // the second matrix is the inverse of the projection matrix used in the
    // shader for drawing
    var projectionMatrixInv = GVol.transform.invert(this.projectionMatrix.slice());

    // the third matrix scales to the image dimensions and flips the y-axis again
    var transform = GVol.transform.create();
    GVol.transform.translate(transform, 0, imageSize[1]);
    GVol.transform.scale(transform, 1, -1);
    GVol.transform.scale(transform, imageSize[0] / 2, imageSize[1] / 2);
    GVol.transform.translate(transform, 1, 1);

    GVol.transform.multiply(transform, projectionMatrixInv);
    GVol.transform.multiply(transform, mapCoordTransform);

    return transform;
  };

}

goog.provide('GVol.layer.Image');

goog.require('GVol');
goog.require('GVol.layer.Layer');
goog.require('GVol.renderer.Type');
goog.require('GVol.renderer.canvas.ImageLayer');
goog.require('GVol.renderer.webgl.ImageLayer');


/**
 * @classdesc
 * Server-rendered images that are available for arbitrary extents and
 * resGVolutions.
 * Note that any property set in the options is set as a {@link GVol.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 {GVol.layer.Layer}
 * @fires GVol.render.Event
 * @param {GVolx.layer.ImageOptions=} opt_options Layer options.
 * @api
 */
GVol.layer.Image = function(opt_options) {
  var options = opt_options ? opt_options : {};
  GVol.layer.Layer.call(this,  /** @type {GVolx.layer.LayerOptions} */ (options));
};
GVol.inherits(GVol.layer.Image, GVol.layer.Layer);


/**
 * @inheritDoc
 */
GVol.layer.Image.prototype.createRenderer = function(mapRenderer) {
  var renderer = null;
  var type = mapRenderer.getType();
  if (GVol.ENABLE_CANVAS && type === GVol.renderer.Type.CANVAS) {
    renderer = new GVol.renderer.canvas.ImageLayer(this);
  } else if (GVol.ENABLE_WEBGL && type === GVol.renderer.Type.WEBGL) {
    renderer = new GVol.renderer.webgl.ImageLayer(/** @type {GVol.renderer.webgl.Map} */ (mapRenderer), this);
  }
  return renderer;
};


/**
 * Return the associated {@link GVol.source.Image source} of the image layer.
 * @function
 * @return {GVol.source.Image} Source.
 * @api
 */
GVol.layer.Image.prototype.getSource;

goog.provide('GVol.layer.TileProperty');

/**
 * @enum {string}
 */
GVol.layer.TileProperty = {
  PRELOAD: 'preload',
  USE_INTERIM_TILES_ON_ERROR: 'useInterimTilesOnError'
};

// FIXME find correct globalCompositeOperation

goog.provide('GVol.renderer.canvas.TileLayer');

goog.require('GVol');
goog.require('GVol.TileRange');
goog.require('GVol.TileState');
goog.require('GVol.ViewHint');
goog.require('GVol.array');
goog.require('GVol.dom');
goog.require('GVol.extent');
goog.require('GVol.renderer.canvas.IntermediateCanvas');
goog.require('GVol.transform');


/**
 * @constructor
 * @extends {GVol.renderer.canvas.IntermediateCanvas}
 * @param {GVol.layer.Tile|GVol.layer.VectorTile} tileLayer Tile layer.
 */
GVol.renderer.canvas.TileLayer = function(tileLayer) {

  GVol.renderer.canvas.IntermediateCanvas.call(this, tileLayer);

  /**
   * @protected
   * @type {CanvasRenderingContext2D}
   */
  this.context = this.context === null ? null :  GVol.dom.createCanvasContext2D();

  /**
   * @private
   * @type {number}
   */
  this.oversampling_;

  /**
   * @private
   * @type {GVol.Extent}
   */
  this.renderedExtent_ = null;

  /**
   * @protected
   * @type {number}
   */
  this.renderedRevision;

  /**
   * @protected
   * @type {!Array.<GVol.Tile>}
   */
  this.renderedTiles = [];

  /**
   * @protected
   * @type {GVol.Extent}
   */
  this.tmpExtent = GVol.extent.createEmpty();

  /**
   * @private
   * @type {GVol.TileRange}
   */
  this.tmpTileRange_ = new GVol.TileRange(0, 0, 0, 0);

  /**
   * @private
   * @type {GVol.Transform}
   */
  this.imageTransform_ = GVol.transform.create();

  /**
   * @protected
   * @type {number}
   */
  this.zDirection = 0;

};
GVol.inherits(GVol.renderer.canvas.TileLayer, GVol.renderer.canvas.IntermediateCanvas);


/**
 * @private
 * @param {GVol.Tile} tile Tile.
 * @return {boGVolean} Tile is drawable.
 */
GVol.renderer.canvas.TileLayer.prototype.isDrawableTile_ = function(tile) {
  var tileState = tile.getState();
  var useInterimTilesOnError = this.getLayer().getUseInterimTilesOnError();
  return tileState == GVol.TileState.LOADED ||
      tileState == GVol.TileState.EMPTY ||
      tileState == GVol.TileState.ERROR && !useInterimTilesOnError;
};

/**
 * @inheritDoc
 */
GVol.renderer.canvas.TileLayer.prototype.prepareFrame = function(frameState, layerState) {

  var pixelRatio = frameState.pixelRatio;
  var size = frameState.size;
  var viewState = frameState.viewState;
  var projection = viewState.projection;
  var viewResGVolution = viewState.resGVolution;
  var viewCenter = viewState.center;

  var tileLayer = this.getLayer();
  var tileSource = /** @type {GVol.source.Tile} */ (tileLayer.getSource());
  var sourceRevision = tileSource.getRevision();
  var tileGrid = tileSource.getTileGridForProjection(projection);
  var z = tileGrid.getZForResGVolution(viewResGVolution, this.zDirection);
  var tileResGVolution = tileGrid.getResGVolution(z);
  var oversampling = Math.round(viewResGVolution / tileResGVolution) || 1;
  var extent = frameState.extent;

  if (layerState.extent !== undefined) {
    extent = GVol.extent.getIntersection(extent, layerState.extent);
  }
  if (GVol.extent.isEmpty(extent)) {
    // Return false to prevent the rendering of the layer.
    return false;
  }

  var tileRange = tileGrid.getTileRangeForExtentAndResGVolution(
      extent, tileResGVolution);
  var imageExtent = tileGrid.getTileRangeExtent(z, tileRange);

  var tilePixelRatio = tileSource.getTilePixelRatio(pixelRatio);

  /**
   * @type {Object.<number, Object.<string, GVol.Tile>>}
   */
  var tilesToDrawByZ = {};
  tilesToDrawByZ[z] = {};

  var findLoadedTiles = this.createLoadedTileFinder(
      tileSource, projection, tilesToDrawByZ);

  var tmpExtent = this.tmpExtent;
  var tmpTileRange = this.tmpTileRange_;
  var newTiles = false;
  var tile, 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);
      if (tile.getState() == GVol.TileState.ERROR) {
        if (!tileLayer.getUseInterimTilesOnError()) {
          // When useInterimTilesOnError is false, we consider the error tile as loaded.
          tile.setState(GVol.TileState.LOADED);
        } else if (tileLayer.getPreload() > 0) {
          // Preloaded tiles for lower resGVolutions might have finished loading.
          newTiles = true;
        }
      }
      if (!this.isDrawableTile_(tile)) {
        tile = tile.getInterimTile();
      }
      if (this.isDrawableTile_(tile)) {
        if (tile.getState() == GVol.TileState.LOADED) {
          tilesToDrawByZ[z][tile.tileCoord.toString()] = tile;
          if (!newTiles && this.renderedTiles.indexOf(tile) == -1) {
            newTiles = true;
          }
        }
        continue;
      }

      var fullyLoaded = tileGrid.forEachTileCoordParentTileRange(
          tile.tileCoord, findLoadedTiles, null, tmpTileRange, tmpExtent);
      if (!fullyLoaded) {
        var childTileRange = tileGrid.getTileCoordChildTileRange(
            tile.tileCoord, tmpTileRange, tmpExtent);
        if (childTileRange) {
          findLoadedTiles(z + 1, childTileRange);
        }
      }

    }
  }

  var renderedResGVolution = tileResGVolution * pixelRatio / tilePixelRatio * oversampling;
  var hints = frameState.viewHints;
  var animatingOrInteracting = hints[GVol.ViewHint.ANIMATING] || hints[GVol.ViewHint.INTERACTING];
  if (!(this.renderedResGVolution && Date.now() - frameState.time > 16 && animatingOrInteracting) && (
    newTiles ||
        !(this.renderedExtent_ && GVol.extent.containsExtent(this.renderedExtent_, extent)) ||
        this.renderedRevision != sourceRevision ||
        oversampling != this.oversampling_ ||
        !animatingOrInteracting && renderedResGVolution != this.renderedResGVolution
  )) {

    var context = this.context;
    if (context) {
      var tilePixelSize = tileSource.getTilePixelSize(z, pixelRatio, projection);
      var width = Math.round(tileRange.getWidth() * tilePixelSize[0] / oversampling);
      var height = Math.round(tileRange.getHeight() * tilePixelSize[1] / oversampling);
      var canvas = context.canvas;
      if (canvas.width != width || canvas.height != height) {
        this.oversampling_ = oversampling;
        canvas.width = width;
        canvas.height = height;
      } else {
        context.clearRect(0, 0, width, height);
        oversampling = this.oversampling_;
      }
    }

    this.renderedTiles.length = 0;
    /** @type {Array.<number>} */
    var zs = Object.keys(tilesToDrawByZ).map(Number);
    zs.sort(GVol.array.numberSafeCompareFunction);
    var currentResGVolution, currentScale, currentTilePixelSize, currentZ, i, ii;
    var tileExtent, tileGutter, tilesToDraw, w, h;
    for (i = 0, ii = zs.length; i < ii; ++i) {
      currentZ = zs[i];
      currentTilePixelSize = tileSource.getTilePixelSize(currentZ, pixelRatio, projection);
      currentResGVolution = tileGrid.getResGVolution(currentZ);
      currentScale = currentResGVolution / tileResGVolution;
      tileGutter = tilePixelRatio * tileSource.getGutter(projection);
      tilesToDraw = tilesToDrawByZ[currentZ];
      for (var tileCoordKey in tilesToDraw) {
        tile = tilesToDraw[tileCoordKey];
        tileExtent = tileGrid.getTileCoordExtent(tile.getTileCoord(), tmpExtent);
        x = (tileExtent[0] - imageExtent[0]) / tileResGVolution * tilePixelRatio / oversampling;
        y = (imageExtent[3] - tileExtent[3]) / tileResGVolution * tilePixelRatio / oversampling;
        w = currentTilePixelSize[0] * currentScale / oversampling;
        h = currentTilePixelSize[1] * currentScale / oversampling;
        this.drawTileImage(tile, frameState, layerState, x, y, w, h, tileGutter);
        this.renderedTiles.push(tile);
      }
    }

    this.renderedRevision = sourceRevision;
    this.renderedResGVolution = tileResGVolution * pixelRatio / tilePixelRatio * oversampling;
    this.renderedExtent_ = imageExtent;
  }

  var scale = this.renderedResGVolution / viewResGVolution;
  var transform = GVol.transform.compose(this.imageTransform_,
      pixelRatio * size[0] / 2, pixelRatio * size[1] / 2,
      scale, scale,
      0,
      (this.renderedExtent_[0] - viewCenter[0]) / this.renderedResGVolution * pixelRatio,
      (viewCenter[1] - this.renderedExtent_[3]) / this.renderedResGVolution * pixelRatio);
  GVol.transform.compose(this.coordinateToCanvasPixelTransform,
      pixelRatio * size[0] / 2 - transform[4], pixelRatio * size[1] / 2 - transform[5],
      pixelRatio / viewResGVolution, -pixelRatio / viewResGVolution,
      0,
      -viewCenter[0], -viewCenter[1]);


  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 this.renderedTiles.length > 0;
};


/**
 * @param {GVol.Tile} tile Tile.
 * @param {GVolx.FrameState} frameState Frame state.
 * @param {GVol.LayerState} layerState Layer state.
 * @param {number} x Left of the tile.
 * @param {number} y Top of the tile.
 * @param {number} w Width of the tile.
 * @param {number} h Height of the tile.
 * @param {number} gutter Tile gutter.
 */
GVol.renderer.canvas.TileLayer.prototype.drawTileImage = function(tile, frameState, layerState, x, y, w, h, gutter) {
  if (!this.getLayer().getSource().getOpaque(frameState.viewState.projection)) {
    this.context.clearRect(x, y, w, h);
  }
  var image = tile.getImage(this.getLayer());
  if (image) {
    this.context.drawImage(image, gutter, gutter,
        image.width - 2 * gutter, image.height - 2 * gutter, x, y, w, h);
  }
};


/**
 * @inheritDoc
 */
GVol.renderer.canvas.TileLayer.prototype.getImage = function() {
  var context = this.context;
  return context ? context.canvas : null;
};


/**
 * @function
 * @return {GVol.layer.Tile|GVol.layer.VectorTile}
 */
GVol.renderer.canvas.TileLayer.prototype.getLayer;


/**
 * @inheritDoc
 */
GVol.renderer.canvas.TileLayer.prototype.getImageTransform = function() {
  return this.imageTransform_;
};

// This file is automatically generated, do not edit
/* eslint openlayers-internal/no-missing-requires: 0 */
goog.provide('GVol.renderer.webgl.tilelayershader');

goog.require('GVol');
goog.require('GVol.webgl.Fragment');
goog.require('GVol.webgl.Vertex');

if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @extends {GVol.webgl.Fragment}
   * @struct
   */
  GVol.renderer.webgl.tilelayershader.Fragment = function() {
    GVol.webgl.Fragment.call(this, GVol.renderer.webgl.tilelayershader.Fragment.SOURCE);
  };
  GVol.inherits(GVol.renderer.webgl.tilelayershader.Fragment, GVol.webgl.Fragment);


  /**
   * @const
   * @type {string}
   */
  GVol.renderer.webgl.tilelayershader.Fragment.DEBUG_SOURCE = 'precision mediump float;\nvarying vec2 v_texCoord;\n\n\nuniform sampler2D u_texture;\n\nvoid main(void) {\n  gl_FragCGVolor = texture2D(u_texture, v_texCoord);\n}\n';


  /**
   * @const
   * @type {string}
   */
  GVol.renderer.webgl.tilelayershader.Fragment.OPTIMIZED_SOURCE = 'precision mediump float;varying vec2 a;uniform sampler2D e;void main(void){gl_FragCGVolor=texture2D(e,a);}';


  /**
   * @const
   * @type {string}
   */
  GVol.renderer.webgl.tilelayershader.Fragment.SOURCE = GVol.DEBUG_WEBGL ?
    GVol.renderer.webgl.tilelayershader.Fragment.DEBUG_SOURCE :
    GVol.renderer.webgl.tilelayershader.Fragment.OPTIMIZED_SOURCE;


  GVol.renderer.webgl.tilelayershader.fragment = new GVol.renderer.webgl.tilelayershader.Fragment();


  /**
   * @constructor
   * @extends {GVol.webgl.Vertex}
   * @struct
   */
  GVol.renderer.webgl.tilelayershader.Vertex = function() {
    GVol.webgl.Vertex.call(this, GVol.renderer.webgl.tilelayershader.Vertex.SOURCE);
  };
  GVol.inherits(GVol.renderer.webgl.tilelayershader.Vertex, GVol.webgl.Vertex);


  /**
   * @const
   * @type {string}
   */
  GVol.renderer.webgl.tilelayershader.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}
   */
  GVol.renderer.webgl.tilelayershader.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}
   */
  GVol.renderer.webgl.tilelayershader.Vertex.SOURCE = GVol.DEBUG_WEBGL ?
    GVol.renderer.webgl.tilelayershader.Vertex.DEBUG_SOURCE :
    GVol.renderer.webgl.tilelayershader.Vertex.OPTIMIZED_SOURCE;


  GVol.renderer.webgl.tilelayershader.vertex = new GVol.renderer.webgl.tilelayershader.Vertex();


  /**
   * @constructor
   * @param {WebGLRenderingContext} gl GL.
   * @param {WebGLProgram} program Program.
   * @struct
   */
  GVol.renderer.webgl.tilelayershader.Locations = function(gl, program) {

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_texture = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_texture' : 'e');

    /**
     * @type {WebGLUniformLocation}
     */
    this.u_tileOffset = gl.getUniformLocation(
        program, GVol.DEBUG_WEBGL ? 'u_tileOffset' : 'd');

    /**
     * @type {number}
     */
    this.a_position = gl.getAttribLocation(
        program, GVol.DEBUG_WEBGL ? 'a_position' : 'b');

    /**
     * @type {number}
     */
    this.a_texCoord = gl.getAttribLocation(
        program, GVol.DEBUG_WEBGL ? 'a_texCoord' : 'c');
  };

}

// FIXME large resGVolutions lead to too large framebuffers :-(
// FIXME animated shaders! check in redraw

goog.provide('GVol.renderer.webgl.TileLayer');

goog.require('GVol');
goog.require('GVol.TileState');
goog.require('GVol.TileRange');
goog.require('GVol.array');
goog.require('GVol.extent');
goog.require('GVol.math');
goog.require('GVol.renderer.webgl.Layer');
goog.require('GVol.renderer.webgl.tilelayershader');
goog.require('GVol.size');
goog.require('GVol.transform');
goog.require('GVol.webgl');
goog.require('GVol.webgl.Buffer');


if (GVol.ENABLE_WEBGL) {

  /**
   * @constructor
   * @extends {GVol.renderer.webgl.Layer}
   * @param {GVol.renderer.webgl.Map} mapRenderer Map renderer.
   * @param {GVol.layer.Tile} tileLayer Tile layer.
   */
  GVol.renderer.webgl.TileLayer = function(mapRenderer, tileLayer) {

    GVol.renderer.webgl.Layer.call(this, mapRenderer, tileLayer);

    /**
     * @private
     * @type {GVol.webgl.Fragment}
     */
    this.fragmentShader_ = GVol.renderer.webgl.tilelayershader.fragment;

    /**
     * @private
     * @type {GVol.webgl.Vertex}
     */
    this.vertexShader_ = GVol.renderer.webgl.tilelayershader.vertex;

    /**
     * @private
     * @type {GVol.renderer.webgl.tilelayershader.Locations}
     */
    this.locations_ = null;

    /**
     * @private
     * @type {GVol.webgl.Buffer}
     */
    this.renderArrayBuffer_ = new GVol.webgl.Buffer([
      0, 0, 0, 1,
      1, 0, 1, 1,
      0, 1, 0, 0,
      1, 1, 1, 0
    ]);

    /**
     * @private
     * @type {GVol.TileRange}
     */
    this.renderedTileRange_ = null;

    /**
     * @private
     * @type {GVol.Extent}
     */
    this.renderedFramebufferExtent_ = null;

    /**
     * @private
     * @type {number}
     */
    this.renderedRevision_ = -1;

    /**
     * @private
     * @type {GVol.Size}
     */
    this.tmpSize_ = [0, 0];

  };
  GVol.inherits(GVol.renderer.webgl.TileLayer, GVol.renderer.webgl.Layer);


  /**
   * @inheritDoc
   */
  GVol.renderer.webgl.TileLayer.prototype.disposeInternal = function() {
    var context = this.mapRenderer.getContext();
    context.deleteBuffer(this.renderArrayBuffer_);
    GVol.renderer.webgl.Layer.prototype.disposeInternal.call(this);
  };


  /**
   * @inheritDoc
   */
  GVol.renderer.webgl.TileLayer.prototype.createLoadedTileFinder = function(source, projection, tiles) {
    var mapRenderer = this.mapRenderer;

    return (
      /**
       * @param {number} zoom Zoom level.
       * @param {GVol.TileRange} tileRange Tile range.
       * @return {boGVolean} 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
   */
  GVol.renderer.webgl.TileLayer.prototype.handleWebGLContextLost = function() {
    GVol.renderer.webgl.Layer.prototype.handleWebGLContextLost.call(this);
    this.locations_ = null;
  };


  /**
   * @inheritDoc
   */
  GVol.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 = /** @type {GVol.layer.Tile} */ (this.getLayer());
    var tileSource = tileLayer.getSource();
    var tileGrid = tileSource.getTileGridForProjection(projection);
    var z = tileGrid.getZForResGVolution(viewState.resGVolution);
    var tileResGVolution = tileGrid.getResGVolution(z);

    var tilePixelSize =
        tileSource.getTilePixelSize(z, frameState.pixelRatio, projection);
    var pixelRatio = tilePixelSize[0] /
        GVol.size.toSize(tileGrid.getTileSize(z), this.tmpSize_)[0];
    var tilePixelResGVolution = tileResGVolution / pixelRatio;
    var tileGutter = tileSource.getTilePixelRatio(pixelRatio) * tileSource.getGutter(projection);

    var center = viewState.center;
    var extent = frameState.extent;
    var tileRange = tileGrid.getTileRangeForExtentAndResGVolution(
        extent, tileResGVolution);

    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 = GVol.math.roundUpToPowerOfTwo(maxDimension);
      var framebufferExtentDimension = tilePixelResGVolution * framebufferDimension;
      var origin = tileGrid.getOrigin(z);
      var minX = origin[0] +
          tileRange.minX * tilePixelSize[0] * tilePixelResGVolution;
      var minY = origin[1] +
          tileRange.minY * tilePixelSize[1] * tilePixelResGVolution;
      framebufferExtent = [
        minX, minY,
        minX + framebufferExtentDimension, minY + framebufferExtentDimension
      ];

      this.bindFramebuffer(frameState, framebufferDimension);
      gl.viewport(0, 0, framebufferDimension, framebufferDimension);

      gl.clearCGVolor(0, 0, 0, 0);
      gl.clear(GVol.webgl.COLOR_BUFFER_BIT);
      gl.disable(GVol.webgl.BLEND);

      var program = context.getProgram(this.fragmentShader_, this.vertexShader_);
      context.useProgram(program);
      if (!this.locations_) {
        // eslint-disable-next-line openlayers-internal/no-missing-requires
        this.locations_ = new GVol.renderer.webgl.tilelayershader.Locations(gl, program);
      }

      context.bindBuffer(GVol.webgl.ARRAY_BUFFER, this.renderArrayBuffer_);
      gl.enableVertexAttribArray(this.locations_.a_position);
      gl.vertexAttribPointer(
          this.locations_.a_position, 2, GVol.webgl.FLOAT, false, 16, 0);
      gl.enableVertexAttribArray(this.locations_.a_texCoord);
      gl.vertexAttribPointer(
          this.locations_.a_texCoord, 2, GVol.webgl.FLOAT, false, 16, 8);
      gl.uniform1i(this.locations_.u_texture, 0);

      /**
       * @type {Object.<number, Object.<string, GVol.Tile>>}
       */
      var tilesToDrawByZ = {};
      tilesToDrawByZ[z] = {};

      var findLoadedTiles = this.createLoadedTileFinder(
          tileSource, projection, tilesToDrawByZ);

      var useInterimTilesOnError = tileLayer.getUseInterimTilesOnError();
      var allTilesLoaded = true;
      var tmpExtent = GVol.extent.createEmpty();
      var tmpTileRange = new GVol.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 (!GVol.extent.intersects(tileExtent, layerState.extent)) {
              continue;
            }
          }
          tileState = tile.getState();
          drawable = tileState == GVol.TileState.LOADED ||
              tileState == GVol.TileState.EMPTY ||
              tileState == GVol.TileState.ERROR && !useInterimTilesOnError;
          if (!drawable) {
            tile = tile.getInterimTile();
          }
          tileState = tile.getState();
          if (tileState == GVol.TileState.LOADED) {
            if (mapRenderer.isTileTextureLoaded(tile)) {
              tilesToDrawByZ[z][tile.tileCoord.toString()] = tile;
              continue;
            }
          } else if (tileState == GVol.TileState.EMPTY ||
                     (tileState == GVol.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(GVol.array.numberSafeCompareFunction);
      var u_tileOffset = new Float32Array(4);
      var i, ii, tileKey, tilesToDraw;
      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);
          u_tileOffset[0] = 2 * (tileExtent[2] - tileExtent[0]) /
              framebufferExtentDimension;
          u_tileOffset[1] = 2 * (tileExtent[3] - tileExtent[1]) /
              framebufferExtentDimension;
          u_tileOffset[2] = 2 * (tileExtent[0] - framebufferExtent[0]) /
              framebufferExtentDimension - 1;
          u_tileOffset[3] = 2 * (tileExtent[1] - framebufferExtent[1]) /
              framebufferExtentDimension - 1;
          gl.uniform4fv(this.locations_.u_tileOffset, u_tileOffset);
          mapRenderer.bindTileTexture(tile, tilePixelSize,
              tileGutter * pixelRatio, GVol.webgl.LINEAR, GVol.webgl.LINEAR);
          gl.drawArrays(GVol.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 {GVol.Tile} tile Tile.
         */
        function(tile) {
          if (tile.getState() == GVol.TileState.LOADED &&
              !mapRenderer.isTileTextureLoaded(tile) &&
              !tileTextureQueue.isKeyQueued(tile.getKey())) {
            tileTextureQueue.enqueue([
              tile,
              tileGrid.getTileCoordCenter(tile.tileCoord),
              tileGrid.getResGVolution(tile.tileCoord[0]),
              tilePixelSize, tileGutter * pixelRatio
            ]);
          }
        }, this);
    this.scheduleExpireCache(frameState, tileSource);
    this.updateLogos(frameState, tileSource);

    var texCoordMatrix = this.texCoordMatrix;
    GVol.transform.reset(texCoordMatrix);
    GVol.transform.translate(texCoordMatrix,
        (Math.round(center[0] / tileResGVolution) * tileResGVolution - framebufferExtent[0]) /
            (framebufferExtent[2] - framebufferExtent[0]),
        (Math.round(center[1] / tileResGVolution) * tileResGVolution - framebufferExtent[1]) /
            (framebufferExtent[3] - framebufferExtent[1]));
    if (viewState.rotation !== 0) {
      GVol.transform.rotate(texCoordMatrix, viewState.rotation);
    }
    GVol.transform.scale(texCoordMatrix,
        frameState.size[0] * viewState.resGVolution /
            (framebufferExtent[2] - framebufferExtent[0]),
        frameState.size[1] * viewState.resGVolution /
            (framebufferExtent[3] - framebufferExtent[1]));
    GVol.transform.translate(texCoordMatrix, -0.5, -0.5);

    return true;
  };


  /**
   * @inheritDoc
   */
  GVol.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 = GVol.transform.apply(
        this.texCoordMatrix, pixelOnMapScaled.slice());
    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(), imageData);
    } else {
      return undefined;
    }
  };

}

goog.provide('GVol.layer.Tile');

goog.require('GVol');
goog.require('GVol.layer.Layer');
goog.require('GVol.layer.TileProperty');
goog.require('GVol.obj');
goog.require('GVol.renderer.Type');
goog.require('GVol.renderer.canvas.TileLayer');
goog.require('GVol.renderer.webgl.TileLayer');


/**
 * @classdesc
 * For layer sources that provide pre-rendered, tiled images in grids that are
 * organized by zoom levels for specific resGVolutions.
 * Note that any property set in the options is set as a {@link GVol.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 {GVol.layer.Layer}
 * @fires GVol.render.Event
 * @param {GVolx.layer.TileOptions=} opt_options Tile layer options.
 * @api
 */
GVol.layer.Tile = function(opt_options) {
  var options = opt_options ? opt_options : {};

  var baseOptions = GVol.obj.assign({}, options);

  delete baseOptions.preload;
  delete baseOptions.useInterimTilesOnError;
  GVol.layer.Layer.call(this,  /** @type {GVolx.layer.LayerOptions} */ (baseOptions));

  this.setPreload(options.preload !== undefined ? options.preload : 0);
  this.setUseInterimTilesOnError(options.useInterimTilesOnError !== undefined ?
    options.useInterimTilesOnError : true);
};
GVol.inherits(GVol.layer.Tile, GVol.layer.Layer);


/**
 * @inheritDoc
 */
GVol.layer.Tile.prototype.createRenderer = function(mapRenderer) {
  var renderer = null;
  var type = mapRenderer.getType();
  if (GVol.ENABLE_CANVAS && type === GVol.renderer.Type.CANVAS) {
    renderer = new GVol.renderer.canvas.TileLayer(this);
  } else if (GVol.ENABLE_WEBGL && type === GVol.renderer.Type.WEBGL) {
    renderer = new GVol.renderer.webgl.TileLayer(/** @type {GVol.renderer.webgl.Map} */ (mapRenderer), this);
  }
  return renderer;
};


/**
 * Return the level as number to which we will preload tiles up to.
 * @return {number} The level to preload tiles up to.
 * @observable
 * @api
 */
GVol.layer.Tile.prototype.getPreload = function() {
  return /** @type {number} */ (this.get(GVol.layer.TileProperty.PRELOAD));
};


/**
 * Return the associated {@link GVol.source.Tile tilesource} of the layer.
 * @function
 * @return {GVol.source.Tile} Source.
 * @api
 */
GVol.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
 */
GVol.layer.Tile.prototype.setPreload = function(preload) {
  this.set(GVol.layer.TileProperty.PRELOAD, preload);
};


/**
 * Whether we use interim tiles on error.
 * @return {boGVolean} Use interim tiles on error.
 * @observable
 * @api
 */
GVol.layer.Tile.prototype.getUseInterimTilesOnError = function() {
  return /** @type {boGVolean} */ (
    this.get(GVol.layer.TileProperty.USE_INTERIM_TILES_ON_ERROR));
};


/**
 * Set whether we use interim tiles on error.
 * @param {boGVolean} useInterimTilesOnError Use interim tiles on error.
 * @observable
 * @api
 */
GVol.layer.Tile.prototype.setUseInterimTilesOnError = function(useInterimTilesOnError) {
  this.set(
      GVol.layer.TileProperty.USE_INTERIM_TILES_ON_ERROR, useInterimTilesOnError);
};

goog.provide('GVol.layer.VectorTileRenderType');

/**
 * @enum {string}
 * Render mode for vector tiles:
 *  * `'image'`: Vector tiles are rendered as images. Great performance, but
 *    point symbGVols and texts are always rotated with the view and pixels are
 *    scaled during zoom animations.
 *  * `'hybrid'`: PGVolygon and line elements are rendered as images, so pixels
 *    are scaled during zoom animations. Point symbGVols and texts are accurately
 *    rendered as vectors and can stay upright on rotated views.
 *  * `'vector'`: Vector tiles are rendered as vectors. Most accurate rendering
 *    even during animations, but slower performance than the other options.
 * @api
 */
GVol.layer.VectorTileRenderType = {
  IMAGE: 'image',
  HYBRID: 'hybrid',
  VECTOR: 'vector'
};

goog.provide('GVol.renderer.canvas.VectorTileLayer');

goog.require('GVol');
goog.require('GVol.TileState');
goog.require('GVol.dom');
goog.require('GVol.extent');
goog.require('GVol.proj');
goog.require('GVol.proj.Units');
goog.require('GVol.layer.VectorTileRenderType');
goog.require('GVol.render.ReplayType');
goog.require('GVol.render.canvas');
goog.require('GVol.render.canvas.ReplayGroup');
goog.require('GVol.render.replay');
goog.require('GVol.renderer.canvas.TileLayer');
goog.require('GVol.renderer.vector');
goog.require('GVol.size');
goog.require('GVol.transform');


/**
 * @constructor
 * @extends {GVol.renderer.canvas.TileLayer}
 * @param {GVol.layer.VectorTile} layer VectorTile layer.
 */
GVol.renderer.canvas.VectorTileLayer = function(layer) {

  /**
   * @type {CanvasRenderingContext2D}
   */
  this.context = null;

  GVol.renderer.canvas.TileLayer.call(this, layer);

  /**
   * @private
   * @type {boGVolean}
   */
  this.dirty_ = false;

  /**
   * @private
   * @type {number}
   */
  this.renderedLayerRevision_;

  /**
   * @private
   * @type {GVol.Transform}
   */
  this.tmpTransform_ = GVol.transform.create();

  // Use lower resGVolution for pure vector rendering. Closest resGVolution otherwise.
  this.zDirection =
      layer.getRenderMode() == GVol.layer.VectorTileRenderType.VECTOR ? 1 : 0;

};
GVol.inherits(GVol.renderer.canvas.VectorTileLayer, GVol.renderer.canvas.TileLayer);


/**
 * @const
 * @type {!Object.<string, Array.<GVol.render.ReplayType>>}
 */
GVol.renderer.canvas.VectorTileLayer.IMAGE_REPLAYS = {
  'image': [GVol.render.ReplayType.POLYGON, GVol.render.ReplayType.CIRCLE,
    GVol.render.ReplayType.LINE_STRING, GVol.render.ReplayType.IMAGE, GVol.render.ReplayType.TEXT],
  'hybrid': [GVol.render.ReplayType.POLYGON, GVol.render.ReplayType.LINE_STRING]
};


/**
 * @const
 * @type {!Object.<string, Array.<GVol.render.ReplayType>>}
 */
GVol.renderer.canvas.VectorTileLayer.VECTOR_REPLAYS = {
  'image': [GVol.render.ReplayType.DEFAULT],
  'hybrid': [GVol.render.ReplayType.IMAGE, GVol.render.ReplayType.TEXT, GVol.render.ReplayType.DEFAULT],
  'vector': GVol.render.replay.ORDER
};


/**
 * @inheritDoc
 */
GVol.renderer.canvas.VectorTileLayer.prototype.prepareFrame = function(frameState, layerState) {
  var layer = this.getLayer();
  var layerRevision = layer.getRevision();
  if (this.renderedLayerRevision_ != layerRevision) {
    this.renderedTiles.length = 0;
    var renderMode = layer.getRenderMode();
    if (!this.context && renderMode != GVol.layer.VectorTileRenderType.VECTOR) {
      this.context = GVol.dom.createCanvasContext2D();
    }
    if (this.context && renderMode == GVol.layer.VectorTileRenderType.VECTOR) {
      this.context = null;
    }
  }
  this.renderedLayerRevision_ = layerRevision;
  return GVol.renderer.canvas.TileLayer.prototype.prepareFrame.apply(this, arguments);
};


/**
 * @param {GVol.VectorImageTile} tile Tile.
 * @param {GVolx.FrameState} frameState Frame state.
 * @private
 */
GVol.renderer.canvas.VectorTileLayer.prototype.createReplayGroup_ = function(
    tile, frameState) {
  var layer = this.getLayer();
  var pixelRatio = frameState.pixelRatio;
  var projection = frameState.viewState.projection;
  var revision = layer.getRevision();
  var renderOrder = /** @type {GVol.RenderOrderFunction} */
      (layer.getRenderOrder()) || null;

  var replayState = tile.getReplayState(layer);
  if (!replayState.dirty && replayState.renderedRevision == revision &&
      replayState.renderedRenderOrder == renderOrder) {
    return;
  }

  var source = /** @type {GVol.source.VectorTile} */ (layer.getSource());
  var sourceTileGrid = source.getTileGrid();
  var tileGrid = source.getTileGridForProjection(projection);
  var resGVolution = tileGrid.getResGVolution(tile.tileCoord[0]);
  var tileExtent = tileGrid.getTileCoordExtent(tile.wrappedTileCoord);

  for (var t = 0, tt = tile.tileKeys.length; t < tt; ++t) {
    var sourceTile = tile.getTile(tile.tileKeys[t]);
    replayState.dirty = false;

    var sourceTileCoord = sourceTile.tileCoord;
    var tileProjection = sourceTile.getProjection();
    var sourceTileResGVolution = sourceTileGrid.getResGVolution(sourceTile.tileCoord[0]);
    var sourceTileExtent = sourceTileGrid.getTileCoordExtent(sourceTileCoord);
    var sharedExtent = GVol.extent.getIntersection(tileExtent, sourceTileExtent);
    var extent, reproject, tileResGVolution;
    if (tileProjection.getUnits() == GVol.proj.Units.TILE_PIXELS) {
      var tilePixelRatio = tileResGVolution = this.getTilePixelRatio_(source, sourceTile);
      var transform = GVol.transform.compose(this.tmpTransform_,
          0, 0,
          1 / sourceTileResGVolution * tilePixelRatio, -1 / sourceTileResGVolution * tilePixelRatio,
          0,
          -sourceTileExtent[0], -sourceTileExtent[3]);
      extent = (GVol.transform.apply(transform, [sharedExtent[0], sharedExtent[3]])
          .concat(GVol.transform.apply(transform, [sharedExtent[2], sharedExtent[1]])));
    } else {
      tileResGVolution = resGVolution;
      extent = sharedExtent;
      if (!GVol.proj.equivalent(projection, tileProjection)) {
        reproject = true;
        sourceTile.setProjection(projection);
      }
    }
    replayState.dirty = false;
    var replayGroup = new GVol.render.canvas.ReplayGroup(0, extent,
        tileResGVolution, source.getOverlaps(), layer.getRenderBuffer());
    var squaredTGVolerance = GVol.renderer.vector.getSquaredTGVolerance(
        tileResGVolution, pixelRatio);

    /**
     * @param {GVol.Feature|GVol.render.Feature} feature Feature.
     * @this {GVol.renderer.canvas.VectorTileLayer}
     */
    var renderFeature = function(feature) {
      var styles;
      var styleFunction = feature.getStyleFunction();
      if (styleFunction) {
        styles = styleFunction.call(/** @type {GVol.Feature} */ (feature), resGVolution);
      } else {
        styleFunction = layer.getStyleFunction();
        if (styleFunction) {
          styles = styleFunction(feature, resGVolution);
        }
      }
      if (styles) {
        if (!Array.isArray(styles)) {
          styles = [styles];
        }
        var dirty = this.renderFeature(feature, squaredTGVolerance, styles,
            replayGroup);
        this.dirty_ = this.dirty_ || dirty;
        replayState.dirty = replayState.dirty || dirty;
      }
    };

    var features = sourceTile.getFeatures();
    if (renderOrder && renderOrder !== replayState.renderedRenderOrder) {
      features.sort(renderOrder);
    }
    var feature;
    for (var i = 0, ii = features.length; i < ii; ++i) {
      feature = features[i];
      if (reproject) {
        feature.getGeometry().transform(tileProjection, projection);
      }
      renderFeature.call(this, feature);
    }
    replayGroup.finish();
    sourceTile.setReplayGroup(layer, tile.tileCoord.toString(), replayGroup);
  }
  replayState.renderedRevision = revision;
  replayState.renderedRenderOrder = renderOrder;
};


/**
 * @inheritDoc
 */
GVol.renderer.canvas.VectorTileLayer.prototype.drawTileImage = function(
    tile, frameState, layerState, x, y, w, h, gutter) {
  var vectorImageTile = /** @type {GVol.VectorImageTile} */ (tile);
  this.createReplayGroup_(vectorImageTile, frameState);
  if (this.context) {
    this.renderTileImage_(vectorImageTile, frameState, layerState);
    GVol.renderer.canvas.TileLayer.prototype.drawTileImage.apply(this, arguments);
  }
};


/**
 * @inheritDoc
 */
GVol.renderer.canvas.VectorTileLayer.prototype.forEachFeatureAtCoordinate = function(coordinate, frameState, hitTGVolerance, callback, thisArg) {
  var resGVolution = frameState.viewState.resGVolution;
  var rotation = frameState.viewState.rotation;
  hitTGVolerance = hitTGVolerance == undefined ? 0 : hitTGVolerance;
  var layer = this.getLayer();
  /** @type {Object.<string, boGVolean>} */
  var features = {};

  /** @type {Array.<GVol.VectorImageTile>} */
  var renderedTiles = this.renderedTiles;

  var source = /** @type {GVol.source.VectorTile} */ (layer.getSource());
  var tileGrid = source.getTileGridForProjection(frameState.viewState.projection);
  var sourceTileGrid = source.getTileGrid();
  var bufferedExtent, found, tileSpaceCoordinate;
  var i, ii, origin, replayGroup;
  var tile, tileCoord, tileExtent, tilePixelRatio, tileRenderResGVolution;
  for (i = 0, ii = renderedTiles.length; i < ii; ++i) {
    tile = renderedTiles[i];
    tileCoord = tile.tileCoord;
    tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent);
    bufferedExtent = GVol.extent.buffer(tileExtent, hitTGVolerance * resGVolution, bufferedExtent);
    if (!GVol.extent.containsCoordinate(bufferedExtent, coordinate)) {
      continue;
    }
    for (var t = 0, tt = tile.tileKeys.length; t < tt; ++t) {
      var sourceTile = tile.getTile(tile.tileKeys[t]);
      if (sourceTile.getProjection().getUnits() === GVol.proj.Units.TILE_PIXELS) {
        var sourceTileCoord = sourceTile.tileCoord;
        var sourceTileExtent = sourceTileGrid.getTileCoordExtent(sourceTileCoord, this.tmpExtent);
        origin = GVol.extent.getTopLeft(sourceTileExtent);
        tilePixelRatio = this.getTilePixelRatio_(source, sourceTile);
        var sourceTileResGVolution = sourceTileGrid.getResGVolution(sourceTileCoord[0]);
        tileRenderResGVolution = sourceTileResGVolution / tilePixelRatio;
        tileSpaceCoordinate = [
          (coordinate[0] - origin[0]) / tileRenderResGVolution,
          (origin[1] - coordinate[1]) / tileRenderResGVolution
        ];
        var upscaling = tileGrid.getResGVolution(tileCoord[0]) / sourceTileResGVolution;
        resGVolution = tilePixelRatio * upscaling;
      } else {
        tileSpaceCoordinate = coordinate;
      }
      replayGroup = sourceTile.getReplayGroup(layer, tile.tileCoord.toString());
      found = found || replayGroup.forEachFeatureAtCoordinate(
          tileSpaceCoordinate, resGVolution, rotation, hitTGVolerance, {},
          /**
           * @param {GVol.Feature|GVol.render.Feature} feature Feature.
           * @return {?} Callback result.
           */
          function(feature) {
            var key = GVol.getUid(feature).toString();
            if (!(key in features)) {
              features[key] = true;
              return callback.call(thisArg, feature, layer);
            }
          });
    }
  }
  return found;
};


/**
 * @param {GVol.VectorTile} tile Tile.
 * @param {GVolx.FrameState} frameState Frame state.
 * @return {GVol.Transform} transform Transform.
 * @private
 */
GVol.renderer.canvas.VectorTileLayer.prototype.getReplayTransform_ = function(tile, frameState) {
  if (tile.getProjection().getUnits() == GVol.proj.Units.TILE_PIXELS) {
    var layer = this.getLayer();
    var source = /** @type {GVol.source.VectorTile} */ (layer.getSource());
    var tileGrid = source.getTileGrid();
    var tileCoord = tile.tileCoord;
    var tileResGVolution =
        tileGrid.getResGVolution(tileCoord[0]) / this.getTilePixelRatio_(source, tile);
    var viewState = frameState.viewState;
    var pixelRatio = frameState.pixelRatio;
    var renderResGVolution = viewState.resGVolution / pixelRatio;
    var tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent);
    var center = viewState.center;
    var origin = GVol.extent.getTopLeft(tileExtent);
    var size = frameState.size;
    var offsetX = Math.round(pixelRatio * size[0] / 2);
    var offsetY = Math.round(pixelRatio * size[1] / 2);
    return GVol.transform.compose(this.tmpTransform_,
        offsetX, offsetY,
        tileResGVolution / renderResGVolution, tileResGVolution / renderResGVolution,
        viewState.rotation,
        (origin[0] - center[0]) / tileResGVolution,
        (center[1] - origin[1]) / tileResGVolution);
  } else {
    return this.getTransform(frameState, 0);
  }
};


/**
 * @private
 * @param {GVol.source.VectorTile} source Source.
 * @param {GVol.VectorTile} tile Tile.
 * @return {number} The tile's pixel ratio.
 */
GVol.renderer.canvas.VectorTileLayer.prototype.getTilePixelRatio_ = function(source, tile) {
  return GVol.extent.getWidth(tile.getExtent()) /
        GVol.size.toSize(source.getTileGrid().getTileSize(tile.tileCoord[0]))[0];
};


/**
 * Handle changes in image style state.
 * @param {GVol.events.Event} event Image style change event.
 * @private
 */
GVol.renderer.canvas.VectorTileLayer.prototype.handleStyleImageChange_ = function(event) {
  this.renderIfReadyAndVisible();
};


/**
 * @inheritDoc
 */
GVol.renderer.canvas.VectorTileLayer.prototype.postCompose = function(context, frameState, layerState) {
  var layer = this.getLayer();
  var source = /** @type {GVol.source.VectorTile} */ (layer.getSource());
  var renderMode = layer.getRenderMode();
  var replays = GVol.renderer.canvas.VectorTileLayer.VECTOR_REPLAYS[renderMode];
  var pixelRatio = frameState.pixelRatio;
  var rotation = frameState.viewState.rotation;
  var size = frameState.size;
  var offsetX = Math.round(pixelRatio * size[0] / 2);
  var offsetY = Math.round(pixelRatio * size[1] / 2);
  var tiles = this.renderedTiles;
  var sourceTileGrid = source.getTileGrid();
  var tileGrid = source.getTileGridForProjection(frameState.viewState.projection);
  var clips = [];
  var zs = [];
  for (var i = tiles.length - 1; i >= 0; --i) {
    var tile = /** @type {GVol.VectorImageTile} */ (tiles[i]);
    if (tile.getState() == GVol.TileState.ABORT) {
      continue;
    }
    var tileCoord = tile.tileCoord;
    var worldOffset = tileGrid.getTileCoordExtent(tileCoord)[0] -
        tileGrid.getTileCoordExtent(tile.wrappedTileCoord)[0];
    for (var t = 0, tt = tile.tileKeys.length; t < tt; ++t) {
      var sourceTile = tile.getTile(tile.tileKeys[t]);
      var tilePixelRatio = this.getTilePixelRatio_(source, sourceTile);
      var replayGroup = sourceTile.getReplayGroup(layer, tileCoord.toString());
      if (renderMode != GVol.layer.VectorTileRenderType.VECTOR && !replayGroup.hasReplays(replays)) {
        continue;
      }
      var currentZ = sourceTile.tileCoord[0];
      var sourceResGVolution = sourceTileGrid.getResGVolution(currentZ);
      var transform = this.getReplayTransform_(sourceTile, frameState);
      GVol.transform.translate(transform, worldOffset * tilePixelRatio / sourceResGVolution, 0);
      var currentClip = replayGroup.getClipCoords(transform);
      context.save();
      context.globalAlpha = layerState.opacity;
      GVol.render.canvas.rotateAtOffset(context, -rotation, offsetX, offsetY);
      // Create a clip mask for regions in this low resGVolution tile that are
      // already filled by a higher resGVolution tile
      for (var j = 0, jj = clips.length; j < jj; ++j) {
        var clip = clips[j];
        if (currentZ < zs[j]) {
          context.beginPath();
          // counter-clockwise (outer ring) for current tile
          context.moveTo(currentClip[0], currentClip[1]);
          context.lineTo(currentClip[2], currentClip[3]);
          context.lineTo(currentClip[4], currentClip[5]);
          context.lineTo(currentClip[6], currentClip[7]);
          // clockwise (inner ring) for higher resGVolution tile
          context.moveTo(clip[6], clip[7]);
          context.lineTo(clip[4], clip[5]);
          context.lineTo(clip[2], clip[3]);
          context.lineTo(clip[0], clip[1]);
          context.clip();
        }
      }
      replayGroup.replay(context, pixelRatio, transform, rotation, {}, replays);
      context.restore();
      clips.push(currentClip);
      zs.push(currentZ);
    }
  }
  GVol.renderer.canvas.TileLayer.prototype.postCompose.apply(this, arguments);
};


/**
 * @param {GVol.Feature|GVol.render.Feature} feature Feature.
 * @param {number} squaredTGVolerance Squared tGVolerance.
 * @param {(GVol.style.Style|Array.<GVol.style.Style>)} styles The style or array of
 *     styles.
 * @param {GVol.render.canvas.ReplayGroup} replayGroup Replay group.
 * @return {boGVolean} `true` if an image is loading.
 */
GVol.renderer.canvas.VectorTileLayer.prototype.renderFeature = function(feature, squaredTGVolerance, styles, replayGroup) {
  if (!styles) {
    return false;
  }
  var loading = false;
  if (Array.isArray(styles)) {
    for (var i = 0, ii = styles.length; i < ii; ++i) {
      loading = GVol.renderer.vector.renderFeature(
          replayGroup, feature, styles[i], squaredTGVolerance,
          this.handleStyleImageChange_, this) || loading;
    }
  } else {
    loading = GVol.renderer.vector.renderFeature(
        replayGroup, feature, styles, squaredTGVolerance,
        this.handleStyleImageChange_, this) || loading;
  }
  return loading;
};


/**
 * @param {GVol.VectorImageTile} tile Tile.
 * @param {GVolx.FrameState} frameState Frame state.
 * @param {GVol.LayerState} layerState Layer state.
 * @private
 */
GVol.renderer.canvas.VectorTileLayer.prototype.renderTileImage_ = function(
    tile, frameState, layerState) {
  var layer = this.getLayer();
  var replayState = tile.getReplayState(layer);
  var revision = layer.getRevision();
  var replays = GVol.renderer.canvas.VectorTileLayer.IMAGE_REPLAYS[layer.getRenderMode()];
  if (replays && replayState.renderedTileRevision !== revision) {
    replayState.renderedTileRevision = revision;
    var tileCoord = tile.wrappedTileCoord;
    var z = tileCoord[0];
    var pixelRatio = frameState.pixelRatio;
    var source = /** @type {GVol.source.VectorTile} */ (layer.getSource());
    var sourceTileGrid = source.getTileGrid();
    var tileGrid = source.getTileGridForProjection(frameState.viewState.projection);
    var resGVolution = tileGrid.getResGVolution(z);
    var context = tile.getContext(layer);
    var size = source.getTilePixelSize(z, pixelRatio, frameState.viewState.projection);
    context.canvas.width = size[0];
    context.canvas.height = size[1];
    var tileExtent = tileGrid.getTileCoordExtent(tileCoord);
    for (var i = 0, ii = tile.tileKeys.length; i < ii; ++i) {
      var sourceTile = tile.getTile(tile.tileKeys[i]);
      var tilePixelRatio = this.getTilePixelRatio_(source, sourceTile);
      var sourceTileCoord = sourceTile.tileCoord;
      var pixelScale = pixelRatio / resGVolution;
      var transform = GVol.transform.reset(this.tmpTransform_);
      if (sourceTile.getProjection().getUnits() == GVol.proj.Units.TILE_PIXELS) {
        var sourceTileExtent = sourceTileGrid.getTileCoordExtent(sourceTileCoord, this.tmpExtent);
        var sourceResGVolution = sourceTileGrid.getResGVolution(sourceTileCoord[0]);
        var renderPixelRatio = pixelRatio / tilePixelRatio * sourceResGVolution / resGVolution;
        GVol.transform.scale(transform, renderPixelRatio, renderPixelRatio);
        var offsetX = (sourceTileExtent[0] - tileExtent[0]) / sourceResGVolution * tilePixelRatio;
        var offsetY = (tileExtent[3] - sourceTileExtent[3]) / sourceResGVolution * tilePixelRatio;
        GVol.transform.translate(transform, Math.round(offsetX), Math.round(offsetY));
      } else {
        GVol.transform.scale(transform, pixelScale, -pixelScale);
        GVol.transform.translate(transform, -tileExtent[0], -tileExtent[3]);
      }
      var replayGroup = sourceTile.getReplayGroup(layer, tile.tileCoord.toString());
      replayGroup.replay(context, pixelRatio, transform, 0, {}, replays, true);
    }
  }
};

goog.provide('GVol.layer.VectorTile');

goog.require('GVol');
goog.require('GVol.asserts');
goog.require('GVol.layer.TileProperty');
goog.require('GVol.layer.Vector');
goog.require('GVol.layer.VectorTileRenderType');
goog.require('GVol.obj');
goog.require('GVol.renderer.Type');
goog.require('GVol.renderer.canvas.VectorTileLayer');


/**
 * @classdesc
 * Layer for vector tile data that is rendered client-side.
 * Note that any property set in the options is set as a {@link GVol.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 {GVol.layer.Vector}
 * @param {GVolx.layer.VectorTileOptions=} opt_options Options.
 * @api
 */
GVol.layer.VectorTile = function(opt_options) {
  var options = opt_options ? opt_options : {};

  var baseOptions = GVol.obj.assign({}, options);

  delete baseOptions.preload;
  delete baseOptions.useInterimTilesOnError;
  GVol.layer.Vector.call(this,  /** @type {GVolx.layer.VectorOptions} */ (baseOptions));

  this.setPreload(options.preload ? options.preload : 0);
  this.setUseInterimTilesOnError(options.useInterimTilesOnError ?
    options.useInterimTilesOnError : true);

  GVol.asserts.assert(options.renderMode == undefined ||
      options.renderMode == GVol.layer.VectorTileRenderType.IMAGE ||
      options.renderMode == GVol.layer.VectorTileRenderType.HYBRID ||
      options.renderMode == GVol.layer.VectorTileRenderType.VECTOR,
  28); // `renderMode` must be `'image'`, `'hybrid'` or `'vector'`

  /**
   * @private
   * @type {GVol.layer.VectorTileRenderType|string}
   */
  this.renderMode_ = options.renderMode || GVol.layer.VectorTileRenderType.HYBRID;

};
GVol.inherits(GVol.layer.VectorTile, GVol.layer.Vector);


/**
 * @inheritDoc
 */
GVol.layer.VectorTile.prototype.createRenderer = function(mapRenderer) {
  var renderer = null;
  var type = mapRenderer.getType();
  if (GVol.ENABLE_CANVAS && type === GVol.renderer.Type.CANVAS) {
    renderer = new GVol.renderer.canvas.VectorTileLayer(this);
  }
  return renderer;
};


/**
 * Return the level as number to which we will preload tiles up to.
 * @return {number} The level to preload tiles up to.
 * @observable
 * @api
 */
GVol.layer.VectorTile.prototype.getPreload = function() {
  return /** @type {number} */ (this.get(GVol.layer.TileProperty.PRELOAD));
};


/**
 * @return {GVol.layer.VectorTileRenderType|string} The render mode.
 */
GVol.layer.VectorTile.prototype.getRenderMode = function() {
  return this.renderMode_;
};


/**
 * Whether we use interim tiles on error.
 * @return {boGVolean} Use interim tiles on error.
 * @observable
 * @api
 */
GVol.layer.VectorTile.prototype.getUseInterimTilesOnError = function() {
  return /** @type {boGVolean} */ (
    this.get(GVol.layer.TileProperty.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
 */
GVol.layer.VectorTile.prototype.setPreload = function(preload) {
  this.set(GVol.layer.TileProperty.PRELOAD, preload);
};


/**
 * Set whether we use interim tiles on error.
 * @param {boGVolean} useInterimTilesOnError Use interim tiles on error.
 * @observable
 * @api
 */
GVol.layer.VectorTile.prototype.setUseInterimTilesOnError = function(useInterimTilesOnError) {
  this.set(
      GVol.layer.TileProperty.USE_INTERIM_TILES_ON_ERROR, useInterimTilesOnError);
};


/**
 * Return the associated {@link GVol.source.VectorTile vectortilesource} of the layer.
 * @function
 * @return {GVol.source.VectorTile} Source.
 * @api
 */
GVol.layer.VectorTile.prototype.getSource;

goog.provide('GVol.net');

goog.require('GVol');


/**
 * Simple JSONP helper. Supports error callbacks and a custom callback param.
 * The error callback will be called when no JSONP is executed after 10 seconds.
 *
 * @param {string} url Request url. A 'callback' query parameter will be
 *     appended.
 * @param {Function} callback Callback on success.
 * @param {function()=} opt_errback Callback on error.
 * @param {string=} opt_callbackParam Custom query parameter for the JSONP
 *     callback. Default is 'callback'.
 */
GVol.net.jsonp = function(url, callback, opt_errback, opt_callbackParam) {
  var script = document.createElement('script');
  var key = 'GVolc_' + GVol.getUid(callback);
  function cleanup() {
    delete window[key];
    script.parentNode.removeChild(script);
  }
  script.async = true;
  script.src = url + (url.indexOf('?') == -1 ? '?' : '&') +
      (opt_callbackParam || 'callback') + '=' + key;
  var timer = setTimeout(function() {
    cleanup();
    if (opt_errback) {
      opt_errback();
    }
  }, 10000);
  window[key] = function(data) {
    clearTimeout(timer);
    cleanup();
    callback(data);
  };
  document.getElementsByTagName('head')[0].appendChild(script);
};

goog.provide('GVol.proj.common');

goog.require('GVol.proj');


/**
 * Deprecated.  Transforms between EPSG:4326 and EPSG:3857 are now included by
 * default.  There is no need to call this function in application code and it
 * will be removed in a future major release.
 * @deprecated This function is no longer necessary.
 * @api
 */
GVol.proj.common.add = GVol.proj.addCommon;

goog.provide('GVol.render');

goog.require('GVol.has');
goog.require('GVol.transform');
goog.require('GVol.render.canvas.Immediate');


/**
 * 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 = GVol.render.toContext(canvas.getContext('2d'),
 *     { size: [100, 100] });
 * render.setFillStrokeStyle(new GVol.style.Fill({ cGVolor: blue }));
 * render.drawPGVolygon(
 *     new GVol.geom.PGVolygon([[[0, 0], [100, 100], [100, 0], [0, 0]]]));
 * ```
 *
 * @param {CanvasRenderingContext2D} context Canvas context.
 * @param {GVolx.render.ToContextOptions=} opt_options Options.
 * @return {GVol.render.canvas.Immediate} Canvas Immediate.
 * @api
 */
GVol.render.toContext = function(context, opt_options) {
  var canvas = context.canvas;
  var options = opt_options ? opt_options : {};
  var pixelRatio = options.pixelRatio || GVol.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 = GVol.transform.scale(GVol.transform.create(), pixelRatio, pixelRatio);
  return new GVol.render.canvas.Immediate(context, pixelRatio, extent, transform,
      0);
};

goog.provide('GVol.reproj.Tile');

goog.require('GVol');
goog.require('GVol.Tile');
goog.require('GVol.TileState');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.extent');
goog.require('GVol.math');
goog.require('GVol.reproj');
goog.require('GVol.reproj.Triangulation');


/**
 * @classdesc
 * Class encapsulating single reprojected tile.
 * See {@link GVol.source.TileImage}.
 *
 * @constructor
 * @extends {GVol.Tile}
 * @param {GVol.proj.Projection} sourceProj Source projection.
 * @param {GVol.tilegrid.TileGrid} sourceTileGrid Source tile grid.
 * @param {GVol.proj.Projection} targetProj Target projection.
 * @param {GVol.tilegrid.TileGrid} targetTileGrid Target tile grid.
 * @param {GVol.TileCoord} tileCoord Coordinate of the tile.
 * @param {GVol.TileCoord} wrappedTileCoord Coordinate of the tile wrapped in X.
 * @param {number} pixelRatio Pixel ratio.
 * @param {number} gutter Gutter of the source tiles.
 * @param {GVol.ReprojTileFunctionType} getTileFunction
 *     Function returning source tiles (z, x, y, pixelRatio).
 * @param {number=} opt_errorThreshGVold Acceptable reprojection error (in px).
 * @param {boGVolean=} opt_renderEdges Render reprojection edges.
 */
GVol.reproj.Tile = function(sourceProj, sourceTileGrid,
    targetProj, targetTileGrid, tileCoord, wrappedTileCoord,
    pixelRatio, gutter, getTileFunction,
    opt_errorThreshGVold,
    opt_renderEdges) {
  GVol.Tile.call(this, tileCoord, GVol.TileState.IDLE);

  /**
   * @private
   * @type {boGVolean}
   */
  this.renderEdges_ = opt_renderEdges !== undefined ? opt_renderEdges : false;

  /**
   * @private
   * @type {number}
   */
  this.pixelRatio_ = pixelRatio;

  /**
   * @private
   * @type {number}
   */
  this.gutter_ = gutter;

  /**
   * @private
   * @type {HTMLCanvasElement}
   */
  this.canvas_ = null;

  /**
   * @private
   * @type {GVol.tilegrid.TileGrid}
   */
  this.sourceTileGrid_ = sourceTileGrid;

  /**
   * @private
   * @type {GVol.tilegrid.TileGrid}
   */
  this.targetTileGrid_ = targetTileGrid;

  /**
   * @private
   * @type {GVol.TileCoord}
   */
  this.wrappedTileCoord_ = wrappedTileCoord ? wrappedTileCoord : tileCoord;

  /**
   * @private
   * @type {!Array.<GVol.Tile>}
   */
  this.sourceTiles_ = [];

  /**
   * @private
   * @type {Array.<GVol.EventsKey>}
   */
  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 ?
    GVol.extent.getIntersection(targetExtent, maxTargetExtent) : targetExtent;

  if (GVol.extent.getArea(limitedTargetExtent) === 0) {
    // Tile is completely outside range -> EMPTY
    // TODO: is it actually correct that the source even creates the tile ?
    this.state = GVol.TileState.EMPTY;
    return;
  }

  var sourceProjExtent = sourceProj.getExtent();
  if (sourceProjExtent) {
    if (!maxSourceExtent) {
      maxSourceExtent = sourceProjExtent;
    } else {
      maxSourceExtent = GVol.extent.getIntersection(
          maxSourceExtent, sourceProjExtent);
    }
  }

  var targetResGVolution = targetTileGrid.getResGVolution(
      this.wrappedTileCoord_[0]);

  var targetCenter = GVol.extent.getCenter(limitedTargetExtent);
  var sourceResGVolution = GVol.reproj.calculateSourceResGVolution(
      sourceProj, targetProj, targetCenter, targetResGVolution);

  if (!isFinite(sourceResGVolution) || sourceResGVolution <= 0) {
    // invalid sourceResGVolution -> EMPTY
    // probably edges of the projections when no extent is defined
    this.state = GVol.TileState.EMPTY;
    return;
  }

  var errorThreshGVoldInPixels = opt_errorThreshGVold !== undefined ?
    opt_errorThreshGVold : GVol.DEFAULT_RASTER_REPROJECTION_ERROR_THRESHOLD;

  /**
   * @private
   * @type {!GVol.reproj.Triangulation}
   */
  this.triangulation_ = new GVol.reproj.Triangulation(
      sourceProj, targetProj, limitedTargetExtent, maxSourceExtent,
      sourceResGVolution * errorThreshGVoldInPixels);

  if (this.triangulation_.getTriangles().length === 0) {
    // no valid triangles -> EMPTY
    this.state = GVol.TileState.EMPTY;
    return;
  }

  this.sourceZ_ = sourceTileGrid.getZForResGVolution(sourceResGVolution);
  var sourceExtent = this.triangulation_.calculateSourceExtent();

  if (maxSourceExtent) {
    if (sourceProj.canWrapX()) {
      sourceExtent[1] = GVol.math.clamp(
          sourceExtent[1], maxSourceExtent[1], maxSourceExtent[3]);
      sourceExtent[3] = GVol.math.clamp(
          sourceExtent[3], maxSourceExtent[1], maxSourceExtent[3]);
    } else {
      sourceExtent = GVol.extent.getIntersection(sourceExtent, maxSourceExtent);
    }
  }

  if (!GVol.extent.getArea(sourceExtent)) {
    this.state = GVol.TileState.EMPTY;
  } else {
    var sourceRange = sourceTileGrid.getTileRangeForExtentAndZ(
        sourceExtent, this.sourceZ_);

    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 = GVol.TileState.EMPTY;
    }
  }
};
GVol.inherits(GVol.reproj.Tile, GVol.Tile);


/**
 * @inheritDoc
 */
GVol.reproj.Tile.prototype.disposeInternal = function() {
  if (this.state == GVol.TileState.LOADING) {
    this.unlistenSources_();
  }
  GVol.Tile.prototype.disposeInternal.call(this);
};


/**
 * Get the HTML Canvas element for this tile.
 * @return {HTMLCanvasElement} Canvas.
 */
GVol.reproj.Tile.prototype.getImage = function() {
  return this.canvas_;
};


/**
 * @private
 */
GVol.reproj.Tile.prototype.reproject_ = function() {
  var sources = [];
  this.sourceTiles_.forEach(function(tile, i, arr) {
    if (tile && tile.getState() == GVol.TileState.LOADED) {
      sources.push({
        extent: this.sourceTileGrid_.getTileCoordExtent(tile.tileCoord),
        image: tile.getImage()
      });
    }
  }, this);
  this.sourceTiles_.length = 0;

  if (sources.length === 0) {
    this.state = GVol.TileState.ERROR;
  } else {
    var z = this.wrappedTileCoord_[0];
    var size = this.targetTileGrid_.getTileSize(z);
    var width = typeof size === 'number' ? size : size[0];
    var height = typeof size === 'number' ? size : size[1];
    var targetResGVolution = this.targetTileGrid_.getResGVolution(z);
    var sourceResGVolution = this.sourceTileGrid_.getResGVolution(this.sourceZ_);

    var targetExtent = this.targetTileGrid_.getTileCoordExtent(
        this.wrappedTileCoord_);
    this.canvas_ = GVol.reproj.render(width, height, this.pixelRatio_,
        sourceResGVolution, this.sourceTileGrid_.getExtent(),
        targetResGVolution, targetExtent, this.triangulation_, sources,
        this.gutter_, this.renderEdges_);

    this.state = GVol.TileState.LOADED;
  }
  this.changed();
};


/**
 * @inheritDoc
 */
GVol.reproj.Tile.prototype.load = function() {
  if (this.state == GVol.TileState.IDLE) {
    this.state = GVol.TileState.LOADING;
    this.changed();

    var leftToLoad = 0;

    this.sourcesListenerKeys_ = [];
    this.sourceTiles_.forEach(function(tile, i, arr) {
      var state = tile.getState();
      if (state == GVol.TileState.IDLE || state == GVol.TileState.LOADING) {
        leftToLoad++;

        var sourceListenKey;
        sourceListenKey = GVol.events.listen(tile, GVol.events.EventType.CHANGE,
            function(e) {
              var state = tile.getState();
              if (state == GVol.TileState.LOADED ||
                  state == GVol.TileState.ERROR ||
                  state == GVol.TileState.EMPTY) {
                GVol.events.unlistenByKey(sourceListenKey);
                leftToLoad--;
                if (leftToLoad === 0) {
                  this.unlistenSources_();
                  this.reproject_();
                }
              }
            }, this);
        this.sourcesListenerKeys_.push(sourceListenKey);
      }
    }, this);

    this.sourceTiles_.forEach(function(tile, i, arr) {
      var state = tile.getState();
      if (state == GVol.TileState.IDLE) {
        tile.load();
      }
    });

    if (leftToLoad === 0) {
      setTimeout(this.reproject_.bind(this), 0);
    }
  }
};


/**
 * @private
 */
GVol.reproj.Tile.prototype.unlistenSources_ = function() {
  this.sourcesListenerKeys_.forEach(GVol.events.unlistenByKey);
  this.sourcesListenerKeys_ = null;
};

goog.provide('GVol.TileUrlFunction');

goog.require('GVol.asserts');
goog.require('GVol.math');
goog.require('GVol.tilecoord');


/**
 * @param {string} template Template.
 * @param {GVol.tilegrid.TileGrid} tileGrid Tile grid.
 * @return {GVol.TileUrlFunctionType} Tile URL function.
 */
GVol.TileUrlFunction.createFromTemplate = function(template, tileGrid) {
  var zRegEx = /\{z\}/g;
  var xRegEx = /\{x\}/g;
  var yRegEx = /\{y\}/g;
  var dashYRegEx = /\{-y\}/g;
  return (
    /**
     * @param {GVol.TileCoord} tileCoord Tile Coordinate.
     * @param {number} pixelRatio Pixel ratio.
     * @param {GVol.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);
              GVol.asserts.assert(range, 55); // The {-y} placehGVolder requires a tile grid with extent
              var y = range.getHeight() + tileCoord[2];
              return y.toString();
            });
      }
    });
};


/**
 * @param {Array.<string>} templates Templates.
 * @param {GVol.tilegrid.TileGrid} tileGrid Tile grid.
 * @return {GVol.TileUrlFunctionType} Tile URL function.
 */
GVol.TileUrlFunction.createFromTemplates = function(templates, tileGrid) {
  var len = templates.length;
  var tileUrlFunctions = new Array(len);
  for (var i = 0; i < len; ++i) {
    tileUrlFunctions[i] = GVol.TileUrlFunction.createFromTemplate(
        templates[i], tileGrid);
  }
  return GVol.TileUrlFunction.createFromTileUrlFunctions(tileUrlFunctions);
};


/**
 * @param {Array.<GVol.TileUrlFunctionType>} tileUrlFunctions Tile URL Functions.
 * @return {GVol.TileUrlFunctionType} Tile URL function.
 */
GVol.TileUrlFunction.createFromTileUrlFunctions = function(tileUrlFunctions) {
  if (tileUrlFunctions.length === 1) {
    return tileUrlFunctions[0];
  }
  return (
    /**
     * @param {GVol.TileCoord} tileCoord Tile Coordinate.
     * @param {number} pixelRatio Pixel ratio.
     * @param {GVol.proj.Projection} projection Projection.
     * @return {string|undefined} Tile URL.
     */
    function(tileCoord, pixelRatio, projection) {
      if (!tileCoord) {
        return undefined;
      } else {
        var h = GVol.tilecoord.hash(tileCoord);
        var index = GVol.math.modulo(h, tileUrlFunctions.length);
        return tileUrlFunctions[index](tileCoord, pixelRatio, projection);
      }
    });
};


/**
 * @param {GVol.TileCoord} tileCoord Tile coordinate.
 * @param {number} pixelRatio Pixel ratio.
 * @param {GVol.proj.Projection} projection Projection.
 * @return {string|undefined} Tile URL.
 */
GVol.TileUrlFunction.nullTileUrlFunction = function(tileCoord, pixelRatio, projection) {
  return undefined;
};


/**
 * @param {string} url URL.
 * @return {Array.<string>} Array of urls.
 */
GVol.TileUrlFunction.expandUrl = function(url) {
  var urls = [];
  var match = /\{([a-z])-([a-z])\}/.exec(url);
  if (match) {
    // char range
    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)));
    }
    return urls;
  }
  match = match = /\{(\d+)-(\d+)\}/.exec(url);
  if (match) {
    // number range
    var stop = parseInt(match[2], 10);
    for (var i = parseInt(match[1], 10); i <= stop; i++) {
      urls.push(url.replace(match[0], i.toString()));
    }
    return urls;
  }
  urls.push(url);
  return urls;
};

goog.provide('GVol.TileCache');

goog.require('GVol');
goog.require('GVol.structs.LRUCache');


/**
 * @constructor
 * @extends {GVol.structs.LRUCache.<GVol.Tile>}
 * @param {number=} opt_highWaterMark High water mark.
 * @struct
 */
GVol.TileCache = function(opt_highWaterMark) {

  GVol.structs.LRUCache.call(this);

  /**
   * @type {number}
   */
  this.highWaterMark = opt_highWaterMark !== undefined ? opt_highWaterMark : 2048;

};
GVol.inherits(GVol.TileCache, GVol.structs.LRUCache);


/**
 * @return {boGVolean} Can expire cache.
 */
GVol.TileCache.prototype.canExpireCache = function() {
  return this.getCount() > this.highWaterMark;
};


/**
 * @param {Object.<string, GVol.TileRange>} usedTiles Used tiles.
 */
GVol.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();
    }
  }
};

goog.provide('GVol.source.Tile');

goog.require('GVol');
goog.require('GVol.TileCache');
goog.require('GVol.TileState');
goog.require('GVol.events.Event');
goog.require('GVol.proj');
goog.require('GVol.size');
goog.require('GVol.source.Source');
goog.require('GVol.tilecoord');
goog.require('GVol.tilegrid');


/**
 * @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
 * @abstract
 * @extends {GVol.source.Source}
 * @param {GVol.SourceTileOptions} options Tile source options.
 * @api
 */
GVol.source.Tile = function(options) {

  GVol.source.Source.call(this, {
    attributions: options.attributions,
    extent: options.extent,
    logo: options.logo,
    projection: options.projection,
    state: options.state,
    wrapX: options.wrapX
  });

  /**
   * @private
   * @type {boGVolean}
   */
  this.opaque_ = options.opaque !== undefined ? options.opaque : false;

  /**
   * @private
   * @type {number}
   */
  this.tilePixelRatio_ = options.tilePixelRatio !== undefined ?
    options.tilePixelRatio : 1;

  /**
   * @protected
   * @type {GVol.tilegrid.TileGrid}
   */
  this.tileGrid = options.tileGrid !== undefined ? options.tileGrid : null;

  /**
   * @protected
   * @type {GVol.TileCache}
   */
  this.tileCache = new GVol.TileCache(options.cacheSize);

  /**
   * @protected
   * @type {GVol.Size}
   */
  this.tmpSize = [0, 0];

  /**
   * @private
   * @type {string}
   */
  this.key_ = '';

};
GVol.inherits(GVol.source.Tile, GVol.source.Source);


/**
 * @return {boGVolean} Can expire cache.
 */
GVol.source.Tile.prototype.canExpireCache = function() {
  return this.tileCache.canExpireCache();
};


/**
 * @param {GVol.proj.Projection} projection Projection.
 * @param {Object.<string, GVol.TileRange>} usedTiles Used tiles.
 */
GVol.source.Tile.prototype.expireCache = function(projection, usedTiles) {
  var tileCache = this.getTileCacheForProjection(projection);
  if (tileCache) {
    tileCache.expireCache(usedTiles);
  }
};


/**
 * @param {GVol.proj.Projection} projection Projection.
 * @param {number} z Zoom level.
 * @param {GVol.TileRange} tileRange Tile range.
 * @param {function(GVol.Tile):(boGVolean|undefined)} callback Called with each
 *     loaded tile.  If the callback returns `false`, the tile will not be
 *     considered loaded.
 * @return {boGVolean} The tile range is fully covered with loaded tiles.
 */
GVol.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 {!GVol.Tile} */ (tileCache.get(tileCoordKey));
        loaded = tile.getState() === GVol.TileState.LOADED;
        if (loaded) {
          loaded = (callback(tile) !== false);
        }
      }
      if (!loaded) {
        covered = false;
      }
    }
  }
  return covered;
};


/**
 * @param {GVol.proj.Projection} projection Projection.
 * @return {number} Gutter.
 */
GVol.source.Tile.prototype.getGutter = function(projection) {
  return 0;
};


/**
 * Return the key to be used for all tiles in the source.
 * @return {string} The key for all tiles.
 * @protected
 */
GVol.source.Tile.prototype.getKey = function() {
  return this.key_;
};


/**
 * Set the value to be used as the key for all tiles in the source.
 * @param {string} key The key for tiles.
 * @protected
 */
GVol.source.Tile.prototype.setKey = function(key) {
  if (this.key_ !== key) {
    this.key_ = key;
    this.changed();
  }
};


/**
 * @param {number} z Z.
 * @param {number} x X.
 * @param {number} y Y.
 * @return {string} Key.
 * @protected
 */
GVol.source.Tile.prototype.getKeyZXY = GVol.tilecoord.getKeyZXY;


/**
 * @param {GVol.proj.Projection} projection Projection.
 * @return {boGVolean} Opaque.
 */
GVol.source.Tile.prototype.getOpaque = function(projection) {
  return this.opaque_;
};


/**
 * @inheritDoc
 */
GVol.source.Tile.prototype.getResGVolutions = function() {
  return this.tileGrid.getResGVolutions();
};


/**
 * @abstract
 * @param {number} z Tile coordinate z.
 * @param {number} x Tile coordinate x.
 * @param {number} y Tile coordinate y.
 * @param {number} pixelRatio Pixel ratio.
 * @param {GVol.proj.Projection} projection Projection.
 * @return {!GVol.Tile} Tile.
 */
GVol.source.Tile.prototype.getTile = function(z, x, y, pixelRatio, projection) {};


/**
 * Return the tile grid of the tile source.
 * @return {GVol.tilegrid.TileGrid} Tile grid.
 * @api
 */
GVol.source.Tile.prototype.getTileGrid = function() {
  return this.tileGrid;
};


/**
 * @param {GVol.proj.Projection} projection Projection.
 * @return {!GVol.tilegrid.TileGrid} Tile grid.
 */
GVol.source.Tile.prototype.getTileGridForProjection = function(projection) {
  if (!this.tileGrid) {
    return GVol.tilegrid.getForProjection(projection);
  } else {
    return this.tileGrid;
  }
};


/**
 * @param {GVol.proj.Projection} projection Projection.
 * @return {GVol.TileCache} Tile cache.
 * @protected
 */
GVol.source.Tile.prototype.getTileCacheForProjection = function(projection) {
  var thisProj = this.getProjection();
  if (thisProj && !GVol.proj.equivalent(thisProj, projection)) {
    return null;
  } else {
    return this.tileCache;
  }
};


/**
 * Get the tile pixel ratio for this source. Subclasses may override this
 * method, which is meant to return a supported pixel ratio that matches the
 * provided `pixelRatio` as close as possible.
 * @param {number} pixelRatio Pixel ratio.
 * @return {number} Tile pixel ratio.
 */
GVol.source.Tile.prototype.getTilePixelRatio = function(pixelRatio) {
  return this.tilePixelRatio_;
};


/**
 * @param {number} z Z.
 * @param {number} pixelRatio Pixel ratio.
 * @param {GVol.proj.Projection} projection Projection.
 * @return {GVol.Size} Tile size.
 */
GVol.source.Tile.prototype.getTilePixelSize = function(z, pixelRatio, projection) {
  var tileGrid = this.getTileGridForProjection(projection);
  var tilePixelRatio = this.getTilePixelRatio(pixelRatio);
  var tileSize = GVol.size.toSize(tileGrid.getTileSize(z), this.tmpSize);
  if (tilePixelRatio == 1) {
    return tileSize;
  } else {
    return GVol.size.scale(tileSize, tilePixelRatio, this.tmpSize);
  }
};


/**
 * Returns a tile coordinate wrapped around the x-axis. When the tile coordinate
 * is outside the resGVolution and extent range of the tile grid, `null` will be
 * returned.
 * @param {GVol.TileCoord} tileCoord Tile coordinate.
 * @param {GVol.proj.Projection=} opt_projection Projection.
 * @return {GVol.TileCoord} Tile coordinate to be passed to the tileUrlFunction or
 *     null if no tile URL should be created for the passed `tileCoord`.
 */
GVol.source.Tile.prototype.getTileCoordForTileUrlFunction = function(tileCoord, opt_projection) {
  var projection = opt_projection !== undefined ?
    opt_projection : this.getProjection();
  var tileGrid = this.getTileGridForProjection(projection);
  if (this.getWrapX() && projection.isGlobal()) {
    tileCoord = GVol.tilegrid.wrapX(tileGrid, tileCoord, projection);
  }
  return GVol.tilecoord.withinExtentAndZ(tileCoord, tileGrid) ? tileCoord : null;
};


/**
 * @inheritDoc
 */
GVol.source.Tile.prototype.refresh = function() {
  this.tileCache.clear();
  this.changed();
};


/**
 * 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 {GVol.proj.Projection} projection Projection.
 */
GVol.source.Tile.prototype.useTile = GVol.nullFunction;


/**
 * @classdesc
 * Events emitted by {@link GVol.source.Tile} instances are instances of this
 * type.
 *
 * @constructor
 * @extends {GVol.events.Event}
 * @implements {GVoli.source.Tile.Event}
 * @param {string} type Type.
 * @param {GVol.Tile} tile The tile.
 */
GVol.source.Tile.Event = function(type, tile) {

  GVol.events.Event.call(this, type);

  /**
   * The tile related to the event.
   * @type {GVol.Tile}
   * @api
   */
  this.tile = tile;

};
GVol.inherits(GVol.source.Tile.Event, GVol.events.Event);

goog.provide('GVol.source.TileEventType');

/**
 * @enum {string}
 */
GVol.source.TileEventType = {

  /**
   * Triggered when a tile starts loading.
   * @event GVol.source.Tile.Event#tileloadstart
   * @api
   */
  TILELOADSTART: 'tileloadstart',

  /**
   * Triggered when a tile finishes loading.
   * @event GVol.source.Tile.Event#tileloadend
   * @api
   */
  TILELOADEND: 'tileloadend',

  /**
   * Triggered if tile loading results in an error.
   * @event GVol.source.Tile.Event#tileloaderror
   * @api
   */
  TILELOADERROR: 'tileloaderror'

};

goog.provide('GVol.source.UrlTile');

goog.require('GVol');
goog.require('GVol.TileState');
goog.require('GVol.TileUrlFunction');
goog.require('GVol.source.Tile');
goog.require('GVol.source.TileEventType');


/**
 * @classdesc
 * Base class for sources providing tiles divided into a tile grid over http.
 *
 * @constructor
 * @abstract
 * @fires GVol.source.Tile.Event
 * @extends {GVol.source.Tile}
 * @param {GVol.SourceUrlTileOptions} options Image tile options.
 */
GVol.source.UrlTile = function(options) {

  GVol.source.Tile.call(this, {
    attributions: options.attributions,
    cacheSize: options.cacheSize,
    extent: options.extent,
    logo: options.logo,
    opaque: options.opaque,
    projection: options.projection,
    state: options.state,
    tileGrid: options.tileGrid,
    tilePixelRatio: options.tilePixelRatio,
    wrapX: options.wrapX
  });

  /**
   * @protected
   * @type {GVol.TileLoadFunctionType}
   */
  this.tileLoadFunction = options.tileLoadFunction;

  /**
   * @protected
   * @type {GVol.TileUrlFunctionType}
   */
  this.tileUrlFunction = this.fixedTileUrlFunction ?
    this.fixedTileUrlFunction.bind(this) :
    GVol.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);
  }

};
GVol.inherits(GVol.source.UrlTile, GVol.source.Tile);


/**
 * @type {GVol.TileUrlFunctionType|undefined}
 * @protected
 */
GVol.source.UrlTile.prototype.fixedTileUrlFunction;

/**
 * Return the tile load function of the source.
 * @return {GVol.TileLoadFunctionType} TileLoadFunction
 * @api
 */
GVol.source.UrlTile.prototype.getTileLoadFunction = function() {
  return this.tileLoadFunction;
};


/**
 * Return the tile URL function of the source.
 * @return {GVol.TileUrlFunctionType} TileUrlFunction
 * @api
 */
GVol.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
 */
GVol.source.UrlTile.prototype.getUrls = function() {
  return this.urls;
};


/**
 * Handle tile change events.
 * @param {GVol.events.Event} event Event.
 * @protected
 */
GVol.source.UrlTile.prototype.handleTileChange = function(event) {
  var tile = /** @type {GVol.Tile} */ (event.target);
  switch (tile.getState()) {
    case GVol.TileState.LOADING:
      this.dispatchEvent(
          new GVol.source.Tile.Event(GVol.source.TileEventType.TILELOADSTART, tile));
      break;
    case GVol.TileState.LOADED:
      this.dispatchEvent(
          new GVol.source.Tile.Event(GVol.source.TileEventType.TILELOADEND, tile));
      break;
    case GVol.TileState.ERROR:
      this.dispatchEvent(
          new GVol.source.Tile.Event(GVol.source.TileEventType.TILELOADERROR, tile));
      break;
    default:
      // pass
  }
};


/**
 * Set the tile load function of the source.
 * @param {GVol.TileLoadFunctionType} tileLoadFunction Tile load function.
 * @api
 */
GVol.source.UrlTile.prototype.setTileLoadFunction = function(tileLoadFunction) {
  this.tileCache.clear();
  this.tileLoadFunction = tileLoadFunction;
  this.changed();
};


/**
 * Set the tile URL function of the source.
 * @param {GVol.TileUrlFunctionType} tileUrlFunction Tile URL function.
 * @param {string=} opt_key Optional new tile key for the source.
 * @api
 */
GVol.source.UrlTile.prototype.setTileUrlFunction = function(tileUrlFunction, opt_key) {
  this.tileUrlFunction = tileUrlFunction;
  if (typeof opt_key !== 'undefined') {
    this.setKey(opt_key);
  } else {
    this.changed();
  }
};


/**
 * Set the URL to use for requests.
 * @param {string} url URL.
 * @api
 */
GVol.source.UrlTile.prototype.setUrl = function(url) {
  var urls = this.urls = GVol.TileUrlFunction.expandUrl(url);
  this.setTileUrlFunction(this.fixedTileUrlFunction ?
    this.fixedTileUrlFunction.bind(this) :
    GVol.TileUrlFunction.createFromTemplates(urls, this.tileGrid), url);
};


/**
 * Set the URLs to use for requests.
 * @param {Array.<string>} urls URLs.
 * @api
 */
GVol.source.UrlTile.prototype.setUrls = function(urls) {
  this.urls = urls;
  var key = urls.join('\n');
  this.setTileUrlFunction(this.fixedTileUrlFunction ?
    this.fixedTileUrlFunction.bind(this) :
    GVol.TileUrlFunction.createFromTemplates(urls, this.tileGrid), key);
};


/**
 * @inheritDoc
 */
GVol.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('GVol.source.TileImage');

goog.require('GVol');
goog.require('GVol.ImageTile');
goog.require('GVol.TileCache');
goog.require('GVol.TileState');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.proj');
goog.require('GVol.reproj.Tile');
goog.require('GVol.source.UrlTile');
goog.require('GVol.tilegrid');


/**
 * @classdesc
 * Base class for sources providing images divided into a tile grid.
 *
 * @constructor
 * @fires GVol.source.Tile.Event
 * @extends {GVol.source.UrlTile}
 * @param {GVolx.source.TileImageOptions} options Image tile options.
 * @api
 */
GVol.source.TileImage = function(options) {

  GVol.source.UrlTile.call(this, {
    attributions: options.attributions,
    cacheSize: options.cacheSize,
    extent: options.extent,
    logo: options.logo,
    opaque: options.opaque,
    projection: options.projection,
    state: options.state,
    tileGrid: options.tileGrid,
    tileLoadFunction: options.tileLoadFunction ?
      options.tileLoadFunction : GVol.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: GVol.ImageTile, GVol.TileCoord, GVol.TileState, string,
   *        ?string, GVol.TileLoadFunctionType)}
   */
  this.tileClass = options.tileClass !== undefined ?
    options.tileClass : GVol.ImageTile;

  /**
   * @protected
   * @type {Object.<string, GVol.TileCache>}
   */
  this.tileCacheForProjection = {};

  /**
   * @protected
   * @type {Object.<string, GVol.tilegrid.TileGrid>}
   */
  this.tileGridForProjection = {};

  /**
   * @private
   * @type {number|undefined}
   */
  this.reprojectionErrorThreshGVold_ = options.reprojectionErrorThreshGVold;

  /**
   * @private
   * @type {boGVolean}
   */
  this.renderReprojectionEdges_ = false;
};
GVol.inherits(GVol.source.TileImage, GVol.source.UrlTile);


/**
 * @inheritDoc
 */
GVol.source.TileImage.prototype.canExpireCache = function() {
  if (!GVol.ENABLE_RASTER_REPROJECTION) {
    return GVol.source.UrlTile.prototype.canExpireCache.call(this);
  }
  if (this.tileCache.canExpireCache()) {
    return true;
  } else {
    for (var key in this.tileCacheForProjection) {
      if (this.tileCacheForProjection[key].canExpireCache()) {
        return true;
      }
    }
  }
  return false;
};


/**
 * @inheritDoc
 */
GVol.source.TileImage.prototype.expireCache = function(projection, usedTiles) {
  if (!GVol.ENABLE_RASTER_REPROJECTION) {
    GVol.source.UrlTile.prototype.expireCache.call(this, projection, usedTiles);
    return;
  }
  var usedTileCache = this.getTileCacheForProjection(projection);

  this.tileCache.expireCache(this.tileCache == usedTileCache ? usedTiles : {});
  for (var id in this.tileCacheForProjection) {
    var tileCache = this.tileCacheForProjection[id];
    tileCache.expireCache(tileCache == usedTileCache ? usedTiles : {});
  }
};


/**
 * @inheritDoc
 */
GVol.source.TileImage.prototype.getGutter = function(projection) {
  if (GVol.ENABLE_RASTER_REPROJECTION &&
      this.getProjection() && projection &&
      !GVol.proj.equivalent(this.getProjection(), projection)) {
    return 0;
  } else {
    return this.getGutterInternal();
  }
};


/**
 * @protected
 * @return {number} Gutter.
 */
GVol.source.TileImage.prototype.getGutterInternal = function() {
  return 0;
};


/**
 * @inheritDoc
 */
GVol.source.TileImage.prototype.getOpaque = function(projection) {
  if (GVol.ENABLE_RASTER_REPROJECTION &&
      this.getProjection() && projection &&
      !GVol.proj.equivalent(this.getProjection(), projection)) {
    return false;
  } else {
    return GVol.source.UrlTile.prototype.getOpaque.call(this, projection);
  }
};


/**
 * @inheritDoc
 */
GVol.source.TileImage.prototype.getTileGridForProjection = function(projection) {
  if (!GVol.ENABLE_RASTER_REPROJECTION) {
    return GVol.source.UrlTile.prototype.getTileGridForProjection.call(this, projection);
  }
  var thisProj = this.getProjection();
  if (this.tileGrid &&
      (!thisProj || GVol.proj.equivalent(thisProj, projection))) {
    return this.tileGrid;
  } else {
    var projKey = GVol.getUid(projection).toString();
    if (!(projKey in this.tileGridForProjection)) {
      this.tileGridForProjection[projKey] =
          GVol.tilegrid.getForProjection(projection);
    }
    return /** @type {!GVol.tilegrid.TileGrid} */ (this.tileGridForProjection[projKey]);
  }
};


/**
 * @inheritDoc
 */
GVol.source.TileImage.prototype.getTileCacheForProjection = function(projection) {
  if (!GVol.ENABLE_RASTER_REPROJECTION) {
    return GVol.source.UrlTile.prototype.getTileCacheForProjection.call(this, projection);
  }
  var thisProj = this.getProjection();
  if (!thisProj || GVol.proj.equivalent(thisProj, projection)) {
    return this.tileCache;
  } else {
    var projKey = GVol.getUid(projection).toString();
    if (!(projKey in this.tileCacheForProjection)) {
      this.tileCacheForProjection[projKey] = new GVol.TileCache(this.tileCache.highWaterMark);
    }
    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 {GVol.proj.Projection} projection Projection.
 * @param {string} key The key set on the tile.
 * @return {!GVol.Tile} Tile.
 * @private
 */
GVol.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 ? GVol.TileState.IDLE : GVol.TileState.EMPTY,
      tileUrl !== undefined ? tileUrl : '',
      this.crossOrigin,
      this.tileLoadFunction);
  tile.key = key;
  GVol.events.listen(tile, GVol.events.EventType.CHANGE,
      this.handleTileChange, this);
  return tile;
};


/**
 * @inheritDoc
 */
GVol.source.TileImage.prototype.getTile = function(z, x, y, pixelRatio, projection) {
  if (!GVol.ENABLE_RASTER_REPROJECTION ||
      !this.getProjection() ||
      !projection ||
      GVol.proj.equivalent(this.getProjection(), projection)) {
    return this.getTileInternal(z, x, y, pixelRatio, /** @type {!GVol.proj.Projection} */ (projection));
  } else {
    var cache = this.getTileCacheForProjection(projection);
    var tileCoord = [z, x, y];
    var tile;
    var tileCoordKey = this.getKeyZXY.apply(this, tileCoord);
    if (cache.containsKey(tileCoordKey)) {
      tile = /** @type {!GVol.Tile} */ (cache.get(tileCoordKey));
    }
    var key = this.getKey();
    if (tile && tile.key == key) {
      return tile;
    } else {
      var sourceProjection = /** @type {!GVol.proj.Projection} */ (this.getProjection());
      var sourceTileGrid = this.getTileGridForProjection(sourceProjection);
      var targetTileGrid = this.getTileGridForProjection(projection);
      var wrappedTileCoord =
          this.getTileCoordForTileUrlFunction(tileCoord, projection);
      var newTile = new GVol.reproj.Tile(
          sourceProjection, sourceTileGrid,
          projection, targetTileGrid,
          tileCoord, wrappedTileCoord, this.getTilePixelRatio(pixelRatio),
          this.getGutterInternal(),
          function(z, x, y, pixelRatio) {
            return this.getTileInternal(z, x, y, pixelRatio, sourceProjection);
          }.bind(this), this.reprojectionErrorThreshGVold_,
          this.renderReprojectionEdges_);
      newTile.key = key;

      if (tile) {
        newTile.interimTile = tile;
        newTile.refreshInterimChain();
        cache.replace(tileCoordKey, newTile);
      } else {
        cache.set(tileCoordKey, newTile);
      }
      return newTile;
    }
  }
};


/**
 * @param {number} z Tile coordinate z.
 * @param {number} x Tile coordinate x.
 * @param {number} y Tile coordinate y.
 * @param {number} pixelRatio Pixel ratio.
 * @param {!GVol.proj.Projection} projection Projection.
 * @return {!GVol.Tile} Tile.
 * @protected
 */
GVol.source.TileImage.prototype.getTileInternal = function(z, x, y, pixelRatio, projection) {
  var tile = null;
  var tileCoordKey = this.getKeyZXY(z, x, y);
  var key = this.getKey();
  if (!this.tileCache.containsKey(tileCoordKey)) {
    tile = this.createTile_(z, x, y, pixelRatio, projection, key);
    this.tileCache.set(tileCoordKey, tile);
  } else {
    tile = this.tileCache.get(tileCoordKey);
    if (tile.key != key) {
      // 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 interimTile = tile;
      tile = this.createTile_(z, x, y, pixelRatio, projection, key);

      //make the new tile the head of the list,
      if (interimTile.getState() == GVol.TileState.IDLE) {
        //the GVold tile hasn't begun loading yet, and is now outdated, so we can simply discard it
        tile.interimTile = interimTile.interimTile;
      } else {
        tile.interimTile = interimTile;
      }
      tile.refreshInterimChain();
      this.tileCache.replace(tileCoordKey, tile);
    }
  }
  return tile;
};


/**
 * Sets whether to render reprojection edges or not (usually for debugging).
 * @param {boGVolean} render Render the edges.
 * @api
 */
GVol.source.TileImage.prototype.setRenderReprojectionEdges = function(render) {
  if (!GVol.ENABLE_RASTER_REPROJECTION ||
      this.renderReprojectionEdges_ == render) {
    return;
  }
  this.renderReprojectionEdges_ = render;
  for (var id in this.tileCacheForProjection) {
    this.tileCacheForProjection[id].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, resGVolutions, ...).
 *
 * @param {GVol.ProjectionLike} projection Projection.
 * @param {GVol.tilegrid.TileGrid} tilegrid Tile grid to use for the projection.
 * @api
 */
GVol.source.TileImage.prototype.setTileGridForProjection = function(projection, tilegrid) {
  if (GVol.ENABLE_RASTER_REPROJECTION) {
    var proj = GVol.proj.get(projection);
    if (proj) {
      var projKey = GVol.getUid(proj).toString();
      if (!(projKey in this.tileGridForProjection)) {
        this.tileGridForProjection[projKey] = tilegrid;
      }
    }
  }
};


/**
 * @param {GVol.ImageTile} imageTile Image tile.
 * @param {string} src Source.
 */
GVol.source.TileImage.defaultTileLoadFunction = function(imageTile, src) {
  imageTile.getImage().src = src;
};

goog.provide('GVol.source.BingMaps');

goog.require('GVol');
goog.require('GVol.Attribution');
goog.require('GVol.TileUrlFunction');
goog.require('GVol.extent');
goog.require('GVol.net');
goog.require('GVol.proj');
goog.require('GVol.source.State');
goog.require('GVol.source.TileImage');
goog.require('GVol.tilecoord');
goog.require('GVol.tilegrid');


/**
 * @classdesc
 * Layer source for Bing Maps tile data.
 *
 * @constructor
 * @extends {GVol.source.TileImage}
 * @param {GVolx.source.BingMapsOptions} options Bing Maps options.
 * @api
 */
GVol.source.BingMaps = function(options) {

  /**
   * @private
   * @type {boGVolean}
   */
  this.hidpi_ = options.hidpi !== undefined ? options.hidpi : false;

  GVol.source.TileImage.call(this, {
    cacheSize: options.cacheSize,
    crossOrigin: 'anonymous',
    opaque: true,
    projection: GVol.proj.get('EPSG:3857'),
    reprojectionErrorThreshGVold: options.reprojectionErrorThreshGVold,
    state: GVol.source.State.LOADING,
    tileLoadFunction: options.tileLoadFunction,
    tilePixelRatio: this.hidpi_ ? 2 : 1,
    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;

  /**
   * @private
   * @type {string}
   */
  this.apiKey_ = options.key;

  /**
   * @private
   * @type {string}
   */
  this.imagerySet_ = options.imagerySet;

  var url = 'https://dev.virtualearth.net/REST/v1/Imagery/Metadata/' +
      this.imagerySet_ +
      '?uriScheme=https&include=ImageryProviders&key=' + this.apiKey_;

  GVol.net.jsonp(url, this.handleImageryMetadataResponse.bind(this), undefined,
      'jsonp');

};
GVol.inherits(GVol.source.BingMaps, GVol.source.TileImage);


/**
 * The attribution containing a link to the Microsoft® Bing™ Maps Platform APIs’
 * Terms Of Use.
 * @const
 * @type {GVol.Attribution}
 * @api
 */
GVol.source.BingMaps.TOS_ATTRIBUTION = new GVol.Attribution({
  html: '<a class="GVol-attribution-bing-tos" ' +
      'href="https://www.microsoft.com/maps/product/terms.html">' +
      'Terms of Use</a>'
});


/**
 * Get the api key used for this source.
 *
 * @return {string} The api key.
 * @api
 */
GVol.source.BingMaps.prototype.getApiKey = function() {
  return this.apiKey_;
};


/**
 * Get the imagery set associated with this source.
 *
 * @return {string} The imagery set.
 * @api
 */
GVol.source.BingMaps.prototype.getImagerySet = function() {
  return this.imagerySet_;
};


/**
 * @param {BingMapsImageryMetadataResponse} response Response.
 */
GVol.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(GVol.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];
  var maxZoom = this.maxZoom_ == -1 ? resource.zoomMax : this.maxZoom_;

  var sourceProjection = this.getProjection();
  var extent = GVol.tilegrid.extentFromProjection(sourceProjection);
  var tileSize = resource.imageWidth == resource.imageHeight ?
    resource.imageWidth : [resource.imageWidth, resource.imageHeight];
  var tileGrid = GVol.tilegrid.createXYZ({
    extent: extent,
    minZoom: resource.zoomMin,
    maxZoom: maxZoom,
    tileSize: tileSize / (this.hidpi_ ? 2 : 1)
  });
  this.tileGrid = tileGrid;

  var culture = this.culture_;
  var hidpi = this.hidpi_;
  this.tileUrlFunction = GVol.TileUrlFunction.createFromTileUrlFunctions(
      resource.imageUrlSubdomains.map(function(subdomain) {
        var quadKeyTileCoord = [0, 0, 0];
        var imageUrl = resource.imageUrl
            .replace('{subdomain}', subdomain)
            .replace('{culture}', culture);
        return (
          /**
           * @param {GVol.TileCoord} tileCoord Tile coordinate.
           * @param {number} pixelRatio Pixel ratio.
           * @param {GVol.proj.Projection} projection Projection.
           * @return {string|undefined} Tile URL.
           */
          function(tileCoord, pixelRatio, projection) {
            if (!tileCoord) {
              return undefined;
            } else {
              GVol.tilecoord.createOrUpdate(tileCoord[0], tileCoord[1],
                  -tileCoord[2] - 1, quadKeyTileCoord);
              var url = imageUrl;
              if (hidpi) {
                url += '&dpi=d1&device=mobile';
              }
              return url.replace('{quadkey}', GVol.tilecoord.quadKey(
                  quadKeyTileCoord));
            }
          });
      }));

  if (resource.imageryProviders) {
    var transform = GVol.proj.getTransformFromProjections(
        GVol.proj.get('EPSG:4326'), this.getProjection());

    var attributions = resource.imageryProviders.map(function(imageryProvider) {
      var html = imageryProvider.attribution;
      /** @type {Object.<string, Array.<GVol.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 = GVol.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 GVol.Attribution({html: html, tileRanges: tileRanges});
    });
    attributions.push(GVol.source.BingMaps.TOS_ATTRIBUTION);
    this.setAttributions(attributions);
  }

  this.setLogo(brandLogoUri);

  this.setState(GVol.source.State.READY);
};

goog.provide('GVol.source.XYZ');

goog.require('GVol');
goog.require('GVol.source.TileImage');
goog.require('GVol.tilegrid');


/**
 * @classdesc
 * Layer source for tile data with URLs in a set XYZ format that are
 * defined in a URL template. By default, this fGVollows 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}` placehGVolder in the URL template, so long as the
 * source does not have a custom tile grid. In this case,
 * {@link GVol.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 {GVol.source.TileImage}
 * @param {GVolx.source.XYZOptions=} opt_options XYZ options.
 * @api
 */
GVol.source.XYZ = function(opt_options) {
  var options = opt_options || {};
  var projection = options.projection !== undefined ?
    options.projection : 'EPSG:3857';

  var tileGrid = options.tileGrid !== undefined ? options.tileGrid :
    GVol.tilegrid.createXYZ({
      extent: GVol.tilegrid.extentFromProjection(projection),
      maxZoom: options.maxZoom,
      minZoom: options.minZoom,
      tileSize: options.tileSize
    });

  GVol.source.TileImage.call(this, {
    attributions: options.attributions,
    cacheSize: options.cacheSize,
    crossOrigin: options.crossOrigin,
    logo: options.logo,
    opaque: options.opaque,
    projection: projection,
    reprojectionErrorThreshGVold: options.reprojectionErrorThreshGVold,
    tileGrid: tileGrid,
    tileLoadFunction: options.tileLoadFunction,
    tilePixelRatio: options.tilePixelRatio,
    tileUrlFunction: options.tileUrlFunction,
    url: options.url,
    urls: options.urls,
    wrapX: options.wrapX !== undefined ? options.wrapX : true
  });

};
GVol.inherits(GVol.source.XYZ, GVol.source.TileImage);

goog.provide('GVol.source.CartoDB');

goog.require('GVol');
goog.require('GVol.obj');
goog.require('GVol.source.State');
goog.require('GVol.source.XYZ');


/**
 * @classdesc
 * Layer source for the CartoDB tiles.
 *
 * @constructor
 * @extends {GVol.source.XYZ}
 * @param {GVolx.source.CartoDBOptions} options CartoDB options.
 * @api
 */
GVol.source.CartoDB = function(options) {

  /**
   * @type {string}
   * @private
   */
  this.account_ = options.account;

  /**
   * @type {string}
   * @private
   */
  this.mapId_ = options.map || '';

  /**
   * @type {!Object}
   * @private
   */
  this.config_ = options.config || {};

  /**
   * @type {!Object.<string, CartoDBLayerInfo>}
   * @private
   */
  this.templateCache_ = {};

  GVol.source.XYZ.call(this, {
    attributions: options.attributions,
    cacheSize: options.cacheSize,
    crossOrigin: options.crossOrigin,
    logo: options.logo,
    maxZoom: options.maxZoom !== undefined ? options.maxZoom : 18,
    minZoom: options.minZoom,
    projection: options.projection,
    state: GVol.source.State.LOADING,
    wrapX: options.wrapX
  });
  this.initializeMap_();
};
GVol.inherits(GVol.source.CartoDB, GVol.source.XYZ);


/**
 * Returns the current config.
 * @return {!Object} The current configuration.
 * @api
 */
GVol.source.CartoDB.prototype.getConfig = function() {
  return this.config_;
};


/**
 * Updates the carto db config.
 * @param {Object} config a key-value lookup. Values will replace current values
 *     in the config.
 * @api
 */
GVol.source.CartoDB.prototype.updateConfig = function(config) {
  GVol.obj.assign(this.config_, config);
  this.initializeMap_();
};


/**
 * Sets the CartoDB config
 * @param {Object} config In the case of anonymous maps, a CartoDB configuration
 *     object.
 * If using named maps, a key-value lookup with the template parameters.
 * @api
 */
GVol.source.CartoDB.prototype.setConfig = function(config) {
  this.config_ = config || {};
  this.initializeMap_();
};


/**
 * Issue a request to initialize the CartoDB map.
 * @private
 */
GVol.source.CartoDB.prototype.initializeMap_ = function() {
  var paramHash = JSON.stringify(this.config_);
  if (this.templateCache_[paramHash]) {
    this.applyTemplate_(this.templateCache_[paramHash]);
    return;
  }
  var mapUrl = 'https://' + this.account_ + '.cartodb.com/api/v1/map';

  if (this.mapId_) {
    mapUrl += '/named/' + this.mapId_;
  }

  var client = new XMLHttpRequest();
  client.addEventListener('load', this.handleInitResponse_.bind(this, paramHash));
  client.addEventListener('error', this.handleInitError_.bind(this));
  client.open('POST', mapUrl);
  client.setRequestHeader('Content-type', 'application/json');
  client.send(JSON.stringify(this.config_));
};


/**
 * Handle map initialization response.
 * @param {string} paramHash a hash representing the parameter set that was used
 *     for the request
 * @param {Event} event Event.
 * @private
 */
GVol.source.CartoDB.prototype.handleInitResponse_ = function(paramHash, event) {
  var client = /** @type {XMLHttpRequest} */ (event.target);
  // status will be 0 for file:// urls
  if (!client.status || client.status >= 200 && client.status < 300) {
    var response;
    try {
      response = /** @type {CartoDBLayerInfo} */(JSON.parse(client.responseText));
    } catch (err) {
      this.setState(GVol.source.State.ERROR);
      return;
    }
    this.applyTemplate_(response);
    this.templateCache_[paramHash] = response;
    this.setState(GVol.source.State.READY);
  } else {
    this.setState(GVol.source.State.ERROR);
  }
};


/**
 * @private
 * @param {Event} event Event.
 */
GVol.source.CartoDB.prototype.handleInitError_ = function(event) {
  this.setState(GVol.source.State.ERROR);
};


/**
 * Apply the new tile urls returned by carto db
 * @param {CartoDBLayerInfo} data Result of carto db call.
 * @private
 */
GVol.source.CartoDB.prototype.applyTemplate_ = function(data) {
  var tilesUrl = 'https://' + data.cdn_url.https + '/' + this.account_ +
      '/api/v1/map/' + data.layergroupid + '/{z}/{x}/{y}.png';
  this.setUrl(tilesUrl);
};

// FIXME keep cluster cache by resGVolution ?
// FIXME distance not respected because of the centroid

goog.provide('GVol.source.Cluster');

goog.require('GVol');
goog.require('GVol.asserts');
goog.require('GVol.Feature');
goog.require('GVol.coordinate');
goog.require('GVol.events.EventType');
goog.require('GVol.extent');
goog.require('GVol.geom.Point');
goog.require('GVol.source.Vector');


/**
 * @classdesc
 * Layer source to cluster vector data. Works out of the box with point
 * geometries. For other geometry types, or if not all geometries should be
 * considered for clustering, a custom `geometryFunction` can be defined.
 *
 * @constructor
 * @param {GVolx.source.ClusterOptions} options Constructor options.
 * @extends {GVol.source.Vector}
 * @api
 */
GVol.source.Cluster = function(options) {
  GVol.source.Vector.call(this, {
    attributions: options.attributions,
    extent: options.extent,
    logo: options.logo,
    projection: options.projection,
    wrapX: options.wrapX
  });

  /**
   * @type {number|undefined}
   * @protected
   */
  this.resGVolution = undefined;

  /**
   * @type {number}
   * @protected
   */
  this.distance = options.distance !== undefined ? options.distance : 20;

  /**
   * @type {Array.<GVol.Feature>}
   * @protected
   */
  this.features = [];

  /**
   * @param {GVol.Feature} feature Feature.
   * @return {GVol.geom.Point} Cluster calculation point.
   * @protected
   */
  this.geometryFunction = options.geometryFunction || function(feature) {
    var geometry = /** @type {GVol.geom.Point} */ (feature.getGeometry());
    GVol.asserts.assert(geometry instanceof GVol.geom.Point,
        10); // The default `geometryFunction` can only handle `GVol.geom.Point` geometries
    return geometry;
  };

  /**
   * @type {GVol.source.Vector}
   * @protected
   */
  this.source = options.source;

  this.source.on(GVol.events.EventType.CHANGE,
      GVol.source.Cluster.prototype.refresh, this);
};
GVol.inherits(GVol.source.Cluster, GVol.source.Vector);


/**
 * Get the distance in pixels between clusters.
 * @return {number} Distance.
 * @api
 */
GVol.source.Cluster.prototype.getDistance = function() {
  return this.distance;
};


/**
 * Get a reference to the wrapped source.
 * @return {GVol.source.Vector} Source.
 * @api
 */
GVol.source.Cluster.prototype.getSource = function() {
  return this.source;
};


/**
 * @inheritDoc
 */
GVol.source.Cluster.prototype.loadFeatures = function(extent, resGVolution,
    projection) {
  this.source.loadFeatures(extent, resGVolution, projection);
  if (resGVolution !== this.resGVolution) {
    this.clear();
    this.resGVolution = resGVolution;
    this.cluster();
    this.addFeatures(this.features);
  }
};


/**
 * Set the distance in pixels between clusters.
 * @param {number} distance The distance in pixels.
 * @api
 */
GVol.source.Cluster.prototype.setDistance = function(distance) {
  this.distance = distance;
  this.refresh();
};


/**
 * handle the source changing
 * @override
 */
GVol.source.Cluster.prototype.refresh = function() {
  this.clear();
  this.cluster();
  this.addFeatures(this.features);
  GVol.source.Vector.prototype.refresh.call(this);
};


/**
 * @protected
 */
GVol.source.Cluster.prototype.cluster = function() {
  if (this.resGVolution === undefined) {
    return;
  }
  this.features.length = 0;
  var extent = GVol.extent.createEmpty();
  var mapDistance = this.distance * this.resGVolution;
  var features = this.source.getFeatures();

  /**
   * @type {!Object.<string, boGVolean>}
   */
  var clustered = {};

  for (var i = 0, ii = features.length; i < ii; i++) {
    var feature = features[i];
    if (!(GVol.getUid(feature).toString() in clustered)) {
      var geometry = this.geometryFunction(feature);
      if (geometry) {
        var coordinates = geometry.getCoordinates();
        GVol.extent.createOrUpdateFromCoordinate(coordinates, extent);
        GVol.extent.buffer(extent, mapDistance, extent);

        var neighbors = this.source.getFeaturesInExtent(extent);
        neighbors = neighbors.filter(function(neighbor) {
          var uid = GVol.getUid(neighbor).toString();
          if (!(uid in clustered)) {
            clustered[uid] = true;
            return true;
          } else {
            return false;
          }
        });
        this.features.push(this.createCluster(neighbors));
      }
    }
  }
};


/**
 * @param {Array.<GVol.Feature>} features Features
 * @return {GVol.Feature} The cluster feature.
 * @protected
 */
GVol.source.Cluster.prototype.createCluster = function(features) {
  var centroid = [0, 0];
  for (var i = features.length - 1; i >= 0; --i) {
    var geometry = this.geometryFunction(features[i]);
    if (geometry) {
      GVol.coordinate.add(centroid, geometry.getCoordinates());
    } else {
      features.splice(i, 1);
    }
  }
  GVol.coordinate.scale(centroid, 1 / features.length);

  var cluster = new GVol.Feature(new GVol.geom.Point(centroid));
  cluster.set('features', features);
  return cluster;
};

goog.provide('GVol.uri');


/**
 * Appends query parameters to a URI.
 *
 * @param {string} uri The original URI, which may already have query data.
 * @param {!Object} params An object where keys are URI-encoded parameter keys,
 *     and the values are arbitrary types or arrays.
 * @return {string} The new URI.
 */
GVol.uri.appendParams = function(uri, params) {
  var keyParams = [];
  // Skip any null or undefined parameter values
  Object.keys(params).forEach(function(k) {
    if (params[k] !== null && params[k] !== undefined) {
      keyParams.push(k + '=' + encodeURIComponent(params[k]));
    }
  });
  var qs = keyParams.join('&');
  // remove any trailing ? or &
  uri = uri.replace(/[?&]$/, '');
  // append ? or & depending on whether uri has existing parameters
  uri = uri.indexOf('?') === -1 ? uri + '?' : uri + '&';
  return uri + qs;
};

goog.provide('GVol.source.ImageArcGISRest');

goog.require('GVol');
goog.require('GVol.Image');
goog.require('GVol.asserts');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.extent');
goog.require('GVol.obj');
goog.require('GVol.source.Image');
goog.require('GVol.uri');


/**
 * @classdesc
 * Source for data from ArcGIS Rest services providing single, untiled images.
 * Useful when underlying map service has labels.
 *
 * If underlying map service is not using labels,
 * take advantage of GVol image caching and use
 * {@link GVol.source.TileArcGISRest} data source.
 *
 * @constructor
 * @fires GVol.source.Image.Event
 * @extends {GVol.source.Image}
 * @param {GVolx.source.ImageArcGISRestOptions=} opt_options Image ArcGIS Rest Options.
 * @api
 */
GVol.source.ImageArcGISRest = function(opt_options) {

  var options = opt_options || {};

  GVol.source.Image.call(this, {
    attributions: options.attributions,
    logo: options.logo,
    projection: options.projection,
    resGVolutions: options.resGVolutions
  });

  /**
   * @private
   * @type {?string}
   */
  this.crossOrigin_ =
      options.crossOrigin !== undefined ? options.crossOrigin : null;

  /**
   * @private
   * @type {boGVolean}
   */
  this.hidpi_ = options.hidpi !== undefined ? options.hidpi : true;

  /**
   * @private
   * @type {string|undefined}
   */
  this.url_ = options.url;

  /**
   * @private
   * @type {GVol.ImageLoadFunctionType}
   */
  this.imageLoadFunction_ = options.imageLoadFunction !== undefined ?
    options.imageLoadFunction : GVol.source.Image.defaultImageLoadFunction;


  /**
   * @private
   * @type {!Object}
   */
  this.params_ = options.params || {};

  /**
   * @private
   * @type {GVol.Image}
   */
  this.image_ = null;

  /**
   * @private
   * @type {GVol.Size}
   */
  this.imageSize_ = [0, 0];


  /**
   * @private
   * @type {number}
   */
  this.renderedRevision_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.ratio_ = options.ratio !== undefined ? options.ratio : 1.5;

};
GVol.inherits(GVol.source.ImageArcGISRest, GVol.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
 */
GVol.source.ImageArcGISRest.prototype.getParams = function() {
  return this.params_;
};


/**
 * @inheritDoc
 */
GVol.source.ImageArcGISRest.prototype.getImageInternal = function(extent, resGVolution, pixelRatio, projection) {

  if (this.url_ === undefined) {
    return null;
  }

  resGVolution = this.findNearestResGVolution(resGVolution);
  pixelRatio = this.hidpi_ ? pixelRatio : 1;

  var image = this.image_;
  if (image &&
      this.renderedRevision_ == this.getRevision() &&
      image.getResGVolution() == resGVolution &&
      image.getPixelRatio() == pixelRatio &&
      GVol.extent.containsExtent(image.getExtent(), extent)) {
    return image;
  }

  var params = {
    'F': 'image',
    'FORMAT': 'PNG32',
    'TRANSPARENT': true
  };
  GVol.obj.assign(params, this.params_);

  extent = extent.slice();
  var centerX = (extent[0] + extent[2]) / 2;
  var centerY = (extent[1] + extent[3]) / 2;
  if (this.ratio_ != 1) {
    var halfWidth = this.ratio_ * GVol.extent.getWidth(extent) / 2;
    var halfHeight = this.ratio_ * GVol.extent.getHeight(extent) / 2;
    extent[0] = centerX - halfWidth;
    extent[1] = centerY - halfHeight;
    extent[2] = centerX + halfWidth;
    extent[3] = centerY + halfHeight;
  }

  var imageResGVolution = resGVolution / pixelRatio;

  // Compute an integer width and height.
  var width = Math.ceil(GVol.extent.getWidth(extent) / imageResGVolution);
  var height = Math.ceil(GVol.extent.getHeight(extent) / imageResGVolution);

  // Modify the extent to match the integer width and height.
  extent[0] = centerX - imageResGVolution * width / 2;
  extent[2] = centerX + imageResGVolution * width / 2;
  extent[1] = centerY - imageResGVolution * height / 2;
  extent[3] = centerY + imageResGVolution * height / 2;

  this.imageSize_[0] = width;
  this.imageSize_[1] = height;

  var url = this.getRequestUrl_(extent, this.imageSize_, pixelRatio,
      projection, params);

  this.image_ = new GVol.Image(extent, resGVolution, pixelRatio,
      this.getAttributions(), url, this.crossOrigin_, this.imageLoadFunction_);

  this.renderedRevision_ = this.getRevision();

  GVol.events.listen(this.image_, GVol.events.EventType.CHANGE,
      this.handleImageChange, this);

  return this.image_;

};


/**
 * Return the image load function of the source.
 * @return {GVol.ImageLoadFunctionType} The image load function.
 * @api
 */
GVol.source.ImageArcGISRest.prototype.getImageLoadFunction = function() {
  return this.imageLoadFunction_;
};


/**
 * @param {GVol.Extent} extent Extent.
 * @param {GVol.Size} size Size.
 * @param {number} pixelRatio Pixel ratio.
 * @param {GVol.proj.Projection} projection Projection.
 * @param {Object} params Params.
 * @return {string} Request URL.
 * @private
 */
GVol.source.ImageArcGISRest.prototype.getRequestUrl_ = function(extent, size, pixelRatio, projection, params) {
  // ArcGIS Server only wants the numeric portion of the projection ID.
  var srid = projection.getCode().split(':').pop();

  params['SIZE'] = size[0] + ',' + size[1];
  params['BBOX'] = extent.join(',');
  params['BBOXSR'] = srid;
  params['IMAGESR'] = srid;
  params['DPI'] = Math.round(90 * pixelRatio);

  var url = this.url_;

  var modifiedUrl = url
      .replace(/MapServer\/?$/, 'MapServer/export')
      .replace(/ImageServer\/?$/, 'ImageServer/exportImage');
  if (modifiedUrl == url) {
    GVol.asserts.assert(false, 50); // `options.featureTypes` should be an Array
  }
  return GVol.uri.appendParams(modifiedUrl, params);
};


/**
 * Return the URL used for this ArcGIS source.
 * @return {string|undefined} URL.
 * @api
 */
GVol.source.ImageArcGISRest.prototype.getUrl = function() {
  return this.url_;
};


/**
 * Set the image load function of the source.
 * @param {GVol.ImageLoadFunctionType} imageLoadFunction Image load function.
 * @api
 */
GVol.source.ImageArcGISRest.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
 */
GVol.source.ImageArcGISRest.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
 */
GVol.source.ImageArcGISRest.prototype.updateParams = function(params) {
  GVol.obj.assign(this.params_, params);
  this.image_ = null;
  this.changed();
};

goog.provide('GVol.source.ImageMapGuide');

goog.require('GVol');
goog.require('GVol.Image');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.extent');
goog.require('GVol.obj');
goog.require('GVol.source.Image');
goog.require('GVol.uri');


/**
 * @classdesc
 * Source for images from Mapguide servers
 *
 * @constructor
 * @fires GVol.source.Image.Event
 * @extends {GVol.source.Image}
 * @param {GVolx.source.ImageMapGuideOptions} options Options.
 * @api
 */
GVol.source.ImageMapGuide = function(options) {

  GVol.source.Image.call(this, {
    projection: options.projection,
    resGVolutions: options.resGVolutions
  });

  /**
   * @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 || {};

  /**
   * @private
   * @type {string|undefined}
   */
  this.url_ = options.url;

  /**
   * @private
   * @type {GVol.ImageLoadFunctionType}
   */
  this.imageLoadFunction_ = options.imageLoadFunction !== undefined ?
    options.imageLoadFunction : GVol.source.Image.defaultImageLoadFunction;

  /**
   * @private
   * @type {boGVolean}
   */
  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 {boGVolean}
   */
  this.useOverlay_ = options.useOverlay !== undefined ?
    options.useOverlay : false;

  /**
   * @private
   * @type {GVol.Image}
   */
  this.image_ = null;

  /**
   * @private
   * @type {number}
   */
  this.renderedRevision_ = 0;

};
GVol.inherits(GVol.source.ImageMapGuide, GVol.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
 */
GVol.source.ImageMapGuide.prototype.getParams = function() {
  return this.params_;
};


/**
 * @inheritDoc
 */
GVol.source.ImageMapGuide.prototype.getImageInternal = function(extent, resGVolution, pixelRatio, projection) {
  resGVolution = this.findNearestResGVolution(resGVolution);
  pixelRatio = this.hidpi_ ? pixelRatio : 1;

  var image = this.image_;
  if (image &&
      this.renderedRevision_ == this.getRevision() &&
      image.getResGVolution() == resGVolution &&
      image.getPixelRatio() == pixelRatio &&
      GVol.extent.containsExtent(image.getExtent(), extent)) {
    return image;
  }

  if (this.ratio_ != 1) {
    extent = extent.slice();
    GVol.extent.scaleFromCenter(extent, this.ratio_);
  }
  var width = GVol.extent.getWidth(extent) / resGVolution;
  var height = GVol.extent.getHeight(extent) / resGVolution;
  var size = [width * pixelRatio, height * pixelRatio];

  if (this.url_ !== undefined) {
    var imageUrl = this.getUrl(this.url_, this.params_, extent, size,
        projection);
    image = new GVol.Image(extent, resGVolution, pixelRatio,
        this.getAttributions(), imageUrl, this.crossOrigin_,
        this.imageLoadFunction_);
    GVol.events.listen(image, GVol.events.EventType.CHANGE,
        this.handleImageChange, this);
  } else {
    image = null;
  }
  this.image_ = image;
  this.renderedRevision_ = this.getRevision();

  return image;
};


/**
 * Return the image load function of the source.
 * @return {GVol.ImageLoadFunctionType} The image load function.
 * @api
 */
GVol.source.ImageMapGuide.prototype.getImageLoadFunction = function() {
  return this.imageLoadFunction_;
};


/**
 * @param {GVol.Extent} extent The map extents.
 * @param {GVol.Size} size The viewport size.
 * @param {number} metersPerUnit The meters-per-unit value.
 * @param {number} dpi The display resGVolution.
 * @return {number} The computed map scale.
 */
GVol.source.ImageMapGuide.getScale = function(extent, size, metersPerUnit, dpi) {
  var mcsW = GVol.extent.getWidth(extent);
  var mcsH = GVol.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
 */
GVol.source.ImageMapGuide.prototype.updateParams = function(params) {
  GVol.obj.assign(this.params_, params);
  this.changed();
};


/**
 * @param {string} baseUrl The mapagent url.
 * @param {Object.<string, string|number>} params Request parameters.
 * @param {GVol.Extent} extent Extent.
 * @param {GVol.Size} size Size.
 * @param {GVol.proj.Projection} projection Projection.
 * @return {string} The mapagent map image request URL.
 */
GVol.source.ImageMapGuide.prototype.getUrl = function(baseUrl, params, extent, size, projection) {
  var scale = GVol.source.ImageMapGuide.getScale(extent, size,
      this.metersPerUnit_, this.displayDpi_);
  var center = GVol.extent.getCenter(extent);
  var baseParams = {
    'OPERATION': this.useOverlay_ ? 'GETDYNAMICMAPOVERLAYIMAGE' : 'GETMAPIMAGE',
    'VERSION': '2.0.0',
    'LOCALE': 'en',
    'CLIENTAGENT': 'GVol.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]
  };
  GVol.obj.assign(baseParams, params);
  return GVol.uri.appendParams(baseUrl, baseParams);
};


/**
 * Set the image load function of the MapGuide source.
 * @param {GVol.ImageLoadFunctionType} imageLoadFunction Image load function.
 * @api
 */
GVol.source.ImageMapGuide.prototype.setImageLoadFunction = function(
    imageLoadFunction) {
  this.image_ = null;
  this.imageLoadFunction_ = imageLoadFunction;
  this.changed();
};

goog.provide('GVol.source.ImageStatic');

goog.require('GVol');
goog.require('GVol.Image');
goog.require('GVol.ImageState');
goog.require('GVol.dom');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.extent');
goog.require('GVol.proj');
goog.require('GVol.source.Image');


/**
 * @classdesc
 * A layer source for displaying a single, static image.
 *
 * @constructor
 * @extends {GVol.source.Image}
 * @param {GVolx.source.ImageStaticOptions} options Options.
 * @api
 */
GVol.source.ImageStatic = function(options) {
  var imageExtent = options.imageExtent;

  var crossOrigin = options.crossOrigin !== undefined ?
    options.crossOrigin : null;

  var /** @type {GVol.ImageLoadFunctionType} */ imageLoadFunction =
      options.imageLoadFunction !== undefined ?
        options.imageLoadFunction : GVol.source.Image.defaultImageLoadFunction;

  GVol.source.Image.call(this, {
    attributions: options.attributions,
    logo: options.logo,
    projection: GVol.proj.get(options.projection)
  });

  /**
   * @private
   * @type {GVol.Image}
   */
  this.image_ = new GVol.Image(imageExtent, undefined, 1, this.getAttributions(),
      options.url, crossOrigin, imageLoadFunction);

  /**
   * @private
   * @type {GVol.Size}
   */
  this.imageSize_ = options.imageSize ? options.imageSize : null;

  GVol.events.listen(this.image_, GVol.events.EventType.CHANGE,
      this.handleImageChange, this);

};
GVol.inherits(GVol.source.ImageStatic, GVol.source.Image);


/**
 * @inheritDoc
 */
GVol.source.ImageStatic.prototype.getImageInternal = function(extent, resGVolution, pixelRatio, projection) {
  if (GVol.extent.intersects(extent, this.image_.getExtent())) {
    return this.image_;
  }
  return null;
};


/**
 * @inheritDoc
 */
GVol.source.ImageStatic.prototype.handleImageChange = function(evt) {
  if (this.image_.getState() == GVol.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 resGVolution = GVol.extent.getHeight(imageExtent) / imageHeight;
    var targetWidth = Math.ceil(GVol.extent.getWidth(imageExtent) / resGVolution);
    if (targetWidth != imageWidth) {
      var context = GVol.dom.createCanvasContext2D(targetWidth, imageHeight);
      var canvas = context.canvas;
      context.drawImage(image, 0, 0, imageWidth, imageHeight,
          0, 0, canvas.width, canvas.height);
      this.image_.setImage(canvas);
    }
  }
  GVol.source.Image.prototype.handleImageChange.call(this, evt);
};

goog.provide('GVol.source.WMSServerType');


/**
 * 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}
 */
GVol.source.WMSServerType = {
  CARMENTA_SERVER: 'carmentaserver',
  GEOSERVER: 'geoserver',
  MAPSERVER: 'mapserver',
  QGIS: 'qgis'
};

// FIXME cannot be shared between maps with different projections

goog.provide('GVol.source.ImageWMS');

goog.require('GVol');
goog.require('GVol.Image');
goog.require('GVol.asserts');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.extent');
goog.require('GVol.obj');
goog.require('GVol.proj');
goog.require('GVol.source.Image');
goog.require('GVol.source.WMSServerType');
goog.require('GVol.string');
goog.require('GVol.uri');


/**
 * @classdesc
 * Source for WMS servers providing single, untiled images.
 *
 * @constructor
 * @fires GVol.source.Image.Event
 * @extends {GVol.source.Image}
 * @param {GVolx.source.ImageWMSOptions=} opt_options Options.
 * @api
 */
GVol.source.ImageWMS = function(opt_options) {

  var options = opt_options || {};

  GVol.source.Image.call(this, {
    attributions: options.attributions,
    logo: options.logo,
    projection: options.projection,
    resGVolutions: options.resGVolutions
  });

  /**
   * @private
   * @type {?string}
   */
  this.crossOrigin_ =
      options.crossOrigin !== undefined ? options.crossOrigin : null;

  /**
   * @private
   * @type {string|undefined}
   */
  this.url_ = options.url;

  /**
   * @private
   * @type {GVol.ImageLoadFunctionType}
   */
  this.imageLoadFunction_ = options.imageLoadFunction !== undefined ?
    options.imageLoadFunction : GVol.source.Image.defaultImageLoadFunction;

  /**
   * @private
   * @type {!Object}
   */
  this.params_ = options.params || {};

  /**
   * @private
   * @type {boGVolean}
   */
  this.v13_ = true;
  this.updateV13_();

  /**
   * @private
   * @type {GVol.source.WMSServerType|undefined}
   */
  this.serverType_ = /** @type {GVol.source.WMSServerType|undefined} */ (options.serverType);

  /**
   * @private
   * @type {boGVolean}
   */
  this.hidpi_ = options.hidpi !== undefined ? options.hidpi : true;

  /**
   * @private
   * @type {GVol.Image}
   */
  this.image_ = null;

  /**
   * @private
   * @type {GVol.Size}
   */
  this.imageSize_ = [0, 0];

  /**
   * @private
   * @type {number}
   */
  this.renderedRevision_ = 0;

  /**
   * @private
   * @type {number}
   */
  this.ratio_ = options.ratio !== undefined ? options.ratio : 1.5;

};
GVol.inherits(GVol.source.ImageWMS, GVol.source.Image);


/**
 * @const
 * @type {GVol.Size}
 * @private
 */
GVol.source.ImageWMS.GETFEATUREINFO_IMAGE_SIZE_ = [101, 101];


/**
 * Return the GetFeatureInfo URL for the passed coordinate, resGVolution, and
 * projection. Return `undefined` if the GetFeatureInfo URL cannot be
 * constructed.
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @param {number} resGVolution ResGVolution.
 * @param {GVol.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
 */
GVol.source.ImageWMS.prototype.getGetFeatureInfoUrl = function(coordinate, resGVolution, projection, params) {
  if (this.url_ === undefined) {
    return undefined;
  }

  var extent = GVol.extent.getForViewAndSize(
      coordinate, resGVolution, 0,
      GVol.source.ImageWMS.GETFEATUREINFO_IMAGE_SIZE_);

  var baseParams = {
    'SERVICE': 'WMS',
    'VERSION': GVol.DEFAULT_WMS_VERSION,
    'REQUEST': 'GetFeatureInfo',
    'FORMAT': 'image/png',
    'TRANSPARENT': true,
    'QUERY_LAYERS': this.params_['LAYERS']
  };
  GVol.obj.assign(baseParams, this.params_, params);

  var x = Math.floor((coordinate[0] - extent[0]) / resGVolution);
  var y = Math.floor((extent[3] - coordinate[1]) / resGVolution);
  baseParams[this.v13_ ? 'I' : 'X'] = x;
  baseParams[this.v13_ ? 'J' : 'Y'] = y;

  return this.getRequestUrl_(
      extent, GVol.source.ImageWMS.GETFEATUREINFO_IMAGE_SIZE_,
      1, GVol.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
 */
GVol.source.ImageWMS.prototype.getParams = function() {
  return this.params_;
};


/**
 * @inheritDoc
 */
GVol.source.ImageWMS.prototype.getImageInternal = function(extent, resGVolution, pixelRatio, projection) {

  if (this.url_ === undefined) {
    return null;
  }

  resGVolution = this.findNearestResGVolution(resGVolution);

  if (pixelRatio != 1 && (!this.hidpi_ || this.serverType_ === undefined)) {
    pixelRatio = 1;
  }

  var imageResGVolution = resGVolution / pixelRatio;

  var center = GVol.extent.getCenter(extent);
  var viewWidth = Math.ceil(GVol.extent.getWidth(extent) / imageResGVolution);
  var viewHeight = Math.ceil(GVol.extent.getHeight(extent) / imageResGVolution);
  var viewExtent = GVol.extent.getForViewAndSize(center, imageResGVolution, 0,
      [viewWidth, viewHeight]);
  var requestWidth = Math.ceil(this.ratio_ * GVol.extent.getWidth(extent) / imageResGVolution);
  var requestHeight = Math.ceil(this.ratio_ * GVol.extent.getHeight(extent) / imageResGVolution);
  var requestExtent = GVol.extent.getForViewAndSize(center, imageResGVolution, 0,
      [requestWidth, requestHeight]);

  var image = this.image_;
  if (image &&
      this.renderedRevision_ == this.getRevision() &&
      image.getResGVolution() == resGVolution &&
      image.getPixelRatio() == pixelRatio &&
      GVol.extent.containsExtent(image.getExtent(), viewExtent)) {
    return image;
  }

  var params = {
    'SERVICE': 'WMS',
    'VERSION': GVol.DEFAULT_WMS_VERSION,
    'REQUEST': 'GetMap',
    'FORMAT': 'image/png',
    'TRANSPARENT': true
  };
  GVol.obj.assign(params, this.params_);

  this.imageSize_[0] = Math.round(GVol.extent.getWidth(requestExtent) / imageResGVolution);
  this.imageSize_[1] = Math.round(GVol.extent.getHeight(requestExtent) / imageResGVolution);

  var url = this.getRequestUrl_(requestExtent, this.imageSize_, pixelRatio,
      projection, params);

  this.image_ = new GVol.Image(requestExtent, resGVolution, pixelRatio,
      this.getAttributions(), url, this.crossOrigin_, this.imageLoadFunction_);

  this.renderedRevision_ = this.getRevision();

  GVol.events.listen(this.image_, GVol.events.EventType.CHANGE,
      this.handleImageChange, this);

  return this.image_;

};


/**
 * Return the image load function of the source.
 * @return {GVol.ImageLoadFunctionType} The image load function.
 * @api
 */
GVol.source.ImageWMS.prototype.getImageLoadFunction = function() {
  return this.imageLoadFunction_;
};


/**
 * @param {GVol.Extent} extent Extent.
 * @param {GVol.Size} size Size.
 * @param {number} pixelRatio Pixel ratio.
 * @param {GVol.proj.Projection} projection Projection.
 * @param {Object} params Params.
 * @return {string} Request URL.
 * @private
 */
GVol.source.ImageWMS.prototype.getRequestUrl_ = function(extent, size, pixelRatio, projection, params) {

  GVol.asserts.assert(this.url_ !== undefined, 9); // `url` must be configured or set using `#setUrl()`

  params[this.v13_ ? 'CRS' : 'SRS'] = projection.getCode();

  if (!('STYLES' in this.params_)) {
    params['STYLES'] = '';
  }

  if (pixelRatio != 1) {
    switch (this.serverType_) {
      case GVol.source.WMSServerType.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 GVol.source.WMSServerType.MAPSERVER:
        params['MAP_RESOLUTION'] = 90 * pixelRatio;
        break;
      case GVol.source.WMSServerType.CARMENTA_SERVER:
      case GVol.source.WMSServerType.QGIS:
        params['DPI'] = 90 * pixelRatio;
        break;
      default:
        GVol.asserts.assert(false, 8); // 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 GVol.uri.appendParams(/** @type {string} */ (this.url_), params);
};


/**
 * Return the URL used for this WMS source.
 * @return {string|undefined} URL.
 * @api
 */
GVol.source.ImageWMS.prototype.getUrl = function() {
  return this.url_;
};


/**
 * Set the image load function of the source.
 * @param {GVol.ImageLoadFunctionType} imageLoadFunction Image load function.
 * @api
 */
GVol.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
 */
GVol.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
 */
GVol.source.ImageWMS.prototype.updateParams = function(params) {
  GVol.obj.assign(this.params_, params);
  this.updateV13_();
  this.image_ = null;
  this.changed();
};


/**
 * @private
 */
GVol.source.ImageWMS.prototype.updateV13_ = function() {
  var version = this.params_['VERSION'] || GVol.DEFAULT_WMS_VERSION;
  this.v13_ = GVol.string.compareVersions(version, '1.3') >= 0;
};

goog.provide('GVol.source.OSM');

goog.require('GVol');
goog.require('GVol.Attribution');
goog.require('GVol.source.XYZ');


/**
 * @classdesc
 * Layer source for the OpenStreetMap tile server.
 *
 * @constructor
 * @extends {GVol.source.XYZ}
 * @param {GVolx.source.OSMOptions=} opt_options Open Street Map options.
 * @api
 */
GVol.source.OSM = function(opt_options) {

  var options = opt_options || {};

  var attributions;
  if (options.attributions !== undefined) {
    attributions = options.attributions;
  } else {
    attributions = [GVol.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';

  GVol.source.XYZ.call(this, {
    attributions: attributions,
    cacheSize: options.cacheSize,
    crossOrigin: crossOrigin,
    opaque: options.opaque !== undefined ? options.opaque : true,
    maxZoom: options.maxZoom !== undefined ? options.maxZoom : 19,
    reprojectionErrorThreshGVold: options.reprojectionErrorThreshGVold,
    tileLoadFunction: options.tileLoadFunction,
    url: url,
    wrapX: options.wrapX
  });

};
GVol.inherits(GVol.source.OSM, GVol.source.XYZ);


/**
 * The attribution containing a link to the OpenStreetMap Copyright and License
 * page.
 * @const
 * @type {GVol.Attribution}
 * @api
 */
GVol.source.OSM.ATTRIBUTION = new GVol.Attribution({
  html: '&copy; ' +
      '<a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> ' +
      'contributors.'
});


/**
 * @fileoverview
 * @suppress {accessContrGVols, ambiguousFunctionDecl, checkDebuggerStatement, checkRegExp, checkTypes, checkVars, const, constantProperty, deprecated, duplicate, es5Strict, fileoverviewTags, missingProperties, nonStandardJsDocs, strictModuleDepCheck, suspiciousCode, undefinedNames, undefinedVars, unknownDefines, unusedLocalVariables, uselessCode, visibility}
 */
goog.provide('GVol.ext.pixelworks.Processor');

/** @typedef {function(*)} */
GVol.ext.pixelworks.Processor = function() {};

(function() {(function (exports) {
'use strict';

var hasImageData = true;
try {
  new ImageData(10, 10);
} catch (_) {
  hasImageData = false;
}
var context = document.createElement('canvas').getContext('2d');
function newImageData$1(data, width, height) {
  if (hasImageData) {
    return new ImageData(data, width, height);
  } else {
    var imageData = context.createImageData(width, height);
    imageData.data.set(data);
    return imageData;
  }
}
var newImageData_1 = newImageData$1;
var util = {
	newImageData: newImageData_1
};

var newImageData = util.newImageData;
function createMinion(operation) {
  var workerHasImageData = true;
  try {
    new ImageData(10, 10);
  } catch (_) {
    workerHasImageData = false;
  }
  function newWorkerImageData(data, width, height) {
    if (workerHasImageData) {
      return new ImageData(data, width, height);
    } else {
      return {data: data, width: width, height: height};
    }
  }
  return function(data) {
    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] = newWorkerImageData(
            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;
  };
}
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;
}
function createFauxWorker(config, onMessage) {
  var minion = createMinion(config.operation);
  return {
    postMessage: function(data) {
      setTimeout(function() {
        onMessage({'data': {'buffer': minion(data), 'meta': data['meta']}});
      }, 0);
    }
  };
}
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;
}
Processor.prototype.process = function(inputs, meta, callback) {
  this._enqueue({
    inputs: inputs,
    meta: meta,
    callback: callback
  });
  this._dispatch();
};
Processor.prototype.destroy = function() {
  for (var key in this) {
    this[key] = null;
  }
  this._destroyed = true;
};
Processor.prototype._enqueue = function(job) {
  this._queue.push(job);
  while (this._queue.length > this._maxQueueLength) {
    this._queue.shift().callback(null, null);
  }
};
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);
      }
    }
  }
};
Processor.prototype._onWorkerMessage = function(index, event) {
  if (this._destroyed) {
    return;
  }
  this._dataLookup[index] = event.data;
  --this._running;
  if (this._running === 0) {
    this._resGVolveJob();
  }
};
Processor.prototype._resGVolveJob = 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,
      newImageData(data, job.inputs[0].width, job.inputs[0].height), meta);
  this._dispatch();
};
var processor = Processor;

var Processor_1 = processor;
var lib = {
	Processor: Processor_1
};

exports['default'] = lib;
exports.Processor = Processor_1;

}((this.pixelworks = this.pixelworks || {})));}).call(GVol.ext);

goog.provide('GVol.source.RasterOperationType');

/**
 * Raster operation type. Supported values are `'pixel'` and `'image'`.
 * @enum {string}
 */
GVol.source.RasterOperationType = {
  PIXEL: 'pixel',
  IMAGE: 'image'
};

goog.provide('GVol.source.Raster');

goog.require('GVol');
goog.require('GVol.ImageCanvas');
goog.require('GVol.TileQueue');
goog.require('GVol.dom');
goog.require('GVol.events');
goog.require('GVol.events.Event');
goog.require('GVol.events.EventType');
goog.require('GVol.ext.pixelworks.Processor');
goog.require('GVol.extent');
goog.require('GVol.layer.Image');
goog.require('GVol.layer.Tile');
goog.require('GVol.obj');
goog.require('GVol.renderer.canvas.ImageLayer');
goog.require('GVol.renderer.canvas.TileLayer');
goog.require('GVol.source.Image');
goog.require('GVol.source.RasterOperationType');
goog.require('GVol.source.State');
goog.require('GVol.source.Tile');
goog.require('GVol.transform');


/**
 * @classdesc
 * A source that transforms data from any number of input sources using an array
 * of {@link GVol.RasterOperation} functions to transform input pixel values into
 * output pixel values.
 *
 * @constructor
 * @extends {GVol.source.Image}
 * @fires GVol.source.Raster.Event
 * @param {GVolx.source.RasterOptions} options Options.
 * @api
 */
GVol.source.Raster = function(options) {

  /**
   * @private
   * @type {*}
   */
  this.worker_ = null;

  /**
   * @private
   * @type {GVol.source.RasterOperationType}
   */
  this.operationType_ = options.operationType !== undefined ?
    options.operationType : GVol.source.RasterOperationType.PIXEL;

  /**
   * @private
   * @type {number}
   */
  this.threads_ = options.threads !== undefined ? options.threads : 1;

  /**
   * @private
   * @type {Array.<GVol.renderer.canvas.Layer>}
   */
  this.renderers_ = GVol.source.Raster.createRenderers_(options.sources);

  for (var r = 0, rr = this.renderers_.length; r < rr; ++r) {
    GVol.events.listen(this.renderers_[r], GVol.events.EventType.CHANGE,
        this.changed, this);
  }

  /**
   * @private
   * @type {GVol.TileQueue}
   */
  this.tileQueue_ = new GVol.TileQueue(
      function() {
        return 1;
      },
      this.changed.bind(this));

  var layerStatesArray = GVol.source.Raster.getLayerStatesArray_(this.renderers_);
  var layerStates = {};
  for (var i = 0, ii = layerStatesArray.length; i < ii; ++i) {
    layerStates[GVol.getUid(layerStatesArray[i].layer)] = layerStatesArray[i];
  }

  /**
   * The most recently requested frame state.
   * @type {GVolx.FrameState}
   * @private
   */
  this.requestedFrameState_;

  /**
   * The most recently rendered image canvas.
   * @type {GVol.ImageCanvas}
   * @private
   */
  this.renderedImageCanvas_ = null;

  /**
   * The most recently rendered revision.
   * @type {number}
   */
  this.renderedRevision_;

  /**
   * @private
   * @type {GVolx.FrameState}
   */
  this.frameState_ = {
    animate: false,
    attributions: {},
    coordinateToPixelTransform: GVol.transform.create(),
    extent: null,
    focus: null,
    index: 0,
    layerStates: layerStates,
    layerStatesArray: layerStatesArray,
    logos: {},
    pixelRatio: 1,
    pixelToCoordinateTransform: GVol.transform.create(),
    postRenderFunctions: [],
    size: [0, 0],
    skippedFeatureUids: {},
    tileQueue: this.tileQueue_,
    time: Date.now(),
    usedTiles: {},
    viewState: /** @type {GVolx.ViewState} */ ({
      rotation: 0
    }),
    viewHints: [],
    wantedTiles: {}
  };

  GVol.source.Image.call(this, {});

  if (options.operation !== undefined) {
    this.setOperation(options.operation, options.lib);
  }

};
GVol.inherits(GVol.source.Raster, GVol.source.Image);


/**
 * Set the operation.
 * @param {GVol.RasterOperation} operation New operation.
 * @param {Object=} opt_lib Functions that will be available to operations run
 *     in a worker.
 * @api
 */
GVol.source.Raster.prototype.setOperation = function(operation, opt_lib) {
  this.worker_ = new GVol.ext.pixelworks.Processor({
    operation: operation,
    imageOps: this.operationType_ === GVol.source.RasterOperationType.IMAGE,
    queue: 1,
    lib: opt_lib,
    threads: this.threads_
  });
  this.changed();
};


/**
 * Update the stored frame state.
 * @param {GVol.Extent} extent The view extent (in map units).
 * @param {number} resGVolution The view resGVolution.
 * @param {GVol.proj.Projection} projection The view projection.
 * @return {GVolx.FrameState} The updated frame state.
 * @private
 */
GVol.source.Raster.prototype.updateFrameState_ = function(extent, resGVolution, projection) {

  var frameState = /** @type {GVolx.FrameState} */ (
    GVol.obj.assign({}, this.frameState_));

  frameState.viewState = /** @type {GVolx.ViewState} */ (
    GVol.obj.assign({}, frameState.viewState));

  var center = GVol.extent.getCenter(extent);

  frameState.extent = extent.slice();
  frameState.focus = center;
  frameState.size[0] = Math.round(GVol.extent.getWidth(extent) / resGVolution);
  frameState.size[1] = Math.round(GVol.extent.getHeight(extent) / resGVolution);

  var viewState = frameState.viewState;
  viewState.center = center;
  viewState.projection = projection;
  viewState.resGVolution = resGVolution;
  return frameState;
};


/**
 * Determine if all sources are ready.
 * @return {boGVolean} All sources are ready.
 * @private
 */
GVol.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() !== GVol.source.State.READY) {
      ready = false;
      break;
    }
  }
  return ready;
};


/**
 * @inheritDoc
 */
GVol.source.Raster.prototype.getImage = function(extent, resGVolution, pixelRatio, projection) {
  if (!this.allSourcesReady_()) {
    return null;
  }

  var frameState = this.updateFrameState_(extent, resGVolution, projection);
  this.requestedFrameState_ = frameState;

  // check if we can't reuse the existing GVol.ImageCanvas
  if (this.renderedImageCanvas_) {
    var renderedResGVolution = this.renderedImageCanvas_.getResGVolution();
    var renderedExtent = this.renderedImageCanvas_.getExtent();
    if (resGVolution !== renderedResGVolution || !GVol.extent.equals(extent, renderedExtent)) {
      this.renderedImageCanvas_ = null;
    }
  }

  if (!this.renderedImageCanvas_ || this.getRevision() !== this.renderedRevision_) {
    this.processSources_();
  }

  frameState.tileQueue.loadMoreTiles(16, 16);
  return this.renderedImageCanvas_;
};


/**
 * Start processing source data.
 * @private
 */
GVol.source.Raster.prototype.processSources_ = function() {
  var frameState = this.requestedFrameState_;
  var len = this.renderers_.length;
  var imageDatas = new Array(len);
  for (var i = 0; i < len; ++i) {
    var imageData = GVol.source.Raster.getImageData_(
        this.renderers_[i], frameState, frameState.layerStatesArray[i]);
    if (imageData) {
      imageDatas[i] = imageData;
    } else {
      return;
    }
  }

  var data = {};
  this.dispatchEvent(new GVol.source.Raster.Event(
      GVol.source.Raster.EventType_.BEFOREOPERATIONS, frameState, data));
  this.worker_.process(imageDatas, data,
      this.onWorkerComplete_.bind(this, frameState));
};


/**
 * Called when pixel processing is complete.
 * @param {GVolx.FrameState} frameState The frame state.
 * @param {Error} err Any error during processing.
 * @param {ImageData} output The output image data.
 * @param {Object} data The user data.
 * @private
 */
GVol.source.Raster.prototype.onWorkerComplete_ = function(frameState, err, output, data) {
  if (err || !output) {
    return;
  }

  // do nothing if extent or resGVolution changed
  var extent = frameState.extent;
  var resGVolution = frameState.viewState.resGVolution;
  if (resGVolution !== this.requestedFrameState_.viewState.resGVolution ||
      !GVol.extent.equals(extent, this.requestedFrameState_.extent)) {
    return;
  }

  var context;
  if (this.renderedImageCanvas_) {
    context = this.renderedImageCanvas_.getImage().getContext('2d');
  } else {
    var width = Math.round(GVol.extent.getWidth(extent) / resGVolution);
    var height = Math.round(GVol.extent.getHeight(extent) / resGVolution);
    context = GVol.dom.createCanvasContext2D(width, height);
    this.renderedImageCanvas_ = new GVol.ImageCanvas(
        extent, resGVolution, 1, this.getAttributions(), context.canvas);
  }
  context.putImageData(output, 0, 0);

  this.changed();
  this.renderedRevision_ = this.getRevision();

  this.dispatchEvent(new GVol.source.Raster.Event(
      GVol.source.Raster.EventType_.AFTEROPERATIONS, frameState, data));
};


/**
 * Get image data from a renderer.
 * @param {GVol.renderer.canvas.Layer} renderer Layer renderer.
 * @param {GVolx.FrameState} frameState The frame state.
 * @param {GVol.LayerState} layerState The layer state.
 * @return {ImageData} The image data.
 * @private
 */
GVol.source.Raster.getImageData_ = function(renderer, frameState, layerState) {
  if (!renderer.prepareFrame(frameState, layerState)) {
    return null;
  }
  var width = frameState.size[0];
  var height = frameState.size[1];
  if (!GVol.source.Raster.context_) {
    GVol.source.Raster.context_ = GVol.dom.createCanvasContext2D(width, height);
  } else {
    var canvas = GVol.source.Raster.context_.canvas;
    if (canvas.width !== width || canvas.height !== height) {
      GVol.source.Raster.context_ = GVol.dom.createCanvasContext2D(width, height);
    } else {
      GVol.source.Raster.context_.clearRect(0, 0, width, height);
    }
  }
  renderer.composeFrame(frameState, layerState, GVol.source.Raster.context_);
  return GVol.source.Raster.context_.getImageData(0, 0, width, height);
};


/**
 * A reusable canvas context.
 * @type {CanvasRenderingContext2D}
 * @private
 */
GVol.source.Raster.context_ = null;


/**
 * Get a list of layer states from a list of renderers.
 * @param {Array.<GVol.renderer.canvas.Layer>} renderers Layer renderers.
 * @return {Array.<GVol.LayerState>} The layer states.
 * @private
 */
GVol.source.Raster.getLayerStatesArray_ = function(renderers) {
  return renderers.map(function(renderer) {
    return renderer.getLayer().getLayerState();
  });
};


/**
 * Create renderers for all sources.
 * @param {Array.<GVol.source.Source>} sources The sources.
 * @return {Array.<GVol.renderer.canvas.Layer>} Array of layer renderers.
 * @private
 */
GVol.source.Raster.createRenderers_ = function(sources) {
  var len = sources.length;
  var renderers = new Array(len);
  for (var i = 0; i < len; ++i) {
    renderers[i] = GVol.source.Raster.createRenderer_(sources[i]);
  }
  return renderers;
};


/**
 * Create a renderer for the provided source.
 * @param {GVol.source.Source} source The source.
 * @return {GVol.renderer.canvas.Layer} The renderer.
 * @private
 */
GVol.source.Raster.createRenderer_ = function(source) {
  var renderer = null;
  if (source instanceof GVol.source.Tile) {
    renderer = GVol.source.Raster.createTileRenderer_(source);
  } else if (source instanceof GVol.source.Image) {
    renderer = GVol.source.Raster.createImageRenderer_(source);
  }
  return renderer;
};


/**
 * Create an image renderer for the provided source.
 * @param {GVol.source.Image} source The source.
 * @return {GVol.renderer.canvas.Layer} The renderer.
 * @private
 */
GVol.source.Raster.createImageRenderer_ = function(source) {
  var layer = new GVol.layer.Image({source: source});
  return new GVol.renderer.canvas.ImageLayer(layer);
};


/**
 * Create a tile renderer for the provided source.
 * @param {GVol.source.Tile} source The source.
 * @return {GVol.renderer.canvas.Layer} The renderer.
 * @private
 */
GVol.source.Raster.createTileRenderer_ = function(source) {
  var layer = new GVol.layer.Tile({source: source});
  return new GVol.renderer.canvas.TileLayer(layer);
};


/**
 * @classdesc
 * Events emitted by {@link GVol.source.Raster} instances are instances of this
 * type.
 *
 * @constructor
 * @extends {GVol.events.Event}
 * @implements {GVoli.source.RasterEvent}
 * @param {string} type Type.
 * @param {GVolx.FrameState} frameState The frame state.
 * @param {Object} data An object made available to operations.
 */
GVol.source.Raster.Event = function(type, frameState, data) {
  GVol.events.Event.call(this, type);

  /**
   * The raster extent.
   * @type {GVol.Extent}
   * @api
   */
  this.extent = frameState.extent;

  /**
   * The pixel resGVolution (map units per pixel).
   * @type {number}
   * @api
   */
  this.resGVolution = frameState.viewState.resGVolution / 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;

};
GVol.inherits(GVol.source.Raster.Event, GVol.events.Event);


/**
 * @override
 */
GVol.source.Raster.prototype.getImageInternal = function() {
  return null; // not implemented
};


/**
 * @enum {string}
 * @private
 */
GVol.source.Raster.EventType_ = {
  /**
   * Triggered before operations are run.
   * @event GVol.source.Raster.Event#beforeoperations
   * @api
   */
  BEFOREOPERATIONS: 'beforeoperations',

  /**
   * Triggered after operations are run.
   * @event GVol.source.Raster.Event#afteroperations
   * @api
   */
  AFTEROPERATIONS: 'afteroperations'
};

goog.provide('GVol.source.Stamen');

goog.require('GVol');
goog.require('GVol.Attribution');
goog.require('GVol.source.OSM');
goog.require('GVol.source.XYZ');


/**
 * @classdesc
 * Layer source for the Stamen tile server.
 *
 * @constructor
 * @extends {GVol.source.XYZ}
 * @param {GVolx.source.StamenOptions} options Stamen options.
 * @api
 */
GVol.source.Stamen = function(options) {
  var i = options.layer.indexOf('-');
  var provider = i == -1 ? options.layer : options.layer.slice(0, i);
  var providerConfig = GVol.source.Stamen.ProviderConfig[provider];

  var layerConfig = GVol.source.Stamen.LayerConfig[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;

  GVol.source.XYZ.call(this, {
    attributions: GVol.source.Stamen.ATTRIBUTIONS,
    cacheSize: options.cacheSize,
    crossOrigin: 'anonymous',
    maxZoom: options.maxZoom != undefined ? options.maxZoom : providerConfig.maxZoom,
    minZoom: options.minZoom != undefined ? options.minZoom : providerConfig.minZoom,
    opaque: layerConfig.opaque,
    reprojectionErrorThreshGVold: options.reprojectionErrorThreshGVold,
    tileLoadFunction: options.tileLoadFunction,
    url: url,
    wrapX: options.wrapX
  });
};
GVol.inherits(GVol.source.Stamen, GVol.source.XYZ);


/**
 * @const
 * @type {Array.<GVol.Attribution>}
 */
GVol.source.Stamen.ATTRIBUTIONS = [
  new GVol.Attribution({
    html: 'Map tiles by <a href="https://stamen.com/">Stamen Design</a>, ' +
        'under <a href="https://creativecommons.org/licenses/by/3.0/">CC BY' +
        ' 3.0</a>.'
  }),
  GVol.source.OSM.ATTRIBUTION
];

/**
 * @type {Object.<string, {extension: string, opaque: boGVolean}>}
 */
GVol.source.Stamen.LayerConfig = {
  '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
  },
  'watercGVolor': {
    extension: 'jpg',
    opaque: true
  }
};

/**
 * @type {Object.<string, {minZoom: number, maxZoom: number}>}
 */
GVol.source.Stamen.ProviderConfig = {
  'terrain': {
    minZoom: 4,
    maxZoom: 18
  },
  'toner': {
    minZoom: 0,
    maxZoom: 20
  },
  'watercGVolor': {
    minZoom: 1,
    maxZoom: 16
  }
};

goog.provide('GVol.source.TileArcGISRest');

goog.require('GVol');
goog.require('GVol.extent');
goog.require('GVol.math');
goog.require('GVol.obj');
goog.require('GVol.size');
goog.require('GVol.source.TileImage');
goog.require('GVol.tilecoord');
goog.require('GVol.uri');


/**
 * @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 GVol.source.XYZ} data source.
 *
 * @constructor
 * @extends {GVol.source.TileImage}
 * @param {GVolx.source.TileArcGISRestOptions=} opt_options Tile ArcGIS Rest
 *     options.
 * @api
 */
GVol.source.TileArcGISRest = function(opt_options) {

  var options = opt_options || {};

  GVol.source.TileImage.call(this, {
    attributions: options.attributions,
    cacheSize: options.cacheSize,
    crossOrigin: options.crossOrigin,
    logo: options.logo,
    projection: options.projection,
    reprojectionErrorThreshGVold: options.reprojectionErrorThreshGVold,
    tileGrid: options.tileGrid,
    tileLoadFunction: options.tileLoadFunction,
    url: options.url,
    urls: options.urls,
    wrapX: options.wrapX !== undefined ? options.wrapX : true
  });

  /**
   * @private
   * @type {!Object}
   */
  this.params_ = options.params || {};

  /**
   * @private
   * @type {GVol.Extent}
   */
  this.tmpExtent_ = GVol.extent.createEmpty();

  this.setKey(this.getKeyForParams_());
};
GVol.inherits(GVol.source.TileArcGISRest, GVol.source.TileImage);


/**
 * @private
 * @return {string} The key for the current params.
 */
GVol.source.TileArcGISRest.prototype.getKeyForParams_ = function() {
  var i = 0;
  var res = [];
  for (var key in this.params_) {
    res[i++] = key + '-' + this.params_[key];
  }
  return res.join('/');
};


/**
 * 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
 */
GVol.source.TileArcGISRest.prototype.getParams = function() {
  return this.params_;
};


/**
 * @param {GVol.TileCoord} tileCoord Tile coordinate.
 * @param {GVol.Size} tileSize Tile size.
 * @param {GVol.Extent} tileExtent Tile extent.
 * @param {number} pixelRatio Pixel ratio.
 * @param {GVol.proj.Projection} projection Projection.
 * @param {Object} params Params.
 * @return {string|undefined} Request URL.
 * @private
 */
GVol.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 = GVol.math.modulo(GVol.tilecoord.hash(tileCoord), urls.length);
    url = urls[index];
  }

  var modifiedUrl = url
      .replace(/MapServer\/?$/, 'MapServer/export')
      .replace(/ImageServer\/?$/, 'ImageServer/exportImage');
  return GVol.uri.appendParams(modifiedUrl, params);
};


/**
 * @inheritDoc
 */
GVol.source.TileArcGISRest.prototype.getTilePixelRatio = function(pixelRatio) {
  return /** @type {number} */ (pixelRatio);
};


/**
 * @inheritDoc
 */
GVol.source.TileArcGISRest.prototype.fixedTileUrlFunction = function(tileCoord, pixelRatio, projection) {

  var tileGrid = this.getTileGrid();
  if (!tileGrid) {
    tileGrid = this.getTileGridForProjection(projection);
  }

  if (tileGrid.getResGVolutions().length <= tileCoord[0]) {
    return undefined;
  }

  var tileExtent = tileGrid.getTileCoordExtent(
      tileCoord, this.tmpExtent_);
  var tileSize = GVol.size.toSize(
      tileGrid.getTileSize(tileCoord[0]), this.tmpSize);

  if (pixelRatio != 1) {
    tileSize = GVol.size.scale(tileSize, pixelRatio, this.tmpSize);
  }

  // Apply default params and override with user specified values.
  var baseParams = {
    'F': 'image',
    'FORMAT': 'PNG32',
    'TRANSPARENT': true
  };
  GVol.obj.assign(baseParams, this.params_);

  return this.getRequestUrl_(tileCoord, tileSize, tileExtent,
      pixelRatio, projection, baseParams);
};


/**
 * Update the user-provided params.
 * @param {Object} params Params.
 * @api
 */
GVol.source.TileArcGISRest.prototype.updateParams = function(params) {
  GVol.obj.assign(this.params_, params);
  this.setKey(this.getKeyForParams_());
};

goog.provide('GVol.source.TileDebug');

goog.require('GVol');
goog.require('GVol.Tile');
goog.require('GVol.TileState');
goog.require('GVol.dom');
goog.require('GVol.size');
goog.require('GVol.source.Tile');


/**
 * @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 {GVol.source.Tile}
 * @param {GVolx.source.TileDebugOptions} options Debug tile options.
 * @api
 */
GVol.source.TileDebug = function(options) {

  GVol.source.Tile.call(this, {
    opaque: false,
    projection: options.projection,
    tileGrid: options.tileGrid,
    wrapX: options.wrapX !== undefined ? options.wrapX : true
  });

};
GVol.inherits(GVol.source.TileDebug, GVol.source.Tile);


/**
 * @inheritDoc
 */
GVol.source.TileDebug.prototype.getTile = function(z, x, y) {
  var tileCoordKey = this.getKeyZXY(z, x, y);
  if (this.tileCache.containsKey(tileCoordKey)) {
    return /** @type {!GVol.source.TileDebug.Tile_} */ (this.tileCache.get(tileCoordKey));
  } else {
    var tileSize = GVol.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 GVol.source.TileDebug.Tile_(tileCoord, tileSize, text);
    this.tileCache.set(tileCoordKey, tile);
    return tile;
  }
};


/**
 * @constructor
 * @extends {GVol.Tile}
 * @param {GVol.TileCoord} tileCoord Tile coordinate.
 * @param {GVol.Size} tileSize Tile size.
 * @param {string} text Text.
 * @private
 */
GVol.source.TileDebug.Tile_ = function(tileCoord, tileSize, text) {

  GVol.Tile.call(this, tileCoord, GVol.TileState.LOADED);

  /**
   * @private
   * @type {GVol.Size}
   */
  this.tileSize_ = tileSize;

  /**
   * @private
   * @type {string}
   */
  this.text_ = text;

  /**
   * @private
   * @type {HTMLCanvasElement}
   */
  this.canvas_ = null;

};
GVol.inherits(GVol.source.TileDebug.Tile_, GVol.Tile);


/**
 * Get the image element for this tile.
 * @return {HTMLCanvasElement} Image.
 */
GVol.source.TileDebug.Tile_.prototype.getImage = function() {
  if (this.canvas_) {
    return this.canvas_;
  } else {
    var tileSize = this.tileSize_;
    var context = GVol.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.canvas_ = context.canvas;
    return context.canvas;
  }
};


/**
 * @override
 */
GVol.source.TileDebug.Tile_.prototype.load = function() {};

// FIXME check order of async callbacks

/**
 * @see http://mapbox.com/developers/api/
 */

goog.provide('GVol.source.TileJSON');

goog.require('GVol');
goog.require('GVol.Attribution');
goog.require('GVol.TileUrlFunction');
goog.require('GVol.asserts');
goog.require('GVol.extent');
goog.require('GVol.net');
goog.require('GVol.proj');
goog.require('GVol.source.State');
goog.require('GVol.source.TileImage');
goog.require('GVol.tilegrid');


/**
 * @classdesc
 * Layer source for tile data in TileJSON format.
 *
 * @constructor
 * @extends {GVol.source.TileImage}
 * @param {GVolx.source.TileJSONOptions} options TileJSON options.
 * @api
 */
GVol.source.TileJSON = function(options) {

  /**
   * @type {TileJSON}
   * @private
   */
  this.tileJSON_ = null;

  GVol.source.TileImage.call(this, {
    attributions: options.attributions,
    cacheSize: options.cacheSize,
    crossOrigin: options.crossOrigin,
    projection: GVol.proj.get('EPSG:3857'),
    reprojectionErrorThreshGVold: options.reprojectionErrorThreshGVold,
    state: GVol.source.State.LOADING,
    tileLoadFunction: options.tileLoadFunction,
    wrapX: options.wrapX !== undefined ? options.wrapX : true
  });

  if (options.url) {
    if (options.jsonp) {
      GVol.net.jsonp(options.url, this.handleTileJSONResponse.bind(this),
          this.handleTileJSONError.bind(this));
    } else {
      var client = new XMLHttpRequest();
      client.addEventListener('load', this.onXHRLoad_.bind(this));
      client.addEventListener('error', this.onXHRError_.bind(this));
      client.open('GET', options.url);
      client.send();
    }
  } else if (options.tileJSON) {
    this.handleTileJSONResponse(options.tileJSON);
  } else {
    GVol.asserts.assert(false, 51); // Either `url` or `tileJSON` options must be provided
  }

};
GVol.inherits(GVol.source.TileJSON, GVol.source.TileImage);


/**
 * @private
 * @param {Event} event The load event.
 */
GVol.source.TileJSON.prototype.onXHRLoad_ = function(event) {
  var client = /** @type {XMLHttpRequest} */ (event.target);
  // status will be 0 for file:// urls
  if (!client.status || client.status >= 200 && client.status < 300) {
    var response;
    try {
      response = /** @type {TileJSON} */(JSON.parse(client.responseText));
    } catch (err) {
      this.handleTileJSONError();
      return;
    }
    this.handleTileJSONResponse(response);
  } else {
    this.handleTileJSONError();
  }
};


/**
 * @private
 * @param {Event} event The error event.
 */
GVol.source.TileJSON.prototype.onXHRError_ = function(event) {
  this.handleTileJSONError();
};


/**
 * @return {TileJSON} The tilejson object.
 * @api
 */
GVol.source.TileJSON.prototype.getTileJSON = function() {
  return this.tileJSON_;
};


/**
 * @protected
 * @param {TileJSON} tileJSON Tile JSON.
 */
GVol.source.TileJSON.prototype.handleTileJSONResponse = function(tileJSON) {

  var epsg4326Projection = GVol.proj.get('EPSG:4326');

  var sourceProjection = this.getProjection();
  var extent;
  if (tileJSON.bounds !== undefined) {
    var transform = GVol.proj.getTransformFromProjections(
        epsg4326Projection, sourceProjection);
    extent = GVol.extent.applyTransform(tileJSON.bounds, transform);
  }

  var minZoom = tileJSON.minzoom || 0;
  var maxZoom = tileJSON.maxzoom || 22;
  var tileGrid = GVol.tilegrid.createXYZ({
    extent: GVol.tilegrid.extentFromProjection(sourceProjection),
    maxZoom: maxZoom,
    minZoom: minZoom
  });
  this.tileGrid = tileGrid;

  this.tileUrlFunction =
      GVol.TileUrlFunction.createFromTemplates(tileJSON.tiles, tileGrid);

  if (tileJSON.attribution !== undefined && !this.getAttributions()) {
    var attributionExtent = extent !== undefined ?
      extent : epsg4326Projection.getExtent();
    /** @type {Object.<string, Array.<GVol.TileRange>>} */
    var tileRanges = {};
    var z, zKey;
    for (z = minZoom; z <= maxZoom; ++z) {
      zKey = z.toString();
      tileRanges[zKey] =
          [tileGrid.getTileRangeForExtentAndZ(attributionExtent, z)];
    }
    this.setAttributions([
      new GVol.Attribution({
        html: tileJSON.attribution,
        tileRanges: tileRanges
      })
    ]);
  }
  this.tileJSON_ = tileJSON;
  this.setState(GVol.source.State.READY);

};


/**
 * @protected
 */
GVol.source.TileJSON.prototype.handleTileJSONError = function() {
  this.setState(GVol.source.State.ERROR);
};

goog.provide('GVol.source.TileUTFGrid');

goog.require('GVol');
goog.require('GVol.Attribution');
goog.require('GVol.Tile');
goog.require('GVol.TileState');
goog.require('GVol.TileUrlFunction');
goog.require('GVol.asserts');
goog.require('GVol.events');
goog.require('GVol.events.EventType');
goog.require('GVol.extent');
goog.require('GVol.net');
goog.require('GVol.proj');
goog.require('GVol.source.State');
goog.require('GVol.source.Tile');
goog.require('GVol.tilegrid');


/**
 * @classdesc
 * Layer source for UTFGrid interaction data loaded from TileJSON format.
 *
 * @constructor
 * @extends {GVol.source.Tile}
 * @param {GVolx.source.TileUTFGridOptions} options Source options.
 * @api
 */
GVol.source.TileUTFGrid = function(options) {
  GVol.source.Tile.call(this, {
    projection: GVol.proj.get('EPSG:3857'),
    state: GVol.source.State.LOADING
  });

  /**
   * @private
   * @type {boGVolean}
   */
  this.preemptive_ = options.preemptive !== undefined ?
    options.preemptive : true;

  /**
   * @private
   * @type {!GVol.TileUrlFunctionType}
   */
  this.tileUrlFunction_ = GVol.TileUrlFunction.nullTileUrlFunction;

  /**
   * @private
   * @type {string|undefined}
   */
  this.template_ = undefined;

  /**
   * @private
   * @type {boGVolean}
   */
  this.jsonp_ = options.jsonp || false;

  if (options.url) {
    if (this.jsonp_) {
      GVol.net.jsonp(options.url, this.handleTileJSONResponse.bind(this),
          this.handleTileJSONError.bind(this));
    } else {
      var client = new XMLHttpRequest();
      client.addEventListener('load', this.onXHRLoad_.bind(this));
      client.addEventListener('error', this.onXHRError_.bind(this));
      client.open('GET', options.url);
      client.send();
    }
  } else if (options.tileJSON) {
    this.handleTileJSONResponse(options.tileJSON);
  } else {
    GVol.asserts.assert(false, 51); // Either `url` or `tileJSON` options must be provided
  }
};
GVol.inherits(GVol.source.TileUTFGrid, GVol.source.Tile);


/**
 * @private
 * @param {Event} event The load event.
 */
GVol.source.TileUTFGrid.prototype.onXHRLoad_ = function(event) {
  var client = /** @type {XMLHttpRequest} */ (event.target);
  // status will be 0 for file:// urls
  if (!client.status || client.status >= 200 && client.status < 300) {
    var response;
    try {
      response = /** @type {TileJSON} */(JSON.parse(client.responseText));
    } catch (err) {
      this.handleTileJSONError();
      return;
    }
    this.handleTileJSONResponse(response);
  } else {
    this.handleTileJSONError();
  }
};


/**
 * @private
 * @param {Event} event The error event.
 */
GVol.source.TileUTFGrid.prototype.onXHRError_ = function(event) {
  this.handleTileJSONError();
};


/**
 * Return the template from TileJSON.
 * @return {string|undefined} The template from TileJSON.
 * @api
 */
GVol.source.TileUTFGrid.prototype.getTemplate = function() {
  return this.template_;
};


/**
 * Calls the callback (synchronously by default) with the available data
 * for given coordinate and resGVolution (or `null` if not yet loaded or
 * in case of an error).
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @param {number} resGVolution ResGVolution.
 * @param {function(this: T, *)} callback Callback.
 * @param {T=} opt_this The object to use as `this` in the callback.
 * @param {boGVolean=} opt_request If `true` the callback is always async.
 *                               The tile data is requested if not yet loaded.
 * @template T
 * @api
 */
GVol.source.TileUTFGrid.prototype.forDataAtCoordinateAndResGVolution = function(
    coordinate, resGVolution, callback, opt_this, opt_request) {
  if (this.tileGrid) {
    var tileCoord = this.tileGrid.getTileCoordForCoordAndResGVolution(
        coordinate, resGVolution);
    var tile = /** @type {!GVol.source.TileUTFGrid.Tile_} */(this.getTile(
        tileCoord[0], tileCoord[1], tileCoord[2], 1, this.getProjection()));
    tile.forDataAtCoordinate(coordinate, callback, opt_this, opt_request);
  } else {
    if (opt_request === true) {
      setTimeout(function() {
        callback.call(opt_this, null);
      }, 0);
    } else {
      callback.call(opt_this, null);
    }
  }
};


/**
 * @protected
 */
GVol.source.TileUTFGrid.prototype.handleTileJSONError = function() {
  this.setState(GVol.source.State.ERROR);
};


/**
 * TODO: very similar to GVol.source.TileJSON#handleTileJSONResponse
 * @protected
 * @param {TileJSON} tileJSON Tile JSON.
 */
GVol.source.TileUTFGrid.prototype.handleTileJSONResponse = function(tileJSON) {

  var epsg4326Projection = GVol.proj.get('EPSG:4326');

  var sourceProjection = this.getProjection();
  var extent;
  if (tileJSON.bounds !== undefined) {
    var transform = GVol.proj.getTransformFromProjections(
        epsg4326Projection, sourceProjection);
    extent = GVol.extent.applyTransform(tileJSON.bounds, transform);
  }

  var minZoom = tileJSON.minzoom || 0;
  var maxZoom = tileJSON.maxzoom || 22;
  var tileGrid = GVol.tilegrid.createXYZ({
    extent: GVol.tilegrid.extentFromProjection(sourceProjection),
    maxZoom: maxZoom,
    minZoom: minZoom
  });
  this.tileGrid = tileGrid;

  this.template_ = tileJSON.template;

  var grids = tileJSON.grids;
  if (!grids) {
    this.setState(GVol.source.State.ERROR);
    return;
  }

  this.tileUrlFunction_ =
      GVol.TileUrlFunction.createFromTemplates(grids, tileGrid);

  if (tileJSON.attribution !== undefined) {
    var attributionExtent = extent !== undefined ?
      extent : epsg4326Projection.getExtent();
    /** @type {Object.<string, Array.<GVol.TileRange>>} */
    var tileRanges = {};
    var z, zKey;
    for (z = minZoom; z <= maxZoom; ++z) {
      zKey = z.toString();
      tileRanges[zKey] =
          [tileGrid.getTileRangeForExtentAndZ(attributionExtent, z)];
    }
    this.setAttributions([
      new GVol.Attribution({
        html: tileJSON.attribution,
        tileRanges: tileRanges
      })
    ]);
  }

  this.setState(GVol.source.State.READY);

};


/**
 * @inheritDoc
 */
GVol.source.TileUTFGrid.prototype.getTile = function(z, x, y, pixelRatio, projection) {
  var tileCoordKey = this.getKeyZXY(z, x, y);
  if (this.tileCache.containsKey(tileCoordKey)) {
    return /** @type {!GVol.Tile} */ (this.tileCache.get(tileCoordKey));
  } else {
    var tileCoord = [z, x, y];
    var urlTileCoord =
        this.getTileCoordForTileUrlFunction(tileCoord, projection);
    var tileUrl = this.tileUrlFunction_(urlTileCoord, pixelRatio, projection);
    var tile = new GVol.source.TileUTFGrid.Tile_(
        tileCoord,
        tileUrl !== undefined ? GVol.TileState.IDLE : GVol.TileState.EMPTY,
        tileUrl !== undefined ? tileUrl : '',
        this.tileGrid.getTileCoordExtent(tileCoord),
        this.preemptive_,
        this.jsonp_);
    this.tileCache.set(tileCoordKey, tile);
    return tile;
  }
};


/**
 * @inheritDoc
 */
GVol.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 {GVol.Tile}
 * @param {GVol.TileCoord} tileCoord Tile coordinate.
 * @param {GVol.TileState} state State.
 * @param {string} src Image source URI.
 * @param {GVol.Extent} extent Extent of the tile.
 * @param {boGVolean} preemptive Load the tile when visible (before it's needed).
 * @param {boGVolean} jsonp Load the tile as a script.
 * @private
 */
GVol.source.TileUTFGrid.Tile_ = function(tileCoord, state, src, extent, preemptive, jsonp) {

  GVol.Tile.call(this, tileCoord, state);

  /**
   * @private
   * @type {string}
   */
  this.src_ = src;

  /**
   * @private
   * @type {GVol.Extent}
   */
  this.extent_ = extent;

  /**
   * @private
   * @type {boGVolean}
   */
  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;


  /**
   * @private
   * @type {boGVolean}
   */
  this.jsonp_ = jsonp;

};
GVol.inherits(GVol.source.TileUTFGrid.Tile_, GVol.Tile);


/**
 * Get the image element for this tile.
 * @return {Image} Image.
 */
GVol.source.TileUTFGrid.Tile_.prototype.getImage = function() {
  return null;
};


/**
 * Synchronously returns data at given coordinate (if available).
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @return {*} The data.
 */
GVol.source.TileUTFGrid.Tile_.prototype.getData = function(coordinate) {
  if (!this.grid_ || !this.keys_) {
    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 (typeof row !== 'string') {
    return null;
  }

  var code = row.charCodeAt(Math.floor(xRelative * row.length));
  if (code >= 93) {
    code--;
  }
  if (code >= 35) {
    code--;
  }
  code -= 32;

  var data = null;
  if (code in this.keys_) {
    var id = this.keys_[code];
    if (this.data_ && id in this.data_) {
      data = this.data_[id];
    } else {
      data = id;
    }
  }
  return data;
};


/**
 * Calls the callback (synchronously by default) with the available data
 * for given coordinate (or `null` if not yet loaded).
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @param {function(this: T, *)} callback Callback.
 * @param {T=} opt_this The object to use as `this` in the callback.
 * @param {boGVolean=} opt_request If `true` the callback is always async.
 *                               The tile data is requested if not yet loaded.
 * @template T
 */
GVol.source.TileUTFGrid.Tile_.prototype.forDataAtCoordinate = function(coordinate, callback, opt_this, opt_request) {
  if (this.state == GVol.TileState.IDLE && opt_request === true) {
    GVol.events.listenOnce(this, GVol.events.EventType.CHANGE, function(e) {
      callback.call(opt_this, this.getData(coordinate));
    }, this);
    this.loadInternal_();
  } else {
    if (opt_request === true) {
      setTimeout(function() {
        callback.call(opt_this, this.getData(coordinate));
      }.bind(this), 0);
    } else {
      callback.call(opt_this, this.getData(coordinate));
    }
  }
};


/**
 * @inheritDoc
 */
GVol.source.TileUTFGrid.Tile_.prototype.getKey = function() {
  return this.src_;
};


/**
 * @private
 */
GVol.source.TileUTFGrid.Tile_.prototype.handleError_ = function() {
  this.state = GVol.TileState.ERROR;
  this.changed();
};


/**
 * @param {!UTFGridJSON} json UTFGrid data.
 * @private
 */
GVol.source.TileUTFGrid.Tile_.prototype.handleLoad_ = function(json) {
  this.grid_ = json.grid;
  this.keys_ = json.keys;
  this.data_ = json.data;

  this.state = GVol.TileState.EMPTY;
  this.changed();
};


/**
 * @private
 */
GVol.source.TileUTFGrid.Tile_.prototype.loadInternal_ = function() {
  if (this.state == GVol.TileState.IDLE) {
    this.state = GVol.TileState.LOADING;
    if (this.jsonp_) {
      GVol.net.jsonp(this.src_, this.handleLoad_.bind(this),
          this.handleError_.bind(this));
    } else {
      var client = new XMLHttpRequest();
      client.addEventListener('load', this.onXHRLoad_.bind(this));
      client.addEventListener('error', this.onXHRError_.bind(this));
      client.open('GET', this.src_);
      client.send();
    }
  }
};


/**
 * @private
 * @param {Event} event The load event.
 */
GVol.source.TileUTFGrid.Tile_.prototype.onXHRLoad_ = function(event) {
  var client = /** @type {XMLHttpRequest} */ (event.target);
  // status will be 0 for file:// urls
  if (!client.status || client.status >= 200 && client.status < 300) {
    var response;
    try {
      response = /** @type {!UTFGridJSON} */(JSON.parse(client.responseText));
    } catch (err) {
      this.handleError_();
      return;
    }
    this.handleLoad_(response);
  } else {
    this.handleError_();
  }
};


/**
 * @private
 * @param {Event} event The error event.
 */
GVol.source.TileUTFGrid.Tile_.prototype.onXHRError_ = function(event) {
  this.handleError_();
};


/**
 * @override
 */
GVol.source.TileUTFGrid.Tile_.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('GVol.source.TileWMS');

goog.require('GVol');
goog.require('GVol.asserts');
goog.require('GVol.extent');
goog.require('GVol.obj');
goog.require('GVol.math');
goog.require('GVol.proj');
goog.require('GVol.size');
goog.require('GVol.source.TileImage');
goog.require('GVol.source.WMSServerType');
goog.require('GVol.tilecoord');
goog.require('GVol.string');
goog.require('GVol.uri');

/**
 * @classdesc
 * Layer source for tile data from WMS servers.
 *
 * @constructor
 * @extends {GVol.source.TileImage}
 * @param {GVolx.source.TileWMSOptions=} opt_options Tile WMS options.
 * @api
 */
GVol.source.TileWMS = function(opt_options) {

  var options = opt_options || {};

  var params = options.params || {};

  var transparent = 'TRANSPARENT' in params ? params['TRANSPARENT'] : true;

  GVol.source.TileImage.call(this, {
    attributions: options.attributions,
    cacheSize: options.cacheSize,
    crossOrigin: options.crossOrigin,
    logo: options.logo,
    opaque: !transparent,
    projection: options.projection,
    reprojectionErrorThreshGVold: options.reprojectionErrorThreshGVold,
    tileClass: options.tileClass,
    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 {boGVolean}
   */
  this.v13_ = true;

  /**
   * @private
   * @type {GVol.source.WMSServerType|undefined}
   */
  this.serverType_ = /** @type {GVol.source.WMSServerType|undefined} */ (options.serverType);

  /**
   * @private
   * @type {boGVolean}
   */
  this.hidpi_ = options.hidpi !== undefined ? options.hidpi : true;

  /**
   * @private
   * @type {string}
   */
  this.coordKeyPrefix_ = '';
  this.resetCoordKeyPrefix_();

  /**
   * @private
   * @type {GVol.Extent}
   */
  this.tmpExtent_ = GVol.extent.createEmpty();

  this.updateV13_();
  this.setKey(this.getKeyForParams_());

};
GVol.inherits(GVol.source.TileWMS, GVol.source.TileImage);


/**
 * Return the GetFeatureInfo URL for the passed coordinate, resGVolution, and
 * projection. Return `undefined` if the GetFeatureInfo URL cannot be
 * constructed.
 * @param {GVol.Coordinate} coordinate Coordinate.
 * @param {number} resGVolution ResGVolution.
 * @param {GVol.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
 */
GVol.source.TileWMS.prototype.getGetFeatureInfoUrl = function(coordinate, resGVolution, projection, params) {
  var projectionObj = GVol.proj.get(projection);

  var tileGrid = this.getTileGrid();
  if (!tileGrid) {
    tileGrid = this.getTileGridForProjection(projectionObj);
  }

  var tileCoord = tileGrid.getTileCoordForCoordAndResGVolution(
      coordinate, resGVolution);

  if (tileGrid.getResGVolutions().length <= tileCoord[0]) {
    return undefined;
  }

  var tileResGVolution = tileGrid.getResGVolution(tileCoord[0]);
  var tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent_);
  var tileSize = GVol.size.toSize(
      tileGrid.getTileSize(tileCoord[0]), this.tmpSize);

  var gutter = this.gutter_;
  if (gutter !== 0) {
    tileSize = GVol.size.buffer(tileSize, gutter, this.tmpSize);
    tileExtent = GVol.extent.buffer(tileExtent,
        tileResGVolution * gutter, tileExtent);
  }

  var baseParams = {
    'SERVICE': 'WMS',
    'VERSION': GVol.DEFAULT_WMS_VERSION,
    'REQUEST': 'GetFeatureInfo',
    'FORMAT': 'image/png',
    'TRANSPARENT': true,
    'QUERY_LAYERS': this.params_['LAYERS']
  };
  GVol.obj.assign(baseParams, this.params_, params);

  var x = Math.floor((coordinate[0] - tileExtent[0]) / tileResGVolution);
  var y = Math.floor((tileExtent[3] - coordinate[1]) / tileResGVolution);

  baseParams[this.v13_ ? 'I' : 'X'] = x;
  baseParams[this.v13_ ? 'J' : 'Y'] = y;

  return this.getRequestUrl_(tileCoord, tileSize, tileExtent,
      1, projectionObj, baseParams);
};


/**
 * @inheritDoc
 */
GVol.source.TileWMS.prototype.getGutterInternal = function() {
  return this.gutter_;
};


/**
 * @inheritDoc
 */
GVol.source.TileWMS.prototype.getKeyZXY = function(z, x, y) {
  return this.coordKeyPrefix_ + GVol.source.TileImage.prototype.getKeyZXY.call(this, 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
 */
GVol.source.TileWMS.prototype.getParams = function() {
  return this.params_;
};


/**
 * @param {GVol.TileCoord} tileCoord Tile coordinate.
 * @param {GVol.Size} tileSize Tile size.
 * @param {GVol.Extent} tileExtent Tile extent.
 * @param {number} pixelRatio Pixel ratio.
 * @param {GVol.proj.Projection} projection Projection.
 * @param {Object} params Params.
 * @return {string|undefined} Request URL.
 * @private
 */
GVol.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'] = '';
  }

  if (pixelRatio != 1) {
    switch (this.serverType_) {
      case GVol.source.WMSServerType.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 GVol.source.WMSServerType.MAPSERVER:
        params['MAP_RESOLUTION'] = 90 * pixelRatio;
        break;
      case GVol.source.WMSServerType.CARMENTA_SERVER:
      case GVol.source.WMSServerType.QGIS:
        params['DPI'] = 90 * pixelRatio;
        break;
      default:
        GVol.asserts.assert(false, 52); // 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 = GVol.math.modulo(GVol.tilecoord.hash(tileCoord), urls.length);
    url = urls[index];
  }
  return GVol.uri.appendParams(url, params);
};


/**
 * @inheritDoc
 */
GVol.source.TileWMS.prototype.getTilePixelRatio = function(pixelRatio) {
  return (!this.hidpi_ || this.serverType_ === undefined) ? 1 :
  /** @type {number} */ (pixelRatio);
};


/**
 * @private
 */
GVol.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
 * @return {string} The key for the current params.
 */
GVol.source.TileWMS.prototype.getKeyForParams_ = function() {
  var i = 0;
  var res = [];
  for (var key in this.params_) {
    res[i++] = key + '-' + this.params_[key];
  }
  return res.join('/');
};


/**
 * @inheritDoc
 */
GVol.source.TileWMS.prototype.fixedTileUrlFunction = function(tileCoord, pixelRatio, projection) {

  var tileGrid = this.getTileGrid();
  if (!tileGrid) {
    tileGrid = this.getTileGridForProjection(projection);
  }

  if (tileGrid.getResGVolutions().length <= tileCoord[0]) {
    return undefined;
  }

  if (pixelRatio != 1 && (!this.hidpi_ || this.serverType_ === undefined)) {
    pixelRatio = 1;
  }

  var tileResGVolution = tileGrid.getResGVolution(tileCoord[0]);
  var tileExtent = tileGrid.getTileCoordExtent(tileCoord, this.tmpExtent_);
  var tileSize = GVol.size.toSize(
      tileGrid.getTileSize(tileCoord[0]), this.tmpSize);

  var gutter = this.gutter_;
  if (gutter !== 0) {
    tileSize = GVol.size.buffer(tileSize, gutter, this.tmpSize);
    tileExtent = GVol.extent.buffer(tileExtent,
        tileResGVolution * gutter, tileExtent);
  }

  if (pixelRatio != 1) {
    tileSize = GVol.size.scale(tileSize, pixelRatio, this.tmpSize);
  }

  var baseParams = {
    'SERVICE': 'WMS',
    'VERSION': GVol.DEFAULT_WMS_VERSION,
    'REQUEST': 'GetMap',
    'FORMAT': 'image/png',
    'TRANSPARENT': true
  };
  GVol.obj.assign(baseParams, this.params_);

  return this.getRequestUrl_(tileCoord, tileSize, tileExtent,
      pixelRatio, projection, baseParams);
};

/**
 * @inheritDoc
 */
GVol.source.TileWMS.prototype.setUrls = function(urls) {
  GVol.source.TileImage.prototype.setUrls.call(this, urls);
  this.resetCoordKeyPrefix_();
};


/**
 * Update the user-provided params.
 * @param {Object} params Params.
 * @api
 */
GVol.source.TileWMS.prototype.updateParams = function(params) {
  GVol.obj.assign(this.params_, params);
  this.resetCoordKeyPrefix_();
  this.updateV13_();
  this.setKey(this.getKeyForParams_());
};


/**
 * @private
 */
GVol.source.TileWMS.prototype.updateV13_ = function() {
  var version = this.params_['VERSION'] || GVol.DEFAULT_WMS_VERSION;
  this.v13_ = GVol.string.compareVersions(version, '1.3') >= 0;
};

goog.provide('GVol.VectorImageTile');

goog.require('GVol');
goog.require('GVol.Tile');
goog.require('GVol.TileState');
goog.require('GVol.array');
goog.require('GVol.dom');
goog.require('GVol.events');
goog.require('GVol.extent');
goog.require('GVol.events.EventType');
goog.require('GVol.featureloader');


/**
 * @constructor
 * @extends {GVol.Tile}
 * @param {GVol.TileCoord} tileCoord Tile coordinate.
 * @param {GVol.TileState} state State.
 * @param {string} src Data source url.
 * @param {GVol.format.Feature} format Feature format.
 * @param {GVol.TileLoadFunctionType} tileLoadFunction Tile load function.
 * @param {GVol.TileCoord} urlTileCoord Wrapped tile coordinate for source urls.
 * @param {GVol.TileUrlFunctionType} tileUrlFunction Tile url function.
 * @param {GVol.tilegrid.TileGrid} sourceTileGrid Tile grid of the source.
 * @param {GVol.tilegrid.TileGrid} tileGrid Tile grid of the renderer.
 * @param {Object.<string,GVol.VectorTile>} sourceTiles Source tiles.
 * @param {number} pixelRatio Pixel ratio.
 * @param {GVol.proj.Projection} projection Projection.
 * @param {function(new: GVol.VectorTile, GVol.TileCoord, GVol.TileState, string,
 *     GVol.format.Feature, GVol.TileLoadFunctionType)} tileClass Class to
 *     instantiate for source tiles.
 * @param {function(this: GVol.source.VectorTile, GVol.events.Event)} handleTileChange
 *     Function to call when a source tile's state changes.
 */
GVol.VectorImageTile = function(tileCoord, state, src, format, tileLoadFunction,
    urlTileCoord, tileUrlFunction, sourceTileGrid, tileGrid, sourceTiles,
    pixelRatio, projection, tileClass, handleTileChange) {

  GVol.Tile.call(this, tileCoord, state);

  /**
   * @private
   * @type {Object.<string, CanvasRenderingContext2D>}
   */
  this.context_ = {};

  /**
   * @private
   * @type {GVol.FeatureLoader}
   */
  this.loader_;

  /**
   * @private
   * @type {Object.<string, GVol.TileReplayState>}
   */
  this.replayState_ = {};

  /**
   * @private
   * @type {Object.<string,GVol.VectorTile>}
   */
  this.sourceTiles_ = sourceTiles;

  /**
   * Keys of source tiles used by this tile. Use with {@link #getTile}.
   * @type {Array.<string>}
   */
  this.tileKeys = [];

  /**
   * @type {string}
   */
  this.src_ = src;

  /**
   * @type {GVol.TileCoord}
   */
  this.wrappedTileCoord = urlTileCoord;

  /**
   * @type {Array.<GVol.EventsKey>}
   */
  this.loadListenerKeys_ = [];

  /**
   * @type {Array.<GVol.EventsKey>}
   */
  this.sourceTileListenerKeys_ = [];

  if (urlTileCoord) {
    var extent = tileGrid.getTileCoordExtent(urlTileCoord);
    var resGVolution = tileGrid.getResGVolution(tileCoord[0]);
    var sourceZ = sourceTileGrid.getZForResGVolution(resGVolution);
    sourceTileGrid.forEachTileCoord(extent, sourceZ, function(sourceTileCoord) {
      var sharedExtent = GVol.extent.getIntersection(extent,
          sourceTileGrid.getTileCoordExtent(sourceTileCoord));
      if (GVol.extent.getWidth(sharedExtent) / resGVolution >= 0.5 &&
          GVol.extent.getHeight(sharedExtent) / resGVolution >= 0.5) {
        // only include source tile if overlap is at least 1 pixel
        var sourceTileKey = sourceTileCoord.toString();
        var sourceTile = sourceTiles[sourceTileKey];
        if (!sourceTile) {
          var tileUrl = tileUrlFunction(sourceTileCoord, pixelRatio, projection);
          sourceTile = sourceTiles[sourceTileKey] = new tileClass(sourceTileCoord,
              tileUrl == undefined ? GVol.TileState.EMPTY : GVol.TileState.IDLE,
              tileUrl == undefined ? '' : tileUrl,
              format, tileLoadFunction);
          this.sourceTileListenerKeys_.push(
              GVol.events.listen(sourceTile, GVol.events.EventType.CHANGE, handleTileChange));
        }
        sourceTile.consumers++;
        this.tileKeys.push(sourceTileKey);
      }
    }.bind(this));
  }

};
GVol.inherits(GVol.VectorImageTile, GVol.Tile);


/**
 * @inheritDoc
 */
GVol.VectorImageTile.prototype.disposeInternal = function() {
  for (var i = 0, ii = this.tileKeys.length; i < ii; ++i) {
    var sourceTileKey = this.tileKeys[i];
    var sourceTile = this.getTile(sourceTileKey);
    sourceTile.consumers--;
    if (sourceTile.consumers == 0) {
      delete this.sourceTiles_[sourceTileKey];
      sourceTile.dispose();
    }
  }
  this.tileKeys.length = 0;
  this.sourceTiles_ = null;
  if (this.state == GVol.TileState.LOADING) {
    this.loadListenerKeys_.forEach(GVol.events.unlistenByKey);
    this.loadListenerKeys_.length = 0;
  }
  if (this.interimTile) {
    this.interimTile.dispose();
  }
  this.state = GVol.TileState.ABORT;
  this.changed();
  this.sourceTileListenerKeys_.forEach(GVol.events.unlistenByKey);
  this.sourceTileListenerKeys_.length = 0;
  GVol.Tile.prototype.disposeInternal.call(this);
};


/**
 * @param {GVol.layer.Layer} layer Layer.
 * @return {CanvasRenderingContext2D} The rendering context.
 */
GVol.VectorImageTile.prototype.getContext = function(layer) {
  var key = GVol.getUid(layer).toString();
  if (!(key in this.context_)) {
    this.context_[key] = GVol.dom.createCanvasContext2D();
  }
  return this.context_[key];
};


/**
 * Get the Canvas for this tile.
 * @param {GVol.layer.Layer} layer Layer.
 * @return {HTMLCanvasElement} Canvas.
 */
GVol.VectorImageTile.prototype.getImage = function(layer) {
  return this.getReplayState(layer).renderedTileRevision == -1 ?
    null : this.getContext(layer).canvas;
};


/**
 * @param {GVol.layer.Layer} layer Layer.
 * @return {GVol.TileReplayState} The replay state.
 */
GVol.VectorImageTile.prototype.getReplayState = function(layer) {
  var key = GVol.getUid(layer).toString();
  if (!(key in this.replayState_)) {
    this.replayState_[key] = {
      dirty: false,
      renderedRenderOrder: null,
      renderedRevision: -1,
      renderedTileRevision: -1
    };
  }
  return this.replayState_[key];
};


/**
 * @inheritDoc
 */
GVol.VectorImageTile.prototype.getKey = function() {
  return this.tileKeys.join('/') + '/' + this.src_;
};


/**
 * @param {string} tileKey Key (tileCoord) of the source tile.
 * @return {GVol.VectorTile} Source tile.
 */
GVol.VectorImageTile.prototype.getTile = function(tileKey) {
  return this.sourceTiles_[tileKey];
};


/**
 * @inheritDoc
 */
GVol.VectorImageTile.prototype.load = function() {
  var leftToLoad = 0;
  if (this.state == GVol.TileState.IDLE) {
    this.setState(GVol.TileState.LOADING);
  }
  if (this.state == GVol.TileState.LOADING) {
    this.tileKeys.forEach(function(sourceTileKey) {
      var sourceTile = this.getTile(sourceTileKey);
      if (sourceTile.state == GVol.TileState.IDLE) {
        sourceTile.setLoader(this.loader_);
        sourceTile.load();
      }
      if (sourceTile.state == GVol.TileState.LOADING) {
        var key = GVol.events.listen(sourceTile, GVol.events.EventType.CHANGE, function(e) {
          var state = sourceTile.getState();
          if (state == GVol.TileState.LOADED ||
              state == GVol.TileState.ERROR) {
            --leftToLoad;
            GVol.events.unlistenByKey(key);
            GVol.array.remove(this.loadListenerKeys_, key);
            if (leftToLoad == 0) {
              this.finishLoading_();
            }
          }
        }.bind(this));
        this.loadListenerKeys_.push(key);
        ++leftToLoad;
      }
    }.bind(this));
  }
  if (leftToLoad == 0) {
    setTimeout(this.finishLoading_.bind(this), 0);
  }
};


/**
 * @private
 */
GVol.VectorImageTile.prototype.finishLoading_ = function() {
  var errors = false;
  var loaded = this.tileKeys.length;
  var state;
  for (var i = loaded - 1; i >= 0; --i) {
    state = this.getTile(this.tileKeys[i]).getState();
    if (state != GVol.TileState.LOADED) {
      if (state == GVol.TileState.ERROR) {
        errors = true;
      }
      --loaded;
    }
  }
  this.setState(loaded > 0 ?
    GVol.TileState.LOADED :
    (errors ? GVol.TileState.ERROR : GVol.TileState.EMPTY));
};


/**
 * Sets the loader for a tile.
 * @param {GVol.VectorTile} tile Vector tile.
 * @param {string} url URL.
 */
GVol.VectorImageTile.defaultLoadFunction = function(tile, url) {
  var loader = GVol.featureloader.loadFeaturesXhr(
      url, tile.getFormat(), tile.onLoad.bind(tile), tile.onError.bind(tile));

  tile.setLoader(loader);
};

goog.provide('GVol.VectorTile');

goog.require('GVol');
goog.require('GVol.Tile');
goog.require('GVol.TileState');


/**
 * @constructor
 * @extends {GVol.Tile}
 * @param {GVol.TileCoord} tileCoord Tile coordinate.
 * @param {GVol.TileState} state State.
 * @param {string} src Data source url.
 * @param {GVol.format.Feature} format Feature format.
 * @param {GVol.TileLoadFunctionType} tileLoadFunction Tile load function.
 */
GVol.VectorTile = function(tileCoord, state, src, format, tileLoadFunction) {

  GVol.Tile.call(this, tileCoord, state);

  /**
   * @type {number}
   */
  this.consumers = 0;

  /**
   * @private
   * @type {GVol.Extent}
   */
  this.extent_ = null;

  /**
   * @private
   * @type {GVol.format.Feature}
   */
  this.format_ = format;

  /**
   * @private
   * @type {Array.<GVol.Feature>}
   */
  this.features_ = null;

  /**
   * @private
   * @type {GVol.FeatureLoader}
   */
  this.loader_;

  /**
   * Data projection
   * @private
   * @type {GVol.proj.Projection}
   */
  this.projection_;

  /**
   * @private
   * @type {Object.<string, GVol.render.ReplayGroup>}
   */
  this.replayGroups_ = {};

  /**
   * @private
   * @type {GVol.TileLoadFunctionType}
   */
  this.tileLoadFunction_ = tileLoadFunction;

  /**
   * @private
   * @type {string}
   */
  this.url_ = src;

};
GVol.inherits(GVol.VectorTile, GVol.Tile);


/**
 * @inheritDoc
 */
GVol.VectorTile.prototype.disposeInternal = function() {
  this.features_ = null;
  this.replayGroups_ = {};
  this.state = GVol.TileState.ABORT;
  this.changed();
  GVol.Tile.prototype.disposeInternal.call(this);
};


/**
 * Gets the extent of the vector tile.
 * @return {GVol.Extent} The extent.
 */
GVol.VectorTile.prototype.getExtent = function() {
  return this.extent_ || GVol.VectorTile.DEFAULT_EXTENT;
};


/**
 * Get the feature format assigned for reading this tile's features.
 * @return {GVol.format.Feature} Feature format.
 * @api
 */
GVol.VectorTile.prototype.getFormat = function() {
  return this.format_;
};


/**
 * Get the features for this tile. Geometries will be in the projection returned
 * by {@link GVol.VectorTile#getProjection}.
 * @return {Array.<GVol.Feature|GVol.render.Feature>} Features.
 * @api
 */
GVol.VectorTile.prototype.getFeatures = function() {
  return this.features_;
};


/**
 * @inheritDoc
 */
GVol.VectorTile.prototype.getKey = function() {
  return this.url_;
};


/**
 * Get the feature projection of features returned by
 * {@link GVol.VectorTile#getFeatures}.
 * @return {GVol.proj.Projection} Feature projection.
 * @api
 */
GVol.VectorTile.prototype.getProjection = function() {
  return this.projection_;
};


/**
 * @param {GVol.layer.Layer} layer Layer.
 * @param {string} key Key.
 * @return {GVol.render.ReplayGroup} Replay group.
 */
GVol.VectorTile.prototype.getReplayGroup = function(layer, key) {
  return this.replayGroups_[GVol.getUid(layer) + ',' + key];
};


/**
 * @inheritDoc
 */
GVol.VectorTile.prototype.load = function() {
  if (this.state == GVol.TileState.IDLE) {
    this.setState(GVol.TileState.LOADING);
    this.tileLoadFunction_(this, this.url_);
    this.loader_(null, NaN, null);
  }
};


/**
 * Handler for successful tile load.
 * @param {Array.<GVol.Feature>} features The loaded features.
 * @param {GVol.proj.Projection} dataProjection Data projection.
 * @param {GVol.Extent} extent Extent.
 */
GVol.VectorTile.prototype.onLoad = function(features, dataProjection, extent) {
  this.setProjection(dataProjection);
  this.setFeatures(features);
  this.setExtent(extent);
};


/**
 * Handler for tile load errors.
 */
GVol.VectorTile.prototype.onError = function() {
  this.setState(GVol.TileState.ERROR);
};


/**
 * Function for use in an {@link GVol.source.VectorTile}'s `tileLoadFunction`.
 * Sets the extent of the vector tile. This is only required for tiles in
 * projections with `tile-pixels` as units. The extent should be set to
 * `[0, 0, tilePixelSize, tilePixelSize]`, where `tilePixelSize` is calculated
 * by multiplying the tile size with the tile pixel ratio. For sources using
 * {@link GVol.format.MVT} as feature format, the
 * {@link GVol.format.MVT#getLastExtent} method will return the correct extent.
 * The default is `[0, 0, 4096, 4096]`.
 * @param {GVol.Extent} extent The extent.
 * @api
 */
GVol.VectorTile.prototype.setExtent = function(extent) {
  this.extent_ = extent;
};


/**
 * Function for use in an {@link GVol.source.VectorTile}'s `tileLoadFunction`.
 * Sets the features for the tile.
 * @param {Array.<GVol.Feature>} features Features.
 * @api
 */
GVol.VectorTile.prototype.setFeatures = function(features) {
  this.features_ = features;
  this.setState(GVol.TileState.LOADED);
};


/**
 * Function for use in an {@link GVol.source.VectorTile}'s `tileLoadFunction`.
 * Sets the projection of the features that were added with
 * {@link GVol.VectorTile#setFeatures}.
 * @param {GVol.proj.Projection} projection Feature projection.
 * @api
 */
GVol.VectorTile.prototype.setProjection = function(projection) {
  this.projection_ = projection;
};


/**
 * @param {GVol.layer.Layer} layer Layer.
 * @param {string} key Key.
 * @param {GVol.render.ReplayGroup} replayGroup Replay group.
 */
GVol.VectorTile.prototype.setReplayGroup = function(layer, key, replayGroup) {
  this.replayGroups_[GVol.getUid(layer) + ',' + key] = replayGroup;
};


/**
 * Set the feature loader for reading this tile's features.
 * @param {GVol.FeatureLoader} loader Feature loader.
 * @api
 */
GVol.VectorTile.prototype.setLoader = function(loader) {
  this.loader_ = loader;
};


/**
 * @const
 * @type {GVol.Extent}
 */
GVol.VectorTile.DEFAULT_EXTENT = [0, 0, 4096, 4096];

goog.provide('GVol.source.VectorTile');

goog.require('GVol');
goog.require('GVol.TileState');
goog.require('GVol.VectorImageTile');
goog.require('GVol.VectorTile');
goog.require('GVol.proj');
goog.require('GVol.size');
goog.require('GVol.tilegrid');
goog.require('GVol.source.UrlTile');


/**
 * @classdesc
 * Class for layer sources providing vector data divided into a tile grid, to be
 * used with {@link GVol.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 resGVolution. See
 * {@link GVol.source.Vector} for vector sources that are suitable for feature
 * editing.
 *
 * @constructor
 * @fires GVol.source.Tile.Event
 * @extends {GVol.source.UrlTile}
 * @param {GVolx.source.VectorTileOptions} options Vector tile options.
 * @api
 */
GVol.source.VectorTile = function(options) {
  var projection = options.projection || 'EPSG:3857';

  var extent = options.extent || GVol.tilegrid.extentFromProjection(projection);

  var tileGrid = options.tileGrid || GVol.tilegrid.createXYZ({
    extent: extent,
    maxZoom: options.maxZoom || 22,
    minZoom: options.minZoom,
    tileSize: options.tileSize || 512
  });

  GVol.source.UrlTile.call(this, {
    attributions: options.attributions,
    cacheSize: options.cacheSize !== undefined ? options.cacheSize : 128,
    extent: extent,
    logo: options.logo,
    opaque: false,
    projection: projection,
    state: options.state,
    tileGrid: tileGrid,
    tileLoadFunction: options.tileLoadFunction ?
      options.tileLoadFunction : GVol.VectorImageTile.defaultLoadFunction,
    tileUrlFunction: options.tileUrlFunction,
    url: options.url,
    urls: options.urls,
    wrapX: options.wrapX === undefined ? true : options.wrapX
  });

  /**
   * @private
   * @type {GVol.format.Feature}
   */
  this.format_ = options.format ? options.format : null;

  /**
   * @private
   * @type {Object.<string,GVol.VectorTile>}
   */
  this.sourceTiles_ = {};

  /**
   * @private
   * @type {boGVolean}
   */
  this.overlaps_ = options.overlaps == undefined ? true : options.overlaps;

  /**
   * @protected
   * @type {function(new: GVol.VectorTile, GVol.TileCoord, GVol.TileState, string,
   *        GVol.format.Feature, GVol.TileLoadFunctionType)}
   */
  this.tileClass = options.tileClass ? options.tileClass : GVol.VectorTile;

  /**
   * @private
   * @type {Object.<string,GVol.tilegrid.TileGrid>}
   */
  this.tileGrids_ = {};

  if (!this.tileGrid) {
    this.tileGrid = this.getTileGridForProjection(GVol.proj.get(options.projection || 'EPSG:3857'));
  }

};
GVol.inherits(GVol.source.VectorTile, GVol.source.UrlTile);


/**
 * @return {boGVolean} The source can have overlapping geometries.
 */
GVol.source.VectorTile.prototype.getOverlaps = function() {
  return this.overlaps_;
};


/**
 * @inheritDoc
 */
GVol.source.VectorTile.prototype.getTile = function(z, x, y, pixelRatio, projection) {
  var tileCoordKey = this.getKeyZXY(z, x, y);
  if (this.tileCache.containsKey(tileCoordKey)) {
    return /** @type {!GVol.Tile} */ (this.tileCache.get(tileCoordKey));
  } else {
    var tileCoord = [z, x, y];
    var urlTileCoord = this.getTileCoordForTileUrlFunction(
        tileCoord, projection);
    var tileUrl = urlTileCoord ?
      this.tileUrlFunction(urlTileCoord, pixelRatio, projection) : undefined;
    var tile = new GVol.VectorImageTile(
        tileCoord,
        tileUrl !== undefined ? GVol.TileState.IDLE : GVol.TileState.EMPTY,
        tileUrl !== undefined ? tileUrl : '',
        this.format_, this.tileLoadFunction, urlTileCoord, this.tileUrlFunction,
        this.tileGrid, this.getTileGridForProjection(projection),
        this.sourceTiles_, pixelRatio, projection, this.tileClass,
        this.handleTileChange.bind(this));

    this.tileCache.set(tileCoordKey, tile);
    return tile;
  }
};


/**
 * @inheritDoc
 */
GVol.source.VectorTile.prototype.getTileGridForProjection = function(projection) {
  var code = projection.getCode();
  var tileGrid = this.tileGrids_[code];
  if (!tileGrid) {
    // A tile grid that matches the tile size of the source tile grid is more
    // likely to have 1:1 relationships between source tiles and rendered tiles.
    var sourceTileGrid = this.tileGrid;
    tileGrid = this.tileGrids_[code] = GVol.tilegrid.createForProjection(projection, undefined,
        sourceTileGrid ? sourceTileGrid.getTileSize(sourceTileGrid.getMinZoom()) : undefined);
  }
  return tileGrid;
};


/**
 * @inheritDoc
 */
GVol.source.VectorTile.prototype.getTilePixelRatio = function(pixelRatio) {
  return pixelRatio;
};


/**
 * @inheritDoc
 */
GVol.source.VectorTile.prototype.getTilePixelSize = function(z, pixelRatio, projection) {
  var tileSize = GVol.size.toSize(this.getTileGridForProjection(projection).getTileSize(z));
  return [Math.round(tileSize[0] * pixelRatio), Math.round(tileSize[1] * pixelRatio)];
};

goog.provide('GVol.source.WMTSRequestEncoding');

/**
 * Request encoding. One of 'KVP', 'REST'.
 * @enum {string}
 */
GVol.source.WMTSRequestEncoding = {
  KVP: 'KVP',  // see spec §8
  REST: 'REST' // see spec §10
};

goog.provide('GVol.tilegrid.WMTS');

goog.require('GVol');
goog.require('GVol.array');
goog.require('GVol.proj');
goog.require('GVol.tilegrid.TileGrid');


/**
 * @classdesc
 * Set the grid pattern for sources accessing WMTS tiled-image servers.
 *
 * @constructor
 * @extends {GVol.tilegrid.TileGrid}
 * @param {GVolx.tilegrid.WMTSOptions} options WMTS options.
 * @struct
 * @api
 */
GVol.tilegrid.WMTS = function(options) {
  /**
   * @private
   * @type {!Array.<string>}
   */
  this.matrixIds_ = options.matrixIds;
  // FIXME: should the matrixIds become optional?

  GVol.tilegrid.TileGrid.call(this, {
    extent: options.extent,
    origin: options.origin,
    origins: options.origins,
    resGVolutions: options.resGVolutions,
    tileSize: options.tileSize,
    tileSizes: options.tileSizes,
    sizes: options.sizes
  });
};
GVol.inherits(GVol.tilegrid.WMTS, GVol.tilegrid.TileGrid);


/**
 * @param {number} z Z.
 * @return {string} MatrixId..
 */
GVol.tilegrid.WMTS.prototype.getMatrixId = function(z) {
  return this.matrixIds_[z];
};


/**
 * Get the list of matrix identifiers.
 * @return {Array.<string>} MatrixIds.
 * @api
 */
GVol.tilegrid.WMTS.prototype.getMatrixIds = function() {
  return this.matrixIds_;
};


/**
 * Create a tile grid from a WMTS capabilities matrix set and an
 * optional TileMatrixSetLimits.
 * @param {Object} matrixSet An object representing a matrixSet in the
 *     capabilities document.
 * @param {GVol.Extent=} opt_extent An optional extent to restrict the tile
 *     ranges the server provides.
 * @param {Array.<Object>=} opt_matrixLimits An optional object representing
 *     the available matrices for tileGrid.
 * @return {GVol.tilegrid.WMTS} WMTS tileGrid instance.
 * @api
 */
GVol.tilegrid.WMTS.createFromCapabilitiesMatrixSet = function(matrixSet, opt_extent,
    opt_matrixLimits) {

  /** @type {!Array.<number>} */
  var resGVolutions = [];
  /** @type {!Array.<string>} */
  var matrixIds = [];
  /** @type {!Array.<GVol.Coordinate>} */
  var origins = [];
  /** @type {!Array.<GVol.Size>} */
  var tileSizes = [];
  /** @type {!Array.<GVol.Size>} */
  var sizes = [];

  var matrixLimits = opt_matrixLimits !== undefined ? opt_matrixLimits : [];

  var supportedCRSPropName = 'SupportedCRS';
  var matrixIdsPropName = 'TileMatrix';
  var identifierPropName = 'Identifier';
  var scaleDenominatorPropName = 'ScaleDenominator';
  var topLeftCornerPropName = 'TopLeftCorner';
  var tileWidthPropName = 'TileWidth';
  var tileHeightPropName = 'TileHeight';

  var projection;
  projection = GVol.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) {

    var matrixAvailable;
    // use of matrixLimits to filter TileMatrices from GetCapabilities
    // TileMatrixSet from unavailable matrix levels.
    if (matrixLimits.length > 0) {
      matrixAvailable = GVol.array.find(matrixLimits,
          function(elt_ml, index_ml, array_ml) {
            return elt[identifierPropName] == elt_ml[matrixIdsPropName];
          });
    } else {
      matrixAvailable = true;
    }

    if (matrixAvailable) {
      matrixIds.push(elt[identifierPropName]);
      var resGVolution = 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]);
      }
      resGVolutions.push(resGVolution);
      tileSizes.push(tileWidth == tileHeight ?
        tileWidth : [tileWidth, tileHeight]);
      // top-left origin, so height is negative
      sizes.push([elt['MatrixWidth'], -elt['MatrixHeight']]);
    }
  });

  return new GVol.tilegrid.WMTS({
    extent: opt_extent,
    origins: origins,
    resGVolutions: resGVolutions,
    matrixIds: matrixIds,
    tileSizes: tileSizes,
    sizes: sizes
  });
};

goog.provide('GVol.source.WMTS');

goog.require('GVol');
goog.require('GVol.TileUrlFunction');
goog.require('GVol.array');
goog.require('GVol.extent');
goog.require('GVol.obj');
goog.require('GVol.proj');
goog.require('GVol.source.TileImage');
goog.require('GVol.source.WMTSRequestEncoding');
goog.require('GVol.tilegrid.WMTS');
goog.require('GVol.uri');


/**
 * @classdesc
 * Layer source for tile data from WMTS servers.
 *
 * @constructor
 * @extends {GVol.source.TileImage}
 * @param {GVolx.source.WMTSOptions} options WMTS options.
 * @api
 */
GVol.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.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 = GVol.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 {GVol.source.WMTSRequestEncoding}
   */
  this.requestEncoding_ = options.requestEncoding !== undefined ?
    /** @type {GVol.source.WMTSRequestEncoding} */ (options.requestEncoding) :
    GVol.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 == GVol.source.WMTSRequestEncoding.KVP) {
    GVol.obj.assign(context, {
      'Service': 'WMTS',
      'Request': 'GetTile',
      'Version': this.version_,
      'Format': this.format_
    });
  }

  var dimensions = this.dimensions_;

  /**
   * @param {string} template Template.
   * @return {GVol.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 == GVol.source.WMTSRequestEncoding.KVP) ?
      GVol.uri.appendParams(template, context) :
      template.replace(/\{(\w+?)\}/g, function(m, p) {
        return (p.toLowerCase() in context) ? context[p.toLowerCase()] : m;
      });

    return (
      /**
       * @param {GVol.TileCoord} tileCoord Tile coordinate.
       * @param {number} pixelRatio Pixel ratio.
       * @param {GVol.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]),
            'TileCGVol': tileCoord[1],
            'TileRow': -tileCoord[2] - 1
          };
          GVol.obj.assign(localContext, dimensions);
          var url = template;
          if (requestEncoding == GVol.source.WMTSRequestEncoding.KVP) {
            url = GVol.uri.appendParams(url, localContext);
          } else {
            url = url.replace(/\{(\w+?)\}/g, function(m, p) {
              return localContext[p];
            });
          }
          return url;
        }
      });
  }

  var tileUrlFunction = (urls && urls.length > 0) ?
    GVol.TileUrlFunction.createFromTileUrlFunctions(
        urls.map(createFromWMTSTemplate)) :
    GVol.TileUrlFunction.nullTileUrlFunction;

  GVol.source.TileImage.call(this, {
    attributions: options.attributions,
    cacheSize: options.cacheSize,
    crossOrigin: options.crossOrigin,
    logo: options.logo,
    projection: options.projection,
    reprojectionErrorThreshGVold: options.reprojectionErrorThreshGVold,
    tileClass: options.tileClass,
    tileGrid: tileGrid,
    tileLoadFunction: options.tileLoadFunction,
    tilePixelRatio: options.tilePixelRatio,
    tileUrlFunction: tileUrlFunction,
    urls: urls,
    wrapX: options.wrapX !== undefined ? options.wrapX : false
  });

  this.setKey(this.getKeyForDimensions_());

};
GVol.inherits(GVol.source.WMTS, GVol.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
 */
GVol.source.WMTS.prototype.getDimensions = function() {
  return this.dimensions_;
};


/**
 * Return the image format of the WMTS source.
 * @return {string} Format.
 * @api
 */
GVol.source.WMTS.prototype.getFormat = function() {
  return this.format_;
};


/**
 * Return the layer of the WMTS source.
 * @return {string} Layer.
 * @api
 */
GVol.source.WMTS.prototype.getLayer = function() {
  return this.layer_;
};


/**
 * Return the matrix set of the WMTS source.
 * @return {string} MatrixSet.
 * @api
 */
GVol.source.WMTS.prototype.getMatrixSet = function() {
  return this.matrixSet_;
};


/**
 * Return the request encoding, either "KVP" or "REST".
 * @return {GVol.source.WMTSRequestEncoding} Request encoding.
 * @api
 */
GVol.source.WMTS.prototype.getRequestEncoding = function() {
  return this.requestEncoding_;
};


/**
 * Return the style of the WMTS source.
 * @return {string} Style.
 * @api
 */
GVol.source.WMTS.prototype.getStyle = function() {
  return this.style_;
};


/**
 * Return the version of the WMTS source.
 * @return {string} Version.
 * @api
 */
GVol.source.WMTS.prototype.getVersion = function() {
  return this.version_;
};


/**
 * @private
 * @return {string} The key for the current dimensions.
 */
GVol.source.WMTS.prototype.getKeyForDimensions_ = function() {
  var i = 0;
  var res = [];
  for (var key in this.dimensions_) {
    res[i++] = key + '-' + this.dimensions_[key];
  }
  return res.join('/');
};


/**
 * Update the dimensions.
 * @param {Object} dimensions Dimensions.
 * @api
 */
GVol.source.WMTS.prototype.updateDimensions = function(dimensions) {
  GVol.obj.assign(this.dimensions_, dimensions);
  this.setKey(this.getKeyForDimensions_());
};


/**
 * 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.
 *  - crossOrigin - {string|null|undefined} Cross origin. Default is `undefined`.
 * @return {?GVolx.source.WMTSOptions} WMTS source options object or `null` if the layer was not found.
 * @api
 */
GVol.source.WMTS.optionsFromCapabilities = function(wmtsCap, config) {
  var layers = wmtsCap['Contents']['Layer'];
  var l = GVol.array.find(layers, function(elt, index, array) {
    return elt['Identifier'] == config['layer'];
  });
  if (l === null) {
    return null;
  }
  var tileMatrixSets = wmtsCap['Contents']['TileMatrixSet'];
  var idx, matrixSet, matrixLimits;
  if (l['TileMatrixSetLink'].length > 1) {
    if ('projection' in config) {
      idx = GVol.array.findIndex(l['TileMatrixSetLink'],
          function(elt, index, array) {
            var tileMatrixSet = GVol.array.find(tileMatrixSets, function(el) {
              return el['Identifier'] == elt['TileMatrixSet'];
            });
            var supportedCRS = tileMatrixSet['SupportedCRS'].replace(/urn:ogc:def:crs:(\w+):(.*:)?(\w+)$/, '$1:$3');
            var proj1 = GVol.proj.get(supportedCRS);
            var proj2 = GVol.proj.get(config['projection']);
            if (proj1 && proj2) {
              return GVol.proj.equivalent(proj1, proj2);
            } else {
              return supportedCRS == config['projection'];
            }
          });
    } else {
      idx = GVol.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']);
  matrixLimits = /** @type {Array.<Object>} */
    (l['TileMatrixSetLink'][idx]['TileMatrixSetLimits']);

  var format = /** @type {string} */ (l['Format'][0]);
  if ('format' in config) {
    format = config['format'];
  }
  idx = GVol.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) {
        value = elt['Value'][0];
      }
      dimensions[key] = value;
    });
  }

  var matrixSets = wmtsCap['Contents']['TileMatrixSet'];
  var matrixSetObj = GVol.array.find(matrixSets, function(elt, index, array) {
    return elt['Identifier'] == matrixSet;
  });

  var projection;
  if ('projection' in config) {
    projection = GVol.proj.get(config['projection']);
  } else {
    projection = GVol.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 = GVol.proj.get('EPSG:4326').getExtent();
    wrapX = (wgs84BoundingBox[0] == wgs84ProjectionExtent[0] &&
        wgs84BoundingBox[2] == wgs84ProjectionExtent[2]);
    extent = GVol.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 (!GVol.extent.containsExtent(projectionExtent, extent)) {
        extent = undefined;
      }
    }
  }

  var tileGrid = GVol.tilegrid.WMTS.createFromCapabilitiesMatrixSet(
      matrixSetObj, extent, matrixLimits);

  /** @type {!Array.<string>} */
  var urls = [];
  var requestEncoding = config['requestEncoding'];
  requestEncoding = requestEncoding !== undefined ? requestEncoding : '';

  if ('OperationsMetadata' in wmtsCap && 'GetTile' in wmtsCap['OperationsMetadata']) {
    var gets = wmtsCap['OperationsMetadata']['GetTile']['DCP']['HTTP']['Get'];

    for (var i = 0, ii = gets.length; i < ii; ++i) {
      var constraint = GVol.array.find(gets[i]['Constraint'], function(element) {
        return element['name'] == 'GetEncoding';
      });
      var encodings = constraint['AllowedValues']['Value'];

      if (requestEncoding === '') {
        // requestEncoding not provided, use the first encoding from the list
        requestEncoding = encodings[0];
      }
      if (requestEncoding === GVol.source.WMTSRequestEncoding.KVP) {
        if (GVol.array.includes(encodings, GVol.source.WMTSRequestEncoding.KVP)) {
          urls.push(/** @type {string} */ (gets[i]['href']));
        }
      } else {
        break;
      }
    }
  }
  if (urls.length === 0) {
    requestEncoding = GVol.source.WMTSRequestEncoding.REST;
    l['ResourceURL'].forEach(function(element) {
      if (element['resourceType'] === 'tile') {
        format = element['format'];
        urls.push(/** @type {string} */ (element['template']));
      }
    });
  }

  return {
    urls: urls,
    layer: config['layer'],
    matrixSet: matrixSet,
    format: format,
    projection: projection,
    requestEncoding: requestEncoding,
    tileGrid: tileGrid,
    style: style,
    dimensions: dimensions,
    wrapX: wrapX,
    crossOrigin: config['crossOrigin']
  };
};

goog.provide('GVol.source.Zoomify');

goog.require('GVol');
goog.require('GVol.ImageTile');
goog.require('GVol.TileState');
goog.require('GVol.TileUrlFunction');
goog.require('GVol.asserts');
goog.require('GVol.dom');
goog.require('GVol.extent');
goog.require('GVol.source.TileImage');
goog.require('GVol.tilegrid.TileGrid');


/**
 * @classdesc
 * Layer source for tile data in Zoomify format.
 *
 * @constructor
 * @extends {GVol.source.TileImage}
 * @param {GVolx.source.ZoomifyOptions=} opt_options Options.
 * @api
 */
GVol.source.Zoomify = function(opt_options) {

  var options = opt_options || {};

  var size = options.size;
  var tierSizeCalculation = options.tierSizeCalculation !== undefined ?
    options.tierSizeCalculation :
    GVol.source.Zoomify.TierSizeCalculation_.DEFAULT;

  var imageWidth = size[0];
  var imageHeight = size[1];
  var tierSizeInTiles = [];
  var tileSize = GVol.DEFAULT_TILE_SIZE;

  switch (tierSizeCalculation) {
    case GVol.source.Zoomify.TierSizeCalculation_.DEFAULT:
      while (imageWidth > tileSize || imageHeight > tileSize) {
        tierSizeInTiles.push([
          Math.ceil(imageWidth / tileSize),
          Math.ceil(imageHeight / tileSize)
        ]);
        tileSize += tileSize;
      }
      break;
    case GVol.source.Zoomify.TierSizeCalculation_.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:
      GVol.asserts.assert(false, 53); // Unknown `tierSizeCalculation` configured
      break;
  }

  tierSizeInTiles.push([1, 1]);
  tierSizeInTiles.reverse();

  var resGVolutions = [1];
  var tileCountUpToTier = [0];
  var i, ii;
  for (i = 1, ii = tierSizeInTiles.length; i < ii; i++) {
    resGVolutions.push(1 << i);
    tileCountUpToTier.push(
        tierSizeInTiles[i - 1][0] * tierSizeInTiles[i - 1][1] +
        tileCountUpToTier[i - 1]
    );
  }
  resGVolutions.reverse();

  var extent = [0, -size[1], size[0], 0];
  var tileGrid = new GVol.tilegrid.TileGrid({
    extent: extent,
    origin: GVol.extent.getTopLeft(extent),
    resGVolutions: resGVolutions
  });

  var url = options.url;
  if (url && url.indexOf('{TileGroup}') == -1) {
    url += '{TileGroup}/{z}-{x}-{y}.jpg';
  }
  var urls = GVol.TileUrlFunction.expandUrl(url);

  /**
   * @param {string} template Template.
   * @return {GVol.TileUrlFunctionType} Tile URL function.
   */
  function createFromTemplate(template) {

    return (
      /**
       * @param {GVol.TileCoord} tileCoord Tile Coordinate.
       * @param {number} pixelRatio Pixel ratio.
       * @param {GVol.proj.Projection} projection Projection.
       * @return {string|undefined} Tile URL.
       */
      function(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 / GVol.DEFAULT_TILE_SIZE) | 0;
          var localContext = {
            'z': tileCoordZ,
            'x': tileCoordX,
            'y': tileCoordY,
            'TileGroup': 'TileGroup' + tileGroup
          };
          return template.replace(/\{(\w+?)\}/g, function(m, p) {
            return localContext[p];
          });
        }
      });
  }

  var tileUrlFunction = GVol.TileUrlFunction.createFromTileUrlFunctions(urls.map(createFromTemplate));

  GVol.source.TileImage.call(this, {
    attributions: options.attributions,
    cacheSize: options.cacheSize,
    crossOrigin: options.crossOrigin,
    logo: options.logo,
    projection: options.projection,
    reprojectionErrorThreshGVold: options.reprojectionErrorThreshGVold,
    tileClass: GVol.source.Zoomify.Tile_,
    tileGrid: tileGrid,
    tileUrlFunction: tileUrlFunction
  });

};
GVol.inherits(GVol.source.Zoomify, GVol.source.TileImage);


/**
 * @constructor
 * @extends {GVol.ImageTile}
 * @param {GVol.TileCoord} tileCoord Tile coordinate.
 * @param {GVol.TileState} state State.
 * @param {string} src Image source URI.
 * @param {?string} crossOrigin Cross origin.
 * @param {GVol.TileLoadFunctionType} tileLoadFunction Tile load function.
 * @private
 */
GVol.source.Zoomify.Tile_ = function(
    tileCoord, state, src, crossOrigin, tileLoadFunction) {

  GVol.ImageTile.call(this, tileCoord, state, src, crossOrigin, tileLoadFunction);

  /**
   * @private
   * @type {HTMLCanvasElement|HTMLImageElement|HTMLVideoElement}
   */
  this.zoomifyImage_ = null;

};
GVol.inherits(GVol.source.Zoomify.Tile_, GVol.ImageTile);


/**
 * @inheritDoc
 */
GVol.source.Zoomify.Tile_.prototype.getImage = function() {
  if (this.zoomifyImage_) {
    return this.zoomifyImage_;
  }
  var tileSize = GVol.DEFAULT_TILE_SIZE;
  var image = GVol.ImageTile.prototype.getImage.call(this);
  if (this.state == GVol.TileState.LOADED) {
    if (image.width == tileSize && image.height == tileSize) {
      this.zoomifyImage_ = image;
      return image;
    } else {
      var context = GVol.dom.createCanvasContext2D(tileSize, tileSize);
      context.drawImage(image, 0, 0);
      this.zoomifyImage_ = context.canvas;
      return context.canvas;
    }
  } else {
    return image;
  }
};


/**
 * @enum {string}
 * @private
 */
GVol.source.Zoomify.TierSizeCalculation_ = {
  DEFAULT: 'default',
  TRUNCATED: 'truncated'
};

// 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('GVol');
goog.require('GVol.AssertionError');
goog.require('GVol.Attribution');
goog.require('GVol.CGVollection');
goog.require('GVol.DeviceOrientation');
goog.require('GVol.Feature');
goog.require('GVol.GeGVolocation');
goog.require('GVol.Graticule');
goog.require('GVol.Image');
goog.require('GVol.ImageTile');
goog.require('GVol.Kinetic');
goog.require('GVol.Map');
goog.require('GVol.MapBrowserEvent');
goog.require('GVol.MapEvent');
goog.require('GVol.Object');
goog.require('GVol.Observable');
goog.require('GVol.Overlay');
goog.require('GVol.Sphere');
goog.require('GVol.Tile');
goog.require('GVol.VectorTile');
goog.require('GVol.View');
goog.require('GVol.cGVolor');
goog.require('GVol.cGVolorlike');
goog.require('GVol.contrGVol');
goog.require('GVol.contrGVol.Attribution');
goog.require('GVol.contrGVol.ContrGVol');
goog.require('GVol.contrGVol.FullScreen');
goog.require('GVol.contrGVol.MousePosition');
goog.require('GVol.contrGVol.OverviewMap');
goog.require('GVol.contrGVol.Rotate');
goog.require('GVol.contrGVol.ScaleLine');
goog.require('GVol.contrGVol.Zoom');
goog.require('GVol.contrGVol.ZoomSlider');
goog.require('GVol.contrGVol.ZoomToExtent');
goog.require('GVol.coordinate');
goog.require('GVol.easing');
goog.require('GVol.events.Event');
goog.require('GVol.events.condition');
goog.require('GVol.extent');
goog.require('GVol.featureloader');
goog.require('GVol.format.EsriJSON');
goog.require('GVol.format.Feature');
goog.require('GVol.format.GML');
goog.require('GVol.format.GML2');
goog.require('GVol.format.GML3');
goog.require('GVol.format.GMLBase');
goog.require('GVol.format.GPX');
goog.require('GVol.format.GeoJSON');
goog.require('GVol.format.IGC');
goog.require('GVol.format.KML');
goog.require('GVol.format.MVT');
goog.require('GVol.format.OSMXML');
goog.require('GVol.format.PGVolyline');
goog.require('GVol.format.TopoJSON');
goog.require('GVol.format.WFS');
goog.require('GVol.format.WKT');
goog.require('GVol.format.WMSCapabilities');
goog.require('GVol.format.WMSGetFeatureInfo');
goog.require('GVol.format.WMTSCapabilities');
goog.require('GVol.format.filter');
goog.require('GVol.format.filter.And');
goog.require('GVol.format.filter.Bbox');
goog.require('GVol.format.filter.Comparison');
goog.require('GVol.format.filter.ComparisonBinary');
goog.require('GVol.format.filter.During');
goog.require('GVol.format.filter.EqualTo');
goog.require('GVol.format.filter.Filter');
goog.require('GVol.format.filter.GreaterThan');
goog.require('GVol.format.filter.GreaterThanOrEqualTo');
goog.require('GVol.format.filter.Intersects');
goog.require('GVol.format.filter.IsBetween');
goog.require('GVol.format.filter.IsLike');
goog.require('GVol.format.filter.IsNull');
goog.require('GVol.format.filter.LessThan');
goog.require('GVol.format.filter.LessThanOrEqualTo');
goog.require('GVol.format.filter.Not');
goog.require('GVol.format.filter.NotEqualTo');
goog.require('GVol.format.filter.Or');
goog.require('GVol.format.filter.Spatial');
goog.require('GVol.format.filter.Within');
goog.require('GVol.geom.Circle');
goog.require('GVol.geom.Geometry');
goog.require('GVol.geom.GeometryCGVollection');
goog.require('GVol.geom.LineString');
goog.require('GVol.geom.LinearRing');
goog.require('GVol.geom.MultiLineString');
goog.require('GVol.geom.MultiPoint');
goog.require('GVol.geom.MultiPGVolygon');
goog.require('GVol.geom.Point');
goog.require('GVol.geom.PGVolygon');
goog.require('GVol.geom.SimpleGeometry');
goog.require('GVol.has');
goog.require('GVol.interaction');
goog.require('GVol.interaction.DoubleClickZoom');
goog.require('GVol.interaction.DragAndDrop');
goog.require('GVol.interaction.DragBox');
goog.require('GVol.interaction.DragPan');
goog.require('GVol.interaction.DragRotate');
goog.require('GVol.interaction.DragRotateAndZoom');
goog.require('GVol.interaction.DragZoom');
goog.require('GVol.interaction.Draw');
goog.require('GVol.interaction.Extent');
goog.require('GVol.interaction.Interaction');
goog.require('GVol.interaction.KeyboardPan');
goog.require('GVol.interaction.KeyboardZoom');
goog.require('GVol.interaction.Modify');
goog.require('GVol.interaction.MouseWheelZoom');
goog.require('GVol.interaction.PinchRotate');
goog.require('GVol.interaction.PinchZoom');
goog.require('GVol.interaction.Pointer');
goog.require('GVol.interaction.Select');
goog.require('GVol.interaction.Snap');
goog.require('GVol.interaction.Translate');
goog.require('GVol.layer.Base');
goog.require('GVol.layer.Group');
goog.require('GVol.layer.Heatmap');
goog.require('GVol.layer.Image');
goog.require('GVol.layer.Layer');
goog.require('GVol.layer.Tile');
goog.require('GVol.layer.Vector');
goog.require('GVol.layer.VectorTile');
goog.require('GVol.loadingstrategy');
goog.require('GVol.proj');
goog.require('GVol.proj.Projection');
goog.require('GVol.proj.Units');
goog.require('GVol.proj.common');
goog.require('GVol.render');
goog.require('GVol.render.Event');
goog.require('GVol.render.Feature');
goog.require('GVol.render.VectorContext');
goog.require('GVol.render.canvas.Immediate');
goog.require('GVol.render.webgl.Immediate');
goog.require('GVol.size');
goog.require('GVol.source.BingMaps');
goog.require('GVol.source.CartoDB');
goog.require('GVol.source.Cluster');
goog.require('GVol.source.Image');
goog.require('GVol.source.ImageArcGISRest');
goog.require('GVol.source.ImageCanvas');
goog.require('GVol.source.ImageMapGuide');
goog.require('GVol.source.ImageStatic');
goog.require('GVol.source.ImageVector');
goog.require('GVol.source.ImageWMS');
goog.require('GVol.source.OSM');
goog.require('GVol.source.Raster');
goog.require('GVol.source.Source');
goog.require('GVol.source.Stamen');
goog.require('GVol.source.Tile');
goog.require('GVol.source.TileArcGISRest');
goog.require('GVol.source.TileDebug');
goog.require('GVol.source.TileImage');
goog.require('GVol.source.TileJSON');
goog.require('GVol.source.TileUTFGrid');
goog.require('GVol.source.TileWMS');
goog.require('GVol.source.UrlTile');
goog.require('GVol.source.Vector');
goog.require('GVol.source.VectorTile');
goog.require('GVol.source.WMTS');
goog.require('GVol.source.XYZ');
goog.require('GVol.source.Zoomify');
goog.require('GVol.style.AtlasManager');
goog.require('GVol.style.Circle');
goog.require('GVol.style.Fill');
goog.require('GVol.style.Icon');
goog.require('GVol.style.Image');
goog.require('GVol.style.RegularShape');
goog.require('GVol.style.Stroke');
goog.require('GVol.style.Style');
goog.require('GVol.style.Text');
goog.require('GVol.tilegrid');
goog.require('GVol.tilegrid.TileGrid');
goog.require('GVol.tilegrid.WMTS');
goog.require('GVol.webgl.Context');
goog.require('GVol.xml');



goog.exportProperty(
    GVol.AssertionError.prototype,
    'code',
    GVol.AssertionError.prototype.code);

goog.exportSymbGVol(
    'GVol.Attribution',
    GVol.Attribution,
    OPENLAYERS);

goog.exportProperty(
    GVol.Attribution.prototype,
    'getHTML',
    GVol.Attribution.prototype.getHTML);

goog.exportSymbGVol(
    'GVol.CGVollection',
    GVol.CGVollection,
    OPENLAYERS);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'clear',
    GVol.CGVollection.prototype.clear);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'extend',
    GVol.CGVollection.prototype.extend);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'forEach',
    GVol.CGVollection.prototype.forEach);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'getArray',
    GVol.CGVollection.prototype.getArray);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'item',
    GVol.CGVollection.prototype.item);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'getLength',
    GVol.CGVollection.prototype.getLength);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'insertAt',
    GVol.CGVollection.prototype.insertAt);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'pop',
    GVol.CGVollection.prototype.pop);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'push',
    GVol.CGVollection.prototype.push);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'remove',
    GVol.CGVollection.prototype.remove);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'removeAt',
    GVol.CGVollection.prototype.removeAt);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'setAt',
    GVol.CGVollection.prototype.setAt);

goog.exportProperty(
    GVol.CGVollection.Event.prototype,
    'element',
    GVol.CGVollection.Event.prototype.element);

goog.exportSymbGVol(
    'GVol.cGVolor.asArray',
    GVol.cGVolor.asArray,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.cGVolor.asString',
    GVol.cGVolor.asString,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.cGVolorlike.asCGVolorLike',
    GVol.cGVolorlike.asCGVolorLike,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.contrGVol.defaults',
    GVol.contrGVol.defaults,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.coordinate.add',
    GVol.coordinate.add,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.coordinate.createStringXY',
    GVol.coordinate.createStringXY,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.coordinate.format',
    GVol.coordinate.format,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.coordinate.rotate',
    GVol.coordinate.rotate,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.coordinate.toStringHDMS',
    GVol.coordinate.toStringHDMS,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.coordinate.toStringXY',
    GVol.coordinate.toStringXY,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.DeviceOrientation',
    GVol.DeviceOrientation,
    OPENLAYERS);

goog.exportProperty(
    GVol.DeviceOrientation.prototype,
    'getAlpha',
    GVol.DeviceOrientation.prototype.getAlpha);

goog.exportProperty(
    GVol.DeviceOrientation.prototype,
    'getBeta',
    GVol.DeviceOrientation.prototype.getBeta);

goog.exportProperty(
    GVol.DeviceOrientation.prototype,
    'getGamma',
    GVol.DeviceOrientation.prototype.getGamma);

goog.exportProperty(
    GVol.DeviceOrientation.prototype,
    'getHeading',
    GVol.DeviceOrientation.prototype.getHeading);

goog.exportProperty(
    GVol.DeviceOrientation.prototype,
    'getTracking',
    GVol.DeviceOrientation.prototype.getTracking);

goog.exportProperty(
    GVol.DeviceOrientation.prototype,
    'setTracking',
    GVol.DeviceOrientation.prototype.setTracking);

goog.exportSymbGVol(
    'GVol.easing.easeIn',
    GVol.easing.easeIn,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.easing.easeOut',
    GVol.easing.easeOut,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.easing.inAndOut',
    GVol.easing.inAndOut,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.easing.linear',
    GVol.easing.linear,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.easing.upAndDown',
    GVol.easing.upAndDown,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.extent.boundingExtent',
    GVol.extent.boundingExtent,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.extent.buffer',
    GVol.extent.buffer,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.extent.containsCoordinate',
    GVol.extent.containsCoordinate,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.extent.containsExtent',
    GVol.extent.containsExtent,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.extent.containsXY',
    GVol.extent.containsXY,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.extent.createEmpty',
    GVol.extent.createEmpty,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.extent.equals',
    GVol.extent.equals,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.extent.extend',
    GVol.extent.extend,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.extent.getArea',
    GVol.extent.getArea,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.extent.getBottomLeft',
    GVol.extent.getBottomLeft,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.extent.getBottomRight',
    GVol.extent.getBottomRight,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.extent.getCenter',
    GVol.extent.getCenter,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.extent.getHeight',
    GVol.extent.getHeight,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.extent.getIntersection',
    GVol.extent.getIntersection,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.extent.getSize',
    GVol.extent.getSize,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.extent.getTopLeft',
    GVol.extent.getTopLeft,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.extent.getTopRight',
    GVol.extent.getTopRight,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.extent.getWidth',
    GVol.extent.getWidth,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.extent.intersects',
    GVol.extent.intersects,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.extent.isEmpty',
    GVol.extent.isEmpty,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.extent.applyTransform',
    GVol.extent.applyTransform,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.Feature',
    GVol.Feature,
    OPENLAYERS);

goog.exportProperty(
    GVol.Feature.prototype,
    'clone',
    GVol.Feature.prototype.clone);

goog.exportProperty(
    GVol.Feature.prototype,
    'getGeometry',
    GVol.Feature.prototype.getGeometry);

goog.exportProperty(
    GVol.Feature.prototype,
    'getId',
    GVol.Feature.prototype.getId);

goog.exportProperty(
    GVol.Feature.prototype,
    'getGeometryName',
    GVol.Feature.prototype.getGeometryName);

goog.exportProperty(
    GVol.Feature.prototype,
    'getStyle',
    GVol.Feature.prototype.getStyle);

goog.exportProperty(
    GVol.Feature.prototype,
    'getStyleFunction',
    GVol.Feature.prototype.getStyleFunction);

goog.exportProperty(
    GVol.Feature.prototype,
    'setGeometry',
    GVol.Feature.prototype.setGeometry);

goog.exportProperty(
    GVol.Feature.prototype,
    'setStyle',
    GVol.Feature.prototype.setStyle);

goog.exportProperty(
    GVol.Feature.prototype,
    'setId',
    GVol.Feature.prototype.setId);

goog.exportProperty(
    GVol.Feature.prototype,
    'setGeometryName',
    GVol.Feature.prototype.setGeometryName);

goog.exportSymbGVol(
    'GVol.featureloader.xhr',
    GVol.featureloader.xhr,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.GeGVolocation',
    GVol.GeGVolocation,
    OPENLAYERS);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'getAccuracy',
    GVol.GeGVolocation.prototype.getAccuracy);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'getAccuracyGeometry',
    GVol.GeGVolocation.prototype.getAccuracyGeometry);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'getAltitude',
    GVol.GeGVolocation.prototype.getAltitude);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'getAltitudeAccuracy',
    GVol.GeGVolocation.prototype.getAltitudeAccuracy);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'getHeading',
    GVol.GeGVolocation.prototype.getHeading);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'getPosition',
    GVol.GeGVolocation.prototype.getPosition);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'getProjection',
    GVol.GeGVolocation.prototype.getProjection);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'getSpeed',
    GVol.GeGVolocation.prototype.getSpeed);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'getTracking',
    GVol.GeGVolocation.prototype.getTracking);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'getTrackingOptions',
    GVol.GeGVolocation.prototype.getTrackingOptions);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'setProjection',
    GVol.GeGVolocation.prototype.setProjection);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'setTracking',
    GVol.GeGVolocation.prototype.setTracking);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'setTrackingOptions',
    GVol.GeGVolocation.prototype.setTrackingOptions);

goog.exportSymbGVol(
    'GVol.Graticule',
    GVol.Graticule,
    OPENLAYERS);

goog.exportProperty(
    GVol.Graticule.prototype,
    'getMap',
    GVol.Graticule.prototype.getMap);

goog.exportProperty(
    GVol.Graticule.prototype,
    'getMeridians',
    GVol.Graticule.prototype.getMeridians);

goog.exportProperty(
    GVol.Graticule.prototype,
    'getParallels',
    GVol.Graticule.prototype.getParallels);

goog.exportProperty(
    GVol.Graticule.prototype,
    'setMap',
    GVol.Graticule.prototype.setMap);

goog.exportSymbGVol(
    'GVol.has.DEVICE_PIXEL_RATIO',
    GVol.has.DEVICE_PIXEL_RATIO,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.has.CANVAS',
    GVol.has.CANVAS,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.has.DEVICE_ORIENTATION',
    GVol.has.DEVICE_ORIENTATION,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.has.GEOLOCATION',
    GVol.has.GEOLOCATION,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.has.TOUCH',
    GVol.has.TOUCH,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.has.WEBGL',
    GVol.has.WEBGL,
    OPENLAYERS);

goog.exportProperty(
    GVol.Image.prototype,
    'getImage',
    GVol.Image.prototype.getImage);

goog.exportProperty(
    GVol.Image.prototype,
    'load',
    GVol.Image.prototype.load);

goog.exportProperty(
    GVol.ImageTile.prototype,
    'getImage',
    GVol.ImageTile.prototype.getImage);

goog.exportSymbGVol(
    'GVol.inherits',
    GVol.inherits,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.interaction.defaults',
    GVol.interaction.defaults,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.Kinetic',
    GVol.Kinetic,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.loadingstrategy.all',
    GVol.loadingstrategy.all,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.loadingstrategy.bbox',
    GVol.loadingstrategy.bbox,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.loadingstrategy.tile',
    GVol.loadingstrategy.tile,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.Map',
    GVol.Map,
    OPENLAYERS);

goog.exportProperty(
    GVol.Map.prototype,
    'addContrGVol',
    GVol.Map.prototype.addContrGVol);

goog.exportProperty(
    GVol.Map.prototype,
    'addInteraction',
    GVol.Map.prototype.addInteraction);

goog.exportProperty(
    GVol.Map.prototype,
    'addLayer',
    GVol.Map.prototype.addLayer);

goog.exportProperty(
    GVol.Map.prototype,
    'addOverlay',
    GVol.Map.prototype.addOverlay);

goog.exportProperty(
    GVol.Map.prototype,
    'forEachFeatureAtPixel',
    GVol.Map.prototype.forEachFeatureAtPixel);

goog.exportProperty(
    GVol.Map.prototype,
    'getFeaturesAtPixel',
    GVol.Map.prototype.getFeaturesAtPixel);

goog.exportProperty(
    GVol.Map.prototype,
    'forEachLayerAtPixel',
    GVol.Map.prototype.forEachLayerAtPixel);

goog.exportProperty(
    GVol.Map.prototype,
    'hasFeatureAtPixel',
    GVol.Map.prototype.hasFeatureAtPixel);

goog.exportProperty(
    GVol.Map.prototype,
    'getEventCoordinate',
    GVol.Map.prototype.getEventCoordinate);

goog.exportProperty(
    GVol.Map.prototype,
    'getEventPixel',
    GVol.Map.prototype.getEventPixel);

goog.exportProperty(
    GVol.Map.prototype,
    'getTarget',
    GVol.Map.prototype.getTarget);

goog.exportProperty(
    GVol.Map.prototype,
    'getTargetElement',
    GVol.Map.prototype.getTargetElement);

goog.exportProperty(
    GVol.Map.prototype,
    'getCoordinateFromPixel',
    GVol.Map.prototype.getCoordinateFromPixel);

goog.exportProperty(
    GVol.Map.prototype,
    'getContrGVols',
    GVol.Map.prototype.getContrGVols);

goog.exportProperty(
    GVol.Map.prototype,
    'getOverlays',
    GVol.Map.prototype.getOverlays);

goog.exportProperty(
    GVol.Map.prototype,
    'getOverlayById',
    GVol.Map.prototype.getOverlayById);

goog.exportProperty(
    GVol.Map.prototype,
    'getInteractions',
    GVol.Map.prototype.getInteractions);

goog.exportProperty(
    GVol.Map.prototype,
    'getLayerGroup',
    GVol.Map.prototype.getLayerGroup);

goog.exportProperty(
    GVol.Map.prototype,
    'getLayers',
    GVol.Map.prototype.getLayers);

goog.exportProperty(
    GVol.Map.prototype,
    'getPixelFromCoordinate',
    GVol.Map.prototype.getPixelFromCoordinate);

goog.exportProperty(
    GVol.Map.prototype,
    'getSize',
    GVol.Map.prototype.getSize);

goog.exportProperty(
    GVol.Map.prototype,
    'getView',
    GVol.Map.prototype.getView);

goog.exportProperty(
    GVol.Map.prototype,
    'getViewport',
    GVol.Map.prototype.getViewport);

goog.exportProperty(
    GVol.Map.prototype,
    'renderSync',
    GVol.Map.prototype.renderSync);

goog.exportProperty(
    GVol.Map.prototype,
    'render',
    GVol.Map.prototype.render);

goog.exportProperty(
    GVol.Map.prototype,
    'removeContrGVol',
    GVol.Map.prototype.removeContrGVol);

goog.exportProperty(
    GVol.Map.prototype,
    'removeInteraction',
    GVol.Map.prototype.removeInteraction);

goog.exportProperty(
    GVol.Map.prototype,
    'removeLayer',
    GVol.Map.prototype.removeLayer);

goog.exportProperty(
    GVol.Map.prototype,
    'removeOverlay',
    GVol.Map.prototype.removeOverlay);

goog.exportProperty(
    GVol.Map.prototype,
    'setLayerGroup',
    GVol.Map.prototype.setLayerGroup);

goog.exportProperty(
    GVol.Map.prototype,
    'setSize',
    GVol.Map.prototype.setSize);

goog.exportProperty(
    GVol.Map.prototype,
    'setTarget',
    GVol.Map.prototype.setTarget);

goog.exportProperty(
    GVol.Map.prototype,
    'setView',
    GVol.Map.prototype.setView);

goog.exportProperty(
    GVol.Map.prototype,
    'updateSize',
    GVol.Map.prototype.updateSize);

goog.exportProperty(
    GVol.MapBrowserEvent.prototype,
    'originalEvent',
    GVol.MapBrowserEvent.prototype.originalEvent);

goog.exportProperty(
    GVol.MapBrowserEvent.prototype,
    'pixel',
    GVol.MapBrowserEvent.prototype.pixel);

goog.exportProperty(
    GVol.MapBrowserEvent.prototype,
    'coordinate',
    GVol.MapBrowserEvent.prototype.coordinate);

goog.exportProperty(
    GVol.MapBrowserEvent.prototype,
    'dragging',
    GVol.MapBrowserEvent.prototype.dragging);

goog.exportProperty(
    GVol.MapEvent.prototype,
    'map',
    GVol.MapEvent.prototype.map);

goog.exportProperty(
    GVol.MapEvent.prototype,
    'frameState',
    GVol.MapEvent.prototype.frameState);

goog.exportSymbGVol(
    'GVol.Object',
    GVol.Object,
    OPENLAYERS);

goog.exportProperty(
    GVol.Object.prototype,
    'get',
    GVol.Object.prototype.get);

goog.exportProperty(
    GVol.Object.prototype,
    'getKeys',
    GVol.Object.prototype.getKeys);

goog.exportProperty(
    GVol.Object.prototype,
    'getProperties',
    GVol.Object.prototype.getProperties);

goog.exportProperty(
    GVol.Object.prototype,
    'set',
    GVol.Object.prototype.set);

goog.exportProperty(
    GVol.Object.prototype,
    'setProperties',
    GVol.Object.prototype.setProperties);

goog.exportProperty(
    GVol.Object.prototype,
    'unset',
    GVol.Object.prototype.unset);

goog.exportProperty(
    GVol.Object.Event.prototype,
    'key',
    GVol.Object.Event.prototype.key);

goog.exportProperty(
    GVol.Object.Event.prototype,
    'GVoldValue',
    GVol.Object.Event.prototype.GVoldValue);

goog.exportSymbGVol(
    'GVol.Observable',
    GVol.Observable,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.Observable.unByKey',
    GVol.Observable.unByKey,
    OPENLAYERS);

goog.exportProperty(
    GVol.Observable.prototype,
    'changed',
    GVol.Observable.prototype.changed);

goog.exportProperty(
    GVol.Observable.prototype,
    'dispatchEvent',
    GVol.Observable.prototype.dispatchEvent);

goog.exportProperty(
    GVol.Observable.prototype,
    'getRevision',
    GVol.Observable.prototype.getRevision);

goog.exportProperty(
    GVol.Observable.prototype,
    'on',
    GVol.Observable.prototype.on);

goog.exportProperty(
    GVol.Observable.prototype,
    'once',
    GVol.Observable.prototype.once);

goog.exportProperty(
    GVol.Observable.prototype,
    'un',
    GVol.Observable.prototype.un);

goog.exportSymbGVol(
    'GVol.Overlay',
    GVol.Overlay,
    OPENLAYERS);

goog.exportProperty(
    GVol.Overlay.prototype,
    'getElement',
    GVol.Overlay.prototype.getElement);

goog.exportProperty(
    GVol.Overlay.prototype,
    'getId',
    GVol.Overlay.prototype.getId);

goog.exportProperty(
    GVol.Overlay.prototype,
    'getMap',
    GVol.Overlay.prototype.getMap);

goog.exportProperty(
    GVol.Overlay.prototype,
    'getOffset',
    GVol.Overlay.prototype.getOffset);

goog.exportProperty(
    GVol.Overlay.prototype,
    'getPosition',
    GVol.Overlay.prototype.getPosition);

goog.exportProperty(
    GVol.Overlay.prototype,
    'getPositioning',
    GVol.Overlay.prototype.getPositioning);

goog.exportProperty(
    GVol.Overlay.prototype,
    'setElement',
    GVol.Overlay.prototype.setElement);

goog.exportProperty(
    GVol.Overlay.prototype,
    'setMap',
    GVol.Overlay.prototype.setMap);

goog.exportProperty(
    GVol.Overlay.prototype,
    'setOffset',
    GVol.Overlay.prototype.setOffset);

goog.exportProperty(
    GVol.Overlay.prototype,
    'setPosition',
    GVol.Overlay.prototype.setPosition);

goog.exportProperty(
    GVol.Overlay.prototype,
    'setPositioning',
    GVol.Overlay.prototype.setPositioning);

goog.exportSymbGVol(
    'GVol.proj.METERS_PER_UNIT',
    GVol.proj.METERS_PER_UNIT,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.proj.setProj4',
    GVol.proj.setProj4,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.proj.getPointResGVolution',
    GVol.proj.getPointResGVolution,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.proj.addEquivalentProjections',
    GVol.proj.addEquivalentProjections,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.proj.addProjection',
    GVol.proj.addProjection,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.proj.addCoordinateTransforms',
    GVol.proj.addCoordinateTransforms,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.proj.fromLonLat',
    GVol.proj.fromLonLat,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.proj.toLonLat',
    GVol.proj.toLonLat,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.proj.get',
    GVol.proj.get,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.proj.equivalent',
    GVol.proj.equivalent,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.proj.getTransform',
    GVol.proj.getTransform,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.proj.transform',
    GVol.proj.transform,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.proj.transformExtent',
    GVol.proj.transformExtent,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.render.toContext',
    GVol.render.toContext,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.size.toSize',
    GVol.size.toSize,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.Sphere',
    GVol.Sphere,
    OPENLAYERS);

goog.exportProperty(
    GVol.Sphere.prototype,
    'geodesicArea',
    GVol.Sphere.prototype.geodesicArea);

goog.exportProperty(
    GVol.Sphere.prototype,
    'haversineDistance',
    GVol.Sphere.prototype.haversineDistance);

goog.exportSymbGVol(
    'GVol.Sphere.getLength',
    GVol.Sphere.getLength,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.Sphere.getArea',
    GVol.Sphere.getArea,
    OPENLAYERS);

goog.exportProperty(
    GVol.Tile.prototype,
    'getTileCoord',
    GVol.Tile.prototype.getTileCoord);

goog.exportProperty(
    GVol.Tile.prototype,
    'load',
    GVol.Tile.prototype.load);

goog.exportSymbGVol(
    'GVol.tilegrid.createXYZ',
    GVol.tilegrid.createXYZ,
    OPENLAYERS);

goog.exportProperty(
    GVol.VectorTile.prototype,
    'getFormat',
    GVol.VectorTile.prototype.getFormat);

goog.exportProperty(
    GVol.VectorTile.prototype,
    'getFeatures',
    GVol.VectorTile.prototype.getFeatures);

goog.exportProperty(
    GVol.VectorTile.prototype,
    'getProjection',
    GVol.VectorTile.prototype.getProjection);

goog.exportProperty(
    GVol.VectorTile.prototype,
    'setExtent',
    GVol.VectorTile.prototype.setExtent);

goog.exportProperty(
    GVol.VectorTile.prototype,
    'setFeatures',
    GVol.VectorTile.prototype.setFeatures);

goog.exportProperty(
    GVol.VectorTile.prototype,
    'setProjection',
    GVol.VectorTile.prototype.setProjection);

goog.exportProperty(
    GVol.VectorTile.prototype,
    'setLoader',
    GVol.VectorTile.prototype.setLoader);

goog.exportSymbGVol(
    'GVol.View',
    GVol.View,
    OPENLAYERS);

goog.exportProperty(
    GVol.View.prototype,
    'animate',
    GVol.View.prototype.animate);

goog.exportProperty(
    GVol.View.prototype,
    'getAnimating',
    GVol.View.prototype.getAnimating);

goog.exportProperty(
    GVol.View.prototype,
    'getInteracting',
    GVol.View.prototype.getInteracting);

goog.exportProperty(
    GVol.View.prototype,
    'cancelAnimations',
    GVol.View.prototype.cancelAnimations);

goog.exportProperty(
    GVol.View.prototype,
    'constrainCenter',
    GVol.View.prototype.constrainCenter);

goog.exportProperty(
    GVol.View.prototype,
    'constrainResGVolution',
    GVol.View.prototype.constrainResGVolution);

goog.exportProperty(
    GVol.View.prototype,
    'constrainRotation',
    GVol.View.prototype.constrainRotation);

goog.exportProperty(
    GVol.View.prototype,
    'getCenter',
    GVol.View.prototype.getCenter);

goog.exportProperty(
    GVol.View.prototype,
    'calculateExtent',
    GVol.View.prototype.calculateExtent);

goog.exportProperty(
    GVol.View.prototype,
    'getMaxResGVolution',
    GVol.View.prototype.getMaxResGVolution);

goog.exportProperty(
    GVol.View.prototype,
    'getMinResGVolution',
    GVol.View.prototype.getMinResGVolution);

goog.exportProperty(
    GVol.View.prototype,
    'getMaxZoom',
    GVol.View.prototype.getMaxZoom);

goog.exportProperty(
    GVol.View.prototype,
    'setMaxZoom',
    GVol.View.prototype.setMaxZoom);

goog.exportProperty(
    GVol.View.prototype,
    'getMinZoom',
    GVol.View.prototype.getMinZoom);

goog.exportProperty(
    GVol.View.prototype,
    'setMinZoom',
    GVol.View.prototype.setMinZoom);

goog.exportProperty(
    GVol.View.prototype,
    'getProjection',
    GVol.View.prototype.getProjection);

goog.exportProperty(
    GVol.View.prototype,
    'getResGVolution',
    GVol.View.prototype.getResGVolution);

goog.exportProperty(
    GVol.View.prototype,
    'getResGVolutions',
    GVol.View.prototype.getResGVolutions);

goog.exportProperty(
    GVol.View.prototype,
    'getResGVolutionForExtent',
    GVol.View.prototype.getResGVolutionForExtent);

goog.exportProperty(
    GVol.View.prototype,
    'getRotation',
    GVol.View.prototype.getRotation);

goog.exportProperty(
    GVol.View.prototype,
    'getZoom',
    GVol.View.prototype.getZoom);

goog.exportProperty(
    GVol.View.prototype,
    'getZoomForResGVolution',
    GVol.View.prototype.getZoomForResGVolution);

goog.exportProperty(
    GVol.View.prototype,
    'getResGVolutionForZoom',
    GVol.View.prototype.getResGVolutionForZoom);

goog.exportProperty(
    GVol.View.prototype,
    'fit',
    GVol.View.prototype.fit);

goog.exportProperty(
    GVol.View.prototype,
    'centerOn',
    GVol.View.prototype.centerOn);

goog.exportProperty(
    GVol.View.prototype,
    'rotate',
    GVol.View.prototype.rotate);

goog.exportProperty(
    GVol.View.prototype,
    'setCenter',
    GVol.View.prototype.setCenter);

goog.exportProperty(
    GVol.View.prototype,
    'setResGVolution',
    GVol.View.prototype.setResGVolution);

goog.exportProperty(
    GVol.View.prototype,
    'setRotation',
    GVol.View.prototype.setRotation);

goog.exportProperty(
    GVol.View.prototype,
    'setZoom',
    GVol.View.prototype.setZoom);

goog.exportSymbGVol(
    'GVol.xml.getAllTextContent',
    GVol.xml.getAllTextContent,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.xml.parse',
    GVol.xml.parse,
    OPENLAYERS);

goog.exportProperty(
    GVol.webgl.Context.prototype,
    'getGL',
    GVol.webgl.Context.prototype.getGL);

goog.exportProperty(
    GVol.webgl.Context.prototype,
    'useProgram',
    GVol.webgl.Context.prototype.useProgram);

goog.exportSymbGVol(
    'GVol.tilegrid.TileGrid',
    GVol.tilegrid.TileGrid,
    OPENLAYERS);

goog.exportProperty(
    GVol.tilegrid.TileGrid.prototype,
    'forEachTileCoord',
    GVol.tilegrid.TileGrid.prototype.forEachTileCoord);

goog.exportProperty(
    GVol.tilegrid.TileGrid.prototype,
    'getMaxZoom',
    GVol.tilegrid.TileGrid.prototype.getMaxZoom);

goog.exportProperty(
    GVol.tilegrid.TileGrid.prototype,
    'getMinZoom',
    GVol.tilegrid.TileGrid.prototype.getMinZoom);

goog.exportProperty(
    GVol.tilegrid.TileGrid.prototype,
    'getOrigin',
    GVol.tilegrid.TileGrid.prototype.getOrigin);

goog.exportProperty(
    GVol.tilegrid.TileGrid.prototype,
    'getResGVolution',
    GVol.tilegrid.TileGrid.prototype.getResGVolution);

goog.exportProperty(
    GVol.tilegrid.TileGrid.prototype,
    'getResGVolutions',
    GVol.tilegrid.TileGrid.prototype.getResGVolutions);

goog.exportProperty(
    GVol.tilegrid.TileGrid.prototype,
    'getTileCoordExtent',
    GVol.tilegrid.TileGrid.prototype.getTileCoordExtent);

goog.exportProperty(
    GVol.tilegrid.TileGrid.prototype,
    'getTileCoordForCoordAndResGVolution',
    GVol.tilegrid.TileGrid.prototype.getTileCoordForCoordAndResGVolution);

goog.exportProperty(
    GVol.tilegrid.TileGrid.prototype,
    'getTileCoordForCoordAndZ',
    GVol.tilegrid.TileGrid.prototype.getTileCoordForCoordAndZ);

goog.exportProperty(
    GVol.tilegrid.TileGrid.prototype,
    'getTileSize',
    GVol.tilegrid.TileGrid.prototype.getTileSize);

goog.exportProperty(
    GVol.tilegrid.TileGrid.prototype,
    'getZForResGVolution',
    GVol.tilegrid.TileGrid.prototype.getZForResGVolution);

goog.exportSymbGVol(
    'GVol.tilegrid.WMTS',
    GVol.tilegrid.WMTS,
    OPENLAYERS);

goog.exportProperty(
    GVol.tilegrid.WMTS.prototype,
    'getMatrixIds',
    GVol.tilegrid.WMTS.prototype.getMatrixIds);

goog.exportSymbGVol(
    'GVol.tilegrid.WMTS.createFromCapabilitiesMatrixSet',
    GVol.tilegrid.WMTS.createFromCapabilitiesMatrixSet,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.style.AtlasManager',
    GVol.style.AtlasManager,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.style.Circle',
    GVol.style.Circle,
    OPENLAYERS);

goog.exportProperty(
    GVol.style.Circle.prototype,
    'setRadius',
    GVol.style.Circle.prototype.setRadius);

goog.exportSymbGVol(
    'GVol.style.Fill',
    GVol.style.Fill,
    OPENLAYERS);

goog.exportProperty(
    GVol.style.Fill.prototype,
    'clone',
    GVol.style.Fill.prototype.clone);

goog.exportProperty(
    GVol.style.Fill.prototype,
    'getCGVolor',
    GVol.style.Fill.prototype.getCGVolor);

goog.exportProperty(
    GVol.style.Fill.prototype,
    'setCGVolor',
    GVol.style.Fill.prototype.setCGVolor);

goog.exportSymbGVol(
    'GVol.style.Icon',
    GVol.style.Icon,
    OPENLAYERS);

goog.exportProperty(
    GVol.style.Icon.prototype,
    'clone',
    GVol.style.Icon.prototype.clone);

goog.exportProperty(
    GVol.style.Icon.prototype,
    'getAnchor',
    GVol.style.Icon.prototype.getAnchor);

goog.exportProperty(
    GVol.style.Icon.prototype,
    'getCGVolor',
    GVol.style.Icon.prototype.getCGVolor);

goog.exportProperty(
    GVol.style.Icon.prototype,
    'getImage',
    GVol.style.Icon.prototype.getImage);

goog.exportProperty(
    GVol.style.Icon.prototype,
    'getOrigin',
    GVol.style.Icon.prototype.getOrigin);

goog.exportProperty(
    GVol.style.Icon.prototype,
    'getSrc',
    GVol.style.Icon.prototype.getSrc);

goog.exportProperty(
    GVol.style.Icon.prototype,
    'getSize',
    GVol.style.Icon.prototype.getSize);

goog.exportProperty(
    GVol.style.Icon.prototype,
    'load',
    GVol.style.Icon.prototype.load);

goog.exportSymbGVol(
    'GVol.style.Image',
    GVol.style.Image,
    OPENLAYERS);

goog.exportProperty(
    GVol.style.Image.prototype,
    'getOpacity',
    GVol.style.Image.prototype.getOpacity);

goog.exportProperty(
    GVol.style.Image.prototype,
    'getRotateWithView',
    GVol.style.Image.prototype.getRotateWithView);

goog.exportProperty(
    GVol.style.Image.prototype,
    'getRotation',
    GVol.style.Image.prototype.getRotation);

goog.exportProperty(
    GVol.style.Image.prototype,
    'getScale',
    GVol.style.Image.prototype.getScale);

goog.exportProperty(
    GVol.style.Image.prototype,
    'getSnapToPixel',
    GVol.style.Image.prototype.getSnapToPixel);

goog.exportProperty(
    GVol.style.Image.prototype,
    'setOpacity',
    GVol.style.Image.prototype.setOpacity);

goog.exportProperty(
    GVol.style.Image.prototype,
    'setRotation',
    GVol.style.Image.prototype.setRotation);

goog.exportProperty(
    GVol.style.Image.prototype,
    'setScale',
    GVol.style.Image.prototype.setScale);

goog.exportSymbGVol(
    'GVol.style.RegularShape',
    GVol.style.RegularShape,
    OPENLAYERS);

goog.exportProperty(
    GVol.style.RegularShape.prototype,
    'clone',
    GVol.style.RegularShape.prototype.clone);

goog.exportProperty(
    GVol.style.RegularShape.prototype,
    'getAnchor',
    GVol.style.RegularShape.prototype.getAnchor);

goog.exportProperty(
    GVol.style.RegularShape.prototype,
    'getAngle',
    GVol.style.RegularShape.prototype.getAngle);

goog.exportProperty(
    GVol.style.RegularShape.prototype,
    'getFill',
    GVol.style.RegularShape.prototype.getFill);

goog.exportProperty(
    GVol.style.RegularShape.prototype,
    'getImage',
    GVol.style.RegularShape.prototype.getImage);

goog.exportProperty(
    GVol.style.RegularShape.prototype,
    'getOrigin',
    GVol.style.RegularShape.prototype.getOrigin);

goog.exportProperty(
    GVol.style.RegularShape.prototype,
    'getPoints',
    GVol.style.RegularShape.prototype.getPoints);

goog.exportProperty(
    GVol.style.RegularShape.prototype,
    'getRadius',
    GVol.style.RegularShape.prototype.getRadius);

goog.exportProperty(
    GVol.style.RegularShape.prototype,
    'getRadius2',
    GVol.style.RegularShape.prototype.getRadius2);

goog.exportProperty(
    GVol.style.RegularShape.prototype,
    'getSize',
    GVol.style.RegularShape.prototype.getSize);

goog.exportProperty(
    GVol.style.RegularShape.prototype,
    'getStroke',
    GVol.style.RegularShape.prototype.getStroke);

goog.exportSymbGVol(
    'GVol.style.Stroke',
    GVol.style.Stroke,
    OPENLAYERS);

goog.exportProperty(
    GVol.style.Stroke.prototype,
    'clone',
    GVol.style.Stroke.prototype.clone);

goog.exportProperty(
    GVol.style.Stroke.prototype,
    'getCGVolor',
    GVol.style.Stroke.prototype.getCGVolor);

goog.exportProperty(
    GVol.style.Stroke.prototype,
    'getLineCap',
    GVol.style.Stroke.prototype.getLineCap);

goog.exportProperty(
    GVol.style.Stroke.prototype,
    'getLineDash',
    GVol.style.Stroke.prototype.getLineDash);

goog.exportProperty(
    GVol.style.Stroke.prototype,
    'getLineDashOffset',
    GVol.style.Stroke.prototype.getLineDashOffset);

goog.exportProperty(
    GVol.style.Stroke.prototype,
    'getLineJoin',
    GVol.style.Stroke.prototype.getLineJoin);

goog.exportProperty(
    GVol.style.Stroke.prototype,
    'getMiterLimit',
    GVol.style.Stroke.prototype.getMiterLimit);

goog.exportProperty(
    GVol.style.Stroke.prototype,
    'getWidth',
    GVol.style.Stroke.prototype.getWidth);

goog.exportProperty(
    GVol.style.Stroke.prototype,
    'setCGVolor',
    GVol.style.Stroke.prototype.setCGVolor);

goog.exportProperty(
    GVol.style.Stroke.prototype,
    'setLineCap',
    GVol.style.Stroke.prototype.setLineCap);

goog.exportProperty(
    GVol.style.Stroke.prototype,
    'setLineDash',
    GVol.style.Stroke.prototype.setLineDash);

goog.exportProperty(
    GVol.style.Stroke.prototype,
    'setLineDashOffset',
    GVol.style.Stroke.prototype.setLineDashOffset);

goog.exportProperty(
    GVol.style.Stroke.prototype,
    'setLineJoin',
    GVol.style.Stroke.prototype.setLineJoin);

goog.exportProperty(
    GVol.style.Stroke.prototype,
    'setMiterLimit',
    GVol.style.Stroke.prototype.setMiterLimit);

goog.exportProperty(
    GVol.style.Stroke.prototype,
    'setWidth',
    GVol.style.Stroke.prototype.setWidth);

goog.exportSymbGVol(
    'GVol.style.Style',
    GVol.style.Style,
    OPENLAYERS);

goog.exportProperty(
    GVol.style.Style.prototype,
    'clone',
    GVol.style.Style.prototype.clone);

goog.exportProperty(
    GVol.style.Style.prototype,
    'getRenderer',
    GVol.style.Style.prototype.getRenderer);

goog.exportProperty(
    GVol.style.Style.prototype,
    'setRenderer',
    GVol.style.Style.prototype.setRenderer);

goog.exportProperty(
    GVol.style.Style.prototype,
    'getGeometry',
    GVol.style.Style.prototype.getGeometry);

goog.exportProperty(
    GVol.style.Style.prototype,
    'getGeometryFunction',
    GVol.style.Style.prototype.getGeometryFunction);

goog.exportProperty(
    GVol.style.Style.prototype,
    'getFill',
    GVol.style.Style.prototype.getFill);

goog.exportProperty(
    GVol.style.Style.prototype,
    'setFill',
    GVol.style.Style.prototype.setFill);

goog.exportProperty(
    GVol.style.Style.prototype,
    'getImage',
    GVol.style.Style.prototype.getImage);

goog.exportProperty(
    GVol.style.Style.prototype,
    'setImage',
    GVol.style.Style.prototype.setImage);

goog.exportProperty(
    GVol.style.Style.prototype,
    'getStroke',
    GVol.style.Style.prototype.getStroke);

goog.exportProperty(
    GVol.style.Style.prototype,
    'setStroke',
    GVol.style.Style.prototype.setStroke);

goog.exportProperty(
    GVol.style.Style.prototype,
    'getText',
    GVol.style.Style.prototype.getText);

goog.exportProperty(
    GVol.style.Style.prototype,
    'setText',
    GVol.style.Style.prototype.setText);

goog.exportProperty(
    GVol.style.Style.prototype,
    'getZIndex',
    GVol.style.Style.prototype.getZIndex);

goog.exportProperty(
    GVol.style.Style.prototype,
    'setGeometry',
    GVol.style.Style.prototype.setGeometry);

goog.exportProperty(
    GVol.style.Style.prototype,
    'setZIndex',
    GVol.style.Style.prototype.setZIndex);

goog.exportSymbGVol(
    'GVol.style.Text',
    GVol.style.Text,
    OPENLAYERS);

goog.exportProperty(
    GVol.style.Text.prototype,
    'clone',
    GVol.style.Text.prototype.clone);

goog.exportProperty(
    GVol.style.Text.prototype,
    'getFont',
    GVol.style.Text.prototype.getFont);

goog.exportProperty(
    GVol.style.Text.prototype,
    'getOffsetX',
    GVol.style.Text.prototype.getOffsetX);

goog.exportProperty(
    GVol.style.Text.prototype,
    'getOffsetY',
    GVol.style.Text.prototype.getOffsetY);

goog.exportProperty(
    GVol.style.Text.prototype,
    'getFill',
    GVol.style.Text.prototype.getFill);

goog.exportProperty(
    GVol.style.Text.prototype,
    'getRotateWithView',
    GVol.style.Text.prototype.getRotateWithView);

goog.exportProperty(
    GVol.style.Text.prototype,
    'getRotation',
    GVol.style.Text.prototype.getRotation);

goog.exportProperty(
    GVol.style.Text.prototype,
    'getScale',
    GVol.style.Text.prototype.getScale);

goog.exportProperty(
    GVol.style.Text.prototype,
    'getStroke',
    GVol.style.Text.prototype.getStroke);

goog.exportProperty(
    GVol.style.Text.prototype,
    'getText',
    GVol.style.Text.prototype.getText);

goog.exportProperty(
    GVol.style.Text.prototype,
    'getTextAlign',
    GVol.style.Text.prototype.getTextAlign);

goog.exportProperty(
    GVol.style.Text.prototype,
    'getTextBaseline',
    GVol.style.Text.prototype.getTextBaseline);

goog.exportProperty(
    GVol.style.Text.prototype,
    'setFont',
    GVol.style.Text.prototype.setFont);

goog.exportProperty(
    GVol.style.Text.prototype,
    'setOffsetX',
    GVol.style.Text.prototype.setOffsetX);

goog.exportProperty(
    GVol.style.Text.prototype,
    'setOffsetY',
    GVol.style.Text.prototype.setOffsetY);

goog.exportProperty(
    GVol.style.Text.prototype,
    'setFill',
    GVol.style.Text.prototype.setFill);

goog.exportProperty(
    GVol.style.Text.prototype,
    'setRotation',
    GVol.style.Text.prototype.setRotation);

goog.exportProperty(
    GVol.style.Text.prototype,
    'setScale',
    GVol.style.Text.prototype.setScale);

goog.exportProperty(
    GVol.style.Text.prototype,
    'setStroke',
    GVol.style.Text.prototype.setStroke);

goog.exportProperty(
    GVol.style.Text.prototype,
    'setText',
    GVol.style.Text.prototype.setText);

goog.exportProperty(
    GVol.style.Text.prototype,
    'setTextAlign',
    GVol.style.Text.prototype.setTextAlign);

goog.exportProperty(
    GVol.style.Text.prototype,
    'setTextBaseline',
    GVol.style.Text.prototype.setTextBaseline);

goog.exportSymbGVol(
    'GVol.source.BingMaps',
    GVol.source.BingMaps,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.source.BingMaps.TOS_ATTRIBUTION',
    GVol.source.BingMaps.TOS_ATTRIBUTION,
    OPENLAYERS);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'getApiKey',
    GVol.source.BingMaps.prototype.getApiKey);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'getImagerySet',
    GVol.source.BingMaps.prototype.getImagerySet);

goog.exportSymbGVol(
    'GVol.source.CartoDB',
    GVol.source.CartoDB,
    OPENLAYERS);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'getConfig',
    GVol.source.CartoDB.prototype.getConfig);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'updateConfig',
    GVol.source.CartoDB.prototype.updateConfig);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'setConfig',
    GVol.source.CartoDB.prototype.setConfig);

goog.exportSymbGVol(
    'GVol.source.Cluster',
    GVol.source.Cluster,
    OPENLAYERS);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'getDistance',
    GVol.source.Cluster.prototype.getDistance);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'getSource',
    GVol.source.Cluster.prototype.getSource);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'setDistance',
    GVol.source.Cluster.prototype.setDistance);

goog.exportSymbGVol(
    'GVol.source.Image',
    GVol.source.Image,
    OPENLAYERS);

goog.exportProperty(
    GVol.source.Image.Event.prototype,
    'image',
    GVol.source.Image.Event.prototype.image);

goog.exportSymbGVol(
    'GVol.source.ImageArcGISRest',
    GVol.source.ImageArcGISRest,
    OPENLAYERS);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'getParams',
    GVol.source.ImageArcGISRest.prototype.getParams);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'getImageLoadFunction',
    GVol.source.ImageArcGISRest.prototype.getImageLoadFunction);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'getUrl',
    GVol.source.ImageArcGISRest.prototype.getUrl);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'setImageLoadFunction',
    GVol.source.ImageArcGISRest.prototype.setImageLoadFunction);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'setUrl',
    GVol.source.ImageArcGISRest.prototype.setUrl);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'updateParams',
    GVol.source.ImageArcGISRest.prototype.updateParams);

goog.exportSymbGVol(
    'GVol.source.ImageCanvas',
    GVol.source.ImageCanvas,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.source.ImageMapGuide',
    GVol.source.ImageMapGuide,
    OPENLAYERS);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'getParams',
    GVol.source.ImageMapGuide.prototype.getParams);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'getImageLoadFunction',
    GVol.source.ImageMapGuide.prototype.getImageLoadFunction);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'updateParams',
    GVol.source.ImageMapGuide.prototype.updateParams);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'setImageLoadFunction',
    GVol.source.ImageMapGuide.prototype.setImageLoadFunction);

goog.exportSymbGVol(
    'GVol.source.ImageStatic',
    GVol.source.ImageStatic,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.source.ImageVector',
    GVol.source.ImageVector,
    OPENLAYERS);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'getSource',
    GVol.source.ImageVector.prototype.getSource);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'getStyle',
    GVol.source.ImageVector.prototype.getStyle);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'getStyleFunction',
    GVol.source.ImageVector.prototype.getStyleFunction);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'setStyle',
    GVol.source.ImageVector.prototype.setStyle);

goog.exportSymbGVol(
    'GVol.source.ImageWMS',
    GVol.source.ImageWMS,
    OPENLAYERS);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'getGetFeatureInfoUrl',
    GVol.source.ImageWMS.prototype.getGetFeatureInfoUrl);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'getParams',
    GVol.source.ImageWMS.prototype.getParams);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'getImageLoadFunction',
    GVol.source.ImageWMS.prototype.getImageLoadFunction);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'getUrl',
    GVol.source.ImageWMS.prototype.getUrl);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'setImageLoadFunction',
    GVol.source.ImageWMS.prototype.setImageLoadFunction);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'setUrl',
    GVol.source.ImageWMS.prototype.setUrl);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'updateParams',
    GVol.source.ImageWMS.prototype.updateParams);

goog.exportSymbGVol(
    'GVol.source.OSM',
    GVol.source.OSM,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.source.OSM.ATTRIBUTION',
    GVol.source.OSM.ATTRIBUTION,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.source.Raster',
    GVol.source.Raster,
    OPENLAYERS);

goog.exportProperty(
    GVol.source.Raster.prototype,
    'setOperation',
    GVol.source.Raster.prototype.setOperation);

goog.exportProperty(
    GVol.source.Raster.Event.prototype,
    'extent',
    GVol.source.Raster.Event.prototype.extent);

goog.exportProperty(
    GVol.source.Raster.Event.prototype,
    'resGVolution',
    GVol.source.Raster.Event.prototype.resGVolution);

goog.exportProperty(
    GVol.source.Raster.Event.prototype,
    'data',
    GVol.source.Raster.Event.prototype.data);

goog.exportSymbGVol(
    'GVol.source.Source',
    GVol.source.Source,
    OPENLAYERS);

goog.exportProperty(
    GVol.source.Source.prototype,
    'getAttributions',
    GVol.source.Source.prototype.getAttributions);

goog.exportProperty(
    GVol.source.Source.prototype,
    'getLogo',
    GVol.source.Source.prototype.getLogo);

goog.exportProperty(
    GVol.source.Source.prototype,
    'getProjection',
    GVol.source.Source.prototype.getProjection);

goog.exportProperty(
    GVol.source.Source.prototype,
    'getState',
    GVol.source.Source.prototype.getState);

goog.exportProperty(
    GVol.source.Source.prototype,
    'refresh',
    GVol.source.Source.prototype.refresh);

goog.exportProperty(
    GVol.source.Source.prototype,
    'setAttributions',
    GVol.source.Source.prototype.setAttributions);

goog.exportSymbGVol(
    'GVol.source.Stamen',
    GVol.source.Stamen,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.source.Tile',
    GVol.source.Tile,
    OPENLAYERS);

goog.exportProperty(
    GVol.source.Tile.prototype,
    'getTileGrid',
    GVol.source.Tile.prototype.getTileGrid);

goog.exportProperty(
    GVol.source.Tile.Event.prototype,
    'tile',
    GVol.source.Tile.Event.prototype.tile);

goog.exportSymbGVol(
    'GVol.source.TileArcGISRest',
    GVol.source.TileArcGISRest,
    OPENLAYERS);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'getParams',
    GVol.source.TileArcGISRest.prototype.getParams);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'updateParams',
    GVol.source.TileArcGISRest.prototype.updateParams);

goog.exportSymbGVol(
    'GVol.source.TileDebug',
    GVol.source.TileDebug,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.source.TileImage',
    GVol.source.TileImage,
    OPENLAYERS);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'setRenderReprojectionEdges',
    GVol.source.TileImage.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'setTileGridForProjection',
    GVol.source.TileImage.prototype.setTileGridForProjection);

goog.exportSymbGVol(
    'GVol.source.TileJSON',
    GVol.source.TileJSON,
    OPENLAYERS);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'getTileJSON',
    GVol.source.TileJSON.prototype.getTileJSON);

goog.exportSymbGVol(
    'GVol.source.TileUTFGrid',
    GVol.source.TileUTFGrid,
    OPENLAYERS);

goog.exportProperty(
    GVol.source.TileUTFGrid.prototype,
    'getTemplate',
    GVol.source.TileUTFGrid.prototype.getTemplate);

goog.exportProperty(
    GVol.source.TileUTFGrid.prototype,
    'forDataAtCoordinateAndResGVolution',
    GVol.source.TileUTFGrid.prototype.forDataAtCoordinateAndResGVolution);

goog.exportSymbGVol(
    'GVol.source.TileWMS',
    GVol.source.TileWMS,
    OPENLAYERS);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'getGetFeatureInfoUrl',
    GVol.source.TileWMS.prototype.getGetFeatureInfoUrl);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'getParams',
    GVol.source.TileWMS.prototype.getParams);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'updateParams',
    GVol.source.TileWMS.prototype.updateParams);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'getTileLoadFunction',
    GVol.source.UrlTile.prototype.getTileLoadFunction);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'getTileUrlFunction',
    GVol.source.UrlTile.prototype.getTileUrlFunction);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'getUrls',
    GVol.source.UrlTile.prototype.getUrls);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'setTileLoadFunction',
    GVol.source.UrlTile.prototype.setTileLoadFunction);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'setTileUrlFunction',
    GVol.source.UrlTile.prototype.setTileUrlFunction);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'setUrl',
    GVol.source.UrlTile.prototype.setUrl);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'setUrls',
    GVol.source.UrlTile.prototype.setUrls);

goog.exportSymbGVol(
    'GVol.source.Vector',
    GVol.source.Vector,
    OPENLAYERS);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'addFeature',
    GVol.source.Vector.prototype.addFeature);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'addFeatures',
    GVol.source.Vector.prototype.addFeatures);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'clear',
    GVol.source.Vector.prototype.clear);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'forEachFeature',
    GVol.source.Vector.prototype.forEachFeature);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'forEachFeatureInExtent',
    GVol.source.Vector.prototype.forEachFeatureInExtent);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'forEachFeatureIntersectingExtent',
    GVol.source.Vector.prototype.forEachFeatureIntersectingExtent);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'getFeaturesCGVollection',
    GVol.source.Vector.prototype.getFeaturesCGVollection);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'getFeatures',
    GVol.source.Vector.prototype.getFeatures);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'getFeaturesAtCoordinate',
    GVol.source.Vector.prototype.getFeaturesAtCoordinate);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'getFeaturesInExtent',
    GVol.source.Vector.prototype.getFeaturesInExtent);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'getClosestFeatureToCoordinate',
    GVol.source.Vector.prototype.getClosestFeatureToCoordinate);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'getExtent',
    GVol.source.Vector.prototype.getExtent);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'getFeatureById',
    GVol.source.Vector.prototype.getFeatureById);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'getFormat',
    GVol.source.Vector.prototype.getFormat);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'getUrl',
    GVol.source.Vector.prototype.getUrl);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'removeFeature',
    GVol.source.Vector.prototype.removeFeature);

goog.exportProperty(
    GVol.source.Vector.Event.prototype,
    'feature',
    GVol.source.Vector.Event.prototype.feature);

goog.exportSymbGVol(
    'GVol.source.VectorTile',
    GVol.source.VectorTile,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.source.WMTS',
    GVol.source.WMTS,
    OPENLAYERS);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'getDimensions',
    GVol.source.WMTS.prototype.getDimensions);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'getFormat',
    GVol.source.WMTS.prototype.getFormat);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'getLayer',
    GVol.source.WMTS.prototype.getLayer);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'getMatrixSet',
    GVol.source.WMTS.prototype.getMatrixSet);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'getRequestEncoding',
    GVol.source.WMTS.prototype.getRequestEncoding);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'getStyle',
    GVol.source.WMTS.prototype.getStyle);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'getVersion',
    GVol.source.WMTS.prototype.getVersion);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'updateDimensions',
    GVol.source.WMTS.prototype.updateDimensions);

goog.exportSymbGVol(
    'GVol.source.WMTS.optionsFromCapabilities',
    GVol.source.WMTS.optionsFromCapabilities,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.source.XYZ',
    GVol.source.XYZ,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.source.Zoomify',
    GVol.source.Zoomify,
    OPENLAYERS);

goog.exportProperty(
    GVol.render.Event.prototype,
    'vectorContext',
    GVol.render.Event.prototype.vectorContext);

goog.exportProperty(
    GVol.render.Event.prototype,
    'frameState',
    GVol.render.Event.prototype.frameState);

goog.exportProperty(
    GVol.render.Event.prototype,
    'context',
    GVol.render.Event.prototype.context);

goog.exportProperty(
    GVol.render.Event.prototype,
    'glContext',
    GVol.render.Event.prototype.glContext);

goog.exportProperty(
    GVol.render.Feature.prototype,
    'get',
    GVol.render.Feature.prototype.get);

goog.exportProperty(
    GVol.render.Feature.prototype,
    'getExtent',
    GVol.render.Feature.prototype.getExtent);

goog.exportProperty(
    GVol.render.Feature.prototype,
    'getId',
    GVol.render.Feature.prototype.getId);

goog.exportProperty(
    GVol.render.Feature.prototype,
    'getGeometry',
    GVol.render.Feature.prototype.getGeometry);

goog.exportProperty(
    GVol.render.Feature.prototype,
    'getProperties',
    GVol.render.Feature.prototype.getProperties);

goog.exportProperty(
    GVol.render.Feature.prototype,
    'getType',
    GVol.render.Feature.prototype.getType);

goog.exportSymbGVol(
    'GVol.render.VectorContext',
    GVol.render.VectorContext,
    OPENLAYERS);

goog.exportProperty(
    GVol.render.webgl.Immediate.prototype,
    'setStyle',
    GVol.render.webgl.Immediate.prototype.setStyle);

goog.exportProperty(
    GVol.render.webgl.Immediate.prototype,
    'drawGeometry',
    GVol.render.webgl.Immediate.prototype.drawGeometry);

goog.exportProperty(
    GVol.render.webgl.Immediate.prototype,
    'drawFeature',
    GVol.render.webgl.Immediate.prototype.drawFeature);

goog.exportProperty(
    GVol.render.canvas.Immediate.prototype,
    'drawCircle',
    GVol.render.canvas.Immediate.prototype.drawCircle);

goog.exportProperty(
    GVol.render.canvas.Immediate.prototype,
    'setStyle',
    GVol.render.canvas.Immediate.prototype.setStyle);

goog.exportProperty(
    GVol.render.canvas.Immediate.prototype,
    'drawGeometry',
    GVol.render.canvas.Immediate.prototype.drawGeometry);

goog.exportProperty(
    GVol.render.canvas.Immediate.prototype,
    'drawFeature',
    GVol.render.canvas.Immediate.prototype.drawFeature);

goog.exportSymbGVol(
    'GVol.proj.common.add',
    GVol.proj.common.add,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.proj.Projection',
    GVol.proj.Projection,
    OPENLAYERS);

goog.exportProperty(
    GVol.proj.Projection.prototype,
    'getCode',
    GVol.proj.Projection.prototype.getCode);

goog.exportProperty(
    GVol.proj.Projection.prototype,
    'getExtent',
    GVol.proj.Projection.prototype.getExtent);

goog.exportProperty(
    GVol.proj.Projection.prototype,
    'getUnits',
    GVol.proj.Projection.prototype.getUnits);

goog.exportProperty(
    GVol.proj.Projection.prototype,
    'getMetersPerUnit',
    GVol.proj.Projection.prototype.getMetersPerUnit);

goog.exportProperty(
    GVol.proj.Projection.prototype,
    'getWorldExtent',
    GVol.proj.Projection.prototype.getWorldExtent);

goog.exportProperty(
    GVol.proj.Projection.prototype,
    'isGlobal',
    GVol.proj.Projection.prototype.isGlobal);

goog.exportProperty(
    GVol.proj.Projection.prototype,
    'setGlobal',
    GVol.proj.Projection.prototype.setGlobal);

goog.exportProperty(
    GVol.proj.Projection.prototype,
    'setExtent',
    GVol.proj.Projection.prototype.setExtent);

goog.exportProperty(
    GVol.proj.Projection.prototype,
    'setWorldExtent',
    GVol.proj.Projection.prototype.setWorldExtent);

goog.exportProperty(
    GVol.proj.Projection.prototype,
    'setGetPointResGVolution',
    GVol.proj.Projection.prototype.setGetPointResGVolution);

goog.exportSymbGVol(
    'GVol.proj.Units.METERS_PER_UNIT',
    GVol.proj.Units.METERS_PER_UNIT,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.layer.Base',
    GVol.layer.Base,
    OPENLAYERS);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'getExtent',
    GVol.layer.Base.prototype.getExtent);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'getMaxResGVolution',
    GVol.layer.Base.prototype.getMaxResGVolution);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'getMinResGVolution',
    GVol.layer.Base.prototype.getMinResGVolution);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'getOpacity',
    GVol.layer.Base.prototype.getOpacity);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'getVisible',
    GVol.layer.Base.prototype.getVisible);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'getZIndex',
    GVol.layer.Base.prototype.getZIndex);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'setExtent',
    GVol.layer.Base.prototype.setExtent);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'setMaxResGVolution',
    GVol.layer.Base.prototype.setMaxResGVolution);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'setMinResGVolution',
    GVol.layer.Base.prototype.setMinResGVolution);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'setOpacity',
    GVol.layer.Base.prototype.setOpacity);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'setVisible',
    GVol.layer.Base.prototype.setVisible);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'setZIndex',
    GVol.layer.Base.prototype.setZIndex);

goog.exportSymbGVol(
    'GVol.layer.Group',
    GVol.layer.Group,
    OPENLAYERS);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'getLayers',
    GVol.layer.Group.prototype.getLayers);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'setLayers',
    GVol.layer.Group.prototype.setLayers);

goog.exportSymbGVol(
    'GVol.layer.Heatmap',
    GVol.layer.Heatmap,
    OPENLAYERS);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'getBlur',
    GVol.layer.Heatmap.prototype.getBlur);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'getGradient',
    GVol.layer.Heatmap.prototype.getGradient);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'getRadius',
    GVol.layer.Heatmap.prototype.getRadius);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'setBlur',
    GVol.layer.Heatmap.prototype.setBlur);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'setGradient',
    GVol.layer.Heatmap.prototype.setGradient);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'setRadius',
    GVol.layer.Heatmap.prototype.setRadius);

goog.exportSymbGVol(
    'GVol.layer.Image',
    GVol.layer.Image,
    OPENLAYERS);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'getSource',
    GVol.layer.Image.prototype.getSource);

goog.exportSymbGVol(
    'GVol.layer.Layer',
    GVol.layer.Layer,
    OPENLAYERS);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'getSource',
    GVol.layer.Layer.prototype.getSource);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'setMap',
    GVol.layer.Layer.prototype.setMap);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'setSource',
    GVol.layer.Layer.prototype.setSource);

goog.exportSymbGVol(
    'GVol.layer.Tile',
    GVol.layer.Tile,
    OPENLAYERS);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'getPreload',
    GVol.layer.Tile.prototype.getPreload);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'getSource',
    GVol.layer.Tile.prototype.getSource);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'setPreload',
    GVol.layer.Tile.prototype.setPreload);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'getUseInterimTilesOnError',
    GVol.layer.Tile.prototype.getUseInterimTilesOnError);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'setUseInterimTilesOnError',
    GVol.layer.Tile.prototype.setUseInterimTilesOnError);

goog.exportSymbGVol(
    'GVol.layer.Vector',
    GVol.layer.Vector,
    OPENLAYERS);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'getSource',
    GVol.layer.Vector.prototype.getSource);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'getStyle',
    GVol.layer.Vector.prototype.getStyle);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'getStyleFunction',
    GVol.layer.Vector.prototype.getStyleFunction);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'setStyle',
    GVol.layer.Vector.prototype.setStyle);

goog.exportSymbGVol(
    'GVol.layer.VectorTile',
    GVol.layer.VectorTile,
    OPENLAYERS);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'getPreload',
    GVol.layer.VectorTile.prototype.getPreload);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'getUseInterimTilesOnError',
    GVol.layer.VectorTile.prototype.getUseInterimTilesOnError);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'setPreload',
    GVol.layer.VectorTile.prototype.setPreload);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'setUseInterimTilesOnError',
    GVol.layer.VectorTile.prototype.setUseInterimTilesOnError);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'getSource',
    GVol.layer.VectorTile.prototype.getSource);

goog.exportSymbGVol(
    'GVol.interaction.DoubleClickZoom',
    GVol.interaction.DoubleClickZoom,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.interaction.DoubleClickZoom.handleEvent',
    GVol.interaction.DoubleClickZoom.handleEvent,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.interaction.DragAndDrop',
    GVol.interaction.DragAndDrop,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.interaction.DragAndDrop.handleEvent',
    GVol.interaction.DragAndDrop.handleEvent,
    OPENLAYERS);

goog.exportProperty(
    GVol.interaction.DragAndDrop.Event.prototype,
    'features',
    GVol.interaction.DragAndDrop.Event.prototype.features);

goog.exportProperty(
    GVol.interaction.DragAndDrop.Event.prototype,
    'file',
    GVol.interaction.DragAndDrop.Event.prototype.file);

goog.exportProperty(
    GVol.interaction.DragAndDrop.Event.prototype,
    'projection',
    GVol.interaction.DragAndDrop.Event.prototype.projection);

goog.exportSymbGVol(
    'GVol.interaction.DragBox',
    GVol.interaction.DragBox,
    OPENLAYERS);

goog.exportProperty(
    GVol.interaction.DragBox.prototype,
    'getGeometry',
    GVol.interaction.DragBox.prototype.getGeometry);

goog.exportProperty(
    GVol.interaction.DragBox.Event.prototype,
    'coordinate',
    GVol.interaction.DragBox.Event.prototype.coordinate);

goog.exportProperty(
    GVol.interaction.DragBox.Event.prototype,
    'mapBrowserEvent',
    GVol.interaction.DragBox.Event.prototype.mapBrowserEvent);

goog.exportSymbGVol(
    'GVol.interaction.DragPan',
    GVol.interaction.DragPan,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.interaction.DragRotate',
    GVol.interaction.DragRotate,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.interaction.DragRotateAndZoom',
    GVol.interaction.DragRotateAndZoom,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.interaction.DragZoom',
    GVol.interaction.DragZoom,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.interaction.Draw',
    GVol.interaction.Draw,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.interaction.Draw.handleEvent',
    GVol.interaction.Draw.handleEvent,
    OPENLAYERS);

goog.exportProperty(
    GVol.interaction.Draw.prototype,
    'removeLastPoint',
    GVol.interaction.Draw.prototype.removeLastPoint);

goog.exportProperty(
    GVol.interaction.Draw.prototype,
    'finishDrawing',
    GVol.interaction.Draw.prototype.finishDrawing);

goog.exportProperty(
    GVol.interaction.Draw.prototype,
    'extend',
    GVol.interaction.Draw.prototype.extend);

goog.exportSymbGVol(
    'GVol.interaction.Draw.createRegularPGVolygon',
    GVol.interaction.Draw.createRegularPGVolygon,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.interaction.Draw.createBox',
    GVol.interaction.Draw.createBox,
    OPENLAYERS);

goog.exportProperty(
    GVol.interaction.Draw.Event.prototype,
    'feature',
    GVol.interaction.Draw.Event.prototype.feature);

goog.exportSymbGVol(
    'GVol.interaction.Extent',
    GVol.interaction.Extent,
    OPENLAYERS);

goog.exportProperty(
    GVol.interaction.Extent.prototype,
    'getExtent',
    GVol.interaction.Extent.prototype.getExtent);

goog.exportProperty(
    GVol.interaction.Extent.prototype,
    'setExtent',
    GVol.interaction.Extent.prototype.setExtent);

goog.exportProperty(
    GVol.interaction.Extent.Event.prototype,
    'extent',
    GVol.interaction.Extent.Event.prototype.extent);

goog.exportSymbGVol(
    'GVol.interaction.Interaction',
    GVol.interaction.Interaction,
    OPENLAYERS);

goog.exportProperty(
    GVol.interaction.Interaction.prototype,
    'getActive',
    GVol.interaction.Interaction.prototype.getActive);

goog.exportProperty(
    GVol.interaction.Interaction.prototype,
    'getMap',
    GVol.interaction.Interaction.prototype.getMap);

goog.exportProperty(
    GVol.interaction.Interaction.prototype,
    'setActive',
    GVol.interaction.Interaction.prototype.setActive);

goog.exportSymbGVol(
    'GVol.interaction.KeyboardPan',
    GVol.interaction.KeyboardPan,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.interaction.KeyboardPan.handleEvent',
    GVol.interaction.KeyboardPan.handleEvent,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.interaction.KeyboardZoom',
    GVol.interaction.KeyboardZoom,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.interaction.KeyboardZoom.handleEvent',
    GVol.interaction.KeyboardZoom.handleEvent,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.interaction.Modify',
    GVol.interaction.Modify,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.interaction.Modify.handleEvent',
    GVol.interaction.Modify.handleEvent,
    OPENLAYERS);

goog.exportProperty(
    GVol.interaction.Modify.prototype,
    'removePoint',
    GVol.interaction.Modify.prototype.removePoint);

goog.exportProperty(
    GVol.interaction.Modify.Event.prototype,
    'features',
    GVol.interaction.Modify.Event.prototype.features);

goog.exportProperty(
    GVol.interaction.Modify.Event.prototype,
    'mapBrowserEvent',
    GVol.interaction.Modify.Event.prototype.mapBrowserEvent);

goog.exportSymbGVol(
    'GVol.interaction.MouseWheelZoom',
    GVol.interaction.MouseWheelZoom,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.interaction.MouseWheelZoom.handleEvent',
    GVol.interaction.MouseWheelZoom.handleEvent,
    OPENLAYERS);

goog.exportProperty(
    GVol.interaction.MouseWheelZoom.prototype,
    'setMouseAnchor',
    GVol.interaction.MouseWheelZoom.prototype.setMouseAnchor);

goog.exportSymbGVol(
    'GVol.interaction.PinchRotate',
    GVol.interaction.PinchRotate,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.interaction.PinchZoom',
    GVol.interaction.PinchZoom,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.interaction.Pointer',
    GVol.interaction.Pointer,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.interaction.Pointer.handleEvent',
    GVol.interaction.Pointer.handleEvent,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.interaction.Select',
    GVol.interaction.Select,
    OPENLAYERS);

goog.exportProperty(
    GVol.interaction.Select.prototype,
    'getFeatures',
    GVol.interaction.Select.prototype.getFeatures);

goog.exportProperty(
    GVol.interaction.Select.prototype,
    'getHitTGVolerance',
    GVol.interaction.Select.prototype.getHitTGVolerance);

goog.exportProperty(
    GVol.interaction.Select.prototype,
    'getLayer',
    GVol.interaction.Select.prototype.getLayer);

goog.exportSymbGVol(
    'GVol.interaction.Select.handleEvent',
    GVol.interaction.Select.handleEvent,
    OPENLAYERS);

goog.exportProperty(
    GVol.interaction.Select.prototype,
    'setHitTGVolerance',
    GVol.interaction.Select.prototype.setHitTGVolerance);

goog.exportProperty(
    GVol.interaction.Select.prototype,
    'setMap',
    GVol.interaction.Select.prototype.setMap);

goog.exportProperty(
    GVol.interaction.Select.Event.prototype,
    'selected',
    GVol.interaction.Select.Event.prototype.selected);

goog.exportProperty(
    GVol.interaction.Select.Event.prototype,
    'deselected',
    GVol.interaction.Select.Event.prototype.deselected);

goog.exportProperty(
    GVol.interaction.Select.Event.prototype,
    'mapBrowserEvent',
    GVol.interaction.Select.Event.prototype.mapBrowserEvent);

goog.exportSymbGVol(
    'GVol.interaction.Snap',
    GVol.interaction.Snap,
    OPENLAYERS);

goog.exportProperty(
    GVol.interaction.Snap.prototype,
    'addFeature',
    GVol.interaction.Snap.prototype.addFeature);

goog.exportProperty(
    GVol.interaction.Snap.prototype,
    'removeFeature',
    GVol.interaction.Snap.prototype.removeFeature);

goog.exportSymbGVol(
    'GVol.interaction.Translate',
    GVol.interaction.Translate,
    OPENLAYERS);

goog.exportProperty(
    GVol.interaction.Translate.prototype,
    'getHitTGVolerance',
    GVol.interaction.Translate.prototype.getHitTGVolerance);

goog.exportProperty(
    GVol.interaction.Translate.prototype,
    'setHitTGVolerance',
    GVol.interaction.Translate.prototype.setHitTGVolerance);

goog.exportProperty(
    GVol.interaction.Translate.Event.prototype,
    'features',
    GVol.interaction.Translate.Event.prototype.features);

goog.exportProperty(
    GVol.interaction.Translate.Event.prototype,
    'coordinate',
    GVol.interaction.Translate.Event.prototype.coordinate);

goog.exportSymbGVol(
    'GVol.geom.Circle',
    GVol.geom.Circle,
    OPENLAYERS);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'clone',
    GVol.geom.Circle.prototype.clone);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'getCenter',
    GVol.geom.Circle.prototype.getCenter);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'getRadius',
    GVol.geom.Circle.prototype.getRadius);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'getType',
    GVol.geom.Circle.prototype.getType);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'intersectsExtent',
    GVol.geom.Circle.prototype.intersectsExtent);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'setCenter',
    GVol.geom.Circle.prototype.setCenter);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'setCenterAndRadius',
    GVol.geom.Circle.prototype.setCenterAndRadius);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'setRadius',
    GVol.geom.Circle.prototype.setRadius);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'transform',
    GVol.geom.Circle.prototype.transform);

goog.exportSymbGVol(
    'GVol.geom.Geometry',
    GVol.geom.Geometry,
    OPENLAYERS);

goog.exportProperty(
    GVol.geom.Geometry.prototype,
    'getClosestPoint',
    GVol.geom.Geometry.prototype.getClosestPoint);

goog.exportProperty(
    GVol.geom.Geometry.prototype,
    'intersectsCoordinate',
    GVol.geom.Geometry.prototype.intersectsCoordinate);

goog.exportProperty(
    GVol.geom.Geometry.prototype,
    'getExtent',
    GVol.geom.Geometry.prototype.getExtent);

goog.exportProperty(
    GVol.geom.Geometry.prototype,
    'rotate',
    GVol.geom.Geometry.prototype.rotate);

goog.exportProperty(
    GVol.geom.Geometry.prototype,
    'scale',
    GVol.geom.Geometry.prototype.scale);

goog.exportProperty(
    GVol.geom.Geometry.prototype,
    'simplify',
    GVol.geom.Geometry.prototype.simplify);

goog.exportProperty(
    GVol.geom.Geometry.prototype,
    'transform',
    GVol.geom.Geometry.prototype.transform);

goog.exportSymbGVol(
    'GVol.geom.GeometryCGVollection',
    GVol.geom.GeometryCGVollection,
    OPENLAYERS);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'clone',
    GVol.geom.GeometryCGVollection.prototype.clone);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'getGeometries',
    GVol.geom.GeometryCGVollection.prototype.getGeometries);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'getType',
    GVol.geom.GeometryCGVollection.prototype.getType);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'intersectsExtent',
    GVol.geom.GeometryCGVollection.prototype.intersectsExtent);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'setGeometries',
    GVol.geom.GeometryCGVollection.prototype.setGeometries);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'applyTransform',
    GVol.geom.GeometryCGVollection.prototype.applyTransform);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'translate',
    GVol.geom.GeometryCGVollection.prototype.translate);

goog.exportSymbGVol(
    'GVol.geom.LinearRing',
    GVol.geom.LinearRing,
    OPENLAYERS);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'clone',
    GVol.geom.LinearRing.prototype.clone);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'getArea',
    GVol.geom.LinearRing.prototype.getArea);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'getCoordinates',
    GVol.geom.LinearRing.prototype.getCoordinates);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'getType',
    GVol.geom.LinearRing.prototype.getType);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'setCoordinates',
    GVol.geom.LinearRing.prototype.setCoordinates);

goog.exportSymbGVol(
    'GVol.geom.LineString',
    GVol.geom.LineString,
    OPENLAYERS);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'appendCoordinate',
    GVol.geom.LineString.prototype.appendCoordinate);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'clone',
    GVol.geom.LineString.prototype.clone);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'forEachSegment',
    GVol.geom.LineString.prototype.forEachSegment);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'getCoordinateAtM',
    GVol.geom.LineString.prototype.getCoordinateAtM);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'getCoordinates',
    GVol.geom.LineString.prototype.getCoordinates);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'getCoordinateAt',
    GVol.geom.LineString.prototype.getCoordinateAt);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'getLength',
    GVol.geom.LineString.prototype.getLength);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'getType',
    GVol.geom.LineString.prototype.getType);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'intersectsExtent',
    GVol.geom.LineString.prototype.intersectsExtent);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'setCoordinates',
    GVol.geom.LineString.prototype.setCoordinates);

goog.exportSymbGVol(
    'GVol.geom.MultiLineString',
    GVol.geom.MultiLineString,
    OPENLAYERS);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'appendLineString',
    GVol.geom.MultiLineString.prototype.appendLineString);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'clone',
    GVol.geom.MultiLineString.prototype.clone);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'getCoordinateAtM',
    GVol.geom.MultiLineString.prototype.getCoordinateAtM);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'getCoordinates',
    GVol.geom.MultiLineString.prototype.getCoordinates);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'getLineString',
    GVol.geom.MultiLineString.prototype.getLineString);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'getLineStrings',
    GVol.geom.MultiLineString.prototype.getLineStrings);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'getType',
    GVol.geom.MultiLineString.prototype.getType);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'intersectsExtent',
    GVol.geom.MultiLineString.prototype.intersectsExtent);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'setCoordinates',
    GVol.geom.MultiLineString.prototype.setCoordinates);

goog.exportSymbGVol(
    'GVol.geom.MultiPoint',
    GVol.geom.MultiPoint,
    OPENLAYERS);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'appendPoint',
    GVol.geom.MultiPoint.prototype.appendPoint);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'clone',
    GVol.geom.MultiPoint.prototype.clone);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'getCoordinates',
    GVol.geom.MultiPoint.prototype.getCoordinates);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'getPoint',
    GVol.geom.MultiPoint.prototype.getPoint);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'getPoints',
    GVol.geom.MultiPoint.prototype.getPoints);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'getType',
    GVol.geom.MultiPoint.prototype.getType);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'intersectsExtent',
    GVol.geom.MultiPoint.prototype.intersectsExtent);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'setCoordinates',
    GVol.geom.MultiPoint.prototype.setCoordinates);

goog.exportSymbGVol(
    'GVol.geom.MultiPGVolygon',
    GVol.geom.MultiPGVolygon,
    OPENLAYERS);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'appendPGVolygon',
    GVol.geom.MultiPGVolygon.prototype.appendPGVolygon);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'clone',
    GVol.geom.MultiPGVolygon.prototype.clone);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'getArea',
    GVol.geom.MultiPGVolygon.prototype.getArea);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'getCoordinates',
    GVol.geom.MultiPGVolygon.prototype.getCoordinates);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'getInteriorPoints',
    GVol.geom.MultiPGVolygon.prototype.getInteriorPoints);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'getPGVolygon',
    GVol.geom.MultiPGVolygon.prototype.getPGVolygon);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'getPGVolygons',
    GVol.geom.MultiPGVolygon.prototype.getPGVolygons);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'getType',
    GVol.geom.MultiPGVolygon.prototype.getType);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'intersectsExtent',
    GVol.geom.MultiPGVolygon.prototype.intersectsExtent);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'setCoordinates',
    GVol.geom.MultiPGVolygon.prototype.setCoordinates);

goog.exportSymbGVol(
    'GVol.geom.Point',
    GVol.geom.Point,
    OPENLAYERS);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'clone',
    GVol.geom.Point.prototype.clone);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'getCoordinates',
    GVol.geom.Point.prototype.getCoordinates);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'getType',
    GVol.geom.Point.prototype.getType);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'intersectsExtent',
    GVol.geom.Point.prototype.intersectsExtent);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'setCoordinates',
    GVol.geom.Point.prototype.setCoordinates);

goog.exportSymbGVol(
    'GVol.geom.PGVolygon',
    GVol.geom.PGVolygon,
    OPENLAYERS);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'appendLinearRing',
    GVol.geom.PGVolygon.prototype.appendLinearRing);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'clone',
    GVol.geom.PGVolygon.prototype.clone);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'getArea',
    GVol.geom.PGVolygon.prototype.getArea);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'getCoordinates',
    GVol.geom.PGVolygon.prototype.getCoordinates);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'getInteriorPoint',
    GVol.geom.PGVolygon.prototype.getInteriorPoint);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'getLinearRingCount',
    GVol.geom.PGVolygon.prototype.getLinearRingCount);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'getLinearRing',
    GVol.geom.PGVolygon.prototype.getLinearRing);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'getLinearRings',
    GVol.geom.PGVolygon.prototype.getLinearRings);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'getType',
    GVol.geom.PGVolygon.prototype.getType);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'intersectsExtent',
    GVol.geom.PGVolygon.prototype.intersectsExtent);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'setCoordinates',
    GVol.geom.PGVolygon.prototype.setCoordinates);

goog.exportSymbGVol(
    'GVol.geom.PGVolygon.circular',
    GVol.geom.PGVolygon.circular,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.geom.PGVolygon.fromExtent',
    GVol.geom.PGVolygon.fromExtent,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.geom.PGVolygon.fromCircle',
    GVol.geom.PGVolygon.fromCircle,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.geom.SimpleGeometry',
    GVol.geom.SimpleGeometry,
    OPENLAYERS);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'getFirstCoordinate',
    GVol.geom.SimpleGeometry.prototype.getFirstCoordinate);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'getLastCoordinate',
    GVol.geom.SimpleGeometry.prototype.getLastCoordinate);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'getLayout',
    GVol.geom.SimpleGeometry.prototype.getLayout);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'applyTransform',
    GVol.geom.SimpleGeometry.prototype.applyTransform);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'translate',
    GVol.geom.SimpleGeometry.prototype.translate);

goog.exportSymbGVol(
    'GVol.format.EsriJSON',
    GVol.format.EsriJSON,
    OPENLAYERS);

goog.exportProperty(
    GVol.format.EsriJSON.prototype,
    'readFeature',
    GVol.format.EsriJSON.prototype.readFeature);

goog.exportProperty(
    GVol.format.EsriJSON.prototype,
    'readFeatures',
    GVol.format.EsriJSON.prototype.readFeatures);

goog.exportProperty(
    GVol.format.EsriJSON.prototype,
    'readGeometry',
    GVol.format.EsriJSON.prototype.readGeometry);

goog.exportProperty(
    GVol.format.EsriJSON.prototype,
    'readProjection',
    GVol.format.EsriJSON.prototype.readProjection);

goog.exportProperty(
    GVol.format.EsriJSON.prototype,
    'writeGeometry',
    GVol.format.EsriJSON.prototype.writeGeometry);

goog.exportProperty(
    GVol.format.EsriJSON.prototype,
    'writeGeometryObject',
    GVol.format.EsriJSON.prototype.writeGeometryObject);

goog.exportProperty(
    GVol.format.EsriJSON.prototype,
    'writeFeature',
    GVol.format.EsriJSON.prototype.writeFeature);

goog.exportProperty(
    GVol.format.EsriJSON.prototype,
    'writeFeatureObject',
    GVol.format.EsriJSON.prototype.writeFeatureObject);

goog.exportProperty(
    GVol.format.EsriJSON.prototype,
    'writeFeatures',
    GVol.format.EsriJSON.prototype.writeFeatures);

goog.exportProperty(
    GVol.format.EsriJSON.prototype,
    'writeFeaturesObject',
    GVol.format.EsriJSON.prototype.writeFeaturesObject);

goog.exportSymbGVol(
    'GVol.format.Feature',
    GVol.format.Feature,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.and',
    GVol.format.filter.and,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.or',
    GVol.format.filter.or,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.not',
    GVol.format.filter.not,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.bbox',
    GVol.format.filter.bbox,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.intersects',
    GVol.format.filter.intersects,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.within',
    GVol.format.filter.within,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.equalTo',
    GVol.format.filter.equalTo,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.notEqualTo',
    GVol.format.filter.notEqualTo,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.lessThan',
    GVol.format.filter.lessThan,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.lessThanOrEqualTo',
    GVol.format.filter.lessThanOrEqualTo,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.greaterThan',
    GVol.format.filter.greaterThan,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.greaterThanOrEqualTo',
    GVol.format.filter.greaterThanOrEqualTo,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.isNull',
    GVol.format.filter.isNull,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.between',
    GVol.format.filter.between,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.like',
    GVol.format.filter.like,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.during',
    GVol.format.filter.during,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.GeoJSON',
    GVol.format.GeoJSON,
    OPENLAYERS);

goog.exportProperty(
    GVol.format.GeoJSON.prototype,
    'readFeature',
    GVol.format.GeoJSON.prototype.readFeature);

goog.exportProperty(
    GVol.format.GeoJSON.prototype,
    'readFeatures',
    GVol.format.GeoJSON.prototype.readFeatures);

goog.exportProperty(
    GVol.format.GeoJSON.prototype,
    'readGeometry',
    GVol.format.GeoJSON.prototype.readGeometry);

goog.exportProperty(
    GVol.format.GeoJSON.prototype,
    'readProjection',
    GVol.format.GeoJSON.prototype.readProjection);

goog.exportProperty(
    GVol.format.GeoJSON.prototype,
    'writeFeature',
    GVol.format.GeoJSON.prototype.writeFeature);

goog.exportProperty(
    GVol.format.GeoJSON.prototype,
    'writeFeatureObject',
    GVol.format.GeoJSON.prototype.writeFeatureObject);

goog.exportProperty(
    GVol.format.GeoJSON.prototype,
    'writeFeatures',
    GVol.format.GeoJSON.prototype.writeFeatures);

goog.exportProperty(
    GVol.format.GeoJSON.prototype,
    'writeFeaturesObject',
    GVol.format.GeoJSON.prototype.writeFeaturesObject);

goog.exportProperty(
    GVol.format.GeoJSON.prototype,
    'writeGeometry',
    GVol.format.GeoJSON.prototype.writeGeometry);

goog.exportProperty(
    GVol.format.GeoJSON.prototype,
    'writeGeometryObject',
    GVol.format.GeoJSON.prototype.writeGeometryObject);

goog.exportSymbGVol(
    'GVol.format.GML',
    GVol.format.GML,
    OPENLAYERS);

goog.exportProperty(
    GVol.format.GML.prototype,
    'writeFeatures',
    GVol.format.GML.prototype.writeFeatures);

goog.exportProperty(
    GVol.format.GML.prototype,
    'writeFeaturesNode',
    GVol.format.GML.prototype.writeFeaturesNode);

goog.exportSymbGVol(
    'GVol.format.GML2',
    GVol.format.GML2,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.GML3',
    GVol.format.GML3,
    OPENLAYERS);

goog.exportProperty(
    GVol.format.GML3.prototype,
    'writeGeometryNode',
    GVol.format.GML3.prototype.writeGeometryNode);

goog.exportProperty(
    GVol.format.GML3.prototype,
    'writeFeatures',
    GVol.format.GML3.prototype.writeFeatures);

goog.exportProperty(
    GVol.format.GML3.prototype,
    'writeFeaturesNode',
    GVol.format.GML3.prototype.writeFeaturesNode);

goog.exportProperty(
    GVol.format.GMLBase.prototype,
    'readFeatures',
    GVol.format.GMLBase.prototype.readFeatures);

goog.exportSymbGVol(
    'GVol.format.GPX',
    GVol.format.GPX,
    OPENLAYERS);

goog.exportProperty(
    GVol.format.GPX.prototype,
    'readFeature',
    GVol.format.GPX.prototype.readFeature);

goog.exportProperty(
    GVol.format.GPX.prototype,
    'readFeatures',
    GVol.format.GPX.prototype.readFeatures);

goog.exportProperty(
    GVol.format.GPX.prototype,
    'readProjection',
    GVol.format.GPX.prototype.readProjection);

goog.exportProperty(
    GVol.format.GPX.prototype,
    'writeFeatures',
    GVol.format.GPX.prototype.writeFeatures);

goog.exportProperty(
    GVol.format.GPX.prototype,
    'writeFeaturesNode',
    GVol.format.GPX.prototype.writeFeaturesNode);

goog.exportSymbGVol(
    'GVol.format.IGC',
    GVol.format.IGC,
    OPENLAYERS);

goog.exportProperty(
    GVol.format.IGC.prototype,
    'readFeature',
    GVol.format.IGC.prototype.readFeature);

goog.exportProperty(
    GVol.format.IGC.prototype,
    'readFeatures',
    GVol.format.IGC.prototype.readFeatures);

goog.exportProperty(
    GVol.format.IGC.prototype,
    'readProjection',
    GVol.format.IGC.prototype.readProjection);

goog.exportSymbGVol(
    'GVol.format.KML',
    GVol.format.KML,
    OPENLAYERS);

goog.exportProperty(
    GVol.format.KML.prototype,
    'readFeature',
    GVol.format.KML.prototype.readFeature);

goog.exportProperty(
    GVol.format.KML.prototype,
    'readFeatures',
    GVol.format.KML.prototype.readFeatures);

goog.exportProperty(
    GVol.format.KML.prototype,
    'readName',
    GVol.format.KML.prototype.readName);

goog.exportProperty(
    GVol.format.KML.prototype,
    'readNetworkLinks',
    GVol.format.KML.prototype.readNetworkLinks);

goog.exportProperty(
    GVol.format.KML.prototype,
    'readRegion',
    GVol.format.KML.prototype.readRegion);

goog.exportProperty(
    GVol.format.KML.prototype,
    'readRegionFromNode',
    GVol.format.KML.prototype.readRegionFromNode);

goog.exportProperty(
    GVol.format.KML.prototype,
    'readProjection',
    GVol.format.KML.prototype.readProjection);

goog.exportProperty(
    GVol.format.KML.prototype,
    'writeFeatures',
    GVol.format.KML.prototype.writeFeatures);

goog.exportProperty(
    GVol.format.KML.prototype,
    'writeFeaturesNode',
    GVol.format.KML.prototype.writeFeaturesNode);

goog.exportSymbGVol(
    'GVol.format.MVT',
    GVol.format.MVT,
    OPENLAYERS);

goog.exportProperty(
    GVol.format.MVT.prototype,
    'getLastExtent',
    GVol.format.MVT.prototype.getLastExtent);

goog.exportProperty(
    GVol.format.MVT.prototype,
    'readFeatures',
    GVol.format.MVT.prototype.readFeatures);

goog.exportProperty(
    GVol.format.MVT.prototype,
    'readProjection',
    GVol.format.MVT.prototype.readProjection);

goog.exportProperty(
    GVol.format.MVT.prototype,
    'setLayers',
    GVol.format.MVT.prototype.setLayers);

goog.exportSymbGVol(
    'GVol.format.OSMXML',
    GVol.format.OSMXML,
    OPENLAYERS);

goog.exportProperty(
    GVol.format.OSMXML.prototype,
    'readFeatures',
    GVol.format.OSMXML.prototype.readFeatures);

goog.exportProperty(
    GVol.format.OSMXML.prototype,
    'readProjection',
    GVol.format.OSMXML.prototype.readProjection);

goog.exportSymbGVol(
    'GVol.format.PGVolyline',
    GVol.format.PGVolyline,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.PGVolyline.encodeDeltas',
    GVol.format.PGVolyline.encodeDeltas,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.PGVolyline.decodeDeltas',
    GVol.format.PGVolyline.decodeDeltas,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.PGVolyline.encodeFloats',
    GVol.format.PGVolyline.encodeFloats,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.PGVolyline.decodeFloats',
    GVol.format.PGVolyline.decodeFloats,
    OPENLAYERS);

goog.exportProperty(
    GVol.format.PGVolyline.prototype,
    'readFeature',
    GVol.format.PGVolyline.prototype.readFeature);

goog.exportProperty(
    GVol.format.PGVolyline.prototype,
    'readFeatures',
    GVol.format.PGVolyline.prototype.readFeatures);

goog.exportProperty(
    GVol.format.PGVolyline.prototype,
    'readGeometry',
    GVol.format.PGVolyline.prototype.readGeometry);

goog.exportProperty(
    GVol.format.PGVolyline.prototype,
    'readProjection',
    GVol.format.PGVolyline.prototype.readProjection);

goog.exportProperty(
    GVol.format.PGVolyline.prototype,
    'writeGeometry',
    GVol.format.PGVolyline.prototype.writeGeometry);

goog.exportSymbGVol(
    'GVol.format.TopoJSON',
    GVol.format.TopoJSON,
    OPENLAYERS);

goog.exportProperty(
    GVol.format.TopoJSON.prototype,
    'readFeatures',
    GVol.format.TopoJSON.prototype.readFeatures);

goog.exportProperty(
    GVol.format.TopoJSON.prototype,
    'readProjection',
    GVol.format.TopoJSON.prototype.readProjection);

goog.exportSymbGVol(
    'GVol.format.WFS',
    GVol.format.WFS,
    OPENLAYERS);

goog.exportProperty(
    GVol.format.WFS.prototype,
    'readFeatures',
    GVol.format.WFS.prototype.readFeatures);

goog.exportProperty(
    GVol.format.WFS.prototype,
    'readTransactionResponse',
    GVol.format.WFS.prototype.readTransactionResponse);

goog.exportProperty(
    GVol.format.WFS.prototype,
    'readFeatureCGVollectionMetadata',
    GVol.format.WFS.prototype.readFeatureCGVollectionMetadata);

goog.exportSymbGVol(
    'GVol.format.WFS.writeFilter',
    GVol.format.WFS.writeFilter,
    OPENLAYERS);

goog.exportProperty(
    GVol.format.WFS.prototype,
    'writeGetFeature',
    GVol.format.WFS.prototype.writeGetFeature);

goog.exportProperty(
    GVol.format.WFS.prototype,
    'writeTransaction',
    GVol.format.WFS.prototype.writeTransaction);

goog.exportProperty(
    GVol.format.WFS.prototype,
    'readProjection',
    GVol.format.WFS.prototype.readProjection);

goog.exportSymbGVol(
    'GVol.format.WKT',
    GVol.format.WKT,
    OPENLAYERS);

goog.exportProperty(
    GVol.format.WKT.prototype,
    'readFeature',
    GVol.format.WKT.prototype.readFeature);

goog.exportProperty(
    GVol.format.WKT.prototype,
    'readFeatures',
    GVol.format.WKT.prototype.readFeatures);

goog.exportProperty(
    GVol.format.WKT.prototype,
    'readGeometry',
    GVol.format.WKT.prototype.readGeometry);

goog.exportProperty(
    GVol.format.WKT.prototype,
    'writeFeature',
    GVol.format.WKT.prototype.writeFeature);

goog.exportProperty(
    GVol.format.WKT.prototype,
    'writeFeatures',
    GVol.format.WKT.prototype.writeFeatures);

goog.exportProperty(
    GVol.format.WKT.prototype,
    'writeGeometry',
    GVol.format.WKT.prototype.writeGeometry);

goog.exportSymbGVol(
    'GVol.format.WMSCapabilities',
    GVol.format.WMSCapabilities,
    OPENLAYERS);

goog.exportProperty(
    GVol.format.WMSCapabilities.prototype,
    'read',
    GVol.format.WMSCapabilities.prototype.read);

goog.exportSymbGVol(
    'GVol.format.WMSGetFeatureInfo',
    GVol.format.WMSGetFeatureInfo,
    OPENLAYERS);

goog.exportProperty(
    GVol.format.WMSGetFeatureInfo.prototype,
    'readFeatures',
    GVol.format.WMSGetFeatureInfo.prototype.readFeatures);

goog.exportSymbGVol(
    'GVol.format.WMTSCapabilities',
    GVol.format.WMTSCapabilities,
    OPENLAYERS);

goog.exportProperty(
    GVol.format.WMTSCapabilities.prototype,
    'read',
    GVol.format.WMTSCapabilities.prototype.read);

goog.exportSymbGVol(
    'GVol.format.filter.And',
    GVol.format.filter.And,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.Bbox',
    GVol.format.filter.Bbox,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.Comparison',
    GVol.format.filter.Comparison,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.ComparisonBinary',
    GVol.format.filter.ComparisonBinary,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.During',
    GVol.format.filter.During,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.EqualTo',
    GVol.format.filter.EqualTo,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.Filter',
    GVol.format.filter.Filter,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.GreaterThan',
    GVol.format.filter.GreaterThan,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.GreaterThanOrEqualTo',
    GVol.format.filter.GreaterThanOrEqualTo,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.Intersects',
    GVol.format.filter.Intersects,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.IsBetween',
    GVol.format.filter.IsBetween,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.IsLike',
    GVol.format.filter.IsLike,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.IsNull',
    GVol.format.filter.IsNull,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.LessThan',
    GVol.format.filter.LessThan,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.LessThanOrEqualTo',
    GVol.format.filter.LessThanOrEqualTo,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.Not',
    GVol.format.filter.Not,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.NotEqualTo',
    GVol.format.filter.NotEqualTo,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.Or',
    GVol.format.filter.Or,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.Spatial',
    GVol.format.filter.Spatial,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.format.filter.Within',
    GVol.format.filter.Within,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.events.condition.altKeyOnly',
    GVol.events.condition.altKeyOnly,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.events.condition.altShiftKeysOnly',
    GVol.events.condition.altShiftKeysOnly,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.events.condition.always',
    GVol.events.condition.always,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.events.condition.click',
    GVol.events.condition.click,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.events.condition.never',
    GVol.events.condition.never,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.events.condition.pointerMove',
    GVol.events.condition.pointerMove,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.events.condition.singleClick',
    GVol.events.condition.singleClick,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.events.condition.doubleClick',
    GVol.events.condition.doubleClick,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.events.condition.noModifierKeys',
    GVol.events.condition.noModifierKeys,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.events.condition.platformModifierKeyOnly',
    GVol.events.condition.platformModifierKeyOnly,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.events.condition.shiftKeyOnly',
    GVol.events.condition.shiftKeyOnly,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.events.condition.targetNotEditable',
    GVol.events.condition.targetNotEditable,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.events.condition.mouseOnly',
    GVol.events.condition.mouseOnly,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.events.condition.primaryAction',
    GVol.events.condition.primaryAction,
    OPENLAYERS);

goog.exportProperty(
    GVol.events.Event.prototype,
    'type',
    GVol.events.Event.prototype.type);

goog.exportProperty(
    GVol.events.Event.prototype,
    'target',
    GVol.events.Event.prototype.target);

goog.exportProperty(
    GVol.events.Event.prototype,
    'preventDefault',
    GVol.events.Event.prototype.preventDefault);

goog.exportProperty(
    GVol.events.Event.prototype,
    'stopPropagation',
    GVol.events.Event.prototype.stopPropagation);

goog.exportSymbGVol(
    'GVol.contrGVol.Attribution',
    GVol.contrGVol.Attribution,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.contrGVol.Attribution.render',
    GVol.contrGVol.Attribution.render,
    OPENLAYERS);

goog.exportProperty(
    GVol.contrGVol.Attribution.prototype,
    'getCGVollapsible',
    GVol.contrGVol.Attribution.prototype.getCGVollapsible);

goog.exportProperty(
    GVol.contrGVol.Attribution.prototype,
    'setCGVollapsible',
    GVol.contrGVol.Attribution.prototype.setCGVollapsible);

goog.exportProperty(
    GVol.contrGVol.Attribution.prototype,
    'setCGVollapsed',
    GVol.contrGVol.Attribution.prototype.setCGVollapsed);

goog.exportProperty(
    GVol.contrGVol.Attribution.prototype,
    'getCGVollapsed',
    GVol.contrGVol.Attribution.prototype.getCGVollapsed);

goog.exportSymbGVol(
    'GVol.contrGVol.ContrGVol',
    GVol.contrGVol.ContrGVol,
    OPENLAYERS);

goog.exportProperty(
    GVol.contrGVol.ContrGVol.prototype,
    'getMap',
    GVol.contrGVol.ContrGVol.prototype.getMap);

goog.exportProperty(
    GVol.contrGVol.ContrGVol.prototype,
    'setMap',
    GVol.contrGVol.ContrGVol.prototype.setMap);

goog.exportProperty(
    GVol.contrGVol.ContrGVol.prototype,
    'setTarget',
    GVol.contrGVol.ContrGVol.prototype.setTarget);

goog.exportSymbGVol(
    'GVol.contrGVol.FullScreen',
    GVol.contrGVol.FullScreen,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.contrGVol.MousePosition',
    GVol.contrGVol.MousePosition,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.contrGVol.MousePosition.render',
    GVol.contrGVol.MousePosition.render,
    OPENLAYERS);

goog.exportProperty(
    GVol.contrGVol.MousePosition.prototype,
    'getCoordinateFormat',
    GVol.contrGVol.MousePosition.prototype.getCoordinateFormat);

goog.exportProperty(
    GVol.contrGVol.MousePosition.prototype,
    'getProjection',
    GVol.contrGVol.MousePosition.prototype.getProjection);

goog.exportProperty(
    GVol.contrGVol.MousePosition.prototype,
    'setCoordinateFormat',
    GVol.contrGVol.MousePosition.prototype.setCoordinateFormat);

goog.exportProperty(
    GVol.contrGVol.MousePosition.prototype,
    'setProjection',
    GVol.contrGVol.MousePosition.prototype.setProjection);

goog.exportSymbGVol(
    'GVol.contrGVol.OverviewMap',
    GVol.contrGVol.OverviewMap,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.contrGVol.OverviewMap.render',
    GVol.contrGVol.OverviewMap.render,
    OPENLAYERS);

goog.exportProperty(
    GVol.contrGVol.OverviewMap.prototype,
    'getCGVollapsible',
    GVol.contrGVol.OverviewMap.prototype.getCGVollapsible);

goog.exportProperty(
    GVol.contrGVol.OverviewMap.prototype,
    'setCGVollapsible',
    GVol.contrGVol.OverviewMap.prototype.setCGVollapsible);

goog.exportProperty(
    GVol.contrGVol.OverviewMap.prototype,
    'setCGVollapsed',
    GVol.contrGVol.OverviewMap.prototype.setCGVollapsed);

goog.exportProperty(
    GVol.contrGVol.OverviewMap.prototype,
    'getCGVollapsed',
    GVol.contrGVol.OverviewMap.prototype.getCGVollapsed);

goog.exportProperty(
    GVol.contrGVol.OverviewMap.prototype,
    'getOverviewMap',
    GVol.contrGVol.OverviewMap.prototype.getOverviewMap);

goog.exportSymbGVol(
    'GVol.contrGVol.Rotate',
    GVol.contrGVol.Rotate,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.contrGVol.Rotate.render',
    GVol.contrGVol.Rotate.render,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.contrGVol.ScaleLine',
    GVol.contrGVol.ScaleLine,
    OPENLAYERS);

goog.exportProperty(
    GVol.contrGVol.ScaleLine.prototype,
    'getUnits',
    GVol.contrGVol.ScaleLine.prototype.getUnits);

goog.exportSymbGVol(
    'GVol.contrGVol.ScaleLine.render',
    GVol.contrGVol.ScaleLine.render,
    OPENLAYERS);

goog.exportProperty(
    GVol.contrGVol.ScaleLine.prototype,
    'setUnits',
    GVol.contrGVol.ScaleLine.prototype.setUnits);

goog.exportSymbGVol(
    'GVol.contrGVol.Zoom',
    GVol.contrGVol.Zoom,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.contrGVol.ZoomSlider',
    GVol.contrGVol.ZoomSlider,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.contrGVol.ZoomSlider.render',
    GVol.contrGVol.ZoomSlider.render,
    OPENLAYERS);

goog.exportSymbGVol(
    'GVol.contrGVol.ZoomToExtent',
    GVol.contrGVol.ZoomToExtent,
    OPENLAYERS);

goog.exportProperty(
    GVol.Object.prototype,
    'changed',
    GVol.Object.prototype.changed);

goog.exportProperty(
    GVol.Object.prototype,
    'dispatchEvent',
    GVol.Object.prototype.dispatchEvent);

goog.exportProperty(
    GVol.Object.prototype,
    'getRevision',
    GVol.Object.prototype.getRevision);

goog.exportProperty(
    GVol.Object.prototype,
    'on',
    GVol.Object.prototype.on);

goog.exportProperty(
    GVol.Object.prototype,
    'once',
    GVol.Object.prototype.once);

goog.exportProperty(
    GVol.Object.prototype,
    'un',
    GVol.Object.prototype.un);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'get',
    GVol.CGVollection.prototype.get);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'getKeys',
    GVol.CGVollection.prototype.getKeys);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'getProperties',
    GVol.CGVollection.prototype.getProperties);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'set',
    GVol.CGVollection.prototype.set);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'setProperties',
    GVol.CGVollection.prototype.setProperties);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'unset',
    GVol.CGVollection.prototype.unset);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'changed',
    GVol.CGVollection.prototype.changed);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'dispatchEvent',
    GVol.CGVollection.prototype.dispatchEvent);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'getRevision',
    GVol.CGVollection.prototype.getRevision);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'on',
    GVol.CGVollection.prototype.on);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'once',
    GVol.CGVollection.prototype.once);

goog.exportProperty(
    GVol.CGVollection.prototype,
    'un',
    GVol.CGVollection.prototype.un);

goog.exportProperty(
    GVol.CGVollection.Event.prototype,
    'type',
    GVol.CGVollection.Event.prototype.type);

goog.exportProperty(
    GVol.CGVollection.Event.prototype,
    'target',
    GVol.CGVollection.Event.prototype.target);

goog.exportProperty(
    GVol.CGVollection.Event.prototype,
    'preventDefault',
    GVol.CGVollection.Event.prototype.preventDefault);

goog.exportProperty(
    GVol.CGVollection.Event.prototype,
    'stopPropagation',
    GVol.CGVollection.Event.prototype.stopPropagation);

goog.exportProperty(
    GVol.DeviceOrientation.prototype,
    'get',
    GVol.DeviceOrientation.prototype.get);

goog.exportProperty(
    GVol.DeviceOrientation.prototype,
    'getKeys',
    GVol.DeviceOrientation.prototype.getKeys);

goog.exportProperty(
    GVol.DeviceOrientation.prototype,
    'getProperties',
    GVol.DeviceOrientation.prototype.getProperties);

goog.exportProperty(
    GVol.DeviceOrientation.prototype,
    'set',
    GVol.DeviceOrientation.prototype.set);

goog.exportProperty(
    GVol.DeviceOrientation.prototype,
    'setProperties',
    GVol.DeviceOrientation.prototype.setProperties);

goog.exportProperty(
    GVol.DeviceOrientation.prototype,
    'unset',
    GVol.DeviceOrientation.prototype.unset);

goog.exportProperty(
    GVol.DeviceOrientation.prototype,
    'changed',
    GVol.DeviceOrientation.prototype.changed);

goog.exportProperty(
    GVol.DeviceOrientation.prototype,
    'dispatchEvent',
    GVol.DeviceOrientation.prototype.dispatchEvent);

goog.exportProperty(
    GVol.DeviceOrientation.prototype,
    'getRevision',
    GVol.DeviceOrientation.prototype.getRevision);

goog.exportProperty(
    GVol.DeviceOrientation.prototype,
    'on',
    GVol.DeviceOrientation.prototype.on);

goog.exportProperty(
    GVol.DeviceOrientation.prototype,
    'once',
    GVol.DeviceOrientation.prototype.once);

goog.exportProperty(
    GVol.DeviceOrientation.prototype,
    'un',
    GVol.DeviceOrientation.prototype.un);

goog.exportProperty(
    GVol.Feature.prototype,
    'get',
    GVol.Feature.prototype.get);

goog.exportProperty(
    GVol.Feature.prototype,
    'getKeys',
    GVol.Feature.prototype.getKeys);

goog.exportProperty(
    GVol.Feature.prototype,
    'getProperties',
    GVol.Feature.prototype.getProperties);

goog.exportProperty(
    GVol.Feature.prototype,
    'set',
    GVol.Feature.prototype.set);

goog.exportProperty(
    GVol.Feature.prototype,
    'setProperties',
    GVol.Feature.prototype.setProperties);

goog.exportProperty(
    GVol.Feature.prototype,
    'unset',
    GVol.Feature.prototype.unset);

goog.exportProperty(
    GVol.Feature.prototype,
    'changed',
    GVol.Feature.prototype.changed);

goog.exportProperty(
    GVol.Feature.prototype,
    'dispatchEvent',
    GVol.Feature.prototype.dispatchEvent);

goog.exportProperty(
    GVol.Feature.prototype,
    'getRevision',
    GVol.Feature.prototype.getRevision);

goog.exportProperty(
    GVol.Feature.prototype,
    'on',
    GVol.Feature.prototype.on);

goog.exportProperty(
    GVol.Feature.prototype,
    'once',
    GVol.Feature.prototype.once);

goog.exportProperty(
    GVol.Feature.prototype,
    'un',
    GVol.Feature.prototype.un);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'get',
    GVol.GeGVolocation.prototype.get);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'getKeys',
    GVol.GeGVolocation.prototype.getKeys);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'getProperties',
    GVol.GeGVolocation.prototype.getProperties);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'set',
    GVol.GeGVolocation.prototype.set);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'setProperties',
    GVol.GeGVolocation.prototype.setProperties);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'unset',
    GVol.GeGVolocation.prototype.unset);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'changed',
    GVol.GeGVolocation.prototype.changed);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'dispatchEvent',
    GVol.GeGVolocation.prototype.dispatchEvent);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'getRevision',
    GVol.GeGVolocation.prototype.getRevision);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'on',
    GVol.GeGVolocation.prototype.on);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'once',
    GVol.GeGVolocation.prototype.once);

goog.exportProperty(
    GVol.GeGVolocation.prototype,
    'un',
    GVol.GeGVolocation.prototype.un);

goog.exportProperty(
    GVol.ImageTile.prototype,
    'getTileCoord',
    GVol.ImageTile.prototype.getTileCoord);

goog.exportProperty(
    GVol.ImageTile.prototype,
    'load',
    GVol.ImageTile.prototype.load);

goog.exportProperty(
    GVol.Map.prototype,
    'get',
    GVol.Map.prototype.get);

goog.exportProperty(
    GVol.Map.prototype,
    'getKeys',
    GVol.Map.prototype.getKeys);

goog.exportProperty(
    GVol.Map.prototype,
    'getProperties',
    GVol.Map.prototype.getProperties);

goog.exportProperty(
    GVol.Map.prototype,
    'set',
    GVol.Map.prototype.set);

goog.exportProperty(
    GVol.Map.prototype,
    'setProperties',
    GVol.Map.prototype.setProperties);

goog.exportProperty(
    GVol.Map.prototype,
    'unset',
    GVol.Map.prototype.unset);

goog.exportProperty(
    GVol.Map.prototype,
    'changed',
    GVol.Map.prototype.changed);

goog.exportProperty(
    GVol.Map.prototype,
    'dispatchEvent',
    GVol.Map.prototype.dispatchEvent);

goog.exportProperty(
    GVol.Map.prototype,
    'getRevision',
    GVol.Map.prototype.getRevision);

goog.exportProperty(
    GVol.Map.prototype,
    'on',
    GVol.Map.prototype.on);

goog.exportProperty(
    GVol.Map.prototype,
    'once',
    GVol.Map.prototype.once);

goog.exportProperty(
    GVol.Map.prototype,
    'un',
    GVol.Map.prototype.un);

goog.exportProperty(
    GVol.MapEvent.prototype,
    'type',
    GVol.MapEvent.prototype.type);

goog.exportProperty(
    GVol.MapEvent.prototype,
    'target',
    GVol.MapEvent.prototype.target);

goog.exportProperty(
    GVol.MapEvent.prototype,
    'preventDefault',
    GVol.MapEvent.prototype.preventDefault);

goog.exportProperty(
    GVol.MapEvent.prototype,
    'stopPropagation',
    GVol.MapEvent.prototype.stopPropagation);

goog.exportProperty(
    GVol.MapBrowserEvent.prototype,
    'map',
    GVol.MapBrowserEvent.prototype.map);

goog.exportProperty(
    GVol.MapBrowserEvent.prototype,
    'frameState',
    GVol.MapBrowserEvent.prototype.frameState);

goog.exportProperty(
    GVol.MapBrowserEvent.prototype,
    'type',
    GVol.MapBrowserEvent.prototype.type);

goog.exportProperty(
    GVol.MapBrowserEvent.prototype,
    'target',
    GVol.MapBrowserEvent.prototype.target);

goog.exportProperty(
    GVol.MapBrowserEvent.prototype,
    'preventDefault',
    GVol.MapBrowserEvent.prototype.preventDefault);

goog.exportProperty(
    GVol.MapBrowserEvent.prototype,
    'stopPropagation',
    GVol.MapBrowserEvent.prototype.stopPropagation);

goog.exportProperty(
    GVol.MapBrowserPointerEvent.prototype,
    'originalEvent',
    GVol.MapBrowserPointerEvent.prototype.originalEvent);

goog.exportProperty(
    GVol.MapBrowserPointerEvent.prototype,
    'pixel',
    GVol.MapBrowserPointerEvent.prototype.pixel);

goog.exportProperty(
    GVol.MapBrowserPointerEvent.prototype,
    'coordinate',
    GVol.MapBrowserPointerEvent.prototype.coordinate);

goog.exportProperty(
    GVol.MapBrowserPointerEvent.prototype,
    'dragging',
    GVol.MapBrowserPointerEvent.prototype.dragging);

goog.exportProperty(
    GVol.MapBrowserPointerEvent.prototype,
    'preventDefault',
    GVol.MapBrowserPointerEvent.prototype.preventDefault);

goog.exportProperty(
    GVol.MapBrowserPointerEvent.prototype,
    'stopPropagation',
    GVol.MapBrowserPointerEvent.prototype.stopPropagation);

goog.exportProperty(
    GVol.MapBrowserPointerEvent.prototype,
    'map',
    GVol.MapBrowserPointerEvent.prototype.map);

goog.exportProperty(
    GVol.MapBrowserPointerEvent.prototype,
    'frameState',
    GVol.MapBrowserPointerEvent.prototype.frameState);

goog.exportProperty(
    GVol.MapBrowserPointerEvent.prototype,
    'type',
    GVol.MapBrowserPointerEvent.prototype.type);

goog.exportProperty(
    GVol.MapBrowserPointerEvent.prototype,
    'target',
    GVol.MapBrowserPointerEvent.prototype.target);

goog.exportProperty(
    GVol.Object.Event.prototype,
    'type',
    GVol.Object.Event.prototype.type);

goog.exportProperty(
    GVol.Object.Event.prototype,
    'target',
    GVol.Object.Event.prototype.target);

goog.exportProperty(
    GVol.Object.Event.prototype,
    'preventDefault',
    GVol.Object.Event.prototype.preventDefault);

goog.exportProperty(
    GVol.Object.Event.prototype,
    'stopPropagation',
    GVol.Object.Event.prototype.stopPropagation);

goog.exportProperty(
    GVol.Overlay.prototype,
    'get',
    GVol.Overlay.prototype.get);

goog.exportProperty(
    GVol.Overlay.prototype,
    'getKeys',
    GVol.Overlay.prototype.getKeys);

goog.exportProperty(
    GVol.Overlay.prototype,
    'getProperties',
    GVol.Overlay.prototype.getProperties);

goog.exportProperty(
    GVol.Overlay.prototype,
    'set',
    GVol.Overlay.prototype.set);

goog.exportProperty(
    GVol.Overlay.prototype,
    'setProperties',
    GVol.Overlay.prototype.setProperties);

goog.exportProperty(
    GVol.Overlay.prototype,
    'unset',
    GVol.Overlay.prototype.unset);

goog.exportProperty(
    GVol.Overlay.prototype,
    'changed',
    GVol.Overlay.prototype.changed);

goog.exportProperty(
    GVol.Overlay.prototype,
    'dispatchEvent',
    GVol.Overlay.prototype.dispatchEvent);

goog.exportProperty(
    GVol.Overlay.prototype,
    'getRevision',
    GVol.Overlay.prototype.getRevision);

goog.exportProperty(
    GVol.Overlay.prototype,
    'on',
    GVol.Overlay.prototype.on);

goog.exportProperty(
    GVol.Overlay.prototype,
    'once',
    GVol.Overlay.prototype.once);

goog.exportProperty(
    GVol.Overlay.prototype,
    'un',
    GVol.Overlay.prototype.un);

goog.exportProperty(
    GVol.VectorImageTile.prototype,
    'getTileCoord',
    GVol.VectorImageTile.prototype.getTileCoord);

goog.exportProperty(
    GVol.VectorImageTile.prototype,
    'load',
    GVol.VectorImageTile.prototype.load);

goog.exportProperty(
    GVol.VectorTile.prototype,
    'getTileCoord',
    GVol.VectorTile.prototype.getTileCoord);

goog.exportProperty(
    GVol.VectorTile.prototype,
    'load',
    GVol.VectorTile.prototype.load);

goog.exportProperty(
    GVol.View.prototype,
    'get',
    GVol.View.prototype.get);

goog.exportProperty(
    GVol.View.prototype,
    'getKeys',
    GVol.View.prototype.getKeys);

goog.exportProperty(
    GVol.View.prototype,
    'getProperties',
    GVol.View.prototype.getProperties);

goog.exportProperty(
    GVol.View.prototype,
    'set',
    GVol.View.prototype.set);

goog.exportProperty(
    GVol.View.prototype,
    'setProperties',
    GVol.View.prototype.setProperties);

goog.exportProperty(
    GVol.View.prototype,
    'unset',
    GVol.View.prototype.unset);

goog.exportProperty(
    GVol.View.prototype,
    'changed',
    GVol.View.prototype.changed);

goog.exportProperty(
    GVol.View.prototype,
    'dispatchEvent',
    GVol.View.prototype.dispatchEvent);

goog.exportProperty(
    GVol.View.prototype,
    'getRevision',
    GVol.View.prototype.getRevision);

goog.exportProperty(
    GVol.View.prototype,
    'on',
    GVol.View.prototype.on);

goog.exportProperty(
    GVol.View.prototype,
    'once',
    GVol.View.prototype.once);

goog.exportProperty(
    GVol.View.prototype,
    'un',
    GVol.View.prototype.un);

goog.exportProperty(
    GVol.tilegrid.WMTS.prototype,
    'forEachTileCoord',
    GVol.tilegrid.WMTS.prototype.forEachTileCoord);

goog.exportProperty(
    GVol.tilegrid.WMTS.prototype,
    'getMaxZoom',
    GVol.tilegrid.WMTS.prototype.getMaxZoom);

goog.exportProperty(
    GVol.tilegrid.WMTS.prototype,
    'getMinZoom',
    GVol.tilegrid.WMTS.prototype.getMinZoom);

goog.exportProperty(
    GVol.tilegrid.WMTS.prototype,
    'getOrigin',
    GVol.tilegrid.WMTS.prototype.getOrigin);

goog.exportProperty(
    GVol.tilegrid.WMTS.prototype,
    'getResGVolution',
    GVol.tilegrid.WMTS.prototype.getResGVolution);

goog.exportProperty(
    GVol.tilegrid.WMTS.prototype,
    'getResGVolutions',
    GVol.tilegrid.WMTS.prototype.getResGVolutions);

goog.exportProperty(
    GVol.tilegrid.WMTS.prototype,
    'getTileCoordExtent',
    GVol.tilegrid.WMTS.prototype.getTileCoordExtent);

goog.exportProperty(
    GVol.tilegrid.WMTS.prototype,
    'getTileCoordForCoordAndResGVolution',
    GVol.tilegrid.WMTS.prototype.getTileCoordForCoordAndResGVolution);

goog.exportProperty(
    GVol.tilegrid.WMTS.prototype,
    'getTileCoordForCoordAndZ',
    GVol.tilegrid.WMTS.prototype.getTileCoordForCoordAndZ);

goog.exportProperty(
    GVol.tilegrid.WMTS.prototype,
    'getTileSize',
    GVol.tilegrid.WMTS.prototype.getTileSize);

goog.exportProperty(
    GVol.tilegrid.WMTS.prototype,
    'getZForResGVolution',
    GVol.tilegrid.WMTS.prototype.getZForResGVolution);

goog.exportProperty(
    GVol.style.RegularShape.prototype,
    'getOpacity',
    GVol.style.RegularShape.prototype.getOpacity);

goog.exportProperty(
    GVol.style.RegularShape.prototype,
    'getRotateWithView',
    GVol.style.RegularShape.prototype.getRotateWithView);

goog.exportProperty(
    GVol.style.RegularShape.prototype,
    'getRotation',
    GVol.style.RegularShape.prototype.getRotation);

goog.exportProperty(
    GVol.style.RegularShape.prototype,
    'getScale',
    GVol.style.RegularShape.prototype.getScale);

goog.exportProperty(
    GVol.style.RegularShape.prototype,
    'getSnapToPixel',
    GVol.style.RegularShape.prototype.getSnapToPixel);

goog.exportProperty(
    GVol.style.RegularShape.prototype,
    'setOpacity',
    GVol.style.RegularShape.prototype.setOpacity);

goog.exportProperty(
    GVol.style.RegularShape.prototype,
    'setRotation',
    GVol.style.RegularShape.prototype.setRotation);

goog.exportProperty(
    GVol.style.RegularShape.prototype,
    'setScale',
    GVol.style.RegularShape.prototype.setScale);

goog.exportProperty(
    GVol.style.Circle.prototype,
    'clone',
    GVol.style.Circle.prototype.clone);

goog.exportProperty(
    GVol.style.Circle.prototype,
    'getAngle',
    GVol.style.Circle.prototype.getAngle);

goog.exportProperty(
    GVol.style.Circle.prototype,
    'getFill',
    GVol.style.Circle.prototype.getFill);

goog.exportProperty(
    GVol.style.Circle.prototype,
    'getPoints',
    GVol.style.Circle.prototype.getPoints);

goog.exportProperty(
    GVol.style.Circle.prototype,
    'getRadius',
    GVol.style.Circle.prototype.getRadius);

goog.exportProperty(
    GVol.style.Circle.prototype,
    'getRadius2',
    GVol.style.Circle.prototype.getRadius2);

goog.exportProperty(
    GVol.style.Circle.prototype,
    'getStroke',
    GVol.style.Circle.prototype.getStroke);

goog.exportProperty(
    GVol.style.Circle.prototype,
    'getOpacity',
    GVol.style.Circle.prototype.getOpacity);

goog.exportProperty(
    GVol.style.Circle.prototype,
    'getRotateWithView',
    GVol.style.Circle.prototype.getRotateWithView);

goog.exportProperty(
    GVol.style.Circle.prototype,
    'getRotation',
    GVol.style.Circle.prototype.getRotation);

goog.exportProperty(
    GVol.style.Circle.prototype,
    'getScale',
    GVol.style.Circle.prototype.getScale);

goog.exportProperty(
    GVol.style.Circle.prototype,
    'getSnapToPixel',
    GVol.style.Circle.prototype.getSnapToPixel);

goog.exportProperty(
    GVol.style.Circle.prototype,
    'setOpacity',
    GVol.style.Circle.prototype.setOpacity);

goog.exportProperty(
    GVol.style.Circle.prototype,
    'setRotation',
    GVol.style.Circle.prototype.setRotation);

goog.exportProperty(
    GVol.style.Circle.prototype,
    'setScale',
    GVol.style.Circle.prototype.setScale);

goog.exportProperty(
    GVol.style.Icon.prototype,
    'getOpacity',
    GVol.style.Icon.prototype.getOpacity);

goog.exportProperty(
    GVol.style.Icon.prototype,
    'getRotateWithView',
    GVol.style.Icon.prototype.getRotateWithView);

goog.exportProperty(
    GVol.style.Icon.prototype,
    'getRotation',
    GVol.style.Icon.prototype.getRotation);

goog.exportProperty(
    GVol.style.Icon.prototype,
    'getScale',
    GVol.style.Icon.prototype.getScale);

goog.exportProperty(
    GVol.style.Icon.prototype,
    'getSnapToPixel',
    GVol.style.Icon.prototype.getSnapToPixel);

goog.exportProperty(
    GVol.style.Icon.prototype,
    'setOpacity',
    GVol.style.Icon.prototype.setOpacity);

goog.exportProperty(
    GVol.style.Icon.prototype,
    'setRotation',
    GVol.style.Icon.prototype.setRotation);

goog.exportProperty(
    GVol.style.Icon.prototype,
    'setScale',
    GVol.style.Icon.prototype.setScale);

goog.exportProperty(
    GVol.source.Source.prototype,
    'get',
    GVol.source.Source.prototype.get);

goog.exportProperty(
    GVol.source.Source.prototype,
    'getKeys',
    GVol.source.Source.prototype.getKeys);

goog.exportProperty(
    GVol.source.Source.prototype,
    'getProperties',
    GVol.source.Source.prototype.getProperties);

goog.exportProperty(
    GVol.source.Source.prototype,
    'set',
    GVol.source.Source.prototype.set);

goog.exportProperty(
    GVol.source.Source.prototype,
    'setProperties',
    GVol.source.Source.prototype.setProperties);

goog.exportProperty(
    GVol.source.Source.prototype,
    'unset',
    GVol.source.Source.prototype.unset);

goog.exportProperty(
    GVol.source.Source.prototype,
    'changed',
    GVol.source.Source.prototype.changed);

goog.exportProperty(
    GVol.source.Source.prototype,
    'dispatchEvent',
    GVol.source.Source.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.Source.prototype,
    'getRevision',
    GVol.source.Source.prototype.getRevision);

goog.exportProperty(
    GVol.source.Source.prototype,
    'on',
    GVol.source.Source.prototype.on);

goog.exportProperty(
    GVol.source.Source.prototype,
    'once',
    GVol.source.Source.prototype.once);

goog.exportProperty(
    GVol.source.Source.prototype,
    'un',
    GVol.source.Source.prototype.un);

goog.exportProperty(
    GVol.source.Tile.prototype,
    'getAttributions',
    GVol.source.Tile.prototype.getAttributions);

goog.exportProperty(
    GVol.source.Tile.prototype,
    'getLogo',
    GVol.source.Tile.prototype.getLogo);

goog.exportProperty(
    GVol.source.Tile.prototype,
    'getProjection',
    GVol.source.Tile.prototype.getProjection);

goog.exportProperty(
    GVol.source.Tile.prototype,
    'getState',
    GVol.source.Tile.prototype.getState);

goog.exportProperty(
    GVol.source.Tile.prototype,
    'refresh',
    GVol.source.Tile.prototype.refresh);

goog.exportProperty(
    GVol.source.Tile.prototype,
    'setAttributions',
    GVol.source.Tile.prototype.setAttributions);

goog.exportProperty(
    GVol.source.Tile.prototype,
    'get',
    GVol.source.Tile.prototype.get);

goog.exportProperty(
    GVol.source.Tile.prototype,
    'getKeys',
    GVol.source.Tile.prototype.getKeys);

goog.exportProperty(
    GVol.source.Tile.prototype,
    'getProperties',
    GVol.source.Tile.prototype.getProperties);

goog.exportProperty(
    GVol.source.Tile.prototype,
    'set',
    GVol.source.Tile.prototype.set);

goog.exportProperty(
    GVol.source.Tile.prototype,
    'setProperties',
    GVol.source.Tile.prototype.setProperties);

goog.exportProperty(
    GVol.source.Tile.prototype,
    'unset',
    GVol.source.Tile.prototype.unset);

goog.exportProperty(
    GVol.source.Tile.prototype,
    'changed',
    GVol.source.Tile.prototype.changed);

goog.exportProperty(
    GVol.source.Tile.prototype,
    'dispatchEvent',
    GVol.source.Tile.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.Tile.prototype,
    'getRevision',
    GVol.source.Tile.prototype.getRevision);

goog.exportProperty(
    GVol.source.Tile.prototype,
    'on',
    GVol.source.Tile.prototype.on);

goog.exportProperty(
    GVol.source.Tile.prototype,
    'once',
    GVol.source.Tile.prototype.once);

goog.exportProperty(
    GVol.source.Tile.prototype,
    'un',
    GVol.source.Tile.prototype.un);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'getTileGrid',
    GVol.source.UrlTile.prototype.getTileGrid);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'refresh',
    GVol.source.UrlTile.prototype.refresh);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'getAttributions',
    GVol.source.UrlTile.prototype.getAttributions);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'getLogo',
    GVol.source.UrlTile.prototype.getLogo);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'getProjection',
    GVol.source.UrlTile.prototype.getProjection);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'getState',
    GVol.source.UrlTile.prototype.getState);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'setAttributions',
    GVol.source.UrlTile.prototype.setAttributions);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'get',
    GVol.source.UrlTile.prototype.get);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'getKeys',
    GVol.source.UrlTile.prototype.getKeys);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'getProperties',
    GVol.source.UrlTile.prototype.getProperties);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'set',
    GVol.source.UrlTile.prototype.set);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'setProperties',
    GVol.source.UrlTile.prototype.setProperties);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'unset',
    GVol.source.UrlTile.prototype.unset);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'changed',
    GVol.source.UrlTile.prototype.changed);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'dispatchEvent',
    GVol.source.UrlTile.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'getRevision',
    GVol.source.UrlTile.prototype.getRevision);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'on',
    GVol.source.UrlTile.prototype.on);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'once',
    GVol.source.UrlTile.prototype.once);

goog.exportProperty(
    GVol.source.UrlTile.prototype,
    'un',
    GVol.source.UrlTile.prototype.un);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'getTileLoadFunction',
    GVol.source.TileImage.prototype.getTileLoadFunction);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'getTileUrlFunction',
    GVol.source.TileImage.prototype.getTileUrlFunction);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'getUrls',
    GVol.source.TileImage.prototype.getUrls);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'setTileLoadFunction',
    GVol.source.TileImage.prototype.setTileLoadFunction);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'setTileUrlFunction',
    GVol.source.TileImage.prototype.setTileUrlFunction);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'setUrl',
    GVol.source.TileImage.prototype.setUrl);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'setUrls',
    GVol.source.TileImage.prototype.setUrls);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'getTileGrid',
    GVol.source.TileImage.prototype.getTileGrid);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'refresh',
    GVol.source.TileImage.prototype.refresh);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'getAttributions',
    GVol.source.TileImage.prototype.getAttributions);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'getLogo',
    GVol.source.TileImage.prototype.getLogo);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'getProjection',
    GVol.source.TileImage.prototype.getProjection);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'getState',
    GVol.source.TileImage.prototype.getState);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'setAttributions',
    GVol.source.TileImage.prototype.setAttributions);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'get',
    GVol.source.TileImage.prototype.get);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'getKeys',
    GVol.source.TileImage.prototype.getKeys);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'getProperties',
    GVol.source.TileImage.prototype.getProperties);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'set',
    GVol.source.TileImage.prototype.set);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'setProperties',
    GVol.source.TileImage.prototype.setProperties);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'unset',
    GVol.source.TileImage.prototype.unset);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'changed',
    GVol.source.TileImage.prototype.changed);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'dispatchEvent',
    GVol.source.TileImage.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'getRevision',
    GVol.source.TileImage.prototype.getRevision);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'on',
    GVol.source.TileImage.prototype.on);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'once',
    GVol.source.TileImage.prototype.once);

goog.exportProperty(
    GVol.source.TileImage.prototype,
    'un',
    GVol.source.TileImage.prototype.un);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'setRenderReprojectionEdges',
    GVol.source.BingMaps.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'setTileGridForProjection',
    GVol.source.BingMaps.prototype.setTileGridForProjection);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'getTileLoadFunction',
    GVol.source.BingMaps.prototype.getTileLoadFunction);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'getTileUrlFunction',
    GVol.source.BingMaps.prototype.getTileUrlFunction);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'getUrls',
    GVol.source.BingMaps.prototype.getUrls);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'setTileLoadFunction',
    GVol.source.BingMaps.prototype.setTileLoadFunction);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'setTileUrlFunction',
    GVol.source.BingMaps.prototype.setTileUrlFunction);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'setUrl',
    GVol.source.BingMaps.prototype.setUrl);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'setUrls',
    GVol.source.BingMaps.prototype.setUrls);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'getTileGrid',
    GVol.source.BingMaps.prototype.getTileGrid);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'refresh',
    GVol.source.BingMaps.prototype.refresh);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'getAttributions',
    GVol.source.BingMaps.prototype.getAttributions);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'getLogo',
    GVol.source.BingMaps.prototype.getLogo);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'getProjection',
    GVol.source.BingMaps.prototype.getProjection);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'getState',
    GVol.source.BingMaps.prototype.getState);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'setAttributions',
    GVol.source.BingMaps.prototype.setAttributions);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'get',
    GVol.source.BingMaps.prototype.get);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'getKeys',
    GVol.source.BingMaps.prototype.getKeys);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'getProperties',
    GVol.source.BingMaps.prototype.getProperties);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'set',
    GVol.source.BingMaps.prototype.set);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'setProperties',
    GVol.source.BingMaps.prototype.setProperties);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'unset',
    GVol.source.BingMaps.prototype.unset);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'changed',
    GVol.source.BingMaps.prototype.changed);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'dispatchEvent',
    GVol.source.BingMaps.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'getRevision',
    GVol.source.BingMaps.prototype.getRevision);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'on',
    GVol.source.BingMaps.prototype.on);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'once',
    GVol.source.BingMaps.prototype.once);

goog.exportProperty(
    GVol.source.BingMaps.prototype,
    'un',
    GVol.source.BingMaps.prototype.un);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'setRenderReprojectionEdges',
    GVol.source.XYZ.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'setTileGridForProjection',
    GVol.source.XYZ.prototype.setTileGridForProjection);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'getTileLoadFunction',
    GVol.source.XYZ.prototype.getTileLoadFunction);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'getTileUrlFunction',
    GVol.source.XYZ.prototype.getTileUrlFunction);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'getUrls',
    GVol.source.XYZ.prototype.getUrls);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'setTileLoadFunction',
    GVol.source.XYZ.prototype.setTileLoadFunction);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'setTileUrlFunction',
    GVol.source.XYZ.prototype.setTileUrlFunction);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'setUrl',
    GVol.source.XYZ.prototype.setUrl);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'setUrls',
    GVol.source.XYZ.prototype.setUrls);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'getTileGrid',
    GVol.source.XYZ.prototype.getTileGrid);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'refresh',
    GVol.source.XYZ.prototype.refresh);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'getAttributions',
    GVol.source.XYZ.prototype.getAttributions);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'getLogo',
    GVol.source.XYZ.prototype.getLogo);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'getProjection',
    GVol.source.XYZ.prototype.getProjection);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'getState',
    GVol.source.XYZ.prototype.getState);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'setAttributions',
    GVol.source.XYZ.prototype.setAttributions);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'get',
    GVol.source.XYZ.prototype.get);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'getKeys',
    GVol.source.XYZ.prototype.getKeys);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'getProperties',
    GVol.source.XYZ.prototype.getProperties);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'set',
    GVol.source.XYZ.prototype.set);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'setProperties',
    GVol.source.XYZ.prototype.setProperties);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'unset',
    GVol.source.XYZ.prototype.unset);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'changed',
    GVol.source.XYZ.prototype.changed);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'dispatchEvent',
    GVol.source.XYZ.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'getRevision',
    GVol.source.XYZ.prototype.getRevision);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'on',
    GVol.source.XYZ.prototype.on);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'once',
    GVol.source.XYZ.prototype.once);

goog.exportProperty(
    GVol.source.XYZ.prototype,
    'un',
    GVol.source.XYZ.prototype.un);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'setRenderReprojectionEdges',
    GVol.source.CartoDB.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'setTileGridForProjection',
    GVol.source.CartoDB.prototype.setTileGridForProjection);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'getTileLoadFunction',
    GVol.source.CartoDB.prototype.getTileLoadFunction);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'getTileUrlFunction',
    GVol.source.CartoDB.prototype.getTileUrlFunction);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'getUrls',
    GVol.source.CartoDB.prototype.getUrls);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'setTileLoadFunction',
    GVol.source.CartoDB.prototype.setTileLoadFunction);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'setTileUrlFunction',
    GVol.source.CartoDB.prototype.setTileUrlFunction);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'setUrl',
    GVol.source.CartoDB.prototype.setUrl);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'setUrls',
    GVol.source.CartoDB.prototype.setUrls);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'getTileGrid',
    GVol.source.CartoDB.prototype.getTileGrid);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'refresh',
    GVol.source.CartoDB.prototype.refresh);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'getAttributions',
    GVol.source.CartoDB.prototype.getAttributions);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'getLogo',
    GVol.source.CartoDB.prototype.getLogo);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'getProjection',
    GVol.source.CartoDB.prototype.getProjection);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'getState',
    GVol.source.CartoDB.prototype.getState);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'setAttributions',
    GVol.source.CartoDB.prototype.setAttributions);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'get',
    GVol.source.CartoDB.prototype.get);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'getKeys',
    GVol.source.CartoDB.prototype.getKeys);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'getProperties',
    GVol.source.CartoDB.prototype.getProperties);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'set',
    GVol.source.CartoDB.prototype.set);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'setProperties',
    GVol.source.CartoDB.prototype.setProperties);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'unset',
    GVol.source.CartoDB.prototype.unset);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'changed',
    GVol.source.CartoDB.prototype.changed);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'dispatchEvent',
    GVol.source.CartoDB.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'getRevision',
    GVol.source.CartoDB.prototype.getRevision);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'on',
    GVol.source.CartoDB.prototype.on);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'once',
    GVol.source.CartoDB.prototype.once);

goog.exportProperty(
    GVol.source.CartoDB.prototype,
    'un',
    GVol.source.CartoDB.prototype.un);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'getAttributions',
    GVol.source.Vector.prototype.getAttributions);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'getLogo',
    GVol.source.Vector.prototype.getLogo);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'getProjection',
    GVol.source.Vector.prototype.getProjection);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'getState',
    GVol.source.Vector.prototype.getState);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'refresh',
    GVol.source.Vector.prototype.refresh);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'setAttributions',
    GVol.source.Vector.prototype.setAttributions);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'get',
    GVol.source.Vector.prototype.get);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'getKeys',
    GVol.source.Vector.prototype.getKeys);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'getProperties',
    GVol.source.Vector.prototype.getProperties);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'set',
    GVol.source.Vector.prototype.set);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'setProperties',
    GVol.source.Vector.prototype.setProperties);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'unset',
    GVol.source.Vector.prototype.unset);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'changed',
    GVol.source.Vector.prototype.changed);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'dispatchEvent',
    GVol.source.Vector.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'getRevision',
    GVol.source.Vector.prototype.getRevision);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'on',
    GVol.source.Vector.prototype.on);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'once',
    GVol.source.Vector.prototype.once);

goog.exportProperty(
    GVol.source.Vector.prototype,
    'un',
    GVol.source.Vector.prototype.un);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'addFeature',
    GVol.source.Cluster.prototype.addFeature);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'addFeatures',
    GVol.source.Cluster.prototype.addFeatures);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'clear',
    GVol.source.Cluster.prototype.clear);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'forEachFeature',
    GVol.source.Cluster.prototype.forEachFeature);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'forEachFeatureInExtent',
    GVol.source.Cluster.prototype.forEachFeatureInExtent);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'forEachFeatureIntersectingExtent',
    GVol.source.Cluster.prototype.forEachFeatureIntersectingExtent);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'getFeaturesCGVollection',
    GVol.source.Cluster.prototype.getFeaturesCGVollection);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'getFeatures',
    GVol.source.Cluster.prototype.getFeatures);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'getFeaturesAtCoordinate',
    GVol.source.Cluster.prototype.getFeaturesAtCoordinate);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'getFeaturesInExtent',
    GVol.source.Cluster.prototype.getFeaturesInExtent);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'getClosestFeatureToCoordinate',
    GVol.source.Cluster.prototype.getClosestFeatureToCoordinate);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'getExtent',
    GVol.source.Cluster.prototype.getExtent);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'getFeatureById',
    GVol.source.Cluster.prototype.getFeatureById);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'getFormat',
    GVol.source.Cluster.prototype.getFormat);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'getUrl',
    GVol.source.Cluster.prototype.getUrl);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'removeFeature',
    GVol.source.Cluster.prototype.removeFeature);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'getAttributions',
    GVol.source.Cluster.prototype.getAttributions);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'getLogo',
    GVol.source.Cluster.prototype.getLogo);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'getProjection',
    GVol.source.Cluster.prototype.getProjection);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'getState',
    GVol.source.Cluster.prototype.getState);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'refresh',
    GVol.source.Cluster.prototype.refresh);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'setAttributions',
    GVol.source.Cluster.prototype.setAttributions);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'get',
    GVol.source.Cluster.prototype.get);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'getKeys',
    GVol.source.Cluster.prototype.getKeys);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'getProperties',
    GVol.source.Cluster.prototype.getProperties);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'set',
    GVol.source.Cluster.prototype.set);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'setProperties',
    GVol.source.Cluster.prototype.setProperties);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'unset',
    GVol.source.Cluster.prototype.unset);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'changed',
    GVol.source.Cluster.prototype.changed);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'dispatchEvent',
    GVol.source.Cluster.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'getRevision',
    GVol.source.Cluster.prototype.getRevision);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'on',
    GVol.source.Cluster.prototype.on);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'once',
    GVol.source.Cluster.prototype.once);

goog.exportProperty(
    GVol.source.Cluster.prototype,
    'un',
    GVol.source.Cluster.prototype.un);

goog.exportProperty(
    GVol.source.Image.prototype,
    'getAttributions',
    GVol.source.Image.prototype.getAttributions);

goog.exportProperty(
    GVol.source.Image.prototype,
    'getLogo',
    GVol.source.Image.prototype.getLogo);

goog.exportProperty(
    GVol.source.Image.prototype,
    'getProjection',
    GVol.source.Image.prototype.getProjection);

goog.exportProperty(
    GVol.source.Image.prototype,
    'getState',
    GVol.source.Image.prototype.getState);

goog.exportProperty(
    GVol.source.Image.prototype,
    'refresh',
    GVol.source.Image.prototype.refresh);

goog.exportProperty(
    GVol.source.Image.prototype,
    'setAttributions',
    GVol.source.Image.prototype.setAttributions);

goog.exportProperty(
    GVol.source.Image.prototype,
    'get',
    GVol.source.Image.prototype.get);

goog.exportProperty(
    GVol.source.Image.prototype,
    'getKeys',
    GVol.source.Image.prototype.getKeys);

goog.exportProperty(
    GVol.source.Image.prototype,
    'getProperties',
    GVol.source.Image.prototype.getProperties);

goog.exportProperty(
    GVol.source.Image.prototype,
    'set',
    GVol.source.Image.prototype.set);

goog.exportProperty(
    GVol.source.Image.prototype,
    'setProperties',
    GVol.source.Image.prototype.setProperties);

goog.exportProperty(
    GVol.source.Image.prototype,
    'unset',
    GVol.source.Image.prototype.unset);

goog.exportProperty(
    GVol.source.Image.prototype,
    'changed',
    GVol.source.Image.prototype.changed);

goog.exportProperty(
    GVol.source.Image.prototype,
    'dispatchEvent',
    GVol.source.Image.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.Image.prototype,
    'getRevision',
    GVol.source.Image.prototype.getRevision);

goog.exportProperty(
    GVol.source.Image.prototype,
    'on',
    GVol.source.Image.prototype.on);

goog.exportProperty(
    GVol.source.Image.prototype,
    'once',
    GVol.source.Image.prototype.once);

goog.exportProperty(
    GVol.source.Image.prototype,
    'un',
    GVol.source.Image.prototype.un);

goog.exportProperty(
    GVol.source.Image.Event.prototype,
    'type',
    GVol.source.Image.Event.prototype.type);

goog.exportProperty(
    GVol.source.Image.Event.prototype,
    'target',
    GVol.source.Image.Event.prototype.target);

goog.exportProperty(
    GVol.source.Image.Event.prototype,
    'preventDefault',
    GVol.source.Image.Event.prototype.preventDefault);

goog.exportProperty(
    GVol.source.Image.Event.prototype,
    'stopPropagation',
    GVol.source.Image.Event.prototype.stopPropagation);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'getAttributions',
    GVol.source.ImageArcGISRest.prototype.getAttributions);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'getLogo',
    GVol.source.ImageArcGISRest.prototype.getLogo);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'getProjection',
    GVol.source.ImageArcGISRest.prototype.getProjection);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'getState',
    GVol.source.ImageArcGISRest.prototype.getState);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'refresh',
    GVol.source.ImageArcGISRest.prototype.refresh);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'setAttributions',
    GVol.source.ImageArcGISRest.prototype.setAttributions);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'get',
    GVol.source.ImageArcGISRest.prototype.get);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'getKeys',
    GVol.source.ImageArcGISRest.prototype.getKeys);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'getProperties',
    GVol.source.ImageArcGISRest.prototype.getProperties);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'set',
    GVol.source.ImageArcGISRest.prototype.set);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'setProperties',
    GVol.source.ImageArcGISRest.prototype.setProperties);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'unset',
    GVol.source.ImageArcGISRest.prototype.unset);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'changed',
    GVol.source.ImageArcGISRest.prototype.changed);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'dispatchEvent',
    GVol.source.ImageArcGISRest.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'getRevision',
    GVol.source.ImageArcGISRest.prototype.getRevision);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'on',
    GVol.source.ImageArcGISRest.prototype.on);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'once',
    GVol.source.ImageArcGISRest.prototype.once);

goog.exportProperty(
    GVol.source.ImageArcGISRest.prototype,
    'un',
    GVol.source.ImageArcGISRest.prototype.un);

goog.exportProperty(
    GVol.source.ImageCanvas.prototype,
    'getAttributions',
    GVol.source.ImageCanvas.prototype.getAttributions);

goog.exportProperty(
    GVol.source.ImageCanvas.prototype,
    'getLogo',
    GVol.source.ImageCanvas.prototype.getLogo);

goog.exportProperty(
    GVol.source.ImageCanvas.prototype,
    'getProjection',
    GVol.source.ImageCanvas.prototype.getProjection);

goog.exportProperty(
    GVol.source.ImageCanvas.prototype,
    'getState',
    GVol.source.ImageCanvas.prototype.getState);

goog.exportProperty(
    GVol.source.ImageCanvas.prototype,
    'refresh',
    GVol.source.ImageCanvas.prototype.refresh);

goog.exportProperty(
    GVol.source.ImageCanvas.prototype,
    'setAttributions',
    GVol.source.ImageCanvas.prototype.setAttributions);

goog.exportProperty(
    GVol.source.ImageCanvas.prototype,
    'get',
    GVol.source.ImageCanvas.prototype.get);

goog.exportProperty(
    GVol.source.ImageCanvas.prototype,
    'getKeys',
    GVol.source.ImageCanvas.prototype.getKeys);

goog.exportProperty(
    GVol.source.ImageCanvas.prototype,
    'getProperties',
    GVol.source.ImageCanvas.prototype.getProperties);

goog.exportProperty(
    GVol.source.ImageCanvas.prototype,
    'set',
    GVol.source.ImageCanvas.prototype.set);

goog.exportProperty(
    GVol.source.ImageCanvas.prototype,
    'setProperties',
    GVol.source.ImageCanvas.prototype.setProperties);

goog.exportProperty(
    GVol.source.ImageCanvas.prototype,
    'unset',
    GVol.source.ImageCanvas.prototype.unset);

goog.exportProperty(
    GVol.source.ImageCanvas.prototype,
    'changed',
    GVol.source.ImageCanvas.prototype.changed);

goog.exportProperty(
    GVol.source.ImageCanvas.prototype,
    'dispatchEvent',
    GVol.source.ImageCanvas.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.ImageCanvas.prototype,
    'getRevision',
    GVol.source.ImageCanvas.prototype.getRevision);

goog.exportProperty(
    GVol.source.ImageCanvas.prototype,
    'on',
    GVol.source.ImageCanvas.prototype.on);

goog.exportProperty(
    GVol.source.ImageCanvas.prototype,
    'once',
    GVol.source.ImageCanvas.prototype.once);

goog.exportProperty(
    GVol.source.ImageCanvas.prototype,
    'un',
    GVol.source.ImageCanvas.prototype.un);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'getAttributions',
    GVol.source.ImageMapGuide.prototype.getAttributions);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'getLogo',
    GVol.source.ImageMapGuide.prototype.getLogo);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'getProjection',
    GVol.source.ImageMapGuide.prototype.getProjection);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'getState',
    GVol.source.ImageMapGuide.prototype.getState);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'refresh',
    GVol.source.ImageMapGuide.prototype.refresh);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'setAttributions',
    GVol.source.ImageMapGuide.prototype.setAttributions);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'get',
    GVol.source.ImageMapGuide.prototype.get);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'getKeys',
    GVol.source.ImageMapGuide.prototype.getKeys);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'getProperties',
    GVol.source.ImageMapGuide.prototype.getProperties);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'set',
    GVol.source.ImageMapGuide.prototype.set);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'setProperties',
    GVol.source.ImageMapGuide.prototype.setProperties);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'unset',
    GVol.source.ImageMapGuide.prototype.unset);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'changed',
    GVol.source.ImageMapGuide.prototype.changed);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'dispatchEvent',
    GVol.source.ImageMapGuide.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'getRevision',
    GVol.source.ImageMapGuide.prototype.getRevision);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'on',
    GVol.source.ImageMapGuide.prototype.on);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'once',
    GVol.source.ImageMapGuide.prototype.once);

goog.exportProperty(
    GVol.source.ImageMapGuide.prototype,
    'un',
    GVol.source.ImageMapGuide.prototype.un);

goog.exportProperty(
    GVol.source.ImageStatic.prototype,
    'getAttributions',
    GVol.source.ImageStatic.prototype.getAttributions);

goog.exportProperty(
    GVol.source.ImageStatic.prototype,
    'getLogo',
    GVol.source.ImageStatic.prototype.getLogo);

goog.exportProperty(
    GVol.source.ImageStatic.prototype,
    'getProjection',
    GVol.source.ImageStatic.prototype.getProjection);

goog.exportProperty(
    GVol.source.ImageStatic.prototype,
    'getState',
    GVol.source.ImageStatic.prototype.getState);

goog.exportProperty(
    GVol.source.ImageStatic.prototype,
    'refresh',
    GVol.source.ImageStatic.prototype.refresh);

goog.exportProperty(
    GVol.source.ImageStatic.prototype,
    'setAttributions',
    GVol.source.ImageStatic.prototype.setAttributions);

goog.exportProperty(
    GVol.source.ImageStatic.prototype,
    'get',
    GVol.source.ImageStatic.prototype.get);

goog.exportProperty(
    GVol.source.ImageStatic.prototype,
    'getKeys',
    GVol.source.ImageStatic.prototype.getKeys);

goog.exportProperty(
    GVol.source.ImageStatic.prototype,
    'getProperties',
    GVol.source.ImageStatic.prototype.getProperties);

goog.exportProperty(
    GVol.source.ImageStatic.prototype,
    'set',
    GVol.source.ImageStatic.prototype.set);

goog.exportProperty(
    GVol.source.ImageStatic.prototype,
    'setProperties',
    GVol.source.ImageStatic.prototype.setProperties);

goog.exportProperty(
    GVol.source.ImageStatic.prototype,
    'unset',
    GVol.source.ImageStatic.prototype.unset);

goog.exportProperty(
    GVol.source.ImageStatic.prototype,
    'changed',
    GVol.source.ImageStatic.prototype.changed);

goog.exportProperty(
    GVol.source.ImageStatic.prototype,
    'dispatchEvent',
    GVol.source.ImageStatic.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.ImageStatic.prototype,
    'getRevision',
    GVol.source.ImageStatic.prototype.getRevision);

goog.exportProperty(
    GVol.source.ImageStatic.prototype,
    'on',
    GVol.source.ImageStatic.prototype.on);

goog.exportProperty(
    GVol.source.ImageStatic.prototype,
    'once',
    GVol.source.ImageStatic.prototype.once);

goog.exportProperty(
    GVol.source.ImageStatic.prototype,
    'un',
    GVol.source.ImageStatic.prototype.un);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'getAttributions',
    GVol.source.ImageVector.prototype.getAttributions);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'getLogo',
    GVol.source.ImageVector.prototype.getLogo);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'getProjection',
    GVol.source.ImageVector.prototype.getProjection);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'getState',
    GVol.source.ImageVector.prototype.getState);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'refresh',
    GVol.source.ImageVector.prototype.refresh);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'setAttributions',
    GVol.source.ImageVector.prototype.setAttributions);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'get',
    GVol.source.ImageVector.prototype.get);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'getKeys',
    GVol.source.ImageVector.prototype.getKeys);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'getProperties',
    GVol.source.ImageVector.prototype.getProperties);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'set',
    GVol.source.ImageVector.prototype.set);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'setProperties',
    GVol.source.ImageVector.prototype.setProperties);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'unset',
    GVol.source.ImageVector.prototype.unset);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'changed',
    GVol.source.ImageVector.prototype.changed);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'dispatchEvent',
    GVol.source.ImageVector.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'getRevision',
    GVol.source.ImageVector.prototype.getRevision);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'on',
    GVol.source.ImageVector.prototype.on);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'once',
    GVol.source.ImageVector.prototype.once);

goog.exportProperty(
    GVol.source.ImageVector.prototype,
    'un',
    GVol.source.ImageVector.prototype.un);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'getAttributions',
    GVol.source.ImageWMS.prototype.getAttributions);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'getLogo',
    GVol.source.ImageWMS.prototype.getLogo);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'getProjection',
    GVol.source.ImageWMS.prototype.getProjection);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'getState',
    GVol.source.ImageWMS.prototype.getState);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'refresh',
    GVol.source.ImageWMS.prototype.refresh);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'setAttributions',
    GVol.source.ImageWMS.prototype.setAttributions);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'get',
    GVol.source.ImageWMS.prototype.get);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'getKeys',
    GVol.source.ImageWMS.prototype.getKeys);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'getProperties',
    GVol.source.ImageWMS.prototype.getProperties);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'set',
    GVol.source.ImageWMS.prototype.set);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'setProperties',
    GVol.source.ImageWMS.prototype.setProperties);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'unset',
    GVol.source.ImageWMS.prototype.unset);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'changed',
    GVol.source.ImageWMS.prototype.changed);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'dispatchEvent',
    GVol.source.ImageWMS.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'getRevision',
    GVol.source.ImageWMS.prototype.getRevision);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'on',
    GVol.source.ImageWMS.prototype.on);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'once',
    GVol.source.ImageWMS.prototype.once);

goog.exportProperty(
    GVol.source.ImageWMS.prototype,
    'un',
    GVol.source.ImageWMS.prototype.un);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'setRenderReprojectionEdges',
    GVol.source.OSM.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'setTileGridForProjection',
    GVol.source.OSM.prototype.setTileGridForProjection);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'getTileLoadFunction',
    GVol.source.OSM.prototype.getTileLoadFunction);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'getTileUrlFunction',
    GVol.source.OSM.prototype.getTileUrlFunction);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'getUrls',
    GVol.source.OSM.prototype.getUrls);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'setTileLoadFunction',
    GVol.source.OSM.prototype.setTileLoadFunction);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'setTileUrlFunction',
    GVol.source.OSM.prototype.setTileUrlFunction);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'setUrl',
    GVol.source.OSM.prototype.setUrl);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'setUrls',
    GVol.source.OSM.prototype.setUrls);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'getTileGrid',
    GVol.source.OSM.prototype.getTileGrid);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'refresh',
    GVol.source.OSM.prototype.refresh);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'getAttributions',
    GVol.source.OSM.prototype.getAttributions);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'getLogo',
    GVol.source.OSM.prototype.getLogo);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'getProjection',
    GVol.source.OSM.prototype.getProjection);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'getState',
    GVol.source.OSM.prototype.getState);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'setAttributions',
    GVol.source.OSM.prototype.setAttributions);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'get',
    GVol.source.OSM.prototype.get);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'getKeys',
    GVol.source.OSM.prototype.getKeys);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'getProperties',
    GVol.source.OSM.prototype.getProperties);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'set',
    GVol.source.OSM.prototype.set);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'setProperties',
    GVol.source.OSM.prototype.setProperties);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'unset',
    GVol.source.OSM.prototype.unset);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'changed',
    GVol.source.OSM.prototype.changed);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'dispatchEvent',
    GVol.source.OSM.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'getRevision',
    GVol.source.OSM.prototype.getRevision);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'on',
    GVol.source.OSM.prototype.on);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'once',
    GVol.source.OSM.prototype.once);

goog.exportProperty(
    GVol.source.OSM.prototype,
    'un',
    GVol.source.OSM.prototype.un);

goog.exportProperty(
    GVol.source.Raster.prototype,
    'getAttributions',
    GVol.source.Raster.prototype.getAttributions);

goog.exportProperty(
    GVol.source.Raster.prototype,
    'getLogo',
    GVol.source.Raster.prototype.getLogo);

goog.exportProperty(
    GVol.source.Raster.prototype,
    'getProjection',
    GVol.source.Raster.prototype.getProjection);

goog.exportProperty(
    GVol.source.Raster.prototype,
    'getState',
    GVol.source.Raster.prototype.getState);

goog.exportProperty(
    GVol.source.Raster.prototype,
    'refresh',
    GVol.source.Raster.prototype.refresh);

goog.exportProperty(
    GVol.source.Raster.prototype,
    'setAttributions',
    GVol.source.Raster.prototype.setAttributions);

goog.exportProperty(
    GVol.source.Raster.prototype,
    'get',
    GVol.source.Raster.prototype.get);

goog.exportProperty(
    GVol.source.Raster.prototype,
    'getKeys',
    GVol.source.Raster.prototype.getKeys);

goog.exportProperty(
    GVol.source.Raster.prototype,
    'getProperties',
    GVol.source.Raster.prototype.getProperties);

goog.exportProperty(
    GVol.source.Raster.prototype,
    'set',
    GVol.source.Raster.prototype.set);

goog.exportProperty(
    GVol.source.Raster.prototype,
    'setProperties',
    GVol.source.Raster.prototype.setProperties);

goog.exportProperty(
    GVol.source.Raster.prototype,
    'unset',
    GVol.source.Raster.prototype.unset);

goog.exportProperty(
    GVol.source.Raster.prototype,
    'changed',
    GVol.source.Raster.prototype.changed);

goog.exportProperty(
    GVol.source.Raster.prototype,
    'dispatchEvent',
    GVol.source.Raster.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.Raster.prototype,
    'getRevision',
    GVol.source.Raster.prototype.getRevision);

goog.exportProperty(
    GVol.source.Raster.prototype,
    'on',
    GVol.source.Raster.prototype.on);

goog.exportProperty(
    GVol.source.Raster.prototype,
    'once',
    GVol.source.Raster.prototype.once);

goog.exportProperty(
    GVol.source.Raster.prototype,
    'un',
    GVol.source.Raster.prototype.un);

goog.exportProperty(
    GVol.source.Raster.Event.prototype,
    'type',
    GVol.source.Raster.Event.prototype.type);

goog.exportProperty(
    GVol.source.Raster.Event.prototype,
    'target',
    GVol.source.Raster.Event.prototype.target);

goog.exportProperty(
    GVol.source.Raster.Event.prototype,
    'preventDefault',
    GVol.source.Raster.Event.prototype.preventDefault);

goog.exportProperty(
    GVol.source.Raster.Event.prototype,
    'stopPropagation',
    GVol.source.Raster.Event.prototype.stopPropagation);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'setRenderReprojectionEdges',
    GVol.source.Stamen.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'setTileGridForProjection',
    GVol.source.Stamen.prototype.setTileGridForProjection);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'getTileLoadFunction',
    GVol.source.Stamen.prototype.getTileLoadFunction);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'getTileUrlFunction',
    GVol.source.Stamen.prototype.getTileUrlFunction);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'getUrls',
    GVol.source.Stamen.prototype.getUrls);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'setTileLoadFunction',
    GVol.source.Stamen.prototype.setTileLoadFunction);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'setTileUrlFunction',
    GVol.source.Stamen.prototype.setTileUrlFunction);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'setUrl',
    GVol.source.Stamen.prototype.setUrl);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'setUrls',
    GVol.source.Stamen.prototype.setUrls);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'getTileGrid',
    GVol.source.Stamen.prototype.getTileGrid);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'refresh',
    GVol.source.Stamen.prototype.refresh);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'getAttributions',
    GVol.source.Stamen.prototype.getAttributions);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'getLogo',
    GVol.source.Stamen.prototype.getLogo);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'getProjection',
    GVol.source.Stamen.prototype.getProjection);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'getState',
    GVol.source.Stamen.prototype.getState);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'setAttributions',
    GVol.source.Stamen.prototype.setAttributions);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'get',
    GVol.source.Stamen.prototype.get);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'getKeys',
    GVol.source.Stamen.prototype.getKeys);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'getProperties',
    GVol.source.Stamen.prototype.getProperties);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'set',
    GVol.source.Stamen.prototype.set);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'setProperties',
    GVol.source.Stamen.prototype.setProperties);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'unset',
    GVol.source.Stamen.prototype.unset);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'changed',
    GVol.source.Stamen.prototype.changed);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'dispatchEvent',
    GVol.source.Stamen.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'getRevision',
    GVol.source.Stamen.prototype.getRevision);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'on',
    GVol.source.Stamen.prototype.on);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'once',
    GVol.source.Stamen.prototype.once);

goog.exportProperty(
    GVol.source.Stamen.prototype,
    'un',
    GVol.source.Stamen.prototype.un);

goog.exportProperty(
    GVol.source.Tile.Event.prototype,
    'type',
    GVol.source.Tile.Event.prototype.type);

goog.exportProperty(
    GVol.source.Tile.Event.prototype,
    'target',
    GVol.source.Tile.Event.prototype.target);

goog.exportProperty(
    GVol.source.Tile.Event.prototype,
    'preventDefault',
    GVol.source.Tile.Event.prototype.preventDefault);

goog.exportProperty(
    GVol.source.Tile.Event.prototype,
    'stopPropagation',
    GVol.source.Tile.Event.prototype.stopPropagation);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'setRenderReprojectionEdges',
    GVol.source.TileArcGISRest.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'setTileGridForProjection',
    GVol.source.TileArcGISRest.prototype.setTileGridForProjection);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'getTileLoadFunction',
    GVol.source.TileArcGISRest.prototype.getTileLoadFunction);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'getTileUrlFunction',
    GVol.source.TileArcGISRest.prototype.getTileUrlFunction);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'getUrls',
    GVol.source.TileArcGISRest.prototype.getUrls);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'setTileLoadFunction',
    GVol.source.TileArcGISRest.prototype.setTileLoadFunction);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'setTileUrlFunction',
    GVol.source.TileArcGISRest.prototype.setTileUrlFunction);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'setUrl',
    GVol.source.TileArcGISRest.prototype.setUrl);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'setUrls',
    GVol.source.TileArcGISRest.prototype.setUrls);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'getTileGrid',
    GVol.source.TileArcGISRest.prototype.getTileGrid);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'refresh',
    GVol.source.TileArcGISRest.prototype.refresh);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'getAttributions',
    GVol.source.TileArcGISRest.prototype.getAttributions);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'getLogo',
    GVol.source.TileArcGISRest.prototype.getLogo);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'getProjection',
    GVol.source.TileArcGISRest.prototype.getProjection);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'getState',
    GVol.source.TileArcGISRest.prototype.getState);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'setAttributions',
    GVol.source.TileArcGISRest.prototype.setAttributions);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'get',
    GVol.source.TileArcGISRest.prototype.get);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'getKeys',
    GVol.source.TileArcGISRest.prototype.getKeys);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'getProperties',
    GVol.source.TileArcGISRest.prototype.getProperties);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'set',
    GVol.source.TileArcGISRest.prototype.set);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'setProperties',
    GVol.source.TileArcGISRest.prototype.setProperties);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'unset',
    GVol.source.TileArcGISRest.prototype.unset);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'changed',
    GVol.source.TileArcGISRest.prototype.changed);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'dispatchEvent',
    GVol.source.TileArcGISRest.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'getRevision',
    GVol.source.TileArcGISRest.prototype.getRevision);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'on',
    GVol.source.TileArcGISRest.prototype.on);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'once',
    GVol.source.TileArcGISRest.prototype.once);

goog.exportProperty(
    GVol.source.TileArcGISRest.prototype,
    'un',
    GVol.source.TileArcGISRest.prototype.un);

goog.exportProperty(
    GVol.source.TileDebug.prototype,
    'getTileGrid',
    GVol.source.TileDebug.prototype.getTileGrid);

goog.exportProperty(
    GVol.source.TileDebug.prototype,
    'refresh',
    GVol.source.TileDebug.prototype.refresh);

goog.exportProperty(
    GVol.source.TileDebug.prototype,
    'getAttributions',
    GVol.source.TileDebug.prototype.getAttributions);

goog.exportProperty(
    GVol.source.TileDebug.prototype,
    'getLogo',
    GVol.source.TileDebug.prototype.getLogo);

goog.exportProperty(
    GVol.source.TileDebug.prototype,
    'getProjection',
    GVol.source.TileDebug.prototype.getProjection);

goog.exportProperty(
    GVol.source.TileDebug.prototype,
    'getState',
    GVol.source.TileDebug.prototype.getState);

goog.exportProperty(
    GVol.source.TileDebug.prototype,
    'setAttributions',
    GVol.source.TileDebug.prototype.setAttributions);

goog.exportProperty(
    GVol.source.TileDebug.prototype,
    'get',
    GVol.source.TileDebug.prototype.get);

goog.exportProperty(
    GVol.source.TileDebug.prototype,
    'getKeys',
    GVol.source.TileDebug.prototype.getKeys);

goog.exportProperty(
    GVol.source.TileDebug.prototype,
    'getProperties',
    GVol.source.TileDebug.prototype.getProperties);

goog.exportProperty(
    GVol.source.TileDebug.prototype,
    'set',
    GVol.source.TileDebug.prototype.set);

goog.exportProperty(
    GVol.source.TileDebug.prototype,
    'setProperties',
    GVol.source.TileDebug.prototype.setProperties);

goog.exportProperty(
    GVol.source.TileDebug.prototype,
    'unset',
    GVol.source.TileDebug.prototype.unset);

goog.exportProperty(
    GVol.source.TileDebug.prototype,
    'changed',
    GVol.source.TileDebug.prototype.changed);

goog.exportProperty(
    GVol.source.TileDebug.prototype,
    'dispatchEvent',
    GVol.source.TileDebug.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.TileDebug.prototype,
    'getRevision',
    GVol.source.TileDebug.prototype.getRevision);

goog.exportProperty(
    GVol.source.TileDebug.prototype,
    'on',
    GVol.source.TileDebug.prototype.on);

goog.exportProperty(
    GVol.source.TileDebug.prototype,
    'once',
    GVol.source.TileDebug.prototype.once);

goog.exportProperty(
    GVol.source.TileDebug.prototype,
    'un',
    GVol.source.TileDebug.prototype.un);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'setRenderReprojectionEdges',
    GVol.source.TileJSON.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'setTileGridForProjection',
    GVol.source.TileJSON.prototype.setTileGridForProjection);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'getTileLoadFunction',
    GVol.source.TileJSON.prototype.getTileLoadFunction);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'getTileUrlFunction',
    GVol.source.TileJSON.prototype.getTileUrlFunction);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'getUrls',
    GVol.source.TileJSON.prototype.getUrls);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'setTileLoadFunction',
    GVol.source.TileJSON.prototype.setTileLoadFunction);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'setTileUrlFunction',
    GVol.source.TileJSON.prototype.setTileUrlFunction);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'setUrl',
    GVol.source.TileJSON.prototype.setUrl);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'setUrls',
    GVol.source.TileJSON.prototype.setUrls);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'getTileGrid',
    GVol.source.TileJSON.prototype.getTileGrid);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'refresh',
    GVol.source.TileJSON.prototype.refresh);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'getAttributions',
    GVol.source.TileJSON.prototype.getAttributions);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'getLogo',
    GVol.source.TileJSON.prototype.getLogo);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'getProjection',
    GVol.source.TileJSON.prototype.getProjection);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'getState',
    GVol.source.TileJSON.prototype.getState);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'setAttributions',
    GVol.source.TileJSON.prototype.setAttributions);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'get',
    GVol.source.TileJSON.prototype.get);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'getKeys',
    GVol.source.TileJSON.prototype.getKeys);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'getProperties',
    GVol.source.TileJSON.prototype.getProperties);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'set',
    GVol.source.TileJSON.prototype.set);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'setProperties',
    GVol.source.TileJSON.prototype.setProperties);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'unset',
    GVol.source.TileJSON.prototype.unset);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'changed',
    GVol.source.TileJSON.prototype.changed);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'dispatchEvent',
    GVol.source.TileJSON.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'getRevision',
    GVol.source.TileJSON.prototype.getRevision);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'on',
    GVol.source.TileJSON.prototype.on);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'once',
    GVol.source.TileJSON.prototype.once);

goog.exportProperty(
    GVol.source.TileJSON.prototype,
    'un',
    GVol.source.TileJSON.prototype.un);

goog.exportProperty(
    GVol.source.TileUTFGrid.prototype,
    'getTileGrid',
    GVol.source.TileUTFGrid.prototype.getTileGrid);

goog.exportProperty(
    GVol.source.TileUTFGrid.prototype,
    'refresh',
    GVol.source.TileUTFGrid.prototype.refresh);

goog.exportProperty(
    GVol.source.TileUTFGrid.prototype,
    'getAttributions',
    GVol.source.TileUTFGrid.prototype.getAttributions);

goog.exportProperty(
    GVol.source.TileUTFGrid.prototype,
    'getLogo',
    GVol.source.TileUTFGrid.prototype.getLogo);

goog.exportProperty(
    GVol.source.TileUTFGrid.prototype,
    'getProjection',
    GVol.source.TileUTFGrid.prototype.getProjection);

goog.exportProperty(
    GVol.source.TileUTFGrid.prototype,
    'getState',
    GVol.source.TileUTFGrid.prototype.getState);

goog.exportProperty(
    GVol.source.TileUTFGrid.prototype,
    'setAttributions',
    GVol.source.TileUTFGrid.prototype.setAttributions);

goog.exportProperty(
    GVol.source.TileUTFGrid.prototype,
    'get',
    GVol.source.TileUTFGrid.prototype.get);

goog.exportProperty(
    GVol.source.TileUTFGrid.prototype,
    'getKeys',
    GVol.source.TileUTFGrid.prototype.getKeys);

goog.exportProperty(
    GVol.source.TileUTFGrid.prototype,
    'getProperties',
    GVol.source.TileUTFGrid.prototype.getProperties);

goog.exportProperty(
    GVol.source.TileUTFGrid.prototype,
    'set',
    GVol.source.TileUTFGrid.prototype.set);

goog.exportProperty(
    GVol.source.TileUTFGrid.prototype,
    'setProperties',
    GVol.source.TileUTFGrid.prototype.setProperties);

goog.exportProperty(
    GVol.source.TileUTFGrid.prototype,
    'unset',
    GVol.source.TileUTFGrid.prototype.unset);

goog.exportProperty(
    GVol.source.TileUTFGrid.prototype,
    'changed',
    GVol.source.TileUTFGrid.prototype.changed);

goog.exportProperty(
    GVol.source.TileUTFGrid.prototype,
    'dispatchEvent',
    GVol.source.TileUTFGrid.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.TileUTFGrid.prototype,
    'getRevision',
    GVol.source.TileUTFGrid.prototype.getRevision);

goog.exportProperty(
    GVol.source.TileUTFGrid.prototype,
    'on',
    GVol.source.TileUTFGrid.prototype.on);

goog.exportProperty(
    GVol.source.TileUTFGrid.prototype,
    'once',
    GVol.source.TileUTFGrid.prototype.once);

goog.exportProperty(
    GVol.source.TileUTFGrid.prototype,
    'un',
    GVol.source.TileUTFGrid.prototype.un);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'setRenderReprojectionEdges',
    GVol.source.TileWMS.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'setTileGridForProjection',
    GVol.source.TileWMS.prototype.setTileGridForProjection);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'getTileLoadFunction',
    GVol.source.TileWMS.prototype.getTileLoadFunction);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'getTileUrlFunction',
    GVol.source.TileWMS.prototype.getTileUrlFunction);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'getUrls',
    GVol.source.TileWMS.prototype.getUrls);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'setTileLoadFunction',
    GVol.source.TileWMS.prototype.setTileLoadFunction);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'setTileUrlFunction',
    GVol.source.TileWMS.prototype.setTileUrlFunction);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'setUrl',
    GVol.source.TileWMS.prototype.setUrl);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'setUrls',
    GVol.source.TileWMS.prototype.setUrls);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'getTileGrid',
    GVol.source.TileWMS.prototype.getTileGrid);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'refresh',
    GVol.source.TileWMS.prototype.refresh);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'getAttributions',
    GVol.source.TileWMS.prototype.getAttributions);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'getLogo',
    GVol.source.TileWMS.prototype.getLogo);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'getProjection',
    GVol.source.TileWMS.prototype.getProjection);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'getState',
    GVol.source.TileWMS.prototype.getState);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'setAttributions',
    GVol.source.TileWMS.prototype.setAttributions);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'get',
    GVol.source.TileWMS.prototype.get);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'getKeys',
    GVol.source.TileWMS.prototype.getKeys);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'getProperties',
    GVol.source.TileWMS.prototype.getProperties);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'set',
    GVol.source.TileWMS.prototype.set);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'setProperties',
    GVol.source.TileWMS.prototype.setProperties);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'unset',
    GVol.source.TileWMS.prototype.unset);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'changed',
    GVol.source.TileWMS.prototype.changed);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'dispatchEvent',
    GVol.source.TileWMS.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'getRevision',
    GVol.source.TileWMS.prototype.getRevision);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'on',
    GVol.source.TileWMS.prototype.on);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'once',
    GVol.source.TileWMS.prototype.once);

goog.exportProperty(
    GVol.source.TileWMS.prototype,
    'un',
    GVol.source.TileWMS.prototype.un);

goog.exportProperty(
    GVol.source.Vector.Event.prototype,
    'type',
    GVol.source.Vector.Event.prototype.type);

goog.exportProperty(
    GVol.source.Vector.Event.prototype,
    'target',
    GVol.source.Vector.Event.prototype.target);

goog.exportProperty(
    GVol.source.Vector.Event.prototype,
    'preventDefault',
    GVol.source.Vector.Event.prototype.preventDefault);

goog.exportProperty(
    GVol.source.Vector.Event.prototype,
    'stopPropagation',
    GVol.source.Vector.Event.prototype.stopPropagation);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'getTileLoadFunction',
    GVol.source.VectorTile.prototype.getTileLoadFunction);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'getTileUrlFunction',
    GVol.source.VectorTile.prototype.getTileUrlFunction);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'getUrls',
    GVol.source.VectorTile.prototype.getUrls);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'setTileLoadFunction',
    GVol.source.VectorTile.prototype.setTileLoadFunction);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'setTileUrlFunction',
    GVol.source.VectorTile.prototype.setTileUrlFunction);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'setUrl',
    GVol.source.VectorTile.prototype.setUrl);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'setUrls',
    GVol.source.VectorTile.prototype.setUrls);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'getTileGrid',
    GVol.source.VectorTile.prototype.getTileGrid);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'refresh',
    GVol.source.VectorTile.prototype.refresh);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'getAttributions',
    GVol.source.VectorTile.prototype.getAttributions);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'getLogo',
    GVol.source.VectorTile.prototype.getLogo);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'getProjection',
    GVol.source.VectorTile.prototype.getProjection);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'getState',
    GVol.source.VectorTile.prototype.getState);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'setAttributions',
    GVol.source.VectorTile.prototype.setAttributions);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'get',
    GVol.source.VectorTile.prototype.get);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'getKeys',
    GVol.source.VectorTile.prototype.getKeys);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'getProperties',
    GVol.source.VectorTile.prototype.getProperties);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'set',
    GVol.source.VectorTile.prototype.set);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'setProperties',
    GVol.source.VectorTile.prototype.setProperties);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'unset',
    GVol.source.VectorTile.prototype.unset);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'changed',
    GVol.source.VectorTile.prototype.changed);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'dispatchEvent',
    GVol.source.VectorTile.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'getRevision',
    GVol.source.VectorTile.prototype.getRevision);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'on',
    GVol.source.VectorTile.prototype.on);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'once',
    GVol.source.VectorTile.prototype.once);

goog.exportProperty(
    GVol.source.VectorTile.prototype,
    'un',
    GVol.source.VectorTile.prototype.un);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'setRenderReprojectionEdges',
    GVol.source.WMTS.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'setTileGridForProjection',
    GVol.source.WMTS.prototype.setTileGridForProjection);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'getTileLoadFunction',
    GVol.source.WMTS.prototype.getTileLoadFunction);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'getTileUrlFunction',
    GVol.source.WMTS.prototype.getTileUrlFunction);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'getUrls',
    GVol.source.WMTS.prototype.getUrls);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'setTileLoadFunction',
    GVol.source.WMTS.prototype.setTileLoadFunction);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'setTileUrlFunction',
    GVol.source.WMTS.prototype.setTileUrlFunction);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'setUrl',
    GVol.source.WMTS.prototype.setUrl);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'setUrls',
    GVol.source.WMTS.prototype.setUrls);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'getTileGrid',
    GVol.source.WMTS.prototype.getTileGrid);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'refresh',
    GVol.source.WMTS.prototype.refresh);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'getAttributions',
    GVol.source.WMTS.prototype.getAttributions);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'getLogo',
    GVol.source.WMTS.prototype.getLogo);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'getProjection',
    GVol.source.WMTS.prototype.getProjection);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'getState',
    GVol.source.WMTS.prototype.getState);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'setAttributions',
    GVol.source.WMTS.prototype.setAttributions);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'get',
    GVol.source.WMTS.prototype.get);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'getKeys',
    GVol.source.WMTS.prototype.getKeys);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'getProperties',
    GVol.source.WMTS.prototype.getProperties);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'set',
    GVol.source.WMTS.prototype.set);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'setProperties',
    GVol.source.WMTS.prototype.setProperties);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'unset',
    GVol.source.WMTS.prototype.unset);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'changed',
    GVol.source.WMTS.prototype.changed);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'dispatchEvent',
    GVol.source.WMTS.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'getRevision',
    GVol.source.WMTS.prototype.getRevision);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'on',
    GVol.source.WMTS.prototype.on);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'once',
    GVol.source.WMTS.prototype.once);

goog.exportProperty(
    GVol.source.WMTS.prototype,
    'un',
    GVol.source.WMTS.prototype.un);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'setRenderReprojectionEdges',
    GVol.source.Zoomify.prototype.setRenderReprojectionEdges);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'setTileGridForProjection',
    GVol.source.Zoomify.prototype.setTileGridForProjection);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'getTileLoadFunction',
    GVol.source.Zoomify.prototype.getTileLoadFunction);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'getTileUrlFunction',
    GVol.source.Zoomify.prototype.getTileUrlFunction);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'getUrls',
    GVol.source.Zoomify.prototype.getUrls);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'setTileLoadFunction',
    GVol.source.Zoomify.prototype.setTileLoadFunction);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'setTileUrlFunction',
    GVol.source.Zoomify.prototype.setTileUrlFunction);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'setUrl',
    GVol.source.Zoomify.prototype.setUrl);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'setUrls',
    GVol.source.Zoomify.prototype.setUrls);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'getTileGrid',
    GVol.source.Zoomify.prototype.getTileGrid);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'refresh',
    GVol.source.Zoomify.prototype.refresh);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'getAttributions',
    GVol.source.Zoomify.prototype.getAttributions);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'getLogo',
    GVol.source.Zoomify.prototype.getLogo);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'getProjection',
    GVol.source.Zoomify.prototype.getProjection);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'getState',
    GVol.source.Zoomify.prototype.getState);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'setAttributions',
    GVol.source.Zoomify.prototype.setAttributions);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'get',
    GVol.source.Zoomify.prototype.get);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'getKeys',
    GVol.source.Zoomify.prototype.getKeys);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'getProperties',
    GVol.source.Zoomify.prototype.getProperties);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'set',
    GVol.source.Zoomify.prototype.set);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'setProperties',
    GVol.source.Zoomify.prototype.setProperties);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'unset',
    GVol.source.Zoomify.prototype.unset);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'changed',
    GVol.source.Zoomify.prototype.changed);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'dispatchEvent',
    GVol.source.Zoomify.prototype.dispatchEvent);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'getRevision',
    GVol.source.Zoomify.prototype.getRevision);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'on',
    GVol.source.Zoomify.prototype.on);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'once',
    GVol.source.Zoomify.prototype.once);

goog.exportProperty(
    GVol.source.Zoomify.prototype,
    'un',
    GVol.source.Zoomify.prototype.un);

goog.exportProperty(
    GVol.reproj.Tile.prototype,
    'getTileCoord',
    GVol.reproj.Tile.prototype.getTileCoord);

goog.exportProperty(
    GVol.reproj.Tile.prototype,
    'load',
    GVol.reproj.Tile.prototype.load);

goog.exportProperty(
    GVol.renderer.Layer.prototype,
    'changed',
    GVol.renderer.Layer.prototype.changed);

goog.exportProperty(
    GVol.renderer.Layer.prototype,
    'dispatchEvent',
    GVol.renderer.Layer.prototype.dispatchEvent);

goog.exportProperty(
    GVol.renderer.Layer.prototype,
    'getRevision',
    GVol.renderer.Layer.prototype.getRevision);

goog.exportProperty(
    GVol.renderer.Layer.prototype,
    'on',
    GVol.renderer.Layer.prototype.on);

goog.exportProperty(
    GVol.renderer.Layer.prototype,
    'once',
    GVol.renderer.Layer.prototype.once);

goog.exportProperty(
    GVol.renderer.Layer.prototype,
    'un',
    GVol.renderer.Layer.prototype.un);

goog.exportProperty(
    GVol.renderer.webgl.Layer.prototype,
    'changed',
    GVol.renderer.webgl.Layer.prototype.changed);

goog.exportProperty(
    GVol.renderer.webgl.Layer.prototype,
    'dispatchEvent',
    GVol.renderer.webgl.Layer.prototype.dispatchEvent);

goog.exportProperty(
    GVol.renderer.webgl.Layer.prototype,
    'getRevision',
    GVol.renderer.webgl.Layer.prototype.getRevision);

goog.exportProperty(
    GVol.renderer.webgl.Layer.prototype,
    'on',
    GVol.renderer.webgl.Layer.prototype.on);

goog.exportProperty(
    GVol.renderer.webgl.Layer.prototype,
    'once',
    GVol.renderer.webgl.Layer.prototype.once);

goog.exportProperty(
    GVol.renderer.webgl.Layer.prototype,
    'un',
    GVol.renderer.webgl.Layer.prototype.un);

goog.exportProperty(
    GVol.renderer.webgl.ImageLayer.prototype,
    'changed',
    GVol.renderer.webgl.ImageLayer.prototype.changed);

goog.exportProperty(
    GVol.renderer.webgl.ImageLayer.prototype,
    'dispatchEvent',
    GVol.renderer.webgl.ImageLayer.prototype.dispatchEvent);

goog.exportProperty(
    GVol.renderer.webgl.ImageLayer.prototype,
    'getRevision',
    GVol.renderer.webgl.ImageLayer.prototype.getRevision);

goog.exportProperty(
    GVol.renderer.webgl.ImageLayer.prototype,
    'on',
    GVol.renderer.webgl.ImageLayer.prototype.on);

goog.exportProperty(
    GVol.renderer.webgl.ImageLayer.prototype,
    'once',
    GVol.renderer.webgl.ImageLayer.prototype.once);

goog.exportProperty(
    GVol.renderer.webgl.ImageLayer.prototype,
    'un',
    GVol.renderer.webgl.ImageLayer.prototype.un);

goog.exportProperty(
    GVol.renderer.webgl.TileLayer.prototype,
    'changed',
    GVol.renderer.webgl.TileLayer.prototype.changed);

goog.exportProperty(
    GVol.renderer.webgl.TileLayer.prototype,
    'dispatchEvent',
    GVol.renderer.webgl.TileLayer.prototype.dispatchEvent);

goog.exportProperty(
    GVol.renderer.webgl.TileLayer.prototype,
    'getRevision',
    GVol.renderer.webgl.TileLayer.prototype.getRevision);

goog.exportProperty(
    GVol.renderer.webgl.TileLayer.prototype,
    'on',
    GVol.renderer.webgl.TileLayer.prototype.on);

goog.exportProperty(
    GVol.renderer.webgl.TileLayer.prototype,
    'once',
    GVol.renderer.webgl.TileLayer.prototype.once);

goog.exportProperty(
    GVol.renderer.webgl.TileLayer.prototype,
    'un',
    GVol.renderer.webgl.TileLayer.prototype.un);

goog.exportProperty(
    GVol.renderer.webgl.VectorLayer.prototype,
    'changed',
    GVol.renderer.webgl.VectorLayer.prototype.changed);

goog.exportProperty(
    GVol.renderer.webgl.VectorLayer.prototype,
    'dispatchEvent',
    GVol.renderer.webgl.VectorLayer.prototype.dispatchEvent);

goog.exportProperty(
    GVol.renderer.webgl.VectorLayer.prototype,
    'getRevision',
    GVol.renderer.webgl.VectorLayer.prototype.getRevision);

goog.exportProperty(
    GVol.renderer.webgl.VectorLayer.prototype,
    'on',
    GVol.renderer.webgl.VectorLayer.prototype.on);

goog.exportProperty(
    GVol.renderer.webgl.VectorLayer.prototype,
    'once',
    GVol.renderer.webgl.VectorLayer.prototype.once);

goog.exportProperty(
    GVol.renderer.webgl.VectorLayer.prototype,
    'un',
    GVol.renderer.webgl.VectorLayer.prototype.un);

goog.exportProperty(
    GVol.renderer.canvas.Layer.prototype,
    'changed',
    GVol.renderer.canvas.Layer.prototype.changed);

goog.exportProperty(
    GVol.renderer.canvas.Layer.prototype,
    'dispatchEvent',
    GVol.renderer.canvas.Layer.prototype.dispatchEvent);

goog.exportProperty(
    GVol.renderer.canvas.Layer.prototype,
    'getRevision',
    GVol.renderer.canvas.Layer.prototype.getRevision);

goog.exportProperty(
    GVol.renderer.canvas.Layer.prototype,
    'on',
    GVol.renderer.canvas.Layer.prototype.on);

goog.exportProperty(
    GVol.renderer.canvas.Layer.prototype,
    'once',
    GVol.renderer.canvas.Layer.prototype.once);

goog.exportProperty(
    GVol.renderer.canvas.Layer.prototype,
    'un',
    GVol.renderer.canvas.Layer.prototype.un);

goog.exportProperty(
    GVol.renderer.canvas.IntermediateCanvas.prototype,
    'changed',
    GVol.renderer.canvas.IntermediateCanvas.prototype.changed);

goog.exportProperty(
    GVol.renderer.canvas.IntermediateCanvas.prototype,
    'dispatchEvent',
    GVol.renderer.canvas.IntermediateCanvas.prototype.dispatchEvent);

goog.exportProperty(
    GVol.renderer.canvas.IntermediateCanvas.prototype,
    'getRevision',
    GVol.renderer.canvas.IntermediateCanvas.prototype.getRevision);

goog.exportProperty(
    GVol.renderer.canvas.IntermediateCanvas.prototype,
    'on',
    GVol.renderer.canvas.IntermediateCanvas.prototype.on);

goog.exportProperty(
    GVol.renderer.canvas.IntermediateCanvas.prototype,
    'once',
    GVol.renderer.canvas.IntermediateCanvas.prototype.once);

goog.exportProperty(
    GVol.renderer.canvas.IntermediateCanvas.prototype,
    'un',
    GVol.renderer.canvas.IntermediateCanvas.prototype.un);

goog.exportProperty(
    GVol.renderer.canvas.ImageLayer.prototype,
    'changed',
    GVol.renderer.canvas.ImageLayer.prototype.changed);

goog.exportProperty(
    GVol.renderer.canvas.ImageLayer.prototype,
    'dispatchEvent',
    GVol.renderer.canvas.ImageLayer.prototype.dispatchEvent);

goog.exportProperty(
    GVol.renderer.canvas.ImageLayer.prototype,
    'getRevision',
    GVol.renderer.canvas.ImageLayer.prototype.getRevision);

goog.exportProperty(
    GVol.renderer.canvas.ImageLayer.prototype,
    'on',
    GVol.renderer.canvas.ImageLayer.prototype.on);

goog.exportProperty(
    GVol.renderer.canvas.ImageLayer.prototype,
    'once',
    GVol.renderer.canvas.ImageLayer.prototype.once);

goog.exportProperty(
    GVol.renderer.canvas.ImageLayer.prototype,
    'un',
    GVol.renderer.canvas.ImageLayer.prototype.un);

goog.exportProperty(
    GVol.renderer.canvas.TileLayer.prototype,
    'changed',
    GVol.renderer.canvas.TileLayer.prototype.changed);

goog.exportProperty(
    GVol.renderer.canvas.TileLayer.prototype,
    'dispatchEvent',
    GVol.renderer.canvas.TileLayer.prototype.dispatchEvent);

goog.exportProperty(
    GVol.renderer.canvas.TileLayer.prototype,
    'getRevision',
    GVol.renderer.canvas.TileLayer.prototype.getRevision);

goog.exportProperty(
    GVol.renderer.canvas.TileLayer.prototype,
    'on',
    GVol.renderer.canvas.TileLayer.prototype.on);

goog.exportProperty(
    GVol.renderer.canvas.TileLayer.prototype,
    'once',
    GVol.renderer.canvas.TileLayer.prototype.once);

goog.exportProperty(
    GVol.renderer.canvas.TileLayer.prototype,
    'un',
    GVol.renderer.canvas.TileLayer.prototype.un);

goog.exportProperty(
    GVol.renderer.canvas.VectorLayer.prototype,
    'changed',
    GVol.renderer.canvas.VectorLayer.prototype.changed);

goog.exportProperty(
    GVol.renderer.canvas.VectorLayer.prototype,
    'dispatchEvent',
    GVol.renderer.canvas.VectorLayer.prototype.dispatchEvent);

goog.exportProperty(
    GVol.renderer.canvas.VectorLayer.prototype,
    'getRevision',
    GVol.renderer.canvas.VectorLayer.prototype.getRevision);

goog.exportProperty(
    GVol.renderer.canvas.VectorLayer.prototype,
    'on',
    GVol.renderer.canvas.VectorLayer.prototype.on);

goog.exportProperty(
    GVol.renderer.canvas.VectorLayer.prototype,
    'once',
    GVol.renderer.canvas.VectorLayer.prototype.once);

goog.exportProperty(
    GVol.renderer.canvas.VectorLayer.prototype,
    'un',
    GVol.renderer.canvas.VectorLayer.prototype.un);

goog.exportProperty(
    GVol.renderer.canvas.VectorTileLayer.prototype,
    'changed',
    GVol.renderer.canvas.VectorTileLayer.prototype.changed);

goog.exportProperty(
    GVol.renderer.canvas.VectorTileLayer.prototype,
    'dispatchEvent',
    GVol.renderer.canvas.VectorTileLayer.prototype.dispatchEvent);

goog.exportProperty(
    GVol.renderer.canvas.VectorTileLayer.prototype,
    'getRevision',
    GVol.renderer.canvas.VectorTileLayer.prototype.getRevision);

goog.exportProperty(
    GVol.renderer.canvas.VectorTileLayer.prototype,
    'on',
    GVol.renderer.canvas.VectorTileLayer.prototype.on);

goog.exportProperty(
    GVol.renderer.canvas.VectorTileLayer.prototype,
    'once',
    GVol.renderer.canvas.VectorTileLayer.prototype.once);

goog.exportProperty(
    GVol.renderer.canvas.VectorTileLayer.prototype,
    'un',
    GVol.renderer.canvas.VectorTileLayer.prototype.un);

goog.exportProperty(
    GVol.render.Event.prototype,
    'type',
    GVol.render.Event.prototype.type);

goog.exportProperty(
    GVol.render.Event.prototype,
    'target',
    GVol.render.Event.prototype.target);

goog.exportProperty(
    GVol.render.Event.prototype,
    'preventDefault',
    GVol.render.Event.prototype.preventDefault);

goog.exportProperty(
    GVol.render.Event.prototype,
    'stopPropagation',
    GVol.render.Event.prototype.stopPropagation);

goog.exportProperty(
    GVol.pointer.PointerEvent.prototype,
    'type',
    GVol.pointer.PointerEvent.prototype.type);

goog.exportProperty(
    GVol.pointer.PointerEvent.prototype,
    'target',
    GVol.pointer.PointerEvent.prototype.target);

goog.exportProperty(
    GVol.pointer.PointerEvent.prototype,
    'preventDefault',
    GVol.pointer.PointerEvent.prototype.preventDefault);

goog.exportProperty(
    GVol.pointer.PointerEvent.prototype,
    'stopPropagation',
    GVol.pointer.PointerEvent.prototype.stopPropagation);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'get',
    GVol.layer.Base.prototype.get);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'getKeys',
    GVol.layer.Base.prototype.getKeys);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'getProperties',
    GVol.layer.Base.prototype.getProperties);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'set',
    GVol.layer.Base.prototype.set);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'setProperties',
    GVol.layer.Base.prototype.setProperties);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'unset',
    GVol.layer.Base.prototype.unset);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'changed',
    GVol.layer.Base.prototype.changed);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'dispatchEvent',
    GVol.layer.Base.prototype.dispatchEvent);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'getRevision',
    GVol.layer.Base.prototype.getRevision);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'on',
    GVol.layer.Base.prototype.on);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'once',
    GVol.layer.Base.prototype.once);

goog.exportProperty(
    GVol.layer.Base.prototype,
    'un',
    GVol.layer.Base.prototype.un);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'getExtent',
    GVol.layer.Group.prototype.getExtent);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'getMaxResGVolution',
    GVol.layer.Group.prototype.getMaxResGVolution);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'getMinResGVolution',
    GVol.layer.Group.prototype.getMinResGVolution);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'getOpacity',
    GVol.layer.Group.prototype.getOpacity);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'getVisible',
    GVol.layer.Group.prototype.getVisible);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'getZIndex',
    GVol.layer.Group.prototype.getZIndex);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'setExtent',
    GVol.layer.Group.prototype.setExtent);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'setMaxResGVolution',
    GVol.layer.Group.prototype.setMaxResGVolution);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'setMinResGVolution',
    GVol.layer.Group.prototype.setMinResGVolution);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'setOpacity',
    GVol.layer.Group.prototype.setOpacity);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'setVisible',
    GVol.layer.Group.prototype.setVisible);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'setZIndex',
    GVol.layer.Group.prototype.setZIndex);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'get',
    GVol.layer.Group.prototype.get);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'getKeys',
    GVol.layer.Group.prototype.getKeys);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'getProperties',
    GVol.layer.Group.prototype.getProperties);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'set',
    GVol.layer.Group.prototype.set);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'setProperties',
    GVol.layer.Group.prototype.setProperties);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'unset',
    GVol.layer.Group.prototype.unset);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'changed',
    GVol.layer.Group.prototype.changed);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'dispatchEvent',
    GVol.layer.Group.prototype.dispatchEvent);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'getRevision',
    GVol.layer.Group.prototype.getRevision);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'on',
    GVol.layer.Group.prototype.on);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'once',
    GVol.layer.Group.prototype.once);

goog.exportProperty(
    GVol.layer.Group.prototype,
    'un',
    GVol.layer.Group.prototype.un);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'getExtent',
    GVol.layer.Layer.prototype.getExtent);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'getMaxResGVolution',
    GVol.layer.Layer.prototype.getMaxResGVolution);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'getMinResGVolution',
    GVol.layer.Layer.prototype.getMinResGVolution);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'getOpacity',
    GVol.layer.Layer.prototype.getOpacity);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'getVisible',
    GVol.layer.Layer.prototype.getVisible);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'getZIndex',
    GVol.layer.Layer.prototype.getZIndex);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'setExtent',
    GVol.layer.Layer.prototype.setExtent);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'setMaxResGVolution',
    GVol.layer.Layer.prototype.setMaxResGVolution);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'setMinResGVolution',
    GVol.layer.Layer.prototype.setMinResGVolution);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'setOpacity',
    GVol.layer.Layer.prototype.setOpacity);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'setVisible',
    GVol.layer.Layer.prototype.setVisible);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'setZIndex',
    GVol.layer.Layer.prototype.setZIndex);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'get',
    GVol.layer.Layer.prototype.get);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'getKeys',
    GVol.layer.Layer.prototype.getKeys);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'getProperties',
    GVol.layer.Layer.prototype.getProperties);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'set',
    GVol.layer.Layer.prototype.set);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'setProperties',
    GVol.layer.Layer.prototype.setProperties);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'unset',
    GVol.layer.Layer.prototype.unset);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'changed',
    GVol.layer.Layer.prototype.changed);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'dispatchEvent',
    GVol.layer.Layer.prototype.dispatchEvent);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'getRevision',
    GVol.layer.Layer.prototype.getRevision);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'on',
    GVol.layer.Layer.prototype.on);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'once',
    GVol.layer.Layer.prototype.once);

goog.exportProperty(
    GVol.layer.Layer.prototype,
    'un',
    GVol.layer.Layer.prototype.un);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'setMap',
    GVol.layer.Vector.prototype.setMap);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'setSource',
    GVol.layer.Vector.prototype.setSource);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'getExtent',
    GVol.layer.Vector.prototype.getExtent);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'getMaxResGVolution',
    GVol.layer.Vector.prototype.getMaxResGVolution);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'getMinResGVolution',
    GVol.layer.Vector.prototype.getMinResGVolution);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'getOpacity',
    GVol.layer.Vector.prototype.getOpacity);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'getVisible',
    GVol.layer.Vector.prototype.getVisible);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'getZIndex',
    GVol.layer.Vector.prototype.getZIndex);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'setExtent',
    GVol.layer.Vector.prototype.setExtent);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'setMaxResGVolution',
    GVol.layer.Vector.prototype.setMaxResGVolution);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'setMinResGVolution',
    GVol.layer.Vector.prototype.setMinResGVolution);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'setOpacity',
    GVol.layer.Vector.prototype.setOpacity);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'setVisible',
    GVol.layer.Vector.prototype.setVisible);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'setZIndex',
    GVol.layer.Vector.prototype.setZIndex);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'get',
    GVol.layer.Vector.prototype.get);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'getKeys',
    GVol.layer.Vector.prototype.getKeys);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'getProperties',
    GVol.layer.Vector.prototype.getProperties);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'set',
    GVol.layer.Vector.prototype.set);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'setProperties',
    GVol.layer.Vector.prototype.setProperties);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'unset',
    GVol.layer.Vector.prototype.unset);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'changed',
    GVol.layer.Vector.prototype.changed);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'dispatchEvent',
    GVol.layer.Vector.prototype.dispatchEvent);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'getRevision',
    GVol.layer.Vector.prototype.getRevision);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'on',
    GVol.layer.Vector.prototype.on);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'once',
    GVol.layer.Vector.prototype.once);

goog.exportProperty(
    GVol.layer.Vector.prototype,
    'un',
    GVol.layer.Vector.prototype.un);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'getSource',
    GVol.layer.Heatmap.prototype.getSource);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'getStyle',
    GVol.layer.Heatmap.prototype.getStyle);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'getStyleFunction',
    GVol.layer.Heatmap.prototype.getStyleFunction);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'setStyle',
    GVol.layer.Heatmap.prototype.setStyle);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'setMap',
    GVol.layer.Heatmap.prototype.setMap);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'setSource',
    GVol.layer.Heatmap.prototype.setSource);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'getExtent',
    GVol.layer.Heatmap.prototype.getExtent);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'getMaxResGVolution',
    GVol.layer.Heatmap.prototype.getMaxResGVolution);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'getMinResGVolution',
    GVol.layer.Heatmap.prototype.getMinResGVolution);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'getOpacity',
    GVol.layer.Heatmap.prototype.getOpacity);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'getVisible',
    GVol.layer.Heatmap.prototype.getVisible);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'getZIndex',
    GVol.layer.Heatmap.prototype.getZIndex);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'setExtent',
    GVol.layer.Heatmap.prototype.setExtent);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'setMaxResGVolution',
    GVol.layer.Heatmap.prototype.setMaxResGVolution);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'setMinResGVolution',
    GVol.layer.Heatmap.prototype.setMinResGVolution);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'setOpacity',
    GVol.layer.Heatmap.prototype.setOpacity);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'setVisible',
    GVol.layer.Heatmap.prototype.setVisible);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'setZIndex',
    GVol.layer.Heatmap.prototype.setZIndex);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'get',
    GVol.layer.Heatmap.prototype.get);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'getKeys',
    GVol.layer.Heatmap.prototype.getKeys);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'getProperties',
    GVol.layer.Heatmap.prototype.getProperties);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'set',
    GVol.layer.Heatmap.prototype.set);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'setProperties',
    GVol.layer.Heatmap.prototype.setProperties);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'unset',
    GVol.layer.Heatmap.prototype.unset);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'changed',
    GVol.layer.Heatmap.prototype.changed);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'dispatchEvent',
    GVol.layer.Heatmap.prototype.dispatchEvent);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'getRevision',
    GVol.layer.Heatmap.prototype.getRevision);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'on',
    GVol.layer.Heatmap.prototype.on);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'once',
    GVol.layer.Heatmap.prototype.once);

goog.exportProperty(
    GVol.layer.Heatmap.prototype,
    'un',
    GVol.layer.Heatmap.prototype.un);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'setMap',
    GVol.layer.Image.prototype.setMap);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'setSource',
    GVol.layer.Image.prototype.setSource);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'getExtent',
    GVol.layer.Image.prototype.getExtent);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'getMaxResGVolution',
    GVol.layer.Image.prototype.getMaxResGVolution);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'getMinResGVolution',
    GVol.layer.Image.prototype.getMinResGVolution);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'getOpacity',
    GVol.layer.Image.prototype.getOpacity);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'getVisible',
    GVol.layer.Image.prototype.getVisible);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'getZIndex',
    GVol.layer.Image.prototype.getZIndex);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'setExtent',
    GVol.layer.Image.prototype.setExtent);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'setMaxResGVolution',
    GVol.layer.Image.prototype.setMaxResGVolution);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'setMinResGVolution',
    GVol.layer.Image.prototype.setMinResGVolution);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'setOpacity',
    GVol.layer.Image.prototype.setOpacity);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'setVisible',
    GVol.layer.Image.prototype.setVisible);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'setZIndex',
    GVol.layer.Image.prototype.setZIndex);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'get',
    GVol.layer.Image.prototype.get);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'getKeys',
    GVol.layer.Image.prototype.getKeys);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'getProperties',
    GVol.layer.Image.prototype.getProperties);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'set',
    GVol.layer.Image.prototype.set);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'setProperties',
    GVol.layer.Image.prototype.setProperties);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'unset',
    GVol.layer.Image.prototype.unset);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'changed',
    GVol.layer.Image.prototype.changed);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'dispatchEvent',
    GVol.layer.Image.prototype.dispatchEvent);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'getRevision',
    GVol.layer.Image.prototype.getRevision);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'on',
    GVol.layer.Image.prototype.on);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'once',
    GVol.layer.Image.prototype.once);

goog.exportProperty(
    GVol.layer.Image.prototype,
    'un',
    GVol.layer.Image.prototype.un);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'setMap',
    GVol.layer.Tile.prototype.setMap);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'setSource',
    GVol.layer.Tile.prototype.setSource);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'getExtent',
    GVol.layer.Tile.prototype.getExtent);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'getMaxResGVolution',
    GVol.layer.Tile.prototype.getMaxResGVolution);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'getMinResGVolution',
    GVol.layer.Tile.prototype.getMinResGVolution);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'getOpacity',
    GVol.layer.Tile.prototype.getOpacity);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'getVisible',
    GVol.layer.Tile.prototype.getVisible);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'getZIndex',
    GVol.layer.Tile.prototype.getZIndex);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'setExtent',
    GVol.layer.Tile.prototype.setExtent);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'setMaxResGVolution',
    GVol.layer.Tile.prototype.setMaxResGVolution);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'setMinResGVolution',
    GVol.layer.Tile.prototype.setMinResGVolution);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'setOpacity',
    GVol.layer.Tile.prototype.setOpacity);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'setVisible',
    GVol.layer.Tile.prototype.setVisible);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'setZIndex',
    GVol.layer.Tile.prototype.setZIndex);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'get',
    GVol.layer.Tile.prototype.get);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'getKeys',
    GVol.layer.Tile.prototype.getKeys);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'getProperties',
    GVol.layer.Tile.prototype.getProperties);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'set',
    GVol.layer.Tile.prototype.set);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'setProperties',
    GVol.layer.Tile.prototype.setProperties);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'unset',
    GVol.layer.Tile.prototype.unset);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'changed',
    GVol.layer.Tile.prototype.changed);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'dispatchEvent',
    GVol.layer.Tile.prototype.dispatchEvent);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'getRevision',
    GVol.layer.Tile.prototype.getRevision);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'on',
    GVol.layer.Tile.prototype.on);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'once',
    GVol.layer.Tile.prototype.once);

goog.exportProperty(
    GVol.layer.Tile.prototype,
    'un',
    GVol.layer.Tile.prototype.un);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'getStyle',
    GVol.layer.VectorTile.prototype.getStyle);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'getStyleFunction',
    GVol.layer.VectorTile.prototype.getStyleFunction);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'setStyle',
    GVol.layer.VectorTile.prototype.setStyle);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'setMap',
    GVol.layer.VectorTile.prototype.setMap);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'setSource',
    GVol.layer.VectorTile.prototype.setSource);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'getExtent',
    GVol.layer.VectorTile.prototype.getExtent);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'getMaxResGVolution',
    GVol.layer.VectorTile.prototype.getMaxResGVolution);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'getMinResGVolution',
    GVol.layer.VectorTile.prototype.getMinResGVolution);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'getOpacity',
    GVol.layer.VectorTile.prototype.getOpacity);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'getVisible',
    GVol.layer.VectorTile.prototype.getVisible);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'getZIndex',
    GVol.layer.VectorTile.prototype.getZIndex);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'setExtent',
    GVol.layer.VectorTile.prototype.setExtent);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'setMaxResGVolution',
    GVol.layer.VectorTile.prototype.setMaxResGVolution);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'setMinResGVolution',
    GVol.layer.VectorTile.prototype.setMinResGVolution);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'setOpacity',
    GVol.layer.VectorTile.prototype.setOpacity);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'setVisible',
    GVol.layer.VectorTile.prototype.setVisible);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'setZIndex',
    GVol.layer.VectorTile.prototype.setZIndex);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'get',
    GVol.layer.VectorTile.prototype.get);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'getKeys',
    GVol.layer.VectorTile.prototype.getKeys);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'getProperties',
    GVol.layer.VectorTile.prototype.getProperties);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'set',
    GVol.layer.VectorTile.prototype.set);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'setProperties',
    GVol.layer.VectorTile.prototype.setProperties);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'unset',
    GVol.layer.VectorTile.prototype.unset);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'changed',
    GVol.layer.VectorTile.prototype.changed);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'dispatchEvent',
    GVol.layer.VectorTile.prototype.dispatchEvent);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'getRevision',
    GVol.layer.VectorTile.prototype.getRevision);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'on',
    GVol.layer.VectorTile.prototype.on);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'once',
    GVol.layer.VectorTile.prototype.once);

goog.exportProperty(
    GVol.layer.VectorTile.prototype,
    'un',
    GVol.layer.VectorTile.prototype.un);

goog.exportProperty(
    GVol.interaction.Interaction.prototype,
    'get',
    GVol.interaction.Interaction.prototype.get);

goog.exportProperty(
    GVol.interaction.Interaction.prototype,
    'getKeys',
    GVol.interaction.Interaction.prototype.getKeys);

goog.exportProperty(
    GVol.interaction.Interaction.prototype,
    'getProperties',
    GVol.interaction.Interaction.prototype.getProperties);

goog.exportProperty(
    GVol.interaction.Interaction.prototype,
    'set',
    GVol.interaction.Interaction.prototype.set);

goog.exportProperty(
    GVol.interaction.Interaction.prototype,
    'setProperties',
    GVol.interaction.Interaction.prototype.setProperties);

goog.exportProperty(
    GVol.interaction.Interaction.prototype,
    'unset',
    GVol.interaction.Interaction.prototype.unset);

goog.exportProperty(
    GVol.interaction.Interaction.prototype,
    'changed',
    GVol.interaction.Interaction.prototype.changed);

goog.exportProperty(
    GVol.interaction.Interaction.prototype,
    'dispatchEvent',
    GVol.interaction.Interaction.prototype.dispatchEvent);

goog.exportProperty(
    GVol.interaction.Interaction.prototype,
    'getRevision',
    GVol.interaction.Interaction.prototype.getRevision);

goog.exportProperty(
    GVol.interaction.Interaction.prototype,
    'on',
    GVol.interaction.Interaction.prototype.on);

goog.exportProperty(
    GVol.interaction.Interaction.prototype,
    'once',
    GVol.interaction.Interaction.prototype.once);

goog.exportProperty(
    GVol.interaction.Interaction.prototype,
    'un',
    GVol.interaction.Interaction.prototype.un);

goog.exportProperty(
    GVol.interaction.DoubleClickZoom.prototype,
    'getActive',
    GVol.interaction.DoubleClickZoom.prototype.getActive);

goog.exportProperty(
    GVol.interaction.DoubleClickZoom.prototype,
    'getMap',
    GVol.interaction.DoubleClickZoom.prototype.getMap);

goog.exportProperty(
    GVol.interaction.DoubleClickZoom.prototype,
    'setActive',
    GVol.interaction.DoubleClickZoom.prototype.setActive);

goog.exportProperty(
    GVol.interaction.DoubleClickZoom.prototype,
    'get',
    GVol.interaction.DoubleClickZoom.prototype.get);

goog.exportProperty(
    GVol.interaction.DoubleClickZoom.prototype,
    'getKeys',
    GVol.interaction.DoubleClickZoom.prototype.getKeys);

goog.exportProperty(
    GVol.interaction.DoubleClickZoom.prototype,
    'getProperties',
    GVol.interaction.DoubleClickZoom.prototype.getProperties);

goog.exportProperty(
    GVol.interaction.DoubleClickZoom.prototype,
    'set',
    GVol.interaction.DoubleClickZoom.prototype.set);

goog.exportProperty(
    GVol.interaction.DoubleClickZoom.prototype,
    'setProperties',
    GVol.interaction.DoubleClickZoom.prototype.setProperties);

goog.exportProperty(
    GVol.interaction.DoubleClickZoom.prototype,
    'unset',
    GVol.interaction.DoubleClickZoom.prototype.unset);

goog.exportProperty(
    GVol.interaction.DoubleClickZoom.prototype,
    'changed',
    GVol.interaction.DoubleClickZoom.prototype.changed);

goog.exportProperty(
    GVol.interaction.DoubleClickZoom.prototype,
    'dispatchEvent',
    GVol.interaction.DoubleClickZoom.prototype.dispatchEvent);

goog.exportProperty(
    GVol.interaction.DoubleClickZoom.prototype,
    'getRevision',
    GVol.interaction.DoubleClickZoom.prototype.getRevision);

goog.exportProperty(
    GVol.interaction.DoubleClickZoom.prototype,
    'on',
    GVol.interaction.DoubleClickZoom.prototype.on);

goog.exportProperty(
    GVol.interaction.DoubleClickZoom.prototype,
    'once',
    GVol.interaction.DoubleClickZoom.prototype.once);

goog.exportProperty(
    GVol.interaction.DoubleClickZoom.prototype,
    'un',
    GVol.interaction.DoubleClickZoom.prototype.un);

goog.exportProperty(
    GVol.interaction.DragAndDrop.prototype,
    'getActive',
    GVol.interaction.DragAndDrop.prototype.getActive);

goog.exportProperty(
    GVol.interaction.DragAndDrop.prototype,
    'getMap',
    GVol.interaction.DragAndDrop.prototype.getMap);

goog.exportProperty(
    GVol.interaction.DragAndDrop.prototype,
    'setActive',
    GVol.interaction.DragAndDrop.prototype.setActive);

goog.exportProperty(
    GVol.interaction.DragAndDrop.prototype,
    'get',
    GVol.interaction.DragAndDrop.prototype.get);

goog.exportProperty(
    GVol.interaction.DragAndDrop.prototype,
    'getKeys',
    GVol.interaction.DragAndDrop.prototype.getKeys);

goog.exportProperty(
    GVol.interaction.DragAndDrop.prototype,
    'getProperties',
    GVol.interaction.DragAndDrop.prototype.getProperties);

goog.exportProperty(
    GVol.interaction.DragAndDrop.prototype,
    'set',
    GVol.interaction.DragAndDrop.prototype.set);

goog.exportProperty(
    GVol.interaction.DragAndDrop.prototype,
    'setProperties',
    GVol.interaction.DragAndDrop.prototype.setProperties);

goog.exportProperty(
    GVol.interaction.DragAndDrop.prototype,
    'unset',
    GVol.interaction.DragAndDrop.prototype.unset);

goog.exportProperty(
    GVol.interaction.DragAndDrop.prototype,
    'changed',
    GVol.interaction.DragAndDrop.prototype.changed);

goog.exportProperty(
    GVol.interaction.DragAndDrop.prototype,
    'dispatchEvent',
    GVol.interaction.DragAndDrop.prototype.dispatchEvent);

goog.exportProperty(
    GVol.interaction.DragAndDrop.prototype,
    'getRevision',
    GVol.interaction.DragAndDrop.prototype.getRevision);

goog.exportProperty(
    GVol.interaction.DragAndDrop.prototype,
    'on',
    GVol.interaction.DragAndDrop.prototype.on);

goog.exportProperty(
    GVol.interaction.DragAndDrop.prototype,
    'once',
    GVol.interaction.DragAndDrop.prototype.once);

goog.exportProperty(
    GVol.interaction.DragAndDrop.prototype,
    'un',
    GVol.interaction.DragAndDrop.prototype.un);

goog.exportProperty(
    GVol.interaction.DragAndDrop.Event.prototype,
    'type',
    GVol.interaction.DragAndDrop.Event.prototype.type);

goog.exportProperty(
    GVol.interaction.DragAndDrop.Event.prototype,
    'target',
    GVol.interaction.DragAndDrop.Event.prototype.target);

goog.exportProperty(
    GVol.interaction.DragAndDrop.Event.prototype,
    'preventDefault',
    GVol.interaction.DragAndDrop.Event.prototype.preventDefault);

goog.exportProperty(
    GVol.interaction.DragAndDrop.Event.prototype,
    'stopPropagation',
    GVol.interaction.DragAndDrop.Event.prototype.stopPropagation);

goog.exportProperty(
    GVol.interaction.Pointer.prototype,
    'getActive',
    GVol.interaction.Pointer.prototype.getActive);

goog.exportProperty(
    GVol.interaction.Pointer.prototype,
    'getMap',
    GVol.interaction.Pointer.prototype.getMap);

goog.exportProperty(
    GVol.interaction.Pointer.prototype,
    'setActive',
    GVol.interaction.Pointer.prototype.setActive);

goog.exportProperty(
    GVol.interaction.Pointer.prototype,
    'get',
    GVol.interaction.Pointer.prototype.get);

goog.exportProperty(
    GVol.interaction.Pointer.prototype,
    'getKeys',
    GVol.interaction.Pointer.prototype.getKeys);

goog.exportProperty(
    GVol.interaction.Pointer.prototype,
    'getProperties',
    GVol.interaction.Pointer.prototype.getProperties);

goog.exportProperty(
    GVol.interaction.Pointer.prototype,
    'set',
    GVol.interaction.Pointer.prototype.set);

goog.exportProperty(
    GVol.interaction.Pointer.prototype,
    'setProperties',
    GVol.interaction.Pointer.prototype.setProperties);

goog.exportProperty(
    GVol.interaction.Pointer.prototype,
    'unset',
    GVol.interaction.Pointer.prototype.unset);

goog.exportProperty(
    GVol.interaction.Pointer.prototype,
    'changed',
    GVol.interaction.Pointer.prototype.changed);

goog.exportProperty(
    GVol.interaction.Pointer.prototype,
    'dispatchEvent',
    GVol.interaction.Pointer.prototype.dispatchEvent);

goog.exportProperty(
    GVol.interaction.Pointer.prototype,
    'getRevision',
    GVol.interaction.Pointer.prototype.getRevision);

goog.exportProperty(
    GVol.interaction.Pointer.prototype,
    'on',
    GVol.interaction.Pointer.prototype.on);

goog.exportProperty(
    GVol.interaction.Pointer.prototype,
    'once',
    GVol.interaction.Pointer.prototype.once);

goog.exportProperty(
    GVol.interaction.Pointer.prototype,
    'un',
    GVol.interaction.Pointer.prototype.un);

goog.exportProperty(
    GVol.interaction.DragBox.prototype,
    'getActive',
    GVol.interaction.DragBox.prototype.getActive);

goog.exportProperty(
    GVol.interaction.DragBox.prototype,
    'getMap',
    GVol.interaction.DragBox.prototype.getMap);

goog.exportProperty(
    GVol.interaction.DragBox.prototype,
    'setActive',
    GVol.interaction.DragBox.prototype.setActive);

goog.exportProperty(
    GVol.interaction.DragBox.prototype,
    'get',
    GVol.interaction.DragBox.prototype.get);

goog.exportProperty(
    GVol.interaction.DragBox.prototype,
    'getKeys',
    GVol.interaction.DragBox.prototype.getKeys);

goog.exportProperty(
    GVol.interaction.DragBox.prototype,
    'getProperties',
    GVol.interaction.DragBox.prototype.getProperties);

goog.exportProperty(
    GVol.interaction.DragBox.prototype,
    'set',
    GVol.interaction.DragBox.prototype.set);

goog.exportProperty(
    GVol.interaction.DragBox.prototype,
    'setProperties',
    GVol.interaction.DragBox.prototype.setProperties);

goog.exportProperty(
    GVol.interaction.DragBox.prototype,
    'unset',
    GVol.interaction.DragBox.prototype.unset);

goog.exportProperty(
    GVol.interaction.DragBox.prototype,
    'changed',
    GVol.interaction.DragBox.prototype.changed);

goog.exportProperty(
    GVol.interaction.DragBox.prototype,
    'dispatchEvent',
    GVol.interaction.DragBox.prototype.dispatchEvent);

goog.exportProperty(
    GVol.interaction.DragBox.prototype,
    'getRevision',
    GVol.interaction.DragBox.prototype.getRevision);

goog.exportProperty(
    GVol.interaction.DragBox.prototype,
    'on',
    GVol.interaction.DragBox.prototype.on);

goog.exportProperty(
    GVol.interaction.DragBox.prototype,
    'once',
    GVol.interaction.DragBox.prototype.once);

goog.exportProperty(
    GVol.interaction.DragBox.prototype,
    'un',
    GVol.interaction.DragBox.prototype.un);

goog.exportProperty(
    GVol.interaction.DragBox.Event.prototype,
    'type',
    GVol.interaction.DragBox.Event.prototype.type);

goog.exportProperty(
    GVol.interaction.DragBox.Event.prototype,
    'target',
    GVol.interaction.DragBox.Event.prototype.target);

goog.exportProperty(
    GVol.interaction.DragBox.Event.prototype,
    'preventDefault',
    GVol.interaction.DragBox.Event.prototype.preventDefault);

goog.exportProperty(
    GVol.interaction.DragBox.Event.prototype,
    'stopPropagation',
    GVol.interaction.DragBox.Event.prototype.stopPropagation);

goog.exportProperty(
    GVol.interaction.DragPan.prototype,
    'getActive',
    GVol.interaction.DragPan.prototype.getActive);

goog.exportProperty(
    GVol.interaction.DragPan.prototype,
    'getMap',
    GVol.interaction.DragPan.prototype.getMap);

goog.exportProperty(
    GVol.interaction.DragPan.prototype,
    'setActive',
    GVol.interaction.DragPan.prototype.setActive);

goog.exportProperty(
    GVol.interaction.DragPan.prototype,
    'get',
    GVol.interaction.DragPan.prototype.get);

goog.exportProperty(
    GVol.interaction.DragPan.prototype,
    'getKeys',
    GVol.interaction.DragPan.prototype.getKeys);

goog.exportProperty(
    GVol.interaction.DragPan.prototype,
    'getProperties',
    GVol.interaction.DragPan.prototype.getProperties);

goog.exportProperty(
    GVol.interaction.DragPan.prototype,
    'set',
    GVol.interaction.DragPan.prototype.set);

goog.exportProperty(
    GVol.interaction.DragPan.prototype,
    'setProperties',
    GVol.interaction.DragPan.prototype.setProperties);

goog.exportProperty(
    GVol.interaction.DragPan.prototype,
    'unset',
    GVol.interaction.DragPan.prototype.unset);

goog.exportProperty(
    GVol.interaction.DragPan.prototype,
    'changed',
    GVol.interaction.DragPan.prototype.changed);

goog.exportProperty(
    GVol.interaction.DragPan.prototype,
    'dispatchEvent',
    GVol.interaction.DragPan.prototype.dispatchEvent);

goog.exportProperty(
    GVol.interaction.DragPan.prototype,
    'getRevision',
    GVol.interaction.DragPan.prototype.getRevision);

goog.exportProperty(
    GVol.interaction.DragPan.prototype,
    'on',
    GVol.interaction.DragPan.prototype.on);

goog.exportProperty(
    GVol.interaction.DragPan.prototype,
    'once',
    GVol.interaction.DragPan.prototype.once);

goog.exportProperty(
    GVol.interaction.DragPan.prototype,
    'un',
    GVol.interaction.DragPan.prototype.un);

goog.exportProperty(
    GVol.interaction.DragRotate.prototype,
    'getActive',
    GVol.interaction.DragRotate.prototype.getActive);

goog.exportProperty(
    GVol.interaction.DragRotate.prototype,
    'getMap',
    GVol.interaction.DragRotate.prototype.getMap);

goog.exportProperty(
    GVol.interaction.DragRotate.prototype,
    'setActive',
    GVol.interaction.DragRotate.prototype.setActive);

goog.exportProperty(
    GVol.interaction.DragRotate.prototype,
    'get',
    GVol.interaction.DragRotate.prototype.get);

goog.exportProperty(
    GVol.interaction.DragRotate.prototype,
    'getKeys',
    GVol.interaction.DragRotate.prototype.getKeys);

goog.exportProperty(
    GVol.interaction.DragRotate.prototype,
    'getProperties',
    GVol.interaction.DragRotate.prototype.getProperties);

goog.exportProperty(
    GVol.interaction.DragRotate.prototype,
    'set',
    GVol.interaction.DragRotate.prototype.set);

goog.exportProperty(
    GVol.interaction.DragRotate.prototype,
    'setProperties',
    GVol.interaction.DragRotate.prototype.setProperties);

goog.exportProperty(
    GVol.interaction.DragRotate.prototype,
    'unset',
    GVol.interaction.DragRotate.prototype.unset);

goog.exportProperty(
    GVol.interaction.DragRotate.prototype,
    'changed',
    GVol.interaction.DragRotate.prototype.changed);

goog.exportProperty(
    GVol.interaction.DragRotate.prototype,
    'dispatchEvent',
    GVol.interaction.DragRotate.prototype.dispatchEvent);

goog.exportProperty(
    GVol.interaction.DragRotate.prototype,
    'getRevision',
    GVol.interaction.DragRotate.prototype.getRevision);

goog.exportProperty(
    GVol.interaction.DragRotate.prototype,
    'on',
    GVol.interaction.DragRotate.prototype.on);

goog.exportProperty(
    GVol.interaction.DragRotate.prototype,
    'once',
    GVol.interaction.DragRotate.prototype.once);

goog.exportProperty(
    GVol.interaction.DragRotate.prototype,
    'un',
    GVol.interaction.DragRotate.prototype.un);

goog.exportProperty(
    GVol.interaction.DragRotateAndZoom.prototype,
    'getActive',
    GVol.interaction.DragRotateAndZoom.prototype.getActive);

goog.exportProperty(
    GVol.interaction.DragRotateAndZoom.prototype,
    'getMap',
    GVol.interaction.DragRotateAndZoom.prototype.getMap);

goog.exportProperty(
    GVol.interaction.DragRotateAndZoom.prototype,
    'setActive',
    GVol.interaction.DragRotateAndZoom.prototype.setActive);

goog.exportProperty(
    GVol.interaction.DragRotateAndZoom.prototype,
    'get',
    GVol.interaction.DragRotateAndZoom.prototype.get);

goog.exportProperty(
    GVol.interaction.DragRotateAndZoom.prototype,
    'getKeys',
    GVol.interaction.DragRotateAndZoom.prototype.getKeys);

goog.exportProperty(
    GVol.interaction.DragRotateAndZoom.prototype,
    'getProperties',
    GVol.interaction.DragRotateAndZoom.prototype.getProperties);

goog.exportProperty(
    GVol.interaction.DragRotateAndZoom.prototype,
    'set',
    GVol.interaction.DragRotateAndZoom.prototype.set);

goog.exportProperty(
    GVol.interaction.DragRotateAndZoom.prototype,
    'setProperties',
    GVol.interaction.DragRotateAndZoom.prototype.setProperties);

goog.exportProperty(
    GVol.interaction.DragRotateAndZoom.prototype,
    'unset',
    GVol.interaction.DragRotateAndZoom.prototype.unset);

goog.exportProperty(
    GVol.interaction.DragRotateAndZoom.prototype,
    'changed',
    GVol.interaction.DragRotateAndZoom.prototype.changed);

goog.exportProperty(
    GVol.interaction.DragRotateAndZoom.prototype,
    'dispatchEvent',
    GVol.interaction.DragRotateAndZoom.prototype.dispatchEvent);

goog.exportProperty(
    GVol.interaction.DragRotateAndZoom.prototype,
    'getRevision',
    GVol.interaction.DragRotateAndZoom.prototype.getRevision);

goog.exportProperty(
    GVol.interaction.DragRotateAndZoom.prototype,
    'on',
    GVol.interaction.DragRotateAndZoom.prototype.on);

goog.exportProperty(
    GVol.interaction.DragRotateAndZoom.prototype,
    'once',
    GVol.interaction.DragRotateAndZoom.prototype.once);

goog.exportProperty(
    GVol.interaction.DragRotateAndZoom.prototype,
    'un',
    GVol.interaction.DragRotateAndZoom.prototype.un);

goog.exportProperty(
    GVol.interaction.DragZoom.prototype,
    'getGeometry',
    GVol.interaction.DragZoom.prototype.getGeometry);

goog.exportProperty(
    GVol.interaction.DragZoom.prototype,
    'getActive',
    GVol.interaction.DragZoom.prototype.getActive);

goog.exportProperty(
    GVol.interaction.DragZoom.prototype,
    'getMap',
    GVol.interaction.DragZoom.prototype.getMap);

goog.exportProperty(
    GVol.interaction.DragZoom.prototype,
    'setActive',
    GVol.interaction.DragZoom.prototype.setActive);

goog.exportProperty(
    GVol.interaction.DragZoom.prototype,
    'get',
    GVol.interaction.DragZoom.prototype.get);

goog.exportProperty(
    GVol.interaction.DragZoom.prototype,
    'getKeys',
    GVol.interaction.DragZoom.prototype.getKeys);

goog.exportProperty(
    GVol.interaction.DragZoom.prototype,
    'getProperties',
    GVol.interaction.DragZoom.prototype.getProperties);

goog.exportProperty(
    GVol.interaction.DragZoom.prototype,
    'set',
    GVol.interaction.DragZoom.prototype.set);

goog.exportProperty(
    GVol.interaction.DragZoom.prototype,
    'setProperties',
    GVol.interaction.DragZoom.prototype.setProperties);

goog.exportProperty(
    GVol.interaction.DragZoom.prototype,
    'unset',
    GVol.interaction.DragZoom.prototype.unset);

goog.exportProperty(
    GVol.interaction.DragZoom.prototype,
    'changed',
    GVol.interaction.DragZoom.prototype.changed);

goog.exportProperty(
    GVol.interaction.DragZoom.prototype,
    'dispatchEvent',
    GVol.interaction.DragZoom.prototype.dispatchEvent);

goog.exportProperty(
    GVol.interaction.DragZoom.prototype,
    'getRevision',
    GVol.interaction.DragZoom.prototype.getRevision);

goog.exportProperty(
    GVol.interaction.DragZoom.prototype,
    'on',
    GVol.interaction.DragZoom.prototype.on);

goog.exportProperty(
    GVol.interaction.DragZoom.prototype,
    'once',
    GVol.interaction.DragZoom.prototype.once);

goog.exportProperty(
    GVol.interaction.DragZoom.prototype,
    'un',
    GVol.interaction.DragZoom.prototype.un);

goog.exportProperty(
    GVol.interaction.Draw.prototype,
    'getActive',
    GVol.interaction.Draw.prototype.getActive);

goog.exportProperty(
    GVol.interaction.Draw.prototype,
    'getMap',
    GVol.interaction.Draw.prototype.getMap);

goog.exportProperty(
    GVol.interaction.Draw.prototype,
    'setActive',
    GVol.interaction.Draw.prototype.setActive);

goog.exportProperty(
    GVol.interaction.Draw.prototype,
    'get',
    GVol.interaction.Draw.prototype.get);

goog.exportProperty(
    GVol.interaction.Draw.prototype,
    'getKeys',
    GVol.interaction.Draw.prototype.getKeys);

goog.exportProperty(
    GVol.interaction.Draw.prototype,
    'getProperties',
    GVol.interaction.Draw.prototype.getProperties);

goog.exportProperty(
    GVol.interaction.Draw.prototype,
    'set',
    GVol.interaction.Draw.prototype.set);

goog.exportProperty(
    GVol.interaction.Draw.prototype,
    'setProperties',
    GVol.interaction.Draw.prototype.setProperties);

goog.exportProperty(
    GVol.interaction.Draw.prototype,
    'unset',
    GVol.interaction.Draw.prototype.unset);

goog.exportProperty(
    GVol.interaction.Draw.prototype,
    'changed',
    GVol.interaction.Draw.prototype.changed);

goog.exportProperty(
    GVol.interaction.Draw.prototype,
    'dispatchEvent',
    GVol.interaction.Draw.prototype.dispatchEvent);

goog.exportProperty(
    GVol.interaction.Draw.prototype,
    'getRevision',
    GVol.interaction.Draw.prototype.getRevision);

goog.exportProperty(
    GVol.interaction.Draw.prototype,
    'on',
    GVol.interaction.Draw.prototype.on);

goog.exportProperty(
    GVol.interaction.Draw.prototype,
    'once',
    GVol.interaction.Draw.prototype.once);

goog.exportProperty(
    GVol.interaction.Draw.prototype,
    'un',
    GVol.interaction.Draw.prototype.un);

goog.exportProperty(
    GVol.interaction.Draw.Event.prototype,
    'type',
    GVol.interaction.Draw.Event.prototype.type);

goog.exportProperty(
    GVol.interaction.Draw.Event.prototype,
    'target',
    GVol.interaction.Draw.Event.prototype.target);

goog.exportProperty(
    GVol.interaction.Draw.Event.prototype,
    'preventDefault',
    GVol.interaction.Draw.Event.prototype.preventDefault);

goog.exportProperty(
    GVol.interaction.Draw.Event.prototype,
    'stopPropagation',
    GVol.interaction.Draw.Event.prototype.stopPropagation);

goog.exportProperty(
    GVol.interaction.Extent.prototype,
    'getActive',
    GVol.interaction.Extent.prototype.getActive);

goog.exportProperty(
    GVol.interaction.Extent.prototype,
    'getMap',
    GVol.interaction.Extent.prototype.getMap);

goog.exportProperty(
    GVol.interaction.Extent.prototype,
    'setActive',
    GVol.interaction.Extent.prototype.setActive);

goog.exportProperty(
    GVol.interaction.Extent.prototype,
    'get',
    GVol.interaction.Extent.prototype.get);

goog.exportProperty(
    GVol.interaction.Extent.prototype,
    'getKeys',
    GVol.interaction.Extent.prototype.getKeys);

goog.exportProperty(
    GVol.interaction.Extent.prototype,
    'getProperties',
    GVol.interaction.Extent.prototype.getProperties);

goog.exportProperty(
    GVol.interaction.Extent.prototype,
    'set',
    GVol.interaction.Extent.prototype.set);

goog.exportProperty(
    GVol.interaction.Extent.prototype,
    'setProperties',
    GVol.interaction.Extent.prototype.setProperties);

goog.exportProperty(
    GVol.interaction.Extent.prototype,
    'unset',
    GVol.interaction.Extent.prototype.unset);

goog.exportProperty(
    GVol.interaction.Extent.prototype,
    'changed',
    GVol.interaction.Extent.prototype.changed);

goog.exportProperty(
    GVol.interaction.Extent.prototype,
    'dispatchEvent',
    GVol.interaction.Extent.prototype.dispatchEvent);

goog.exportProperty(
    GVol.interaction.Extent.prototype,
    'getRevision',
    GVol.interaction.Extent.prototype.getRevision);

goog.exportProperty(
    GVol.interaction.Extent.prototype,
    'on',
    GVol.interaction.Extent.prototype.on);

goog.exportProperty(
    GVol.interaction.Extent.prototype,
    'once',
    GVol.interaction.Extent.prototype.once);

goog.exportProperty(
    GVol.interaction.Extent.prototype,
    'un',
    GVol.interaction.Extent.prototype.un);

goog.exportProperty(
    GVol.interaction.Extent.Event.prototype,
    'type',
    GVol.interaction.Extent.Event.prototype.type);

goog.exportProperty(
    GVol.interaction.Extent.Event.prototype,
    'target',
    GVol.interaction.Extent.Event.prototype.target);

goog.exportProperty(
    GVol.interaction.Extent.Event.prototype,
    'preventDefault',
    GVol.interaction.Extent.Event.prototype.preventDefault);

goog.exportProperty(
    GVol.interaction.Extent.Event.prototype,
    'stopPropagation',
    GVol.interaction.Extent.Event.prototype.stopPropagation);

goog.exportProperty(
    GVol.interaction.KeyboardPan.prototype,
    'getActive',
    GVol.interaction.KeyboardPan.prototype.getActive);

goog.exportProperty(
    GVol.interaction.KeyboardPan.prototype,
    'getMap',
    GVol.interaction.KeyboardPan.prototype.getMap);

goog.exportProperty(
    GVol.interaction.KeyboardPan.prototype,
    'setActive',
    GVol.interaction.KeyboardPan.prototype.setActive);

goog.exportProperty(
    GVol.interaction.KeyboardPan.prototype,
    'get',
    GVol.interaction.KeyboardPan.prototype.get);

goog.exportProperty(
    GVol.interaction.KeyboardPan.prototype,
    'getKeys',
    GVol.interaction.KeyboardPan.prototype.getKeys);

goog.exportProperty(
    GVol.interaction.KeyboardPan.prototype,
    'getProperties',
    GVol.interaction.KeyboardPan.prototype.getProperties);

goog.exportProperty(
    GVol.interaction.KeyboardPan.prototype,
    'set',
    GVol.interaction.KeyboardPan.prototype.set);

goog.exportProperty(
    GVol.interaction.KeyboardPan.prototype,
    'setProperties',
    GVol.interaction.KeyboardPan.prototype.setProperties);

goog.exportProperty(
    GVol.interaction.KeyboardPan.prototype,
    'unset',
    GVol.interaction.KeyboardPan.prototype.unset);

goog.exportProperty(
    GVol.interaction.KeyboardPan.prototype,
    'changed',
    GVol.interaction.KeyboardPan.prototype.changed);

goog.exportProperty(
    GVol.interaction.KeyboardPan.prototype,
    'dispatchEvent',
    GVol.interaction.KeyboardPan.prototype.dispatchEvent);

goog.exportProperty(
    GVol.interaction.KeyboardPan.prototype,
    'getRevision',
    GVol.interaction.KeyboardPan.prototype.getRevision);

goog.exportProperty(
    GVol.interaction.KeyboardPan.prototype,
    'on',
    GVol.interaction.KeyboardPan.prototype.on);

goog.exportProperty(
    GVol.interaction.KeyboardPan.prototype,
    'once',
    GVol.interaction.KeyboardPan.prototype.once);

goog.exportProperty(
    GVol.interaction.KeyboardPan.prototype,
    'un',
    GVol.interaction.KeyboardPan.prototype.un);

goog.exportProperty(
    GVol.interaction.KeyboardZoom.prototype,
    'getActive',
    GVol.interaction.KeyboardZoom.prototype.getActive);

goog.exportProperty(
    GVol.interaction.KeyboardZoom.prototype,
    'getMap',
    GVol.interaction.KeyboardZoom.prototype.getMap);

goog.exportProperty(
    GVol.interaction.KeyboardZoom.prototype,
    'setActive',
    GVol.interaction.KeyboardZoom.prototype.setActive);

goog.exportProperty(
    GVol.interaction.KeyboardZoom.prototype,
    'get',
    GVol.interaction.KeyboardZoom.prototype.get);

goog.exportProperty(
    GVol.interaction.KeyboardZoom.prototype,
    'getKeys',
    GVol.interaction.KeyboardZoom.prototype.getKeys);

goog.exportProperty(
    GVol.interaction.KeyboardZoom.prototype,
    'getProperties',
    GVol.interaction.KeyboardZoom.prototype.getProperties);

goog.exportProperty(
    GVol.interaction.KeyboardZoom.prototype,
    'set',
    GVol.interaction.KeyboardZoom.prototype.set);

goog.exportProperty(
    GVol.interaction.KeyboardZoom.prototype,
    'setProperties',
    GVol.interaction.KeyboardZoom.prototype.setProperties);

goog.exportProperty(
    GVol.interaction.KeyboardZoom.prototype,
    'unset',
    GVol.interaction.KeyboardZoom.prototype.unset);

goog.exportProperty(
    GVol.interaction.KeyboardZoom.prototype,
    'changed',
    GVol.interaction.KeyboardZoom.prototype.changed);

goog.exportProperty(
    GVol.interaction.KeyboardZoom.prototype,
    'dispatchEvent',
    GVol.interaction.KeyboardZoom.prototype.dispatchEvent);

goog.exportProperty(
    GVol.interaction.KeyboardZoom.prototype,
    'getRevision',
    GVol.interaction.KeyboardZoom.prototype.getRevision);

goog.exportProperty(
    GVol.interaction.KeyboardZoom.prototype,
    'on',
    GVol.interaction.KeyboardZoom.prototype.on);

goog.exportProperty(
    GVol.interaction.KeyboardZoom.prototype,
    'once',
    GVol.interaction.KeyboardZoom.prototype.once);

goog.exportProperty(
    GVol.interaction.KeyboardZoom.prototype,
    'un',
    GVol.interaction.KeyboardZoom.prototype.un);

goog.exportProperty(
    GVol.interaction.Modify.prototype,
    'getActive',
    GVol.interaction.Modify.prototype.getActive);

goog.exportProperty(
    GVol.interaction.Modify.prototype,
    'getMap',
    GVol.interaction.Modify.prototype.getMap);

goog.exportProperty(
    GVol.interaction.Modify.prototype,
    'setActive',
    GVol.interaction.Modify.prototype.setActive);

goog.exportProperty(
    GVol.interaction.Modify.prototype,
    'get',
    GVol.interaction.Modify.prototype.get);

goog.exportProperty(
    GVol.interaction.Modify.prototype,
    'getKeys',
    GVol.interaction.Modify.prototype.getKeys);

goog.exportProperty(
    GVol.interaction.Modify.prototype,
    'getProperties',
    GVol.interaction.Modify.prototype.getProperties);

goog.exportProperty(
    GVol.interaction.Modify.prototype,
    'set',
    GVol.interaction.Modify.prototype.set);

goog.exportProperty(
    GVol.interaction.Modify.prototype,
    'setProperties',
    GVol.interaction.Modify.prototype.setProperties);

goog.exportProperty(
    GVol.interaction.Modify.prototype,
    'unset',
    GVol.interaction.Modify.prototype.unset);

goog.exportProperty(
    GVol.interaction.Modify.prototype,
    'changed',
    GVol.interaction.Modify.prototype.changed);

goog.exportProperty(
    GVol.interaction.Modify.prototype,
    'dispatchEvent',
    GVol.interaction.Modify.prototype.dispatchEvent);

goog.exportProperty(
    GVol.interaction.Modify.prototype,
    'getRevision',
    GVol.interaction.Modify.prototype.getRevision);

goog.exportProperty(
    GVol.interaction.Modify.prototype,
    'on',
    GVol.interaction.Modify.prototype.on);

goog.exportProperty(
    GVol.interaction.Modify.prototype,
    'once',
    GVol.interaction.Modify.prototype.once);

goog.exportProperty(
    GVol.interaction.Modify.prototype,
    'un',
    GVol.interaction.Modify.prototype.un);

goog.exportProperty(
    GVol.interaction.Modify.Event.prototype,
    'type',
    GVol.interaction.Modify.Event.prototype.type);

goog.exportProperty(
    GVol.interaction.Modify.Event.prototype,
    'target',
    GVol.interaction.Modify.Event.prototype.target);

goog.exportProperty(
    GVol.interaction.Modify.Event.prototype,
    'preventDefault',
    GVol.interaction.Modify.Event.prototype.preventDefault);

goog.exportProperty(
    GVol.interaction.Modify.Event.prototype,
    'stopPropagation',
    GVol.interaction.Modify.Event.prototype.stopPropagation);

goog.exportProperty(
    GVol.interaction.MouseWheelZoom.prototype,
    'getActive',
    GVol.interaction.MouseWheelZoom.prototype.getActive);

goog.exportProperty(
    GVol.interaction.MouseWheelZoom.prototype,
    'getMap',
    GVol.interaction.MouseWheelZoom.prototype.getMap);

goog.exportProperty(
    GVol.interaction.MouseWheelZoom.prototype,
    'setActive',
    GVol.interaction.MouseWheelZoom.prototype.setActive);

goog.exportProperty(
    GVol.interaction.MouseWheelZoom.prototype,
    'get',
    GVol.interaction.MouseWheelZoom.prototype.get);

goog.exportProperty(
    GVol.interaction.MouseWheelZoom.prototype,
    'getKeys',
    GVol.interaction.MouseWheelZoom.prototype.getKeys);

goog.exportProperty(
    GVol.interaction.MouseWheelZoom.prototype,
    'getProperties',
    GVol.interaction.MouseWheelZoom.prototype.getProperties);

goog.exportProperty(
    GVol.interaction.MouseWheelZoom.prototype,
    'set',
    GVol.interaction.MouseWheelZoom.prototype.set);

goog.exportProperty(
    GVol.interaction.MouseWheelZoom.prototype,
    'setProperties',
    GVol.interaction.MouseWheelZoom.prototype.setProperties);

goog.exportProperty(
    GVol.interaction.MouseWheelZoom.prototype,
    'unset',
    GVol.interaction.MouseWheelZoom.prototype.unset);

goog.exportProperty(
    GVol.interaction.MouseWheelZoom.prototype,
    'changed',
    GVol.interaction.MouseWheelZoom.prototype.changed);

goog.exportProperty(
    GVol.interaction.MouseWheelZoom.prototype,
    'dispatchEvent',
    GVol.interaction.MouseWheelZoom.prototype.dispatchEvent);

goog.exportProperty(
    GVol.interaction.MouseWheelZoom.prototype,
    'getRevision',
    GVol.interaction.MouseWheelZoom.prototype.getRevision);

goog.exportProperty(
    GVol.interaction.MouseWheelZoom.prototype,
    'on',
    GVol.interaction.MouseWheelZoom.prototype.on);

goog.exportProperty(
    GVol.interaction.MouseWheelZoom.prototype,
    'once',
    GVol.interaction.MouseWheelZoom.prototype.once);

goog.exportProperty(
    GVol.interaction.MouseWheelZoom.prototype,
    'un',
    GVol.interaction.MouseWheelZoom.prototype.un);

goog.exportProperty(
    GVol.interaction.PinchRotate.prototype,
    'getActive',
    GVol.interaction.PinchRotate.prototype.getActive);

goog.exportProperty(
    GVol.interaction.PinchRotate.prototype,
    'getMap',
    GVol.interaction.PinchRotate.prototype.getMap);

goog.exportProperty(
    GVol.interaction.PinchRotate.prototype,
    'setActive',
    GVol.interaction.PinchRotate.prototype.setActive);

goog.exportProperty(
    GVol.interaction.PinchRotate.prototype,
    'get',
    GVol.interaction.PinchRotate.prototype.get);

goog.exportProperty(
    GVol.interaction.PinchRotate.prototype,
    'getKeys',
    GVol.interaction.PinchRotate.prototype.getKeys);

goog.exportProperty(
    GVol.interaction.PinchRotate.prototype,
    'getProperties',
    GVol.interaction.PinchRotate.prototype.getProperties);

goog.exportProperty(
    GVol.interaction.PinchRotate.prototype,
    'set',
    GVol.interaction.PinchRotate.prototype.set);

goog.exportProperty(
    GVol.interaction.PinchRotate.prototype,
    'setProperties',
    GVol.interaction.PinchRotate.prototype.setProperties);

goog.exportProperty(
    GVol.interaction.PinchRotate.prototype,
    'unset',
    GVol.interaction.PinchRotate.prototype.unset);

goog.exportProperty(
    GVol.interaction.PinchRotate.prototype,
    'changed',
    GVol.interaction.PinchRotate.prototype.changed);

goog.exportProperty(
    GVol.interaction.PinchRotate.prototype,
    'dispatchEvent',
    GVol.interaction.PinchRotate.prototype.dispatchEvent);

goog.exportProperty(
    GVol.interaction.PinchRotate.prototype,
    'getRevision',
    GVol.interaction.PinchRotate.prototype.getRevision);

goog.exportProperty(
    GVol.interaction.PinchRotate.prototype,
    'on',
    GVol.interaction.PinchRotate.prototype.on);

goog.exportProperty(
    GVol.interaction.PinchRotate.prototype,
    'once',
    GVol.interaction.PinchRotate.prototype.once);

goog.exportProperty(
    GVol.interaction.PinchRotate.prototype,
    'un',
    GVol.interaction.PinchRotate.prototype.un);

goog.exportProperty(
    GVol.interaction.PinchZoom.prototype,
    'getActive',
    GVol.interaction.PinchZoom.prototype.getActive);

goog.exportProperty(
    GVol.interaction.PinchZoom.prototype,
    'getMap',
    GVol.interaction.PinchZoom.prototype.getMap);

goog.exportProperty(
    GVol.interaction.PinchZoom.prototype,
    'setActive',
    GVol.interaction.PinchZoom.prototype.setActive);

goog.exportProperty(
    GVol.interaction.PinchZoom.prototype,
    'get',
    GVol.interaction.PinchZoom.prototype.get);

goog.exportProperty(
    GVol.interaction.PinchZoom.prototype,
    'getKeys',
    GVol.interaction.PinchZoom.prototype.getKeys);

goog.exportProperty(
    GVol.interaction.PinchZoom.prototype,
    'getProperties',
    GVol.interaction.PinchZoom.prototype.getProperties);

goog.exportProperty(
    GVol.interaction.PinchZoom.prototype,
    'set',
    GVol.interaction.PinchZoom.prototype.set);

goog.exportProperty(
    GVol.interaction.PinchZoom.prototype,
    'setProperties',
    GVol.interaction.PinchZoom.prototype.setProperties);

goog.exportProperty(
    GVol.interaction.PinchZoom.prototype,
    'unset',
    GVol.interaction.PinchZoom.prototype.unset);

goog.exportProperty(
    GVol.interaction.PinchZoom.prototype,
    'changed',
    GVol.interaction.PinchZoom.prototype.changed);

goog.exportProperty(
    GVol.interaction.PinchZoom.prototype,
    'dispatchEvent',
    GVol.interaction.PinchZoom.prototype.dispatchEvent);

goog.exportProperty(
    GVol.interaction.PinchZoom.prototype,
    'getRevision',
    GVol.interaction.PinchZoom.prototype.getRevision);

goog.exportProperty(
    GVol.interaction.PinchZoom.prototype,
    'on',
    GVol.interaction.PinchZoom.prototype.on);

goog.exportProperty(
    GVol.interaction.PinchZoom.prototype,
    'once',
    GVol.interaction.PinchZoom.prototype.once);

goog.exportProperty(
    GVol.interaction.PinchZoom.prototype,
    'un',
    GVol.interaction.PinchZoom.prototype.un);

goog.exportProperty(
    GVol.interaction.Select.prototype,
    'getActive',
    GVol.interaction.Select.prototype.getActive);

goog.exportProperty(
    GVol.interaction.Select.prototype,
    'getMap',
    GVol.interaction.Select.prototype.getMap);

goog.exportProperty(
    GVol.interaction.Select.prototype,
    'setActive',
    GVol.interaction.Select.prototype.setActive);

goog.exportProperty(
    GVol.interaction.Select.prototype,
    'get',
    GVol.interaction.Select.prototype.get);

goog.exportProperty(
    GVol.interaction.Select.prototype,
    'getKeys',
    GVol.interaction.Select.prototype.getKeys);

goog.exportProperty(
    GVol.interaction.Select.prototype,
    'getProperties',
    GVol.interaction.Select.prototype.getProperties);

goog.exportProperty(
    GVol.interaction.Select.prototype,
    'set',
    GVol.interaction.Select.prototype.set);

goog.exportProperty(
    GVol.interaction.Select.prototype,
    'setProperties',
    GVol.interaction.Select.prototype.setProperties);

goog.exportProperty(
    GVol.interaction.Select.prototype,
    'unset',
    GVol.interaction.Select.prototype.unset);

goog.exportProperty(
    GVol.interaction.Select.prototype,
    'changed',
    GVol.interaction.Select.prototype.changed);

goog.exportProperty(
    GVol.interaction.Select.prototype,
    'dispatchEvent',
    GVol.interaction.Select.prototype.dispatchEvent);

goog.exportProperty(
    GVol.interaction.Select.prototype,
    'getRevision',
    GVol.interaction.Select.prototype.getRevision);

goog.exportProperty(
    GVol.interaction.Select.prototype,
    'on',
    GVol.interaction.Select.prototype.on);

goog.exportProperty(
    GVol.interaction.Select.prototype,
    'once',
    GVol.interaction.Select.prototype.once);

goog.exportProperty(
    GVol.interaction.Select.prototype,
    'un',
    GVol.interaction.Select.prototype.un);

goog.exportProperty(
    GVol.interaction.Select.Event.prototype,
    'type',
    GVol.interaction.Select.Event.prototype.type);

goog.exportProperty(
    GVol.interaction.Select.Event.prototype,
    'target',
    GVol.interaction.Select.Event.prototype.target);

goog.exportProperty(
    GVol.interaction.Select.Event.prototype,
    'preventDefault',
    GVol.interaction.Select.Event.prototype.preventDefault);

goog.exportProperty(
    GVol.interaction.Select.Event.prototype,
    'stopPropagation',
    GVol.interaction.Select.Event.prototype.stopPropagation);

goog.exportProperty(
    GVol.interaction.Snap.prototype,
    'getActive',
    GVol.interaction.Snap.prototype.getActive);

goog.exportProperty(
    GVol.interaction.Snap.prototype,
    'getMap',
    GVol.interaction.Snap.prototype.getMap);

goog.exportProperty(
    GVol.interaction.Snap.prototype,
    'setActive',
    GVol.interaction.Snap.prototype.setActive);

goog.exportProperty(
    GVol.interaction.Snap.prototype,
    'get',
    GVol.interaction.Snap.prototype.get);

goog.exportProperty(
    GVol.interaction.Snap.prototype,
    'getKeys',
    GVol.interaction.Snap.prototype.getKeys);

goog.exportProperty(
    GVol.interaction.Snap.prototype,
    'getProperties',
    GVol.interaction.Snap.prototype.getProperties);

goog.exportProperty(
    GVol.interaction.Snap.prototype,
    'set',
    GVol.interaction.Snap.prototype.set);

goog.exportProperty(
    GVol.interaction.Snap.prototype,
    'setProperties',
    GVol.interaction.Snap.prototype.setProperties);

goog.exportProperty(
    GVol.interaction.Snap.prototype,
    'unset',
    GVol.interaction.Snap.prototype.unset);

goog.exportProperty(
    GVol.interaction.Snap.prototype,
    'changed',
    GVol.interaction.Snap.prototype.changed);

goog.exportProperty(
    GVol.interaction.Snap.prototype,
    'dispatchEvent',
    GVol.interaction.Snap.prototype.dispatchEvent);

goog.exportProperty(
    GVol.interaction.Snap.prototype,
    'getRevision',
    GVol.interaction.Snap.prototype.getRevision);

goog.exportProperty(
    GVol.interaction.Snap.prototype,
    'on',
    GVol.interaction.Snap.prototype.on);

goog.exportProperty(
    GVol.interaction.Snap.prototype,
    'once',
    GVol.interaction.Snap.prototype.once);

goog.exportProperty(
    GVol.interaction.Snap.prototype,
    'un',
    GVol.interaction.Snap.prototype.un);

goog.exportProperty(
    GVol.interaction.Translate.prototype,
    'getActive',
    GVol.interaction.Translate.prototype.getActive);

goog.exportProperty(
    GVol.interaction.Translate.prototype,
    'getMap',
    GVol.interaction.Translate.prototype.getMap);

goog.exportProperty(
    GVol.interaction.Translate.prototype,
    'setActive',
    GVol.interaction.Translate.prototype.setActive);

goog.exportProperty(
    GVol.interaction.Translate.prototype,
    'get',
    GVol.interaction.Translate.prototype.get);

goog.exportProperty(
    GVol.interaction.Translate.prototype,
    'getKeys',
    GVol.interaction.Translate.prototype.getKeys);

goog.exportProperty(
    GVol.interaction.Translate.prototype,
    'getProperties',
    GVol.interaction.Translate.prototype.getProperties);

goog.exportProperty(
    GVol.interaction.Translate.prototype,
    'set',
    GVol.interaction.Translate.prototype.set);

goog.exportProperty(
    GVol.interaction.Translate.prototype,
    'setProperties',
    GVol.interaction.Translate.prototype.setProperties);

goog.exportProperty(
    GVol.interaction.Translate.prototype,
    'unset',
    GVol.interaction.Translate.prototype.unset);

goog.exportProperty(
    GVol.interaction.Translate.prototype,
    'changed',
    GVol.interaction.Translate.prototype.changed);

goog.exportProperty(
    GVol.interaction.Translate.prototype,
    'dispatchEvent',
    GVol.interaction.Translate.prototype.dispatchEvent);

goog.exportProperty(
    GVol.interaction.Translate.prototype,
    'getRevision',
    GVol.interaction.Translate.prototype.getRevision);

goog.exportProperty(
    GVol.interaction.Translate.prototype,
    'on',
    GVol.interaction.Translate.prototype.on);

goog.exportProperty(
    GVol.interaction.Translate.prototype,
    'once',
    GVol.interaction.Translate.prototype.once);

goog.exportProperty(
    GVol.interaction.Translate.prototype,
    'un',
    GVol.interaction.Translate.prototype.un);

goog.exportProperty(
    GVol.interaction.Translate.Event.prototype,
    'type',
    GVol.interaction.Translate.Event.prototype.type);

goog.exportProperty(
    GVol.interaction.Translate.Event.prototype,
    'target',
    GVol.interaction.Translate.Event.prototype.target);

goog.exportProperty(
    GVol.interaction.Translate.Event.prototype,
    'preventDefault',
    GVol.interaction.Translate.Event.prototype.preventDefault);

goog.exportProperty(
    GVol.interaction.Translate.Event.prototype,
    'stopPropagation',
    GVol.interaction.Translate.Event.prototype.stopPropagation);

goog.exportProperty(
    GVol.geom.Geometry.prototype,
    'get',
    GVol.geom.Geometry.prototype.get);

goog.exportProperty(
    GVol.geom.Geometry.prototype,
    'getKeys',
    GVol.geom.Geometry.prototype.getKeys);

goog.exportProperty(
    GVol.geom.Geometry.prototype,
    'getProperties',
    GVol.geom.Geometry.prototype.getProperties);

goog.exportProperty(
    GVol.geom.Geometry.prototype,
    'set',
    GVol.geom.Geometry.prototype.set);

goog.exportProperty(
    GVol.geom.Geometry.prototype,
    'setProperties',
    GVol.geom.Geometry.prototype.setProperties);

goog.exportProperty(
    GVol.geom.Geometry.prototype,
    'unset',
    GVol.geom.Geometry.prototype.unset);

goog.exportProperty(
    GVol.geom.Geometry.prototype,
    'changed',
    GVol.geom.Geometry.prototype.changed);

goog.exportProperty(
    GVol.geom.Geometry.prototype,
    'dispatchEvent',
    GVol.geom.Geometry.prototype.dispatchEvent);

goog.exportProperty(
    GVol.geom.Geometry.prototype,
    'getRevision',
    GVol.geom.Geometry.prototype.getRevision);

goog.exportProperty(
    GVol.geom.Geometry.prototype,
    'on',
    GVol.geom.Geometry.prototype.on);

goog.exportProperty(
    GVol.geom.Geometry.prototype,
    'once',
    GVol.geom.Geometry.prototype.once);

goog.exportProperty(
    GVol.geom.Geometry.prototype,
    'un',
    GVol.geom.Geometry.prototype.un);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'getClosestPoint',
    GVol.geom.SimpleGeometry.prototype.getClosestPoint);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'intersectsCoordinate',
    GVol.geom.SimpleGeometry.prototype.intersectsCoordinate);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'getExtent',
    GVol.geom.SimpleGeometry.prototype.getExtent);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'rotate',
    GVol.geom.SimpleGeometry.prototype.rotate);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'scale',
    GVol.geom.SimpleGeometry.prototype.scale);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'simplify',
    GVol.geom.SimpleGeometry.prototype.simplify);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'transform',
    GVol.geom.SimpleGeometry.prototype.transform);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'get',
    GVol.geom.SimpleGeometry.prototype.get);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'getKeys',
    GVol.geom.SimpleGeometry.prototype.getKeys);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'getProperties',
    GVol.geom.SimpleGeometry.prototype.getProperties);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'set',
    GVol.geom.SimpleGeometry.prototype.set);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'setProperties',
    GVol.geom.SimpleGeometry.prototype.setProperties);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'unset',
    GVol.geom.SimpleGeometry.prototype.unset);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'changed',
    GVol.geom.SimpleGeometry.prototype.changed);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'dispatchEvent',
    GVol.geom.SimpleGeometry.prototype.dispatchEvent);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'getRevision',
    GVol.geom.SimpleGeometry.prototype.getRevision);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'on',
    GVol.geom.SimpleGeometry.prototype.on);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'once',
    GVol.geom.SimpleGeometry.prototype.once);

goog.exportProperty(
    GVol.geom.SimpleGeometry.prototype,
    'un',
    GVol.geom.SimpleGeometry.prototype.un);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'getFirstCoordinate',
    GVol.geom.Circle.prototype.getFirstCoordinate);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'getLastCoordinate',
    GVol.geom.Circle.prototype.getLastCoordinate);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'getLayout',
    GVol.geom.Circle.prototype.getLayout);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'rotate',
    GVol.geom.Circle.prototype.rotate);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'scale',
    GVol.geom.Circle.prototype.scale);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'getClosestPoint',
    GVol.geom.Circle.prototype.getClosestPoint);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'intersectsCoordinate',
    GVol.geom.Circle.prototype.intersectsCoordinate);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'getExtent',
    GVol.geom.Circle.prototype.getExtent);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'simplify',
    GVol.geom.Circle.prototype.simplify);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'get',
    GVol.geom.Circle.prototype.get);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'getKeys',
    GVol.geom.Circle.prototype.getKeys);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'getProperties',
    GVol.geom.Circle.prototype.getProperties);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'set',
    GVol.geom.Circle.prototype.set);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'setProperties',
    GVol.geom.Circle.prototype.setProperties);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'unset',
    GVol.geom.Circle.prototype.unset);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'changed',
    GVol.geom.Circle.prototype.changed);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'dispatchEvent',
    GVol.geom.Circle.prototype.dispatchEvent);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'getRevision',
    GVol.geom.Circle.prototype.getRevision);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'on',
    GVol.geom.Circle.prototype.on);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'once',
    GVol.geom.Circle.prototype.once);

goog.exportProperty(
    GVol.geom.Circle.prototype,
    'un',
    GVol.geom.Circle.prototype.un);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'getClosestPoint',
    GVol.geom.GeometryCGVollection.prototype.getClosestPoint);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'intersectsCoordinate',
    GVol.geom.GeometryCGVollection.prototype.intersectsCoordinate);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'getExtent',
    GVol.geom.GeometryCGVollection.prototype.getExtent);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'rotate',
    GVol.geom.GeometryCGVollection.prototype.rotate);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'scale',
    GVol.geom.GeometryCGVollection.prototype.scale);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'simplify',
    GVol.geom.GeometryCGVollection.prototype.simplify);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'transform',
    GVol.geom.GeometryCGVollection.prototype.transform);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'get',
    GVol.geom.GeometryCGVollection.prototype.get);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'getKeys',
    GVol.geom.GeometryCGVollection.prototype.getKeys);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'getProperties',
    GVol.geom.GeometryCGVollection.prototype.getProperties);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'set',
    GVol.geom.GeometryCGVollection.prototype.set);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'setProperties',
    GVol.geom.GeometryCGVollection.prototype.setProperties);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'unset',
    GVol.geom.GeometryCGVollection.prototype.unset);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'changed',
    GVol.geom.GeometryCGVollection.prototype.changed);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'dispatchEvent',
    GVol.geom.GeometryCGVollection.prototype.dispatchEvent);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'getRevision',
    GVol.geom.GeometryCGVollection.prototype.getRevision);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'on',
    GVol.geom.GeometryCGVollection.prototype.on);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'once',
    GVol.geom.GeometryCGVollection.prototype.once);

goog.exportProperty(
    GVol.geom.GeometryCGVollection.prototype,
    'un',
    GVol.geom.GeometryCGVollection.prototype.un);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'getFirstCoordinate',
    GVol.geom.LinearRing.prototype.getFirstCoordinate);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'getLastCoordinate',
    GVol.geom.LinearRing.prototype.getLastCoordinate);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'getLayout',
    GVol.geom.LinearRing.prototype.getLayout);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'rotate',
    GVol.geom.LinearRing.prototype.rotate);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'scale',
    GVol.geom.LinearRing.prototype.scale);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'getClosestPoint',
    GVol.geom.LinearRing.prototype.getClosestPoint);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'intersectsCoordinate',
    GVol.geom.LinearRing.prototype.intersectsCoordinate);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'getExtent',
    GVol.geom.LinearRing.prototype.getExtent);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'simplify',
    GVol.geom.LinearRing.prototype.simplify);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'transform',
    GVol.geom.LinearRing.prototype.transform);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'get',
    GVol.geom.LinearRing.prototype.get);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'getKeys',
    GVol.geom.LinearRing.prototype.getKeys);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'getProperties',
    GVol.geom.LinearRing.prototype.getProperties);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'set',
    GVol.geom.LinearRing.prototype.set);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'setProperties',
    GVol.geom.LinearRing.prototype.setProperties);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'unset',
    GVol.geom.LinearRing.prototype.unset);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'changed',
    GVol.geom.LinearRing.prototype.changed);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'dispatchEvent',
    GVol.geom.LinearRing.prototype.dispatchEvent);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'getRevision',
    GVol.geom.LinearRing.prototype.getRevision);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'on',
    GVol.geom.LinearRing.prototype.on);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'once',
    GVol.geom.LinearRing.prototype.once);

goog.exportProperty(
    GVol.geom.LinearRing.prototype,
    'un',
    GVol.geom.LinearRing.prototype.un);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'getFirstCoordinate',
    GVol.geom.LineString.prototype.getFirstCoordinate);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'getLastCoordinate',
    GVol.geom.LineString.prototype.getLastCoordinate);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'getLayout',
    GVol.geom.LineString.prototype.getLayout);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'rotate',
    GVol.geom.LineString.prototype.rotate);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'scale',
    GVol.geom.LineString.prototype.scale);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'getClosestPoint',
    GVol.geom.LineString.prototype.getClosestPoint);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'intersectsCoordinate',
    GVol.geom.LineString.prototype.intersectsCoordinate);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'getExtent',
    GVol.geom.LineString.prototype.getExtent);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'simplify',
    GVol.geom.LineString.prototype.simplify);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'transform',
    GVol.geom.LineString.prototype.transform);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'get',
    GVol.geom.LineString.prototype.get);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'getKeys',
    GVol.geom.LineString.prototype.getKeys);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'getProperties',
    GVol.geom.LineString.prototype.getProperties);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'set',
    GVol.geom.LineString.prototype.set);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'setProperties',
    GVol.geom.LineString.prototype.setProperties);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'unset',
    GVol.geom.LineString.prototype.unset);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'changed',
    GVol.geom.LineString.prototype.changed);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'dispatchEvent',
    GVol.geom.LineString.prototype.dispatchEvent);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'getRevision',
    GVol.geom.LineString.prototype.getRevision);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'on',
    GVol.geom.LineString.prototype.on);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'once',
    GVol.geom.LineString.prototype.once);

goog.exportProperty(
    GVol.geom.LineString.prototype,
    'un',
    GVol.geom.LineString.prototype.un);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'getFirstCoordinate',
    GVol.geom.MultiLineString.prototype.getFirstCoordinate);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'getLastCoordinate',
    GVol.geom.MultiLineString.prototype.getLastCoordinate);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'getLayout',
    GVol.geom.MultiLineString.prototype.getLayout);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'rotate',
    GVol.geom.MultiLineString.prototype.rotate);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'scale',
    GVol.geom.MultiLineString.prototype.scale);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'getClosestPoint',
    GVol.geom.MultiLineString.prototype.getClosestPoint);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'intersectsCoordinate',
    GVol.geom.MultiLineString.prototype.intersectsCoordinate);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'getExtent',
    GVol.geom.MultiLineString.prototype.getExtent);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'simplify',
    GVol.geom.MultiLineString.prototype.simplify);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'transform',
    GVol.geom.MultiLineString.prototype.transform);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'get',
    GVol.geom.MultiLineString.prototype.get);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'getKeys',
    GVol.geom.MultiLineString.prototype.getKeys);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'getProperties',
    GVol.geom.MultiLineString.prototype.getProperties);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'set',
    GVol.geom.MultiLineString.prototype.set);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'setProperties',
    GVol.geom.MultiLineString.prototype.setProperties);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'unset',
    GVol.geom.MultiLineString.prototype.unset);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'changed',
    GVol.geom.MultiLineString.prototype.changed);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'dispatchEvent',
    GVol.geom.MultiLineString.prototype.dispatchEvent);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'getRevision',
    GVol.geom.MultiLineString.prototype.getRevision);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'on',
    GVol.geom.MultiLineString.prototype.on);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'once',
    GVol.geom.MultiLineString.prototype.once);

goog.exportProperty(
    GVol.geom.MultiLineString.prototype,
    'un',
    GVol.geom.MultiLineString.prototype.un);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'getFirstCoordinate',
    GVol.geom.MultiPoint.prototype.getFirstCoordinate);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'getLastCoordinate',
    GVol.geom.MultiPoint.prototype.getLastCoordinate);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'getLayout',
    GVol.geom.MultiPoint.prototype.getLayout);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'rotate',
    GVol.geom.MultiPoint.prototype.rotate);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'scale',
    GVol.geom.MultiPoint.prototype.scale);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'getClosestPoint',
    GVol.geom.MultiPoint.prototype.getClosestPoint);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'intersectsCoordinate',
    GVol.geom.MultiPoint.prototype.intersectsCoordinate);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'getExtent',
    GVol.geom.MultiPoint.prototype.getExtent);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'simplify',
    GVol.geom.MultiPoint.prototype.simplify);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'transform',
    GVol.geom.MultiPoint.prototype.transform);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'get',
    GVol.geom.MultiPoint.prototype.get);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'getKeys',
    GVol.geom.MultiPoint.prototype.getKeys);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'getProperties',
    GVol.geom.MultiPoint.prototype.getProperties);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'set',
    GVol.geom.MultiPoint.prototype.set);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'setProperties',
    GVol.geom.MultiPoint.prototype.setProperties);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'unset',
    GVol.geom.MultiPoint.prototype.unset);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'changed',
    GVol.geom.MultiPoint.prototype.changed);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'dispatchEvent',
    GVol.geom.MultiPoint.prototype.dispatchEvent);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'getRevision',
    GVol.geom.MultiPoint.prototype.getRevision);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'on',
    GVol.geom.MultiPoint.prototype.on);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'once',
    GVol.geom.MultiPoint.prototype.once);

goog.exportProperty(
    GVol.geom.MultiPoint.prototype,
    'un',
    GVol.geom.MultiPoint.prototype.un);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'getFirstCoordinate',
    GVol.geom.MultiPGVolygon.prototype.getFirstCoordinate);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'getLastCoordinate',
    GVol.geom.MultiPGVolygon.prototype.getLastCoordinate);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'getLayout',
    GVol.geom.MultiPGVolygon.prototype.getLayout);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'rotate',
    GVol.geom.MultiPGVolygon.prototype.rotate);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'scale',
    GVol.geom.MultiPGVolygon.prototype.scale);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'getClosestPoint',
    GVol.geom.MultiPGVolygon.prototype.getClosestPoint);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'intersectsCoordinate',
    GVol.geom.MultiPGVolygon.prototype.intersectsCoordinate);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'getExtent',
    GVol.geom.MultiPGVolygon.prototype.getExtent);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'simplify',
    GVol.geom.MultiPGVolygon.prototype.simplify);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'transform',
    GVol.geom.MultiPGVolygon.prototype.transform);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'get',
    GVol.geom.MultiPGVolygon.prototype.get);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'getKeys',
    GVol.geom.MultiPGVolygon.prototype.getKeys);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'getProperties',
    GVol.geom.MultiPGVolygon.prototype.getProperties);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'set',
    GVol.geom.MultiPGVolygon.prototype.set);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'setProperties',
    GVol.geom.MultiPGVolygon.prototype.setProperties);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'unset',
    GVol.geom.MultiPGVolygon.prototype.unset);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'changed',
    GVol.geom.MultiPGVolygon.prototype.changed);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'dispatchEvent',
    GVol.geom.MultiPGVolygon.prototype.dispatchEvent);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'getRevision',
    GVol.geom.MultiPGVolygon.prototype.getRevision);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'on',
    GVol.geom.MultiPGVolygon.prototype.on);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'once',
    GVol.geom.MultiPGVolygon.prototype.once);

goog.exportProperty(
    GVol.geom.MultiPGVolygon.prototype,
    'un',
    GVol.geom.MultiPGVolygon.prototype.un);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'getFirstCoordinate',
    GVol.geom.Point.prototype.getFirstCoordinate);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'getLastCoordinate',
    GVol.geom.Point.prototype.getLastCoordinate);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'getLayout',
    GVol.geom.Point.prototype.getLayout);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'rotate',
    GVol.geom.Point.prototype.rotate);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'scale',
    GVol.geom.Point.prototype.scale);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'getClosestPoint',
    GVol.geom.Point.prototype.getClosestPoint);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'intersectsCoordinate',
    GVol.geom.Point.prototype.intersectsCoordinate);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'getExtent',
    GVol.geom.Point.prototype.getExtent);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'simplify',
    GVol.geom.Point.prototype.simplify);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'transform',
    GVol.geom.Point.prototype.transform);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'get',
    GVol.geom.Point.prototype.get);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'getKeys',
    GVol.geom.Point.prototype.getKeys);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'getProperties',
    GVol.geom.Point.prototype.getProperties);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'set',
    GVol.geom.Point.prototype.set);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'setProperties',
    GVol.geom.Point.prototype.setProperties);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'unset',
    GVol.geom.Point.prototype.unset);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'changed',
    GVol.geom.Point.prototype.changed);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'dispatchEvent',
    GVol.geom.Point.prototype.dispatchEvent);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'getRevision',
    GVol.geom.Point.prototype.getRevision);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'on',
    GVol.geom.Point.prototype.on);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'once',
    GVol.geom.Point.prototype.once);

goog.exportProperty(
    GVol.geom.Point.prototype,
    'un',
    GVol.geom.Point.prototype.un);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'getFirstCoordinate',
    GVol.geom.PGVolygon.prototype.getFirstCoordinate);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'getLastCoordinate',
    GVol.geom.PGVolygon.prototype.getLastCoordinate);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'getLayout',
    GVol.geom.PGVolygon.prototype.getLayout);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'rotate',
    GVol.geom.PGVolygon.prototype.rotate);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'scale',
    GVol.geom.PGVolygon.prototype.scale);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'getClosestPoint',
    GVol.geom.PGVolygon.prototype.getClosestPoint);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'intersectsCoordinate',
    GVol.geom.PGVolygon.prototype.intersectsCoordinate);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'getExtent',
    GVol.geom.PGVolygon.prototype.getExtent);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'simplify',
    GVol.geom.PGVolygon.prototype.simplify);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'transform',
    GVol.geom.PGVolygon.prototype.transform);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'get',
    GVol.geom.PGVolygon.prototype.get);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'getKeys',
    GVol.geom.PGVolygon.prototype.getKeys);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'getProperties',
    GVol.geom.PGVolygon.prototype.getProperties);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'set',
    GVol.geom.PGVolygon.prototype.set);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'setProperties',
    GVol.geom.PGVolygon.prototype.setProperties);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'unset',
    GVol.geom.PGVolygon.prototype.unset);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'changed',
    GVol.geom.PGVolygon.prototype.changed);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'dispatchEvent',
    GVol.geom.PGVolygon.prototype.dispatchEvent);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'getRevision',
    GVol.geom.PGVolygon.prototype.getRevision);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'on',
    GVol.geom.PGVolygon.prototype.on);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'once',
    GVol.geom.PGVolygon.prototype.once);

goog.exportProperty(
    GVol.geom.PGVolygon.prototype,
    'un',
    GVol.geom.PGVolygon.prototype.un);

goog.exportProperty(
    GVol.format.GML.prototype,
    'readFeatures',
    GVol.format.GML.prototype.readFeatures);

goog.exportProperty(
    GVol.format.GML2.prototype,
    'readFeatures',
    GVol.format.GML2.prototype.readFeatures);

goog.exportProperty(
    GVol.format.GML3.prototype,
    'readFeatures',
    GVol.format.GML3.prototype.readFeatures);

goog.exportProperty(
    GVol.contrGVol.ContrGVol.prototype,
    'get',
    GVol.contrGVol.ContrGVol.prototype.get);

goog.exportProperty(
    GVol.contrGVol.ContrGVol.prototype,
    'getKeys',
    GVol.contrGVol.ContrGVol.prototype.getKeys);

goog.exportProperty(
    GVol.contrGVol.ContrGVol.prototype,
    'getProperties',
    GVol.contrGVol.ContrGVol.prototype.getProperties);

goog.exportProperty(
    GVol.contrGVol.ContrGVol.prototype,
    'set',
    GVol.contrGVol.ContrGVol.prototype.set);

goog.exportProperty(
    GVol.contrGVol.ContrGVol.prototype,
    'setProperties',
    GVol.contrGVol.ContrGVol.prototype.setProperties);

goog.exportProperty(
    GVol.contrGVol.ContrGVol.prototype,
    'unset',
    GVol.contrGVol.ContrGVol.prototype.unset);

goog.exportProperty(
    GVol.contrGVol.ContrGVol.prototype,
    'changed',
    GVol.contrGVol.ContrGVol.prototype.changed);

goog.exportProperty(
    GVol.contrGVol.ContrGVol.prototype,
    'dispatchEvent',
    GVol.contrGVol.ContrGVol.prototype.dispatchEvent);

goog.exportProperty(
    GVol.contrGVol.ContrGVol.prototype,
    'getRevision',
    GVol.contrGVol.ContrGVol.prototype.getRevision);

goog.exportProperty(
    GVol.contrGVol.ContrGVol.prototype,
    'on',
    GVol.contrGVol.ContrGVol.prototype.on);

goog.exportProperty(
    GVol.contrGVol.ContrGVol.prototype,
    'once',
    GVol.contrGVol.ContrGVol.prototype.once);

goog.exportProperty(
    GVol.contrGVol.ContrGVol.prototype,
    'un',
    GVol.contrGVol.ContrGVol.prototype.un);

goog.exportProperty(
    GVol.contrGVol.Attribution.prototype,
    'getMap',
    GVol.contrGVol.Attribution.prototype.getMap);

goog.exportProperty(
    GVol.contrGVol.Attribution.prototype,
    'setMap',
    GVol.contrGVol.Attribution.prototype.setMap);

goog.exportProperty(
    GVol.contrGVol.Attribution.prototype,
    'setTarget',
    GVol.contrGVol.Attribution.prototype.setTarget);

goog.exportProperty(
    GVol.contrGVol.Attribution.prototype,
    'get',
    GVol.contrGVol.Attribution.prototype.get);

goog.exportProperty(
    GVol.contrGVol.Attribution.prototype,
    'getKeys',
    GVol.contrGVol.Attribution.prototype.getKeys);

goog.exportProperty(
    GVol.contrGVol.Attribution.prototype,
    'getProperties',
    GVol.contrGVol.Attribution.prototype.getProperties);

goog.exportProperty(
    GVol.contrGVol.Attribution.prototype,
    'set',
    GVol.contrGVol.Attribution.prototype.set);

goog.exportProperty(
    GVol.contrGVol.Attribution.prototype,
    'setProperties',
    GVol.contrGVol.Attribution.prototype.setProperties);

goog.exportProperty(
    GVol.contrGVol.Attribution.prototype,
    'unset',
    GVol.contrGVol.Attribution.prototype.unset);

goog.exportProperty(
    GVol.contrGVol.Attribution.prototype,
    'changed',
    GVol.contrGVol.Attribution.prototype.changed);

goog.exportProperty(
    GVol.contrGVol.Attribution.prototype,
    'dispatchEvent',
    GVol.contrGVol.Attribution.prototype.dispatchEvent);

goog.exportProperty(
    GVol.contrGVol.Attribution.prototype,
    'getRevision',
    GVol.contrGVol.Attribution.prototype.getRevision);

goog.exportProperty(
    GVol.contrGVol.Attribution.prototype,
    'on',
    GVol.contrGVol.Attribution.prototype.on);

goog.exportProperty(
    GVol.contrGVol.Attribution.prototype,
    'once',
    GVol.contrGVol.Attribution.prototype.once);

goog.exportProperty(
    GVol.contrGVol.Attribution.prototype,
    'un',
    GVol.contrGVol.Attribution.prototype.un);

goog.exportProperty(
    GVol.contrGVol.FullScreen.prototype,
    'getMap',
    GVol.contrGVol.FullScreen.prototype.getMap);

goog.exportProperty(
    GVol.contrGVol.FullScreen.prototype,
    'setMap',
    GVol.contrGVol.FullScreen.prototype.setMap);

goog.exportProperty(
    GVol.contrGVol.FullScreen.prototype,
    'setTarget',
    GVol.contrGVol.FullScreen.prototype.setTarget);

goog.exportProperty(
    GVol.contrGVol.FullScreen.prototype,
    'get',
    GVol.contrGVol.FullScreen.prototype.get);

goog.exportProperty(
    GVol.contrGVol.FullScreen.prototype,
    'getKeys',
    GVol.contrGVol.FullScreen.prototype.getKeys);

goog.exportProperty(
    GVol.contrGVol.FullScreen.prototype,
    'getProperties',
    GVol.contrGVol.FullScreen.prototype.getProperties);

goog.exportProperty(
    GVol.contrGVol.FullScreen.prototype,
    'set',
    GVol.contrGVol.FullScreen.prototype.set);

goog.exportProperty(
    GVol.contrGVol.FullScreen.prototype,
    'setProperties',
    GVol.contrGVol.FullScreen.prototype.setProperties);

goog.exportProperty(
    GVol.contrGVol.FullScreen.prototype,
    'unset',
    GVol.contrGVol.FullScreen.prototype.unset);

goog.exportProperty(
    GVol.contrGVol.FullScreen.prototype,
    'changed',
    GVol.contrGVol.FullScreen.prototype.changed);

goog.exportProperty(
    GVol.contrGVol.FullScreen.prototype,
    'dispatchEvent',
    GVol.contrGVol.FullScreen.prototype.dispatchEvent);

goog.exportProperty(
    GVol.contrGVol.FullScreen.prototype,
    'getRevision',
    GVol.contrGVol.FullScreen.prototype.getRevision);

goog.exportProperty(
    GVol.contrGVol.FullScreen.prototype,
    'on',
    GVol.contrGVol.FullScreen.prototype.on);

goog.exportProperty(
    GVol.contrGVol.FullScreen.prototype,
    'once',
    GVol.contrGVol.FullScreen.prototype.once);

goog.exportProperty(
    GVol.contrGVol.FullScreen.prototype,
    'un',
    GVol.contrGVol.FullScreen.prototype.un);

goog.exportProperty(
    GVol.contrGVol.MousePosition.prototype,
    'getMap',
    GVol.contrGVol.MousePosition.prototype.getMap);

goog.exportProperty(
    GVol.contrGVol.MousePosition.prototype,
    'setMap',
    GVol.contrGVol.MousePosition.prototype.setMap);

goog.exportProperty(
    GVol.contrGVol.MousePosition.prototype,
    'setTarget',
    GVol.contrGVol.MousePosition.prototype.setTarget);

goog.exportProperty(
    GVol.contrGVol.MousePosition.prototype,
    'get',
    GVol.contrGVol.MousePosition.prototype.get);

goog.exportProperty(
    GVol.contrGVol.MousePosition.prototype,
    'getKeys',
    GVol.contrGVol.MousePosition.prototype.getKeys);

goog.exportProperty(
    GVol.contrGVol.MousePosition.prototype,
    'getProperties',
    GVol.contrGVol.MousePosition.prototype.getProperties);

goog.exportProperty(
    GVol.contrGVol.MousePosition.prototype,
    'set',
    GVol.contrGVol.MousePosition.prototype.set);

goog.exportProperty(
    GVol.contrGVol.MousePosition.prototype,
    'setProperties',
    GVol.contrGVol.MousePosition.prototype.setProperties);

goog.exportProperty(
    GVol.contrGVol.MousePosition.prototype,
    'unset',
    GVol.contrGVol.MousePosition.prototype.unset);

goog.exportProperty(
    GVol.contrGVol.MousePosition.prototype,
    'changed',
    GVol.contrGVol.MousePosition.prototype.changed);

goog.exportProperty(
    GVol.contrGVol.MousePosition.prototype,
    'dispatchEvent',
    GVol.contrGVol.MousePosition.prototype.dispatchEvent);

goog.exportProperty(
    GVol.contrGVol.MousePosition.prototype,
    'getRevision',
    GVol.contrGVol.MousePosition.prototype.getRevision);

goog.exportProperty(
    GVol.contrGVol.MousePosition.prototype,
    'on',
    GVol.contrGVol.MousePosition.prototype.on);

goog.exportProperty(
    GVol.contrGVol.MousePosition.prototype,
    'once',
    GVol.contrGVol.MousePosition.prototype.once);

goog.exportProperty(
    GVol.contrGVol.MousePosition.prototype,
    'un',
    GVol.contrGVol.MousePosition.prototype.un);

goog.exportProperty(
    GVol.contrGVol.OverviewMap.prototype,
    'getMap',
    GVol.contrGVol.OverviewMap.prototype.getMap);

goog.exportProperty(
    GVol.contrGVol.OverviewMap.prototype,
    'setMap',
    GVol.contrGVol.OverviewMap.prototype.setMap);

goog.exportProperty(
    GVol.contrGVol.OverviewMap.prototype,
    'setTarget',
    GVol.contrGVol.OverviewMap.prototype.setTarget);

goog.exportProperty(
    GVol.contrGVol.OverviewMap.prototype,
    'get',
    GVol.contrGVol.OverviewMap.prototype.get);

goog.exportProperty(
    GVol.contrGVol.OverviewMap.prototype,
    'getKeys',
    GVol.contrGVol.OverviewMap.prototype.getKeys);

goog.exportProperty(
    GVol.contrGVol.OverviewMap.prototype,
    'getProperties',
    GVol.contrGVol.OverviewMap.prototype.getProperties);

goog.exportProperty(
    GVol.contrGVol.OverviewMap.prototype,
    'set',
    GVol.contrGVol.OverviewMap.prototype.set);

goog.exportProperty(
    GVol.contrGVol.OverviewMap.prototype,
    'setProperties',
    GVol.contrGVol.OverviewMap.prototype.setProperties);

goog.exportProperty(
    GVol.contrGVol.OverviewMap.prototype,
    'unset',
    GVol.contrGVol.OverviewMap.prototype.unset);

goog.exportProperty(
    GVol.contrGVol.OverviewMap.prototype,
    'changed',
    GVol.contrGVol.OverviewMap.prototype.changed);

goog.exportProperty(
    GVol.contrGVol.OverviewMap.prototype,
    'dispatchEvent',
    GVol.contrGVol.OverviewMap.prototype.dispatchEvent);

goog.exportProperty(
    GVol.contrGVol.OverviewMap.prototype,
    'getRevision',
    GVol.contrGVol.OverviewMap.prototype.getRevision);

goog.exportProperty(
    GVol.contrGVol.OverviewMap.prototype,
    'on',
    GVol.contrGVol.OverviewMap.prototype.on);

goog.exportProperty(
    GVol.contrGVol.OverviewMap.prototype,
    'once',
    GVol.contrGVol.OverviewMap.prototype.once);

goog.exportProperty(
    GVol.contrGVol.OverviewMap.prototype,
    'un',
    GVol.contrGVol.OverviewMap.prototype.un);

goog.exportProperty(
    GVol.contrGVol.Rotate.prototype,
    'getMap',
    GVol.contrGVol.Rotate.prototype.getMap);

goog.exportProperty(
    GVol.contrGVol.Rotate.prototype,
    'setMap',
    GVol.contrGVol.Rotate.prototype.setMap);

goog.exportProperty(
    GVol.contrGVol.Rotate.prototype,
    'setTarget',
    GVol.contrGVol.Rotate.prototype.setTarget);

goog.exportProperty(
    GVol.contrGVol.Rotate.prototype,
    'get',
    GVol.contrGVol.Rotate.prototype.get);

goog.exportProperty(
    GVol.contrGVol.Rotate.prototype,
    'getKeys',
    GVol.contrGVol.Rotate.prototype.getKeys);

goog.exportProperty(
    GVol.contrGVol.Rotate.prototype,
    'getProperties',
    GVol.contrGVol.Rotate.prototype.getProperties);

goog.exportProperty(
    GVol.contrGVol.Rotate.prototype,
    'set',
    GVol.contrGVol.Rotate.prototype.set);

goog.exportProperty(
    GVol.contrGVol.Rotate.prototype,
    'setProperties',
    GVol.contrGVol.Rotate.prototype.setProperties);

goog.exportProperty(
    GVol.contrGVol.Rotate.prototype,
    'unset',
    GVol.contrGVol.Rotate.prototype.unset);

goog.exportProperty(
    GVol.contrGVol.Rotate.prototype,
    'changed',
    GVol.contrGVol.Rotate.prototype.changed);

goog.exportProperty(
    GVol.contrGVol.Rotate.prototype,
    'dispatchEvent',
    GVol.contrGVol.Rotate.prototype.dispatchEvent);

goog.exportProperty(
    GVol.contrGVol.Rotate.prototype,
    'getRevision',
    GVol.contrGVol.Rotate.prototype.getRevision);

goog.exportProperty(
    GVol.contrGVol.Rotate.prototype,
    'on',
    GVol.contrGVol.Rotate.prototype.on);

goog.exportProperty(
    GVol.contrGVol.Rotate.prototype,
    'once',
    GVol.contrGVol.Rotate.prototype.once);

goog.exportProperty(
    GVol.contrGVol.Rotate.prototype,
    'un',
    GVol.contrGVol.Rotate.prototype.un);

goog.exportProperty(
    GVol.contrGVol.ScaleLine.prototype,
    'getMap',
    GVol.contrGVol.ScaleLine.prototype.getMap);

goog.exportProperty(
    GVol.contrGVol.ScaleLine.prototype,
    'setMap',
    GVol.contrGVol.ScaleLine.prototype.setMap);

goog.exportProperty(
    GVol.contrGVol.ScaleLine.prototype,
    'setTarget',
    GVol.contrGVol.ScaleLine.prototype.setTarget);

goog.exportProperty(
    GVol.contrGVol.ScaleLine.prototype,
    'get',
    GVol.contrGVol.ScaleLine.prototype.get);

goog.exportProperty(
    GVol.contrGVol.ScaleLine.prototype,
    'getKeys',
    GVol.contrGVol.ScaleLine.prototype.getKeys);

goog.exportProperty(
    GVol.contrGVol.ScaleLine.prototype,
    'getProperties',
    GVol.contrGVol.ScaleLine.prototype.getProperties);

goog.exportProperty(
    GVol.contrGVol.ScaleLine.prototype,
    'set',
    GVol.contrGVol.ScaleLine.prototype.set);

goog.exportProperty(
    GVol.contrGVol.ScaleLine.prototype,
    'setProperties',
    GVol.contrGVol.ScaleLine.prototype.setProperties);

goog.exportProperty(
    GVol.contrGVol.ScaleLine.prototype,
    'unset',
    GVol.contrGVol.ScaleLine.prototype.unset);

goog.exportProperty(
    GVol.contrGVol.ScaleLine.prototype,
    'changed',
    GVol.contrGVol.ScaleLine.prototype.changed);

goog.exportProperty(
    GVol.contrGVol.ScaleLine.prototype,
    'dispatchEvent',
    GVol.contrGVol.ScaleLine.prototype.dispatchEvent);

goog.exportProperty(
    GVol.contrGVol.ScaleLine.prototype,
    'getRevision',
    GVol.contrGVol.ScaleLine.prototype.getRevision);

goog.exportProperty(
    GVol.contrGVol.ScaleLine.prototype,
    'on',
    GVol.contrGVol.ScaleLine.prototype.on);

goog.exportProperty(
    GVol.contrGVol.ScaleLine.prototype,
    'once',
    GVol.contrGVol.ScaleLine.prototype.once);

goog.exportProperty(
    GVol.contrGVol.ScaleLine.prototype,
    'un',
    GVol.contrGVol.ScaleLine.prototype.un);

goog.exportProperty(
    GVol.contrGVol.Zoom.prototype,
    'getMap',
    GVol.contrGVol.Zoom.prototype.getMap);

goog.exportProperty(
    GVol.contrGVol.Zoom.prototype,
    'setMap',
    GVol.contrGVol.Zoom.prototype.setMap);

goog.exportProperty(
    GVol.contrGVol.Zoom.prototype,
    'setTarget',
    GVol.contrGVol.Zoom.prototype.setTarget);

goog.exportProperty(
    GVol.contrGVol.Zoom.prototype,
    'get',
    GVol.contrGVol.Zoom.prototype.get);

goog.exportProperty(
    GVol.contrGVol.Zoom.prototype,
    'getKeys',
    GVol.contrGVol.Zoom.prototype.getKeys);

goog.exportProperty(
    GVol.contrGVol.Zoom.prototype,
    'getProperties',
    GVol.contrGVol.Zoom.prototype.getProperties);

goog.exportProperty(
    GVol.contrGVol.Zoom.prototype,
    'set',
    GVol.contrGVol.Zoom.prototype.set);

goog.exportProperty(
    GVol.contrGVol.Zoom.prototype,
    'setProperties',
    GVol.contrGVol.Zoom.prototype.setProperties);

goog.exportProperty(
    GVol.contrGVol.Zoom.prototype,
    'unset',
    GVol.contrGVol.Zoom.prototype.unset);

goog.exportProperty(
    GVol.contrGVol.Zoom.prototype,
    'changed',
    GVol.contrGVol.Zoom.prototype.changed);

goog.exportProperty(
    GVol.contrGVol.Zoom.prototype,
    'dispatchEvent',
    GVol.contrGVol.Zoom.prototype.dispatchEvent);

goog.exportProperty(
    GVol.contrGVol.Zoom.prototype,
    'getRevision',
    GVol.contrGVol.Zoom.prototype.getRevision);

goog.exportProperty(
    GVol.contrGVol.Zoom.prototype,
    'on',
    GVol.contrGVol.Zoom.prototype.on);

goog.exportProperty(
    GVol.contrGVol.Zoom.prototype,
    'once',
    GVol.contrGVol.Zoom.prototype.once);

goog.exportProperty(
    GVol.contrGVol.Zoom.prototype,
    'un',
    GVol.contrGVol.Zoom.prototype.un);

goog.exportProperty(
    GVol.contrGVol.ZoomSlider.prototype,
    'getMap',
    GVol.contrGVol.ZoomSlider.prototype.getMap);

goog.exportProperty(
    GVol.contrGVol.ZoomSlider.prototype,
    'setMap',
    GVol.contrGVol.ZoomSlider.prototype.setMap);

goog.exportProperty(
    GVol.contrGVol.ZoomSlider.prototype,
    'setTarget',
    GVol.contrGVol.ZoomSlider.prototype.setTarget);

goog.exportProperty(
    GVol.contrGVol.ZoomSlider.prototype,
    'get',
    GVol.contrGVol.ZoomSlider.prototype.get);

goog.exportProperty(
    GVol.contrGVol.ZoomSlider.prototype,
    'getKeys',
    GVol.contrGVol.ZoomSlider.prototype.getKeys);

goog.exportProperty(
    GVol.contrGVol.ZoomSlider.prototype,
    'getProperties',
    GVol.contrGVol.ZoomSlider.prototype.getProperties);

goog.exportProperty(
    GVol.contrGVol.ZoomSlider.prototype,
    'set',
    GVol.contrGVol.ZoomSlider.prototype.set);

goog.exportProperty(
    GVol.contrGVol.ZoomSlider.prototype,
    'setProperties',
    GVol.contrGVol.ZoomSlider.prototype.setProperties);

goog.exportProperty(
    GVol.contrGVol.ZoomSlider.prototype,
    'unset',
    GVol.contrGVol.ZoomSlider.prototype.unset);

goog.exportProperty(
    GVol.contrGVol.ZoomSlider.prototype,
    'changed',
    GVol.contrGVol.ZoomSlider.prototype.changed);

goog.exportProperty(
    GVol.contrGVol.ZoomSlider.prototype,
    'dispatchEvent',
    GVol.contrGVol.ZoomSlider.prototype.dispatchEvent);

goog.exportProperty(
    GVol.contrGVol.ZoomSlider.prototype,
    'getRevision',
    GVol.contrGVol.ZoomSlider.prototype.getRevision);

goog.exportProperty(
    GVol.contrGVol.ZoomSlider.prototype,
    'on',
    GVol.contrGVol.ZoomSlider.prototype.on);

goog.exportProperty(
    GVol.contrGVol.ZoomSlider.prototype,
    'once',
    GVol.contrGVol.ZoomSlider.prototype.once);

goog.exportProperty(
    GVol.contrGVol.ZoomSlider.prototype,
    'un',
    GVol.contrGVol.ZoomSlider.prototype.un);

goog.exportProperty(
    GVol.contrGVol.ZoomToExtent.prototype,
    'getMap',
    GVol.contrGVol.ZoomToExtent.prototype.getMap);

goog.exportProperty(
    GVol.contrGVol.ZoomToExtent.prototype,
    'setMap',
    GVol.contrGVol.ZoomToExtent.prototype.setMap);

goog.exportProperty(
    GVol.contrGVol.ZoomToExtent.prototype,
    'setTarget',
    GVol.contrGVol.ZoomToExtent.prototype.setTarget);

goog.exportProperty(
    GVol.contrGVol.ZoomToExtent.prototype,
    'get',
    GVol.contrGVol.ZoomToExtent.prototype.get);

goog.exportProperty(
    GVol.contrGVol.ZoomToExtent.prototype,
    'getKeys',
    GVol.contrGVol.ZoomToExtent.prototype.getKeys);

goog.exportProperty(
    GVol.contrGVol.ZoomToExtent.prototype,
    'getProperties',
    GVol.contrGVol.ZoomToExtent.prototype.getProperties);

goog.exportProperty(
    GVol.contrGVol.ZoomToExtent.prototype,
    'set',
    GVol.contrGVol.ZoomToExtent.prototype.set);

goog.exportProperty(
    GVol.contrGVol.ZoomToExtent.prototype,
    'setProperties',
    GVol.contrGVol.ZoomToExtent.prototype.setProperties);

goog.exportProperty(
    GVol.contrGVol.ZoomToExtent.prototype,
    'unset',
    GVol.contrGVol.ZoomToExtent.prototype.unset);

goog.exportProperty(
    GVol.contrGVol.ZoomToExtent.prototype,
    'changed',
    GVol.contrGVol.ZoomToExtent.prototype.changed);

goog.exportProperty(
    GVol.contrGVol.ZoomToExtent.prototype,
    'dispatchEvent',
    GVol.contrGVol.ZoomToExtent.prototype.dispatchEvent);

goog.exportProperty(
    GVol.contrGVol.ZoomToExtent.prototype,
    'getRevision',
    GVol.contrGVol.ZoomToExtent.prototype.getRevision);

goog.exportProperty(
    GVol.contrGVol.ZoomToExtent.prototype,
    'on',
    GVol.contrGVol.ZoomToExtent.prototype.on);

goog.exportProperty(
    GVol.contrGVol.ZoomToExtent.prototype,
    'once',
    GVol.contrGVol.ZoomToExtent.prototype.once);

goog.exportProperty(
    GVol.contrGVol.ZoomToExtent.prototype,
    'un',
    GVol.contrGVol.ZoomToExtent.prototype.un);
GVol.VERSION = 'v4.3.4';
OPENLAYERS.GVol = GVol;

  return OPENLAYERS.GVol;
}));
